forked from baron/baron-sso
chore: consolidate local integration changes
This commit is contained in:
@@ -51,14 +51,17 @@ ensure_frontend_dependencies() {
|
||||
if [ -n "$WORKSPACE_ROOT" ]; then
|
||||
WORKSPACE_DIR="$WORKSPACE_ROOT"
|
||||
LOCK_FILE="$WORKSPACE_ROOT/pnpm-lock.yaml"
|
||||
COMMON_PACKAGE_FILE="$WORKSPACE_ROOT/common/package.json"
|
||||
INSTALL_CMD="cd $WORKSPACE_ROOT && CI=true pnpm install --filter ${APP_PACKAGE_NAME}... --frozen-lockfile --ignore-scripts"
|
||||
elif [ -f "pnpm-lock.yaml" ]; then
|
||||
WORKSPACE_DIR="."
|
||||
LOCK_FILE="pnpm-lock.yaml"
|
||||
COMMON_PACKAGE_FILE="/workspace/common/package.json"
|
||||
INSTALL_CMD="CI=true pnpm install --frozen-lockfile --ignore-scripts"
|
||||
else
|
||||
WORKSPACE_DIR="."
|
||||
LOCK_FILE="package-lock.json"
|
||||
COMMON_PACKAGE_FILE="/workspace/common/package.json"
|
||||
INSTALL_CMD="npm ci"
|
||||
fi
|
||||
|
||||
@@ -100,9 +103,9 @@ ensure_frontend_dependencies() {
|
||||
}
|
||||
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
deps_hash="$(sha256sum "$WORKSPACE_DIR/package.json" "$LOCK_FILE" package.json 2>/dev/null | sha256sum | awk '{print $1}')"
|
||||
deps_hash="$(sha256sum "$WORKSPACE_DIR/package.json" "$LOCK_FILE" "$COMMON_PACKAGE_FILE" package.json 2>/dev/null | sha256sum | awk '{print $1}')"
|
||||
else
|
||||
deps_hash="$(cksum "$WORKSPACE_DIR/package.json" "$LOCK_FILE" package.json 2>/dev/null | cksum | awk '{print $1}')"
|
||||
deps_hash="$(cksum "$WORKSPACE_DIR/package.json" "$LOCK_FILE" "$COMMON_PACKAGE_FILE" package.json 2>/dev/null | cksum | awk '{print $1}')"
|
||||
fi
|
||||
deps_stamp="node_modules/.baron-deps-hash"
|
||||
installed_hash="$(cat "$deps_stamp" 2>/dev/null || true)"
|
||||
@@ -111,9 +114,9 @@ ensure_frontend_dependencies() {
|
||||
echo "Installing frontend dependencies..."
|
||||
acquire_install_lock
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
deps_hash="$(sha256sum "$WORKSPACE_DIR/package.json" "$LOCK_FILE" package.json 2>/dev/null | sha256sum | awk '{print $1}')"
|
||||
deps_hash="$(sha256sum "$WORKSPACE_DIR/package.json" "$LOCK_FILE" "$COMMON_PACKAGE_FILE" package.json 2>/dev/null | sha256sum | awk '{print $1}')"
|
||||
else
|
||||
deps_hash="$(cksum "$WORKSPACE_DIR/package.json" "$LOCK_FILE" package.json 2>/dev/null | cksum | awk '{print $1}')"
|
||||
deps_hash="$(cksum "$WORKSPACE_DIR/package.json" "$LOCK_FILE" "$COMMON_PACKAGE_FILE" package.json 2>/dev/null | cksum | awk '{print $1}')"
|
||||
fi
|
||||
installed_hash="$(cat "$deps_stamp" 2>/dev/null || true)"
|
||||
if [ "$installed_hash" = "$deps_hash" ]; then
|
||||
|
||||
@@ -25,4 +25,18 @@ describe("org chart rank priority", () => {
|
||||
);
|
||||
expect(compareOrgRanks("부장", "차장")).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it("orders top executive ranks before the existing vice president and lower ranks", () => {
|
||||
const ranks = ["부사장", "고문", "부회장", "사장", "회장"];
|
||||
|
||||
expect(ranks.toSorted(compareOrgRanks)).toEqual([
|
||||
"회장",
|
||||
"사장",
|
||||
"부회장",
|
||||
"고문",
|
||||
"부사장",
|
||||
]);
|
||||
expect(getOrgRankWeight("부사장")).toBe(10);
|
||||
expect(getOrgRankWeight("전무")).toBe(20);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,10 @@ export type OrgRankDefinition = {
|
||||
};
|
||||
|
||||
export const ORG_RANK_DEFINITIONS: OrgRankDefinition[] = [
|
||||
{ label: "회장", weight: -10, aliases: ["회장"] },
|
||||
{ label: "사장", weight: 0, aliases: ["사장"] },
|
||||
{ label: "부회장", weight: 2, aliases: ["부회장"] },
|
||||
{ label: "고문", weight: 5, aliases: ["고문"] },
|
||||
{ label: "부사장", weight: 10, aliases: ["부사장"] },
|
||||
{ label: "전무", weight: 20, aliases: ["전무", "전무이사"] },
|
||||
{ label: "상무", weight: 30, aliases: ["상무", "상무이사"] },
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getOrgNodeHeaderFill,
|
||||
getSemanticZoomMode,
|
||||
layoutForest,
|
||||
resolveOrgChartFamilyRoot,
|
||||
type OrgNode,
|
||||
} from "./OrgChartPage";
|
||||
|
||||
@@ -466,7 +467,27 @@ describe("org chart layout", () => {
|
||||
).toEqual(["총괄기획&기술개발센터", "삼안", "한맥기술", "바론그룹"]);
|
||||
});
|
||||
|
||||
it("always hides internal and private organizations from the organization status chart", () => {
|
||||
it("selects hanmac family as the default root even when public sector group is listed first", () => {
|
||||
const publicSector = tenantNode(
|
||||
"public-sector",
|
||||
"COMPANY_GROUP",
|
||||
"공공기관",
|
||||
"public-sector",
|
||||
);
|
||||
const familyRoot = tenantNode(
|
||||
"family",
|
||||
"COMPANY_GROUP",
|
||||
"한맥가족",
|
||||
"hanmac-family",
|
||||
[tenantNode("saman", "COMPANY", "삼안", "saman")],
|
||||
);
|
||||
|
||||
expect(resolveOrgChartFamilyRoot([publicSector, familyRoot])?.id).toBe(
|
||||
"family",
|
||||
);
|
||||
});
|
||||
|
||||
it("hides internal organizations by default and includes them for internal mode", () => {
|
||||
const visibleParent = tenantNode(
|
||||
"visible-parent",
|
||||
"COMPANY",
|
||||
@@ -507,12 +528,25 @@ describe("org chart layout", () => {
|
||||
parentId: "visible-parent",
|
||||
};
|
||||
|
||||
const tenants = [
|
||||
visibleParent,
|
||||
internalOrg,
|
||||
internalChild,
|
||||
privateOrg,
|
||||
publicOrg,
|
||||
];
|
||||
|
||||
expect(
|
||||
filterSystemGlobalTenants(
|
||||
[visibleParent, internalOrg, internalChild, privateOrg, publicOrg],
|
||||
"internal",
|
||||
).map((tenant) => tenant.id),
|
||||
filterSystemGlobalTenants(tenants, "public").map((tenant) => tenant.id),
|
||||
).toEqual(["visible-parent", "public-org"]);
|
||||
expect(
|
||||
filterSystemGlobalTenants(tenants, "internal").map((tenant) => tenant.id),
|
||||
).toEqual([
|
||||
"visible-parent",
|
||||
"internal-org",
|
||||
"internal-child",
|
||||
"public-org",
|
||||
]);
|
||||
});
|
||||
|
||||
it("maps legacy companyCode users to matching tenant slugs", () => {
|
||||
|
||||
@@ -2,10 +2,10 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import type { Node as ReactFlowNode } from "@xyflow/react";
|
||||
import * as React from "react";
|
||||
import { useLocation, useParams } from "react-router-dom";
|
||||
import { Switch } from "../../../components/ui/switch";
|
||||
import {
|
||||
fetchAllTenants,
|
||||
fetchOrgChartSnapshot,
|
||||
fetchPublicOrgChart,
|
||||
fetchUsers,
|
||||
type TenantSummary,
|
||||
type UserSummary,
|
||||
} from "../../../lib/adminApi";
|
||||
@@ -1122,9 +1122,21 @@ function getOrgSelectionLabel(
|
||||
?.name;
|
||||
}
|
||||
|
||||
export function resolveOrgChartFamilyRoot(rootNodes: TenantNode[]) {
|
||||
return (
|
||||
rootNodes.find(
|
||||
(node) =>
|
||||
node.slug.toLowerCase() === "hanmac-family" || node.name === "한맥가족",
|
||||
) ??
|
||||
rootNodes.find((node) => node.type === "COMPANY_GROUP") ??
|
||||
rootNodes[0] ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
export function filterSystemGlobalTenants(
|
||||
tenants: TenantSummary[],
|
||||
_visibilityMode: "internal" | "public" = "public",
|
||||
visibilityMode: "internal" | "public" = "public",
|
||||
) {
|
||||
const excludedIds = new Set(
|
||||
tenants.filter(isSystemGlobalTenant).map((tenant) => tenant.id),
|
||||
@@ -1148,7 +1160,7 @@ export function filterSystemGlobalTenants(
|
||||
const filtered = tenants.filter(
|
||||
(tenant) => !excludedIds.has(tenant.id) && isOrgFrontTenantType(tenant),
|
||||
);
|
||||
return filterTenantsByVisibility(filtered, "public");
|
||||
return filterTenantsByVisibility(filtered, visibilityMode);
|
||||
}
|
||||
|
||||
function filterOrgChartMembershipTenants(tenants: TenantSummary[]) {
|
||||
@@ -1359,8 +1371,10 @@ export function TenantOrgChartPage() {
|
||||
const location = useLocation();
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const shareToken = searchParams.get("token");
|
||||
const visibilityMode =
|
||||
searchParams.get("includeInternal") === "true" ? "internal" : "public";
|
||||
const [includeInternalTenants, setIncludeInternalTenants] = React.useState(
|
||||
() => !shareToken && searchParams.get("includeInternal") === "true",
|
||||
);
|
||||
const visibilityMode = includeInternalTenants ? "internal" : "public";
|
||||
const [selectedTenantFilter, setSelectedTenantFilter] =
|
||||
React.useState(FAMILY_FILTER_ID);
|
||||
const [collapsedIds, setCollapsedIds] = React.useState<Set<string>>(
|
||||
@@ -1387,15 +1401,9 @@ export function TenantOrgChartPage() {
|
||||
enabled: !!shareToken,
|
||||
});
|
||||
|
||||
const tenantsQuery = useQuery({
|
||||
queryKey: ["tenants-full-tree-v2"],
|
||||
queryFn: () => fetchAllTenants(),
|
||||
enabled: !shareToken,
|
||||
});
|
||||
|
||||
const usersQuery = useQuery({
|
||||
queryKey: ["users", { limit: 5000, offset: 0 }],
|
||||
queryFn: () => fetchUsers(5000, 0),
|
||||
const orgChartSnapshotQuery = useQuery({
|
||||
queryKey: ["orgchart-snapshot", { cache: "redis" }],
|
||||
queryFn: () => fetchOrgChartSnapshot(),
|
||||
enabled: !shareToken,
|
||||
});
|
||||
|
||||
@@ -1426,7 +1434,7 @@ export function TenantOrgChartPage() {
|
||||
};
|
||||
}
|
||||
|
||||
if (!tenantsQuery.data?.items || !usersQuery.data?.items) {
|
||||
if (!orgChartSnapshotQuery.data) {
|
||||
return {
|
||||
rootNodes: [],
|
||||
usersMap: new Map<string, UserSummary[]>(),
|
||||
@@ -1435,15 +1443,15 @@ export function TenantOrgChartPage() {
|
||||
}
|
||||
|
||||
const rootNodes = buildTenantFullTree(
|
||||
filterSystemGlobalTenants(tenantsQuery.data.items, visibilityMode),
|
||||
filterSystemGlobalTenants(orgChartSnapshotQuery.data.tenants, visibilityMode),
|
||||
).subTree;
|
||||
const membershipRootNodes = buildTenantFullTree(
|
||||
filterOrgChartMembershipTenants(tenantsQuery.data.items),
|
||||
filterOrgChartMembershipTenants(orgChartSnapshotQuery.data.tenants),
|
||||
).subTree;
|
||||
|
||||
return {
|
||||
rootNodes,
|
||||
usersMap: buildUsersMap(usersQuery.data.items, rootNodes, {
|
||||
usersMap: buildUsersMap(orgChartSnapshotQuery.data.users, rootNodes, {
|
||||
activeOnly: true,
|
||||
membershipRootNodes,
|
||||
}),
|
||||
@@ -1452,17 +1460,12 @@ export function TenantOrgChartPage() {
|
||||
}, [
|
||||
publicQuery.data,
|
||||
shareToken,
|
||||
tenantsQuery.data,
|
||||
usersQuery.data,
|
||||
orgChartSnapshotQuery.data,
|
||||
visibilityMode,
|
||||
]);
|
||||
|
||||
const familyRoot = React.useMemo(() => {
|
||||
return (
|
||||
rootNodes.find((node) => node.type === "COMPANY_GROUP") ??
|
||||
rootNodes[0] ??
|
||||
null
|
||||
);
|
||||
return resolveOrgChartFamilyRoot(rootNodes);
|
||||
}, [rootNodes]);
|
||||
|
||||
const orgSelectionOptions = React.useMemo(
|
||||
@@ -1594,10 +1597,10 @@ export function TenantOrgChartPage() {
|
||||
|
||||
const isLoading = shareToken
|
||||
? publicQuery.isLoading
|
||||
: tenantsQuery.isLoading || usersQuery.isLoading;
|
||||
: orgChartSnapshotQuery.isLoading;
|
||||
const isError = shareToken
|
||||
? publicQuery.isError
|
||||
: tenantsQuery.isError || usersQuery.isError;
|
||||
: orgChartSnapshotQuery.isError;
|
||||
|
||||
const totalUsers = React.useMemo(() => {
|
||||
const ids = new Set<string>();
|
||||
@@ -1635,6 +1638,14 @@ export function TenantOrgChartPage() {
|
||||
setHasUserMovedCanvas(false);
|
||||
};
|
||||
|
||||
const updateInternalTenantVisibility = (checked: boolean) => {
|
||||
setIncludeInternalTenants(checked);
|
||||
setSelectedTenantFilter(FAMILY_FILTER_ID);
|
||||
setCollapsedIds(new Set());
|
||||
setHoveredNodeId(null);
|
||||
setHasUserMovedCanvas(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex h-full min-h-0 w-full flex-col overflow-hidden border-[#e0d5c1] bg-[#f6efe6]"
|
||||
@@ -1665,9 +1676,12 @@ export function TenantOrgChartPage() {
|
||||
testId="orgchart-layout-mode-option"
|
||||
value={childLayoutMode}
|
||||
/>
|
||||
<div className="ml-2 whitespace-nowrap rounded-full border border-[#f2c484]/30 bg-[#f2c484]/10 px-4 py-2 text-xs font-black text-[#f2c484] shadow-sm">
|
||||
총 {totalUsers}명
|
||||
</div>
|
||||
<TotalUsersControl
|
||||
canToggleInternal={!shareToken}
|
||||
includeInternal={includeInternalTenants}
|
||||
onIncludeInternalChange={updateInternalTenantVisibility}
|
||||
totalUsers={totalUsers}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -1750,6 +1764,53 @@ export function TenantOrgChartPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function TotalUsersControl({
|
||||
canToggleInternal,
|
||||
includeInternal,
|
||||
onIncludeInternalChange,
|
||||
totalUsers,
|
||||
}: {
|
||||
canToggleInternal: boolean;
|
||||
includeInternal: boolean;
|
||||
onIncludeInternalChange: (checked: boolean) => void;
|
||||
totalUsers: number;
|
||||
}) {
|
||||
if (!canToggleInternal) {
|
||||
return (
|
||||
<div
|
||||
className="ml-2 whitespace-nowrap rounded-full border border-[#f2c484]/30 bg-[#f2c484]/10 px-4 py-2 text-xs font-black text-[#f2c484] shadow-sm"
|
||||
data-testid="orgchart-total-users-control"
|
||||
>
|
||||
총 {totalUsers}명
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group/total relative ml-2">
|
||||
<button
|
||||
aria-label={`전체 인원: 총 ${totalUsers}명`}
|
||||
className="whitespace-nowrap rounded-full border border-[#f2c484]/30 bg-[#f2c484]/10 px-4 py-2 text-xs font-black text-[#f2c484] shadow-sm transition-colors hover:border-[#f2c484]/60 hover:bg-[#f2c484]/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#f2c484]/70"
|
||||
data-testid="orgchart-total-users-control"
|
||||
type="button"
|
||||
>
|
||||
총 {totalUsers}명
|
||||
</button>
|
||||
<div className="absolute right-0 top-full z-40 mt-2 hidden w-[220px] rounded-md border border-[#8bd3b2]/35 bg-[#0d3d33] p-3 text-[#e1fff1] shadow-xl group-hover/total:block group-focus-within/total:block">
|
||||
<label className="flex items-center justify-between gap-3">
|
||||
<span className="text-xs font-bold">내부조직 보기</span>
|
||||
<Switch
|
||||
aria-label="내부조직 보기"
|
||||
checked={includeInternal}
|
||||
className="data-[state=checked]:bg-[#f2c484]"
|
||||
onCheckedChange={onIncludeInternalChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LayoutOptionPicker<T extends string>({
|
||||
label,
|
||||
onChange,
|
||||
|
||||
@@ -65,6 +65,7 @@ describe("orgfront adminApi user tenant payloads", () => {
|
||||
await adminApi.fetchRelyingParty("client-1");
|
||||
await adminApi.fetchRPOwners("client-1");
|
||||
await adminApi.fetchPublicOrgChart("public-token");
|
||||
await adminApi.fetchOrgChartSnapshot();
|
||||
|
||||
expect(apiClient.get).toHaveBeenCalledWith("/v1/audit", {
|
||||
params: { limit: 10, cursor: "cursor-a" },
|
||||
@@ -90,6 +91,9 @@ describe("orgfront adminApi user tenant payloads", () => {
|
||||
expect(apiClient.get).toHaveBeenCalledWith("/v1/public/orgchart", {
|
||||
params: { token: "public-token" },
|
||||
});
|
||||
expect(apiClient.get).toHaveBeenCalledWith("/v1/admin/orgchart/snapshot", {
|
||||
params: { cache: "redis" },
|
||||
});
|
||||
});
|
||||
|
||||
it("routes mutation APIs to their documented orgfront admin endpoints", async () => {
|
||||
|
||||
@@ -32,6 +32,7 @@ export type TenantSummary = {
|
||||
parentId?: string;
|
||||
config?: Record<string, unknown>;
|
||||
memberCount: number; // Added member count
|
||||
totalMemberCount?: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
@@ -159,6 +160,26 @@ export async function fetchAllTenants({
|
||||
}) as Promise<TenantListResponse>;
|
||||
}
|
||||
|
||||
export type OrgChartSnapshotResponse = {
|
||||
tenants: TenantSummary[];
|
||||
users: UserSummary[];
|
||||
cache?: {
|
||||
source: "redis" | "database";
|
||||
hit: boolean;
|
||||
ttlSeconds?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export async function fetchOrgChartSnapshot() {
|
||||
const { data } = await apiClient.get<OrgChartSnapshotResponse>(
|
||||
"/v1/admin/orgchart/snapshot",
|
||||
{
|
||||
params: { cache: "redis" },
|
||||
},
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function fetchTenant(tenantId: string) {
|
||||
const { data } = await apiClient.get<TenantSummary>(
|
||||
`/v1/admin/tenants/${tenantId}`,
|
||||
|
||||
@@ -194,32 +194,19 @@ test("org chart non-shared title does not render the MH Dashboard eyebrow", asyn
|
||||
window.localStorage.setItem("dev_tenant_id", "group");
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/admin/tenants**", async (route) => {
|
||||
await page.route("**/api/v1/admin/orgchart/snapshot**", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
items: [
|
||||
tenants: [
|
||||
{
|
||||
...tenant("group", "Baron Group", "baron"),
|
||||
type: "COMPANY_GROUP",
|
||||
},
|
||||
tenant("engineering", "Engineering", "engineering", "group"),
|
||||
],
|
||||
limit: 10000,
|
||||
offset: 0,
|
||||
total: 2,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/admin/users**", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
items: [user("u-eng", "Engineering User", "engineering")],
|
||||
limit: 5000,
|
||||
offset: 0,
|
||||
total: 1,
|
||||
users: [user("u-eng", "Engineering User", "engineering")],
|
||||
cache: { source: "redis", hit: true },
|
||||
}),
|
||||
});
|
||||
});
|
||||
@@ -230,6 +217,103 @@ test("org chart non-shared title does not render the MH Dashboard eyebrow", asyn
|
||||
await expect(page.getByText("MH Dashboard", { exact: true })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("org chart toggles internal organizations from the total users tooltip", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("playwright_auth_bypass", "1");
|
||||
window.localStorage.setItem("dev_tenant_id", "group");
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/admin/orgchart/snapshot**", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
tenants: [
|
||||
{
|
||||
...tenant("group", "한맥가족", "hanmac-family"),
|
||||
type: "COMPANY_GROUP",
|
||||
},
|
||||
tenant("company", "삼안", "saman", "group"),
|
||||
{
|
||||
...tenant("open-team", "공개 팀", "open-team", "company"),
|
||||
config: { visibility: "public" },
|
||||
},
|
||||
{
|
||||
...tenant("internal-team", "내부 팀", "internal-team", "company"),
|
||||
config: { visibility: "internal" },
|
||||
},
|
||||
],
|
||||
users: [
|
||||
user("u-open", "Open User", "open-team"),
|
||||
user("u-internal", "Internal User", "internal-team"),
|
||||
],
|
||||
cache: { source: "redis", hit: true },
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/chart");
|
||||
await expect(page.getByRole("heading", { name: "조직 현황" })).toBeVisible();
|
||||
|
||||
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
||||
const totalUsersControl = page.getByTestId("orgchart-total-users-control");
|
||||
await expect(totalUsersControl).toHaveText("총 1명");
|
||||
await expect(svg.getByText("공개 팀", { exact: true })).toBeVisible();
|
||||
await expect(svg.getByText(/Open User/)).toBeVisible();
|
||||
await expect(svg.getByText("내부 팀", { exact: true })).toHaveCount(0);
|
||||
await expect(svg.getByText("Internal User", { exact: true })).toHaveCount(0);
|
||||
|
||||
await totalUsersControl.hover();
|
||||
await page.getByRole("switch", { name: "내부조직 보기" }).setChecked(true);
|
||||
|
||||
await expect(totalUsersControl).toHaveText("총 2명");
|
||||
await expect(svg.getByText("내부 팀", { exact: true })).toBeVisible();
|
||||
await expect(svg.getByText(/Internal User/)).toBeVisible();
|
||||
});
|
||||
|
||||
test("org chart defaults to hanmac family when public sector group is listed first", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("playwright_auth_bypass", "1");
|
||||
window.localStorage.setItem("dev_tenant_id", "family");
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/admin/orgchart/snapshot**", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
tenants: [
|
||||
{
|
||||
...tenant("public-sector", "공공기관", "public-sector"),
|
||||
type: "COMPANY_GROUP",
|
||||
memberCount: 0,
|
||||
},
|
||||
{
|
||||
...tenant("family", "한맥가족", "hanmac-family"),
|
||||
type: "COMPANY_GROUP",
|
||||
},
|
||||
{
|
||||
...tenant("saman", "삼안", "saman", "family"),
|
||||
type: "COMPANY",
|
||||
},
|
||||
],
|
||||
users: [user("u-saman", "Saman User", "saman")],
|
||||
cache: { source: "redis", hit: true },
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/chart");
|
||||
|
||||
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
||||
await expect(page.getByRole("button", { name: "조직: 한맥가족" })).toBeVisible();
|
||||
await expect(svg.getByText("한맥가족", { exact: true })).toBeVisible();
|
||||
await expect(svg.getByText("삼안", { exact: true })).toBeVisible();
|
||||
await expect(svg.getByText("공공기관", { exact: true })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("org chart renders dense member nodes with calculated member columns", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -269,6 +269,58 @@ test("org chart displays user names with short grade aliases and no job details"
|
||||
await expect(svg.getByText(/팀장|Platform Engineer/)).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("org chart orders top executive members by rank priority", async ({
|
||||
page,
|
||||
}) => {
|
||||
const executiveUser = (id: string, name: string, grade: string) => ({
|
||||
...user(id, name, "engineering"),
|
||||
grade,
|
||||
});
|
||||
|
||||
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: [
|
||||
executiveUser("u-vice-president", "Vice President", "부사장"),
|
||||
executiveUser("u-adviser", "Adviser", "고문"),
|
||||
executiveUser("u-vice-chair", "Vice Chair", "부회장"),
|
||||
executiveUser("u-president", "President", "사장"),
|
||||
executiveUser("u-chair", "Chair", "회장"),
|
||||
executiveUser("u-director", "Director", "전무"),
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/chart?token=rank-priority");
|
||||
|
||||
const engineeringNode = page.locator(
|
||||
'[data-testid="orgchart-node-engineering"]',
|
||||
);
|
||||
await expect(engineeringNode).toBeVisible();
|
||||
|
||||
const orderedMemberIds = await engineeringNode
|
||||
.locator('[data-testid^="orgchart-member-"]')
|
||||
.evaluateAll((elements) =>
|
||||
elements.map((element) => element.getAttribute("data-testid")),
|
||||
);
|
||||
|
||||
expect(orderedMemberIds).toEqual([
|
||||
"orgchart-member-u-chair",
|
||||
"orgchart-member-u-president",
|
||||
"orgchart-member-u-vice-chair",
|
||||
"orgchart-member-u-adviser",
|
||||
"orgchart-member-u-vice-president",
|
||||
"orgchart-member-u-director",
|
||||
]);
|
||||
});
|
||||
|
||||
test("org chart only highlights flagged member cards", async ({ page }) => {
|
||||
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
||||
await route.fulfill({
|
||||
@@ -422,23 +474,12 @@ test("org chart places multi-tenant users only on leaf memberships without dupli
|
||||
];
|
||||
const [groupTenant, baronTenant, engineeringTenant, platformTenant] = tenants;
|
||||
|
||||
await page.route("**/api/v1/admin/tenants**", async (route) => {
|
||||
await page.route("**/api/v1/admin/orgchart/snapshot**", 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: [
|
||||
tenants,
|
||||
users: [
|
||||
multiTenantUser("u-shared", "Shared User", "baron", [
|
||||
groupTenant,
|
||||
baronTenant,
|
||||
@@ -446,9 +487,7 @@ test("org chart places multi-tenant users only on leaf memberships without dupli
|
||||
platformTenant,
|
||||
]),
|
||||
],
|
||||
total: 1,
|
||||
limit: 5000,
|
||||
offset: 0,
|
||||
cache: { source: "redis", hit: true },
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user