forked from baron/baron-sso
1039 lines
33 KiB
TypeScript
1039 lines
33 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { captureEvidence } from "./helpers/evidence";
|
|
|
|
function tenant(
|
|
id: string,
|
|
name: string,
|
|
slug: string,
|
|
parentId?: string,
|
|
type?: string,
|
|
config?: Record<string, unknown>,
|
|
) {
|
|
return {
|
|
id,
|
|
type: type ?? (parentId ? "USER_GROUP" : "COMPANY_GROUP"),
|
|
name,
|
|
slug,
|
|
description: "",
|
|
status: "active",
|
|
parentId,
|
|
config,
|
|
memberCount: 1,
|
|
createdAt: "2026-04-01T00:00:00.000Z",
|
|
updatedAt: "2026-04-01T00:00:00.000Z",
|
|
};
|
|
}
|
|
|
|
function user(id: string, name: string, companyCode: string) {
|
|
return {
|
|
id,
|
|
email: `${id}@example.com`,
|
|
name,
|
|
role: "user",
|
|
status: "active",
|
|
companyCode,
|
|
tenant: {
|
|
id: companyCode,
|
|
slug: companyCode,
|
|
type: "USER_GROUP",
|
|
name: companyCode,
|
|
},
|
|
grade: "사원",
|
|
createdAt: "2026-04-01T00:00:00.000Z",
|
|
updatedAt: "2026-04-01T00:00:00.000Z",
|
|
};
|
|
}
|
|
|
|
function multiTenantUser(
|
|
id: string,
|
|
name: string,
|
|
companyCode: string,
|
|
joinedTenants: Array<ReturnType<typeof tenant>>,
|
|
) {
|
|
return {
|
|
...user(id, name, companyCode),
|
|
joinedTenants,
|
|
};
|
|
}
|
|
|
|
function hanmacUser(id: string, name: string, companyCode: string) {
|
|
return {
|
|
...user(id, name, companyCode),
|
|
email: `${id}@hanmac.kr`,
|
|
};
|
|
}
|
|
|
|
test("org chart uses svg viewBox zoom for sharp vector rendering", async ({
|
|
page,
|
|
}) => {
|
|
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
sharedWith: "Playwright",
|
|
tenants: [
|
|
tenant("group", "HMAC Group", "hmac"),
|
|
tenant("baron", "Baron", "baron", "group"),
|
|
tenant("engineering", "Engineering", "engineering", "baron"),
|
|
tenant("platform", "Platform", "platform", "engineering"),
|
|
],
|
|
users: [
|
|
user("u-group", "Group User", "hmac"),
|
|
user("u-baron", "Baron User", "baron"),
|
|
user("u-eng", "Engineering User", "engineering"),
|
|
user("u-platform", "Platform User", "platform"),
|
|
],
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto("/chart?token=vector");
|
|
await expect(page.getByRole("heading", { name: "조직 현황" })).toBeVisible();
|
|
|
|
const viewport = page.locator('[data-testid="orgchart-viewport"]');
|
|
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
|
await expect(svg).toBeVisible();
|
|
await expect(svg.getByText("Engineering User 사원")).toBeVisible();
|
|
|
|
const initialViewBox = await svg.getAttribute("viewBox");
|
|
const transform = await page
|
|
.locator('[data-testid="orgchart-canvas"]')
|
|
.evaluate((element) => window.getComputedStyle(element).transform)
|
|
.catch(() => "none");
|
|
expect(transform).toBe("none");
|
|
|
|
const box = await viewport.boundingBox();
|
|
expect(box).not.toBeNull();
|
|
if (!box) return;
|
|
|
|
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
|
await page.mouse.wheel(0, -500);
|
|
|
|
await expect
|
|
.poll(async () => svg.getAttribute("viewBox"))
|
|
.not.toBe(initialViewBox);
|
|
});
|
|
|
|
test("org chart filters by Hanmac family and company while excluding hanmac.kr accounts", async ({
|
|
page,
|
|
}) => {
|
|
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
sharedWith: "Playwright",
|
|
tenants: [
|
|
tenant("group", "HMAC Group", "hmac"),
|
|
tenant("baron", "Baron", "baron", "group", "COMPANY"),
|
|
tenant("hanmac", "Hanmac", "hanmac", "group", "COMPANY"),
|
|
tenant("engineering", "Engineering", "engineering", "baron"),
|
|
tenant("sales", "Sales", "sales", "hanmac"),
|
|
],
|
|
users: [
|
|
user("u-group", "Group User", "hmac"),
|
|
user("u-baron", "Baron User", "baron"),
|
|
user("u-eng", "Engineering User", "engineering"),
|
|
user("u-sales", "Sales User", "sales"),
|
|
hanmacUser("hidden", "Hidden Hanmac User", "engineering"),
|
|
],
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto("/chart?token=family");
|
|
|
|
await expect(page.getByRole("button", { name: "한맥가족" })).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "Baron" })).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "Hanmac" })).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "전체" })).toHaveCount(0);
|
|
await expect(page.getByText("총 4명")).toBeVisible();
|
|
|
|
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
|
await expect(svg.getByText(/Hidden Hanmac User/)).toHaveCount(0);
|
|
await expect(svg.getByText("Engineering User 사원")).toBeVisible();
|
|
await expect(svg.getByText("Sales User 사원")).toBeVisible();
|
|
|
|
await page.getByRole("button", { name: "Baron" }).click();
|
|
await expect(page.getByText("총 2명")).toBeVisible();
|
|
await expect(page.getByText("총 4명")).toHaveCount(0);
|
|
await expect(svg.getByText("Engineering User 사원")).toBeVisible();
|
|
await expect(svg.getByText(/Sales User/)).toHaveCount(0);
|
|
});
|
|
|
|
test("org chart hides internal and private organizations in the status chart", async ({
|
|
page,
|
|
}) => {
|
|
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
sharedWith: "Playwright",
|
|
tenants: [
|
|
tenant("group", "HMAC Group", "hmac"),
|
|
tenant("visible", "Visible Org", "visible", "group", "ORGANIZATION"),
|
|
tenant(
|
|
"internal",
|
|
"Internal Org",
|
|
"internal",
|
|
"group",
|
|
"ORGANIZATION",
|
|
{
|
|
visibility: "internal",
|
|
},
|
|
),
|
|
tenant(
|
|
"internal-child",
|
|
"Internal Child",
|
|
"internal-child",
|
|
"internal",
|
|
"ORGANIZATION",
|
|
),
|
|
tenant("private", "Private Org", "private", "group", "ORGANIZATION", {
|
|
visibility: "private",
|
|
}),
|
|
],
|
|
users: [
|
|
user("u-visible", "Visible User", "visible"),
|
|
user("u-internal", "Internal User", "internal"),
|
|
user("u-private", "Private User", "private"),
|
|
],
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto("/chart?token=visibility&includeInternal=true");
|
|
|
|
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
|
await expect(svg.getByText("Visible Org")).toBeVisible();
|
|
await expect(svg.getByText("Visible User 사원")).toBeVisible();
|
|
await expect(
|
|
svg.getByText(/Internal Org|Internal Child|Private Org/),
|
|
).toHaveCount(0);
|
|
await expect(svg.getByText(/Internal User|Private User/)).toHaveCount(0);
|
|
});
|
|
|
|
test("org chart balances large member groups with automatic member columns", async ({
|
|
page,
|
|
}) => {
|
|
const members = Array.from({ length: 10 }, (_, index) =>
|
|
user(`u-member-${index + 1}`, `Member ${index + 1}`, "engineering"),
|
|
);
|
|
|
|
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
sharedWith: "Playwright",
|
|
tenants: [
|
|
tenant("group", "HMAC Group", "hmac"),
|
|
tenant("engineering", "Engineering", "engineering", "group"),
|
|
],
|
|
users: members,
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto("/chart?token=member-columns");
|
|
|
|
const engineeringNode = page.locator(
|
|
'[data-testid="orgchart-node-engineering"]',
|
|
);
|
|
await expect(engineeringNode).toBeVisible();
|
|
await expect(
|
|
engineeringNode.locator('[data-member-columns="2"]'),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("org chart displays user names with short grade aliases and no job details", async ({
|
|
page,
|
|
}) => {
|
|
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
sharedWith: "Playwright",
|
|
tenants: [
|
|
tenant("group", "HMAC Group", "hmac"),
|
|
tenant("baron", "Baron", "baron", "group", "COMPANY"),
|
|
tenant("engineering", "Engineering", "engineering", "baron"),
|
|
],
|
|
users: [
|
|
{
|
|
...user("u-eng", "Engineering User", "engineering"),
|
|
jobTitle: "Platform Engineer",
|
|
grade: "책임연구원",
|
|
position: "팀장",
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto("/chart?token=display-name");
|
|
|
|
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
|
await expect(svg.getByText("Engineering User 책임")).toBeVisible();
|
|
await expect(svg.getByText(/팀장|Platform Engineer/)).toHaveCount(0);
|
|
});
|
|
|
|
test("org chart orders managers before top executive members by rank priority", async ({
|
|
page,
|
|
}) => {
|
|
const executiveUser = (id: string, name: string, grade: string) => ({
|
|
...user(id, name, "engineering"),
|
|
grade,
|
|
});
|
|
|
|
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
sharedWith: "Playwright",
|
|
tenants: [
|
|
tenant("group", "HMAC Group", "hmac"),
|
|
tenant("engineering", "Engineering", "engineering", "group"),
|
|
],
|
|
users: [
|
|
executiveUser("u-vice-president", "Vice President", "부사장"),
|
|
executiveUser("u-adviser", "Adviser", "고문"),
|
|
executiveUser("u-vice-chair", "Vice Chair", "부회장"),
|
|
executiveUser("u-president", "President", "사장"),
|
|
executiveUser("u-chair", "Chair", "회장"),
|
|
executiveUser("u-director", "Director", "전무"),
|
|
{
|
|
...executiveUser("u-manager", "Team Manager", "사원"),
|
|
metadata: {
|
|
additionalAppointments: [
|
|
{
|
|
tenantSlug: "engineering",
|
|
isManager: true,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto("/chart?token=rank-priority");
|
|
|
|
const engineeringNode = page.locator(
|
|
'[data-testid="orgchart-node-engineering"]',
|
|
);
|
|
await expect(engineeringNode).toBeVisible();
|
|
|
|
const orderedMemberIds = await engineeringNode
|
|
.locator('[data-testid^="orgchart-member-"]')
|
|
.evaluateAll((elements) =>
|
|
elements.map((element) => element.getAttribute("data-testid")),
|
|
);
|
|
|
|
expect(orderedMemberIds).toEqual([
|
|
"orgchart-member-u-manager",
|
|
"orgchart-member-u-chair",
|
|
"orgchart-member-u-president",
|
|
"orgchart-member-u-vice-chair",
|
|
"orgchart-member-u-adviser",
|
|
"orgchart-member-u-vice-president",
|
|
"orgchart-member-u-director",
|
|
]);
|
|
});
|
|
|
|
test("org chart renders an IS3 super admin when an org appointment exists", async ({
|
|
page,
|
|
}) => {
|
|
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
sharedWith: "Playwright",
|
|
tenants: [
|
|
tenant("hanmac-family", "한맥가족", "hanmac-family"),
|
|
tenant(
|
|
"gpdtdc",
|
|
"총괄기획&기술개발센터",
|
|
"gpdtdc",
|
|
"hanmac-family",
|
|
"COMPANY",
|
|
),
|
|
tenant("gpd", "총괄기획실", "gpd", "gpdtdc", "ORGANIZATION"),
|
|
tenant(
|
|
"intigrated-system",
|
|
"통합시스템",
|
|
"intigrated-system",
|
|
"gpd",
|
|
"ORGANIZATION",
|
|
{ visibility: "public", orgUnitType: "디비전" },
|
|
),
|
|
tenant("is-3", "IS3", "is-3", "intigrated-system", "ORGANIZATION", {
|
|
visibility: "public",
|
|
orgUnitType: "팀",
|
|
}),
|
|
],
|
|
users: [
|
|
{
|
|
id: "675a3d46-45ad-4e8c-8c22-959a38302826",
|
|
email: "cyhan@samaneng.com",
|
|
name: "한치영",
|
|
role: "super_admin",
|
|
status: "active",
|
|
tenantSlug: "is-3",
|
|
companyCode: "is-3",
|
|
tenant: undefined,
|
|
grade: "",
|
|
metadata: {
|
|
additionalAppointments: [
|
|
{
|
|
grade: "책임",
|
|
isManager: true,
|
|
isPrimary: true,
|
|
tenantId: "is-3",
|
|
tenantName: "IS3",
|
|
tenantSlug: "is-3",
|
|
},
|
|
],
|
|
},
|
|
createdAt: "2026-06-16T00:00:00.000Z",
|
|
updatedAt: "2026-06-16T00:00:00.000Z",
|
|
},
|
|
{
|
|
...user("is3-executive", "상위직급", "is-3"),
|
|
grade: "전무이사",
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto("/chart?token=is3-manager");
|
|
|
|
const is3Node = page.locator('[data-testid="orgchart-node-is-3"]');
|
|
await expect(is3Node).toBeVisible();
|
|
await expect(
|
|
is3Node.locator(
|
|
'[data-testid="orgchart-member-675a3d46-45ad-4e8c-8c22-959a38302826"]',
|
|
),
|
|
).toContainText("한치영 책임");
|
|
await expect(
|
|
is3Node.locator('[data-testid="orgchart-member-is3-executive"]'),
|
|
).toContainText("상위직급 전무");
|
|
|
|
const orderedMemberIds = await is3Node
|
|
.locator('[data-testid^="orgchart-member-"]')
|
|
.evaluateAll((elements) =>
|
|
elements.map((element) => element.getAttribute("data-testid")),
|
|
);
|
|
|
|
expect(orderedMemberIds).toEqual([
|
|
"orgchart-member-675a3d46-45ad-4e8c-8c22-959a38302826",
|
|
"orgchart-member-is3-executive",
|
|
]);
|
|
});
|
|
|
|
test("org chart ignores stale scalar org fields without a membership source", async ({
|
|
page,
|
|
}) => {
|
|
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
sharedWith: "Playwright",
|
|
tenants: [
|
|
tenant("hanmac-family", "한맥가족", "hanmac-family"),
|
|
tenant("is-1", "IS1", "is-1", "hanmac-family", "ORGANIZATION"),
|
|
tenant("is-2", "IS2", "is-2", "hanmac-family", "ORGANIZATION"),
|
|
tenant("is-3", "IS3", "is-3", "hanmac-family", "ORGANIZATION"),
|
|
],
|
|
users: [
|
|
{
|
|
id: "stale-system-admin",
|
|
email: "stale-system-admin@example.com",
|
|
name: "Stale System Admin",
|
|
role: "system_admin",
|
|
status: "active",
|
|
tenantSlug: "is-2",
|
|
companyCode: "is-1",
|
|
tenant: undefined,
|
|
joinedTenants: undefined,
|
|
metadata: { additionalAppointments: [] },
|
|
grade: "사원",
|
|
createdAt: "2026-06-16T00:00:00.000Z",
|
|
updatedAt: "2026-06-16T00:00:00.000Z",
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto("/chart?token=stale-scalar");
|
|
|
|
await expect(
|
|
page.locator('[data-testid="orgchart-member-stale-system-admin"]'),
|
|
).toHaveCount(0);
|
|
});
|
|
|
|
test("org chart expands organization node width so long names are not clipped", async ({
|
|
page,
|
|
}) => {
|
|
const longName =
|
|
"초장문 조직 명칭 표시 검증을 위한 구조물 디지털 전환 통합 운영 센터";
|
|
|
|
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
sharedWith: "Playwright",
|
|
tenants: [
|
|
tenant("group", "HMAC Group", "hmac"),
|
|
tenant("long-name", longName, "long-name", "group"),
|
|
],
|
|
users: [user("u-long", "Long Name User", "long-name")],
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto("/chart?token=long-org-name");
|
|
|
|
const longNode = page.locator('[data-testid="orgchart-node-long-name"]');
|
|
await expect(longNode).toBeVisible();
|
|
const title = longNode.getByText(longName, { exact: true });
|
|
await expect(title).toBeVisible();
|
|
|
|
const titleMetrics = await title.evaluate((element) => ({
|
|
clientWidth: element.clientWidth,
|
|
scrollWidth: element.scrollWidth,
|
|
}));
|
|
expect(titleMetrics.scrollWidth).toBeLessThanOrEqual(
|
|
titleMetrics.clientWidth + 1,
|
|
);
|
|
});
|
|
|
|
test("org chart only highlights flagged member cards", async ({ page }) => {
|
|
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
sharedWith: "Playwright",
|
|
tenants: [
|
|
tenant("group", "HMAC Group", "hmac"),
|
|
tenant("engineering", "Engineering", "engineering", "group"),
|
|
],
|
|
users: [
|
|
user("u-normal", "Normal User", "engineering"),
|
|
{
|
|
...user("u-owner", "Owner User", "engineering"),
|
|
metadata: {
|
|
additionalAppointments: [
|
|
{
|
|
tenantSlug: "engineering",
|
|
isOwner: true,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
...user("u-admin", "Admin User", "engineering"),
|
|
metadata: {
|
|
additionalAppointments: [
|
|
{
|
|
tenantSlug: "engineering",
|
|
isAdmin: true,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
...user("u-manager", "Manager User", "engineering"),
|
|
metadata: {
|
|
additionalAppointments: [
|
|
{
|
|
tenantSlug: "engineering",
|
|
isManager: true,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto("/chart?token=highlighted-members");
|
|
|
|
const engineeringNode = page.locator(
|
|
'[data-testid="orgchart-node-engineering"]',
|
|
);
|
|
await expect(
|
|
engineeringNode.locator('[data-testid="orgchart-member-u-normal"]'),
|
|
).toHaveAttribute("data-highlighted", "false");
|
|
await expect(
|
|
engineeringNode.locator('[data-testid="orgchart-member-u-owner"]'),
|
|
).toHaveAttribute("data-highlighted", "true");
|
|
await expect(
|
|
engineeringNode.locator('[data-testid="orgchart-member-u-admin"]'),
|
|
).toHaveAttribute("data-highlighted", "true");
|
|
await expect(
|
|
engineeringNode.locator('[data-testid="orgchart-member-u-manager"]'),
|
|
).toHaveAttribute("data-highlighted", "true");
|
|
});
|
|
|
|
test("org chart places multi-tenant users only on leaf memberships without duplicate rendering", async ({
|
|
page,
|
|
}) => {
|
|
const issuedAt = Math.floor(Date.now() / 1000);
|
|
await page.addInitScript(
|
|
({ issuedAt: seededIssuedAt }) => {
|
|
const mockOidcUser = {
|
|
id_token: "playwright-id-token",
|
|
session_state: "playwright-session",
|
|
access_token: "playwright-access-token",
|
|
refresh_token: "playwright-refresh-token",
|
|
token_type: "Bearer",
|
|
scope: "openid profile email",
|
|
profile: {
|
|
sub: "playwright-user",
|
|
email: "playwright@example.com",
|
|
name: "Playwright User",
|
|
role: "tenant_admin",
|
|
},
|
|
expires_at: seededIssuedAt + 3600,
|
|
};
|
|
const storageKeys = [
|
|
"user:http://localhost:5000/oidc:orgfront",
|
|
"user:http://localhost:5000/oidc/:orgfront",
|
|
"user:http://localhost:5000/oidc:devfront",
|
|
"user:http://localhost:5000/oidc/:devfront",
|
|
"user:http://172.16.9.189:5000/oidc:orgfront",
|
|
"user:http://172.16.9.189:5000/oidc/:orgfront",
|
|
"oidc.user:http://localhost:5000/oidc:orgfront",
|
|
"oidc.user:http://localhost:5000/oidc/:orgfront",
|
|
"oidc.user:http://localhost:5000/oidc:devfront",
|
|
"oidc.user:http://localhost:5000/oidc/:devfront",
|
|
"oidc.user:http://172.16.9.189:5000/oidc:orgfront",
|
|
"oidc.user:http://172.16.9.189:5000/oidc/:orgfront",
|
|
];
|
|
for (const key of storageKeys) {
|
|
window.localStorage.setItem(key, JSON.stringify(mockOidcUser));
|
|
}
|
|
window.localStorage.setItem("playwright_auth_bypass", "1");
|
|
window.localStorage.setItem("dev_tenant_id", "group");
|
|
},
|
|
{ issuedAt },
|
|
);
|
|
|
|
await page.route("**/oidc/**", async (route) => {
|
|
const url = route.request().url();
|
|
if (url.includes(".well-known/openid-configuration")) {
|
|
await route.fulfill({
|
|
json: {
|
|
issuer: "http://localhost:5000/oidc",
|
|
authorization_endpoint: "http://localhost:5000/oidc/auth",
|
|
token_endpoint: "http://localhost:5000/oidc/token",
|
|
jwks_uri: "http://localhost:5000/oidc/jwks",
|
|
userinfo_endpoint: "http://localhost:5000/oidc/userinfo",
|
|
end_session_endpoint: "http://localhost:5000/oidc/session/end",
|
|
},
|
|
headers: { "Access-Control-Allow-Origin": "*" },
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (url.includes("/jwks")) {
|
|
await route.fulfill({
|
|
json: { keys: [] },
|
|
headers: { "Access-Control-Allow-Origin": "*" },
|
|
});
|
|
return;
|
|
}
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
body: "ok",
|
|
headers: { "Access-Control-Allow-Origin": "*" },
|
|
});
|
|
});
|
|
|
|
const tenants = [
|
|
tenant("group", "HMAC Group", "hmac"),
|
|
tenant("baron", "Baron", "baron", "group", "COMPANY"),
|
|
tenant("engineering", "Engineering", "engineering", "baron"),
|
|
tenant("platform", "Platform", "platform", "engineering"),
|
|
];
|
|
const [groupTenant, baronTenant, engineeringTenant, platformTenant] = tenants;
|
|
|
|
await page.route("**/api/v1/admin/orgchart/snapshot**", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
tenants,
|
|
users: [
|
|
multiTenantUser("u-shared", "Shared User", "baron", [
|
|
groupTenant,
|
|
baronTenant,
|
|
engineeringTenant,
|
|
platformTenant,
|
|
]),
|
|
],
|
|
cache: { source: "redis", hit: true },
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto("/chart");
|
|
|
|
await expect(page.getByText("총 1명")).toBeVisible();
|
|
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
|
await expect(svg).toBeVisible();
|
|
await expect(svg.getByText(/Shared User/)).toHaveCount(1);
|
|
await expect(svg.getByText(/^1$/)).toHaveCount(4);
|
|
});
|
|
|
|
test("org chart allows a user in a hanmac-family descendant tenant", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
await page.setViewportSize({ width: 1600, height: 900 });
|
|
let snapshotRequests = 0;
|
|
await page.addInitScript(() => {
|
|
window.localStorage.setItem("playwright_auth_bypass", "1");
|
|
window.localStorage.setItem("dev_tenant_id", "saman-id");
|
|
});
|
|
|
|
await page.route("**/api/v1/admin/orgchart/snapshot**", async (route) => {
|
|
snapshotRequests += 1;
|
|
const url = new URL(route.request().url());
|
|
expect(route.request().headers()["x-tenant-id"]).toBe("saman-id");
|
|
expect(url.searchParams.get("cache")).toBe("redis");
|
|
if (snapshotRequests === 1) {
|
|
expect(url.searchParams.get("refresh")).toBeNull();
|
|
} else {
|
|
expect(url.searchParams.get("refresh")).toBe("true");
|
|
}
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
generatedAt:
|
|
snapshotRequests === 1
|
|
? "2026-06-17T07:10:11Z"
|
|
: "2026-06-17T08:20:21Z",
|
|
tenants: [
|
|
tenant(
|
|
"hanmac-family-id",
|
|
"한맥가족",
|
|
"hanmac-family",
|
|
"hanmac-family-id",
|
|
"COMPANY_GROUP",
|
|
),
|
|
tenant("saman-id", "삼안", "saman", "hanmac-family-id", "COMPANY"),
|
|
tenant("saman-platform-id", "플랫폼팀", "saman-platform", "saman-id"),
|
|
],
|
|
users: [
|
|
{
|
|
...user("u-saman", "Saman Descendant User", "saman-platform"),
|
|
tenantSlug: "saman",
|
|
tenant: tenant(
|
|
"saman-id",
|
|
"삼안",
|
|
"saman",
|
|
"hanmac-family-id",
|
|
"COMPANY",
|
|
),
|
|
joinedTenants: [
|
|
tenant(
|
|
"saman-platform-id",
|
|
"플랫폼팀",
|
|
"saman-platform",
|
|
"saman-id",
|
|
),
|
|
],
|
|
},
|
|
],
|
|
cache: { source: "redis", hit: true },
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto("/chart");
|
|
|
|
await expect(
|
|
page.getByText("조직도를 불러올 수 없거나 만료된 링크입니다."),
|
|
).toHaveCount(0);
|
|
await expect(
|
|
page.getByRole("button", { name: "조직: 한맥가족" }),
|
|
).toBeVisible();
|
|
await expect(page.getByTestId("orgchart-selection-status-panel")).toHaveCount(
|
|
0,
|
|
);
|
|
await expect(page.getByTestId("orgchart-render-status-panel")).toHaveText(
|
|
"2026-06-17 16:10:11 KST",
|
|
);
|
|
await expect(
|
|
page.getByTestId("orgchart-render-status-panel"),
|
|
).not.toContainText("데이터 기준");
|
|
const headerBox = await page.getByTestId("orgfront-topbar").boundingBox();
|
|
const viewportWidth = page.viewportSize()?.width ?? 1600;
|
|
const statusPanelBox = await page
|
|
.getByTestId("orgchart-render-status-panel")
|
|
.boundingBox();
|
|
expect(headerBox).not.toBeNull();
|
|
expect(statusPanelBox).not.toBeNull();
|
|
expect(
|
|
viewportWidth - ((statusPanelBox?.x ?? 0) + (statusPanelBox?.width ?? 0)),
|
|
).toBeLessThanOrEqual(20);
|
|
expect(
|
|
(headerBox?.y ?? 0) +
|
|
(headerBox?.height ?? 0) -
|
|
((statusPanelBox?.y ?? 0) + (statusPanelBox?.height ?? 0)),
|
|
).toBeLessThanOrEqual(6);
|
|
await expect(page.getByTestId("orgchart-render-status-panel")).toHaveCSS(
|
|
"border-top-width",
|
|
"0px",
|
|
);
|
|
await expect(page.getByTestId("orgchart-render-status-panel")).toHaveCSS(
|
|
"box-shadow",
|
|
"none",
|
|
);
|
|
await expect(page.getByTestId("orgchart-render-status-panel")).toHaveCSS(
|
|
"background-color",
|
|
"rgba(0, 0, 0, 0)",
|
|
);
|
|
await expect(page.getByTestId("orgchart-render-status-panel")).toHaveCSS(
|
|
"opacity",
|
|
"0.72",
|
|
);
|
|
await expect(page.getByRole("button", { name: "새로고침" })).toHaveCSS(
|
|
"background-color",
|
|
"rgba(0, 0, 0, 0)",
|
|
);
|
|
await expect(page.getByRole("button", { name: "새로고침" })).toHaveCSS(
|
|
"border-top-width",
|
|
"0px",
|
|
);
|
|
await page.getByRole("button", { name: "새로고침" }).click();
|
|
await expect(page.getByTestId("orgchart-render-status-panel")).toHaveText(
|
|
"2026-06-17 17:20:21 KST",
|
|
);
|
|
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
|
await expect(svg.getByText("한맥가족", { exact: true })).toBeVisible();
|
|
await expect(svg.getByText("삼안", { exact: true })).toBeVisible();
|
|
await expect(svg.getByText(/Saman Descendant User/)).toBeVisible();
|
|
await captureEvidence(page, testInfo, "orgchart-topbar-data-refresh-aligned");
|
|
});
|
|
|
|
test("org chart logs authenticated snapshot failures with actionable diagnostics", async ({
|
|
page,
|
|
}) => {
|
|
const consoleMessages: string[] = [];
|
|
page.on("console", async (message) => {
|
|
if (message.type() !== "error") return;
|
|
const values = await Promise.all(
|
|
message.args().map((arg) => arg.jsonValue().catch(() => "")),
|
|
);
|
|
consoleMessages.push(JSON.stringify(values));
|
|
});
|
|
|
|
await page.addInitScript(() => {
|
|
window.localStorage.setItem("playwright_auth_bypass", "1");
|
|
window.localStorage.setItem("dev_tenant_id", "saman-id");
|
|
});
|
|
|
|
await page.route("**/api/v1/admin/orgchart/snapshot**", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
status: 503,
|
|
body: JSON.stringify({
|
|
code: "service_unavailable",
|
|
error: "tenant root traversal failed",
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto("/chart");
|
|
|
|
await expect(
|
|
page.getByText(
|
|
"조직도를 불러올 수 없습니다. 로그인 상태와 조직 권한을 확인해 주세요.",
|
|
),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByText("조직도를 불러올 수 없거나 만료된 링크입니다."),
|
|
).toHaveCount(0);
|
|
await expect
|
|
.poll(() =>
|
|
consoleMessages.some(
|
|
(message) =>
|
|
message.includes("[orgfront] Org chart load failed") &&
|
|
message.includes("service_unavailable") &&
|
|
message.includes("saman-id") &&
|
|
message.includes("/v1/admin/orgchart/snapshot"),
|
|
),
|
|
)
|
|
.toBe(true);
|
|
});
|
|
|
|
test("org chart places GPDTDC representative users on visible leaf appointments", async ({
|
|
page,
|
|
}) => {
|
|
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
sharedWith: "Playwright",
|
|
tenants: [
|
|
tenant("group", "HMAC Group", "hmac"),
|
|
tenant("gpdtdc", "GPDTDC", "gpdtdc", "group", "COMPANY"),
|
|
tenant("tdc", "기술개발센터", "tdc", "gpdtdc", "ORGANIZATION"),
|
|
tenant("tdc-leaf", "기술개발센터 1팀", "tdc-leaf", "tdc"),
|
|
tenant(
|
|
"internal-planning",
|
|
"내부 구성 조직",
|
|
"internal-planning",
|
|
"group",
|
|
"ORGANIZATION",
|
|
{ visibility: "internal" },
|
|
),
|
|
tenant(
|
|
"internal-leaf",
|
|
"내부 구성 하위 조직",
|
|
"internal-leaf",
|
|
"gpdtdc",
|
|
"USER_GROUP",
|
|
{ visibility: "internal" },
|
|
),
|
|
],
|
|
users: [
|
|
{
|
|
...user("u-gpdtdc-leaf", "GPDTDC Leaf User", "gpdtdc"),
|
|
tenantSlug: "gpdtdc",
|
|
companyCode: undefined,
|
|
metadata: {
|
|
additionalAppointments: [
|
|
{
|
|
tenantSlug: "internal-planning",
|
|
isPrimary: true,
|
|
},
|
|
{
|
|
tenantSlug: "tdc-leaf",
|
|
isPrimary: false,
|
|
grade: "책임",
|
|
position: "팀장",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
...user("u-hidden-only", "Hidden Only User", "gpdtdc"),
|
|
tenantSlug: "gpdtdc",
|
|
companyCode: undefined,
|
|
metadata: {
|
|
additionalAppointments: [
|
|
{
|
|
tenantSlug: "internal-leaf",
|
|
isPrimary: true,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto("/chart?token=gpdtdc-leaf");
|
|
|
|
await expect(page.getByText("총 1명")).toBeVisible();
|
|
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
|
await expect(svg).toBeVisible();
|
|
await expect(svg.getByText("내부 구성 조직")).toHaveCount(0);
|
|
await expect(svg.getByText("내부 구성 하위 조직")).toHaveCount(0);
|
|
await expect(svg.getByText(/Hidden Only User/)).toHaveCount(0);
|
|
await expect(
|
|
page
|
|
.locator('[data-testid="orgchart-node-gpdtdc"]')
|
|
.getByText(/GPDTDC Leaf User/),
|
|
).toHaveCount(0);
|
|
await expect(
|
|
page
|
|
.locator('[data-testid="orgchart-node-tdc-leaf"]')
|
|
.getByText("GPDTDC Leaf User 책임"),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("org chart counts multi-leaf tenant users once in ancestor totals", async ({
|
|
page,
|
|
}) => {
|
|
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
|
const tenants = [
|
|
tenant("group", "HMAC Group", "hmac"),
|
|
tenant("baron", "Baron", "baron", "group", "COMPANY"),
|
|
tenant("engineering", "Engineering", "engineering", "baron"),
|
|
tenant("platform", "Platform", "platform", "engineering"),
|
|
tenant("security", "Security", "security", "engineering"),
|
|
];
|
|
const [groupTenant, baronTenant, engineeringTenant, platformTenant] =
|
|
tenants;
|
|
const securityTenant = tenants[4];
|
|
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
sharedWith: "Playwright",
|
|
tenants,
|
|
users: [
|
|
multiTenantUser("u-shared", "Shared User", "baron", [
|
|
groupTenant,
|
|
baronTenant,
|
|
engineeringTenant,
|
|
platformTenant,
|
|
securityTenant,
|
|
]),
|
|
],
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto("/chart?token=multi-leaf-count");
|
|
|
|
await expect(page.getByText("총 1명")).toBeVisible();
|
|
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
|
await expect(svg).toBeVisible();
|
|
await expect(svg.getByText(/Shared User/)).toHaveCount(2);
|
|
await expect(svg.getByText(/^1$/)).toHaveCount(5);
|
|
});
|
|
|
|
test("org chart hides system global tenant members", async ({ page }) => {
|
|
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
sharedWith: "Playwright",
|
|
tenants: [
|
|
tenant("system", "시스템 전역", "system", undefined, "SYSTEM"),
|
|
tenant("group", "HMAC Group", "hmac"),
|
|
tenant("baron", "Baron", "baron", "group", "COMPANY"),
|
|
],
|
|
users: [
|
|
user("u-global", "Global Admin", "system"),
|
|
{
|
|
...multiTenantUser("u-system-admin", "System Admin", "system", [
|
|
tenant("baron", "Baron", "baron", "group", "COMPANY"),
|
|
]),
|
|
role: "super_admin",
|
|
},
|
|
user("u-baron", "Baron User", "baron"),
|
|
],
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto("/chart?token=hide-system-global");
|
|
|
|
await expect(page.getByText("총 1명")).toBeVisible();
|
|
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
|
await expect(svg).toBeVisible();
|
|
await expect(svg.getByText(/시스템 전역/)).toHaveCount(0);
|
|
await expect(svg.getByText(/Global Admin/)).toHaveCount(0);
|
|
await expect(svg.getByText(/System Admin/)).toHaveCount(0);
|
|
await expect(svg.getByText("Baron User 사원")).toBeVisible();
|
|
});
|