1
0
forked from baron/baron-sso

userfront e2e 전체 테스트

This commit is contained in:
2026-05-29 08:19:34 +09:00
parent dc16958804
commit da01f63c54
22 changed files with 1439 additions and 103 deletions

View File

@@ -4,6 +4,8 @@ import {
buildOrgSelectionOptions,
buildUsersMap,
clampScale,
filterSystemGlobalTenants,
getMemberGridMetrics,
getOrgNodeHeaderFill,
getSemanticZoomMode,
layoutForest,
@@ -83,8 +85,8 @@ describe("org chart layout", () => {
expect(new Set(childNodes.map((node) => node.y)).size).toBe(1);
});
it("uses member columns in node bounds when member count exceeds five", () => {
const compactMembers = Array.from({ length: 6 }, (_, index) =>
it("uses member columns in node bounds when the rendered node aspect ratio needs them", () => {
const compactMembers = Array.from({ length: 10 }, (_, index) =>
member(`member-${index + 1}`),
);
const node = {
@@ -98,11 +100,11 @@ describe("org chart layout", () => {
expect(rootNode).toBeDefined();
expect(rootNode?.width).toBeGreaterThan(340);
expect(rootNode?.height).toBeLessThan(42 + 24 + 6 * 24);
expect(rootNode?.height).toBeLessThan(42 + 24 + 10 * 24);
expect(layout.width).toBeGreaterThan((rootNode?.width ?? 0) + 72 * 2 - 1);
});
it("adds one member column per five-member quotient", () => {
it("keeps modest member groups in one column until another column improves the rendered ratio", () => {
const tenMembers = Array.from({ length: 10 }, (_, index) =>
member(`member-${index + 1}`),
);
@@ -132,10 +134,15 @@ describe("org chart layout", () => {
const sixNode = sixLayout.nodes.find((item) => item.node.id === "six");
const tenNode = tenLayout.nodes.find((item) => item.node.id === "ten");
expect(sixNode?.width).toBeGreaterThan(340);
expect(sixNode?.width).toBe(340);
expect(tenNode?.width).toBeGreaterThan(sixNode?.width ?? 0);
expect(tenNode?.height).toBeLessThan(42 + 24 + 10 * 24);
expect(tenLayout.width).toBeGreaterThan(sixLayout.width);
});
it("chooses member columns from the rendered node aspect ratio instead of fixed five-member buckets", () => {
expect(getMemberGridMetrics(6)).toEqual({ columnCount: 1, rowCount: 6 });
expect(getMemberGridMetrics(10)).toEqual({ columnCount: 2, rowCount: 5 });
expect(getMemberGridMetrics(25)).toEqual({ columnCount: 2, rowCount: 13 });
});
it("uses multi-column layout by default when sibling width crosses the threshold", () => {
@@ -388,6 +395,40 @@ describe("org chart layout", () => {
).toEqual(["총괄기획&기술개발센터", "삼안", "한맥기술", "바론그룹"]);
});
it("always hides internal and private organizations from the organization status chart", () => {
const visibleParent = tenantNode(
"visible-parent",
"COMPANY",
"공개 회사",
"visible-parent",
);
const internalOrg = {
...tenantNode("internal-org", "ORGANIZATION", "내부 조직", "internal-org"),
parentId: "visible-parent",
config: { visibility: "internal" },
};
const internalChild = {
...tenantNode("internal-child", "ORGANIZATION", "내부 하위", "internal-child"),
parentId: "internal-org",
};
const privateOrg = {
...tenantNode("private-org", "ORGANIZATION", "비공개 조직", "private-org"),
parentId: "visible-parent",
config: { visibility: "private" },
};
const publicOrg = {
...tenantNode("public-org", "ORGANIZATION", "공개 조직", "public-org"),
parentId: "visible-parent",
};
expect(
filterSystemGlobalTenants(
[visibleParent, internalOrg, internalChild, privateOrg, publicOrg],
"internal",
).map((tenant) => tenant.id),
).toEqual(["visible-parent", "public-org"]);
});
it("maps legacy companyCode users to matching tenant slugs", () => {
const usersMap = buildUsersMap(
[

View File

@@ -90,6 +90,8 @@ const MEMBER_COLUMN_GAP = 8;
const HEADER_HEIGHT = 42;
const MEMBER_ROW_HEIGHT = 24;
const NODE_PADDING_Y = 12;
const MEMBER_GRID_TARGET_ASPECT_RATIO = 2;
const MAX_MEMBER_COLUMN_COUNT = 8;
const ROOT_GAP_X = 120;
const CHILD_GAP_Y = 96;
const SIBLING_GAP_X = 80;
@@ -155,15 +157,52 @@ function getRankWeight(
return (isLeader ? -100 : 0) + (order === -1 ? 99 : order);
}
export function getMemberGridMetrics(memberCount: number) {
if (memberCount <= 0) return { columnCount: 1, rowCount: 1 };
const maxColumnCount = Math.min(
MAX_MEMBER_COLUMN_COUNT,
Math.max(1, memberCount),
);
let best = { columnCount: 1, rowCount: memberCount };
let bestScore = Number.POSITIVE_INFINITY;
for (let columnCount = 1; columnCount <= maxColumnCount; columnCount += 1) {
const rowCount = Math.ceil(memberCount / columnCount);
const width =
columnCount <= 1
? NODE_WIDTH
: Math.max(
NODE_WIDTH,
NODE_PADDING_Y * 2 +
columnCount * MEMBER_COLUMN_WIDTH +
(columnCount - 1) * MEMBER_COLUMN_GAP,
);
const height =
HEADER_HEIGHT + NODE_PADDING_Y * 2 + rowCount * MEMBER_ROW_HEIGHT;
const aspectRatio = width / height;
const score = Math.abs(
Math.log(aspectRatio / MEMBER_GRID_TARGET_ASPECT_RATIO),
);
if (
score < bestScore ||
(score === bestScore && rowCount < best.rowCount)
) {
best = { columnCount, rowCount };
bestScore = score;
}
}
return best;
}
function getMemberColumnCount(memberCount: number) {
return memberCount > 5 ? Math.floor(memberCount / 5) + 1 : 1;
return getMemberGridMetrics(memberCount).columnCount;
}
function getMemberRowCount(memberCount: number) {
return Math.max(
1,
Math.ceil(memberCount / getMemberColumnCount(memberCount)),
);
return getMemberGridMetrics(memberCount).rowCount;
}
function getNodeWidth(members: UserSummary[]) {
@@ -1048,9 +1087,9 @@ function getOrgSelectionLabel(
?.name;
}
function filterSystemGlobalTenants(
export function filterSystemGlobalTenants(
tenants: TenantSummary[],
visibilityMode: "internal" | "public" = "internal",
_visibilityMode: "internal" | "public" = "public",
) {
const excludedIds = new Set(
tenants.filter(isSystemGlobalTenant).map((tenant) => tenant.id),
@@ -1074,7 +1113,7 @@ function filterSystemGlobalTenants(
const filtered = tenants.filter(
(tenant) => !excludedIds.has(tenant.id) && isOrgFrontTenantType(tenant),
);
return filterTenantsByVisibility(filtered, visibilityMode);
return filterTenantsByVisibility(filtered, "public");
}
type TenantIndexes = {

View File

@@ -6,6 +6,7 @@ function tenant(
slug: string,
parentId?: string,
type?: string,
config?: Record<string, unknown>,
) {
return {
id,
@@ -15,6 +16,7 @@ function tenant(
description: "",
status: "active",
parentId,
config,
memberCount: 1,
createdAt: "2026-04-01T00:00:00.000Z",
updatedAt: "2026-04-01T00:00:00.000Z",
@@ -151,6 +153,83 @@ test("org chart filters by Hanmac family and company while excluding hanmac.kr a
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 grade and optional position", async ({
page,
}) => {