import { expect, test } from "@playwright/test"; function tenant( id: string, name: string, slug: string, parentId?: string, type?: string, ) { return { id, type: type ?? (parentId ? "USER_GROUP" : "COMPANY_GROUP"), name, slug, description: "", status: "active", parentId, 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, position: "사원", 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.locator("text", { hasText: "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.locator("text", { hasText: "Hidden Hanmac User" }), ).toHaveCount(0); await expect( svg.locator("text", { hasText: "Engineering User" }), ).toBeVisible(); await expect(svg.locator("text", { hasText: "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.locator("text", { hasText: "Engineering User" }), ).toBeVisible(); await expect(svg.locator("text", { hasText: "Sales User" })).toHaveCount(0); }); test("org chart displays user names with job title and position", 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", position: "책임", }, ], }), }); }); await page.goto("/chart?token=display-name"); const svg = page.locator('[data-testid="orgchart-vector-svg"]'); await expect( svg.locator("text", { hasText: "Engineering User(Platform Engineer) 책임", }), ).toBeVisible(); }); 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, }; window.localStorage.setItem( "oidc.user:http://localhost:5000/oidc:orgfront", JSON.stringify(mockOidcUser), ); window.localStorage.setItem( "oidc.user:http://localhost:5000/oidc/:orgfront", JSON.stringify(mockOidcUser), ); window.localStorage.setItem( "oidc.user:http://localhost:5000/oidc:devfront", JSON.stringify(mockOidcUser), ); window.localStorage.setItem( "oidc.user:http://localhost:5000/oidc/:devfront", JSON.stringify(mockOidcUser), ); window.localStorage.setItem( "oidc.user:http://172.16.9.189:5000/oidc:orgfront", JSON.stringify(mockOidcUser), ); window.localStorage.setItem( "oidc.user:http://172.16.9.189:5000/oidc/:orgfront", JSON.stringify(mockOidcUser), ); window.localStorage.setItem("dev_tenant_id", "group"); }, { issuedAt }, ); await page.route("**/oidc/**", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ keys: [] }), }); }); 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/tenants**", async (route) => { await route.fulfill({ contentType: "application/json", body: JSON.stringify({ items: tenants, total: tenants.length, limit: 10000, offset: 0, }), }); }); await page.route("**/api/v1/admin/users**", async (route) => { await route.fulfill({ contentType: "application/json", body: JSON.stringify({ items: [ multiTenantUser("u-shared", "Shared User", "baron", [ groupTenant, baronTenant, engineeringTenant, platformTenant, ]), ], total: 1, limit: 5000, offset: 0, }), }); }); 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.locator("text", { hasText: "Shared User" })).toHaveCount(1); await expect(svg.locator("text").filter({ hasText: /^1$/ })).toHaveCount(4); }); 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.locator("text", { hasText: "Shared User" })).toHaveCount(2); await expect(svg.locator("text").filter({ hasText: /^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.locator("text", { hasText: "시스템 전역" })).toHaveCount(0); await expect(svg.locator("text", { hasText: "Global Admin" })).toHaveCount(0); await expect(svg.locator("text", { hasText: "System Admin" })).toHaveCount(0); await expect(svg.locator("text", { hasText: "Baron User" })).toBeVisible(); });