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, ) { 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>, ) { 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(); });