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

738 lines
23 KiB
TypeScript

import { expect, test } from "@playwright/test";
const shareToken = "playwright";
function withShareToken(path: string) {
return path.includes("?")
? `${path}&token=${shareToken}`
: `${path}?token=${shareToken}`;
}
type TenantFixture = {
id: string;
type: string;
name: string;
slug: string;
description: string;
status: string;
parentId?: string;
memberCount: number;
createdAt: string;
updatedAt: string;
};
function tenant(
id: string,
type: string,
name: string,
slug: string,
parentId?: string,
): TenantFixture {
return {
id,
type,
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,
tenantSlug: string,
overrides: Record<string, unknown> = {},
) {
return {
id,
email: `${id}@example.com`,
name,
role: "user",
status: "active",
tenantSlug,
companyCode: tenantSlug,
grade: "사원",
createdAt: "2026-04-01T00:00:00.000Z",
updatedAt: "2026-04-01T00:00:00.000Z",
...overrides,
};
}
async function seedOrgfrontAuth(page: Parameters<typeof test>[0]["page"]) {
const nowInSeconds = Math.floor(Date.now() / 1000);
await page.addInitScript(
({ issuedAt }) => {
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: issuedAt + 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-hmac");
},
{ issuedAt: nowInSeconds },
);
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": "*" },
});
});
}
async function installOrgPickerApiMock(
page: Parameters<typeof test>[0]["page"],
) {
const tenants = [
tenant("group-hmac", "COMPANY_GROUP", "HMAC Group", "hmac"),
tenant("company-baron", "COMPANY", "Baron", "baron", "group-hmac"),
tenant("company-hanmac", "COMPANY", "Hanmac", "hanmac", "group-hmac"),
tenant("dept-center", "USER_GROUP", "센터", "center", "company-baron"),
tenant(
"team-tech-plan",
"USER_GROUP",
"기술기획",
"tech-plan",
"dept-center",
),
tenant("team-bcmf", "USER_GROUP", "bCMf", "bcmf", "dept-center"),
tenant("team-pm", "USER_GROUP", "PM", "pm", "dept-center"),
tenant(
"dept-eng",
"USER_GROUP",
"Engineering",
"engineering",
"company-baron",
),
tenant("team-platform", "USER_GROUP", "Platform", "platform", "dept-eng"),
tenant("dept-sales", "USER_GROUP", "Sales", "sales", "company-hanmac"),
];
const users = [
user("user-root", "Group User", "hmac"),
user("user-baron", "Baron User", "baron"),
user("user-eng", "Engineering User", "engineering"),
user("user-platform", "Platform User", "platform", {
metadata: { employeeNumber: "EMP-9001", skill: "Kubernetes" },
jobTitle: "Platform Engineer",
grade: "책임",
position: "팀장",
}),
user("user-sales", "Sales User", "sales"),
];
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: users,
total: users.length,
limit: 5000,
offset: 0,
}),
});
});
}
test.beforeEach(async ({ page }) => {
await seedOrgfrontAuth(page);
await installOrgPickerApiMock(page);
});
test("developer navigation exposes chart, picker, and embed preview", async ({
page,
}) => {
await page.goto(withShareToken("/chart"));
await expect(page.getByRole("link", { name: "조직도" })).toBeVisible();
await expect(page.getByRole("link", { name: "조직 선택기" })).toBeVisible();
await expect(page.getByRole("link", { name: "임베딩 검증" })).toBeVisible();
await page.getByRole("link", { name: "임베딩 검증" }).click();
await expect(
page.getByRole("heading", { name: "임베딩 검증" }),
).toBeVisible();
await expect(
page
.frameLocator("iframe")
.getByTestId("org-picker-search-section")
.getByText("하위 선택"),
).toBeVisible();
});
test("picker menu lets developers switch selection mode and selectable type", async ({
page,
}) => {
await page.goto(withShareToken("/picker"));
await expect(page.getByLabel("선택 모드")).toHaveValue("multiple");
await expect(page.getByLabel("선택 대상")).toHaveValue("both");
await page.getByLabel("선택 모드").selectOption("single");
await expect(page.frameLocator("iframe").getByText("하위 선택")).toHaveCount(
0,
);
await page.getByLabel("선택 대상").selectOption("tenant");
const picker = page.frameLocator("iframe");
await expect(
picker.getByRole("button", { name: "Engineering User" }),
).toHaveCount(0);
await expect(
picker.getByRole("button", { name: "Engineering", exact: true }),
).toBeVisible();
});
test("picker defaults to the hanmac-family company-group when no tenant id is supplied", async ({
page,
}) => {
await page.unroute("**/api/v1/admin/tenants**");
await page.unroute("**/api/v1/admin/users**");
const tenants = [
tenant("wrong-group", "COMPANY_GROUP", "Wrong Group", "wrong-group"),
tenant(
"wrong-company",
"COMPANY",
"Wrong Company",
"wrong-company",
"wrong-group",
),
tenant("hanmac-family-id", "COMPANY_GROUP", "한맥가족", "hanmac-family"),
tenant("saman-id", "COMPANY", "삼안", "saman", "hanmac-family-id"),
];
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: [],
total: 0,
limit: 5000,
offset: 0,
}),
});
});
await page.goto(withShareToken("/picker"));
const picker = page.frameLocator("iframe");
await expect(picker.getByText("한맥가족", { exact: true })).toBeVisible();
await expect(picker.getByText("삼안", { exact: true })).toBeVisible();
await expect(picker.getByText("Wrong Group", { exact: true })).toHaveCount(0);
});
test("embed preview picker orders hanmac-family tenants by the shared policy", async ({
page,
}) => {
await page.unroute("**/api/v1/admin/tenants**");
await page.unroute("**/api/v1/admin/users**");
const tenants = [
tenant("hanmac-family-id", "COMPANY_GROUP", "한맥가족", "hanmac-family"),
tenant(
"baron-group-id",
"COMPANY_GROUP",
"바론그룹",
"baron-group",
"hanmac-family-id",
),
tenant("hanmac-id", "COMPANY", "한맥기술", "hanmac", "hanmac-family-id"),
tenant("saman-id", "COMPANY", "삼안", "saman", "hanmac-family-id"),
tenant(
"gpdtdc-id",
"ORGANIZATION",
"총괄기획&기술개발센터",
"gpdtdc",
"hanmac-family-id",
),
];
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: [],
total: 0,
limit: 5000,
offset: 0,
}),
});
});
await page.goto(withShareToken("/embed-preview?select=tenant"));
await expect(
page.frameLocator("iframe").getByTestId("org-picker-node-name-tenant"),
).toHaveText([
"한맥가족",
"총괄기획&기술개발센터",
"삼안",
"한맥기술",
"바론그룹",
]);
});
test("picker displays user names with grade and optional position", async ({
page,
}) => {
await page.goto(withShareToken("/embed/picker?mode=single&select=user"));
await expect(
page.getByRole("button", {
name: "Platform User 책임",
}),
).toBeVisible();
});
test("embed preview menu updates the iframe picker source", async ({
page,
}) => {
await page.goto(withShareToken("/embed-preview"));
await expect(page.getByLabel("선택 모드")).toHaveValue("multiple");
await expect(page.getByLabel("선택 대상")).toHaveValue("both");
await expect(page.getByTestId("embed-preview-src")).toContainText(
"mode=multiple",
);
await expect(page.getByTestId("embed-preview-src")).toContainText(
"select=both",
);
await expect(page.getByTestId("embed-preview-src")).toContainText(
"includeDescendants=true",
);
await expect(page.getByTestId("embed-preview-src")).toContainText(
"showDescendantToggle=true",
);
await expect(page.getByTestId("embed-preview-src")).toContainText(
"width=400",
);
await expect(page.getByTestId("embed-preview-src")).toContainText(
"height=600",
);
await expect(page.getByTestId("embed-preview-frame-shell")).toHaveCSS(
"width",
"400px",
);
await page.getByLabel("선택 모드").selectOption("single");
await page.getByLabel("선택 대상").selectOption("user");
await expect(page.getByTestId("embed-preview-src")).toContainText(
"mode=single",
);
await expect(page.getByTestId("embed-preview-src")).toContainText(
"select=user",
);
await expect(page.getByTestId("embed-preview-src")).not.toContainText(
"includeDescendants",
);
await expect(page.getByTestId("embed-preview-src")).not.toContainText(
"showDescendantToggle",
);
await expect(page.frameLocator("iframe").getByText("하위 선택")).toHaveCount(
0,
);
await expect(
page.frameLocator("iframe").getByRole("button", {
name: "Engineering User 사원 user-eng@example.com",
}),
).toBeVisible();
});
test("embed preview passes tenant slug and custom dimensions through the picker url", async ({
page,
}) => {
await page.goto(withShareToken("/embed-preview"));
await page.getByLabel("tenant slug").fill("baron");
await page.getByLabel("임베딩 너비").fill("520");
await page.getByLabel("임베딩 높이").fill("480");
await expect(page.getByTestId("embed-preview-src")).toContainText(
"tenantSlug=baron",
);
await expect(page.getByTestId("embed-preview-src")).toContainText(
"width=520",
);
await expect(page.getByTestId("embed-preview-src")).toContainText(
"height=480",
);
await expect(page.getByTestId("embed-preview-frame-shell")).toHaveCSS(
"width",
"520px",
);
const picker = page.frameLocator("iframe");
await expect(picker.getByText("Engineering User")).toBeVisible();
await expect(picker.getByText("Sales User")).toHaveCount(0);
});
test("embed picker scopes the tree by tenant slug, hides users for tenant selection, and keeps direct members before child tenants", async ({
page,
}) => {
await page.goto(
withShareToken("/embed-preview?tenantSlug=baron&select=tenant"),
);
await expect(page.getByLabel("tenant slug")).toHaveValue("baron");
await expect(page.getByTestId("embed-preview-src")).toContainText(
"tenantSlug=baron",
);
const picker = page.frameLocator("iframe");
await expect(picker.getByText("Baron", { exact: true })).toBeVisible();
await expect(picker.getByText("Hanmac", { exact: true })).toHaveCount(0);
await expect(picker.getByText("Sales User")).toHaveCount(0);
await expect(picker.getByText("Baron User")).toHaveCount(0);
await page.getByLabel("선택 대상").selectOption("both");
await expect(picker.getByText("Baron User")).toBeVisible();
const memberBox = await picker.getByText("Baron User").boundingBox();
const childTenantBox = await picker
.getByText("센터", { exact: true })
.boundingBox();
expect(memberBox).not.toBeNull();
expect(childTenantBox).not.toBeNull();
expect(memberBox?.y ?? 0).toBeLessThan(childTenantBox?.y ?? 0);
});
test("embed picker keeps the lightweight search controls inside the picker section at the default embed width", async ({
page,
}) => {
await page.goto(withShareToken("/embed-preview"));
const picker = page.frameLocator("iframe");
const searchSection = picker.getByTestId("org-picker-search-section");
await expect(searchSection).toBeVisible();
await expect(searchSection.getByLabel("company 필터")).toHaveCount(0);
await expect(searchSection.getByText("선택 결과")).toHaveCount(0);
const searchBox = await searchSection
.getByLabel("조직/구성원 검색")
.boundingBox();
const descendantToggle = await searchSection
.getByTestId("org-picker-descendant-toggle")
.boundingBox();
const sectionBox = await searchSection.boundingBox();
expect(searchBox).not.toBeNull();
expect(descendantToggle).not.toBeNull();
expect(sectionBox).not.toBeNull();
expect(
Math.abs((searchBox?.y ?? 0) - (descendantToggle?.y ?? 0)),
).toBeLessThanOrEqual(8);
expect(sectionBox?.height ?? 0).toBeLessThanOrEqual(72);
});
test("embed picker keeps only the lightweight picker surface scrollable", async ({
page,
}) => {
await page.goto(withShareToken("/embed-preview"));
const picker = page.frameLocator("iframe");
await expect(
picker.getByRole("heading", { name: "조직 선택기" }),
).toHaveCount(0);
await expect(picker.getByTestId("org-picker-search-section")).toBeVisible();
await expect(picker.getByTestId("org-picker-tree-scroll")).toBeVisible();
await expect
.poll(async () =>
picker.locator("body").evaluate((element) => {
const style = window.getComputedStyle(element);
return `${style.overflowX}/${style.overflowY}`;
}),
)
.toBe("hidden/hidden");
await expect
.poll(async () =>
picker
.getByTestId("org-picker-tree-scroll")
.evaluate((element) => window.getComputedStyle(element).overflowY),
)
.toBe("auto");
const rootRowHeight = await picker
.getByRole("button", { name: "HMAC Group 접기" })
.locator("xpath=..")
.evaluate((element) => element.getBoundingClientRect().height);
expect(rootRowHeight).toBeLessThanOrEqual(30);
});
test("embed preview can hide the descendant selection switch", async ({
page,
}) => {
await page.goto(withShareToken("/embed-preview?mode=multiple&select=both"));
await expect(page.getByLabel("하위 선택 스위치 표시")).toBeChecked();
await page.getByLabel("하위 선택 스위치 표시").uncheck();
await expect(page.getByTestId("embed-preview-src")).toContainText(
"includeDescendants=true",
);
await expect(page.getByTestId("embed-preview-src")).toContainText(
"showDescendantToggle=false",
);
await expect(page.frameLocator("iframe").getByText("하위 선택")).toHaveCount(
0,
);
});
test("embed picker renders compact tree rows with member emails", async ({
page,
}) => {
await page.goto(withShareToken("/embed-preview?mode=single&select=user"));
const picker = page.frameLocator("iframe");
await expect(picker.getByText("user-eng@example.com")).toBeVisible();
await expect(
picker.getByRole("button", { name: "Engineering User 구성원" }),
).toHaveCount(0);
await expect(
picker.getByRole("button", {
name: "Engineering User 사원 user-eng@example.com",
}),
).toBeVisible();
});
test("embed picker filters organizations and users by id, name, and metadata", async ({
page,
}) => {
await page.goto(withShareToken("/embed-preview?mode=multiple&select=both"));
const picker = page.frameLocator("iframe");
const search = picker.getByLabel("조직/구성원 검색");
await expect(picker.getByTestId("org-picker-search-section")).toBeVisible();
await expect(search).toBeVisible();
await search.fill("user-platform");
await expect(picker.getByText("Platform User")).toBeVisible();
await expect(picker.getByText("Sales User")).toHaveCount(0);
await search.fill("EMP-9001");
await expect(picker.getByText("Platform User")).toBeVisible();
await expect(picker.getByText("Engineering User")).toHaveCount(0);
await search.fill("Sales");
await expect(picker.getByText("Sales", { exact: true })).toBeVisible();
await expect(picker.getByText("Sales User")).toBeVisible();
await expect(picker.getByText("Platform User")).toHaveCount(0);
});
test("embed picker search does not keep unmatched descendants under a matching organization", async ({
page,
}) => {
await page.goto(withShareToken("/embed-preview?mode=multiple&select=both"));
const picker = page.frameLocator("iframe");
await picker.getByLabel("조직/구성원 검색").fill("센");
await expect(picker.getByText("센터", { exact: true })).toBeVisible();
await expect(picker.getByText("기술기획", { exact: true })).toHaveCount(0);
await expect(picker.getByText("bCMf", { exact: true })).toHaveCount(0);
await expect(picker.getByText("PM", { exact: true })).toHaveCount(0);
});
test("embed picker posts a single user selection with type, id, and name", async ({
page,
}) => {
await page.goto(withShareToken("/embed-preview?mode=single&select=user"));
const picker = page.frameLocator("iframe");
await picker
.getByRole("button", { name: "Engineering User 사원 user-eng@example.com" })
.click();
await picker.getByRole("button", { name: "선택 완료" }).click();
const output = page.getByTestId("embed-preview-output");
await expect(output).toContainText('"type": "user"');
await expect(output).toContainText('"id": "user-eng"');
await expect(output).toContainText('"name": "Engineering User 사원"');
await expect(output).not.toContainText("tenantId");
});
test("embed picker single selection counts only the selected node without descendants", async ({
page,
}) => {
await page.goto(withShareToken("/embed-preview?mode=single&select=both"));
const picker = page.frameLocator("iframe");
await picker
.getByRole("button", { name: "Engineering", exact: true })
.click();
await expect(picker.getByText("1개 항목 선택됨")).toBeVisible();
await expect(picker.getByText("3개 항목 선택됨")).toHaveCount(0);
await expect(picker.getByText("4개 항목 선택됨")).toHaveCount(0);
await picker.getByRole("button", { name: "선택 완료" }).click();
const output = page.getByTestId("embed-preview-output");
await expect(output).toContainText('"id": "dept-eng"');
await expect(output).not.toContainText('"id": "user-eng"');
await expect(output).not.toContainText('"id": "team-platform"');
await expect(output).not.toContainText('"id": "user-platform"');
});
test("embed picker highlights a single selected item without tree connectors", async ({
page,
}) => {
await page.goto(withShareToken("/embed-preview?mode=single&select=both"));
const picker = page.frameLocator("iframe");
await expect(
picker.getByRole("button", { name: "Engineering", exact: true }),
).toBeVisible();
await expect(picker.getByTestId("org-picker-tree-connector")).toHaveCount(0);
const selected = picker.getByRole("button", {
name: "Engineering",
exact: true,
});
await selected.click();
await expect(selected).toHaveAttribute("aria-pressed", "true");
await expect(selected).toHaveAttribute("data-selected", "true");
});
test("embed picker renders tenant names with the dedicated tenant text color", async ({
page,
}) => {
await page.goto(withShareToken("/embed-preview?mode=single&select=both"));
const picker = page.frameLocator("iframe");
const tenantName = picker.getByTestId("org-picker-node-name-tenant").first();
await expect(tenantName).toBeVisible();
await expect
.poll(() =>
tenantName.evaluate((element) => window.getComputedStyle(element).color),
)
.toBe("rgb(10, 33, 20)");
});
test("embed picker includes descendants by default and can disable descendant inclusion", async ({
page,
}) => {
await page.goto(withShareToken("/embed-preview?mode=multiple&select=both"));
let picker = page.frameLocator("iframe");
await expect(
picker.getByTestId("org-picker-search-section").getByText("하위 선택"),
).toBeVisible();
await picker.getByLabel("Engineering 선택").check();
await expect(picker.getByLabel("Platform 선택")).toBeChecked();
await expect(picker.getByLabel("Platform User 책임 선택")).toBeChecked();
await picker.getByRole("button", { name: "선택 완료" }).click();
let output = page.getByTestId("embed-preview-output");
await expect(output).toContainText('"id": "dept-eng"');
await expect(output).toContainText('"id": "team-platform"');
await expect(output).toContainText('"id": "user-platform"');
await page.goto(
withShareToken(
"/embed-preview?mode=multiple&select=both&includeDescendants=false",
),
);
picker = page.frameLocator("iframe");
await picker.getByLabel("Engineering 선택").check();
await expect(picker.getByLabel("Platform 선택")).not.toBeChecked();
await expect(picker.getByLabel("Platform User 책임 선택")).not.toBeChecked();
await picker.getByRole("button", { name: "선택 완료" }).click();
output = page.getByTestId("embed-preview-output");
await expect(output).toContainText('"id": "dept-eng"');
await expect(output).not.toContainText('"id": "team-platform"');
await expect(output).not.toContainText('"id": "user-platform"');
});