forked from baron/baron-sso
userfront e2e 전체 테스트
This commit is contained in:
@@ -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(
|
||||
[
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user