1
0
forked from baron/baron-sso
Files
baron-sso/orgfront/tests/orgchart-vector-render.spec.ts

630 lines
20 KiB
TypeScript

import { expect, test } from "@playwright/test";
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,
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 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/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.getByText(/Shared User/)).toHaveCount(1);
await expect(svg.getByText(/^1$/)).toHaveCount(4);
});
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();
});