forked from baron/baron-sso
833 lines
27 KiB
TypeScript
833 lines
27 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"),
|
|
];
|
|
const orgChartSnapshot = {
|
|
generatedAt: "2026-06-17T07:10:11Z",
|
|
tenants,
|
|
users,
|
|
};
|
|
|
|
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,
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.route("**/api/v1/admin/orgchart/snapshot**", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify(orgChartSnapshot),
|
|
});
|
|
});
|
|
|
|
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ ...orgChartSnapshot, sharedWith: "playwright" }),
|
|
});
|
|
});
|
|
}
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await seedOrgfrontAuth(page);
|
|
await installOrgPickerApiMock(page);
|
|
});
|
|
|
|
test("developer navigation exposes chart, picker, and embed preview", async ({
|
|
page,
|
|
}) => {
|
|
await page.setViewportSize({ width: 1600, height: 900 });
|
|
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 expect(page.getByTestId("orgchart-render-status-panel")).toHaveText(
|
|
"2026-06-17 16:10:11 KST",
|
|
);
|
|
let statusPanelBox = await page
|
|
.getByTestId("orgchart-render-status-panel")
|
|
.boundingBox();
|
|
expect(
|
|
(page.viewportSize()?.width ?? 1600) -
|
|
((statusPanelBox?.x ?? 0) + (statusPanelBox?.width ?? 0)),
|
|
).toBeLessThanOrEqual(20);
|
|
|
|
await page.getByRole("link", { name: "조직 선택기" }).click();
|
|
await expect(page.getByTestId("orgchart-render-status-panel")).toHaveText(
|
|
"2026-06-17 16:10:11 KST",
|
|
);
|
|
statusPanelBox = await page
|
|
.getByTestId("orgchart-render-status-panel")
|
|
.boundingBox();
|
|
expect(
|
|
(page.viewportSize()?.width ?? 1600) -
|
|
((statusPanelBox?.x ?? 0) + (statusPanelBox?.width ?? 0)),
|
|
).toBeLessThanOrEqual(20);
|
|
|
|
await page.getByRole("link", { name: "임베딩 검증" }).click();
|
|
await expect(
|
|
page.getByRole("heading", { name: "임베딩 검증" }),
|
|
).toBeVisible();
|
|
await expect(page.getByTestId("orgchart-render-status-panel")).toHaveText(
|
|
"2026-06-17 16:10:11 KST",
|
|
);
|
|
statusPanelBox = await page
|
|
.getByTestId("orgchart-render-status-panel")
|
|
.boundingBox();
|
|
expect(
|
|
(page.viewportSize()?.width ?? 1600) -
|
|
((statusPanelBox?.x ?? 0) + (statusPanelBox?.width ?? 0)),
|
|
).toBeLessThanOrEqual(20);
|
|
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.route("**/api/v1/admin/orgchart/snapshot**", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ tenants, users: [] }),
|
|
});
|
|
});
|
|
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ tenants, users: [], sharedWith: "playwright" }),
|
|
});
|
|
});
|
|
|
|
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.route("**/api/v1/admin/orgchart/snapshot**", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ tenants, users: [] }),
|
|
});
|
|
});
|
|
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ tenants, users: [], sharedWith: "playwright" }),
|
|
});
|
|
});
|
|
|
|
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 lets user-only multi selection check tenants but posts descendant users only", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(withShareToken("/embed-preview?mode=multiple&select=user"));
|
|
|
|
const picker = page.frameLocator("iframe");
|
|
await picker.getByLabel("Engineering 선택", { exact: true }).check();
|
|
await expect(
|
|
picker.getByLabel("Platform User 책임 선택", { exact: true }),
|
|
).toBeChecked();
|
|
await picker.getByRole("button", { name: "선택 완료" }).click();
|
|
|
|
const output = page.getByTestId("embed-preview-output");
|
|
await expect(output).toContainText('"id": "user-eng"');
|
|
await expect(output).toContainText('"id": "user-platform"');
|
|
await expect(output).not.toContainText('"id": "dept-eng"');
|
|
await expect(output).not.toContainText('"id": "team-platform"');
|
|
});
|
|
|
|
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"');
|
|
});
|