feat(management-navigation-single-company-ux): 收敛管理导航与单公司配置体验
这个提交包含在:
@@ -0,0 +1,130 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import {
|
||||
getVisibleManagementItems,
|
||||
getManagementRoutePermissions,
|
||||
isKnownManagementPath,
|
||||
isManagementPath,
|
||||
MANAGEMENT_ROOT_PERMISSIONS,
|
||||
MANAGEMENT_ROUTES,
|
||||
resolveManagementBreadcrumbs,
|
||||
resolveManagementRoute,
|
||||
} from "../../src/features/navigation/managementRoutes";
|
||||
import { resolvePageMeta } from "../../src/features/navigation/schema";
|
||||
import {
|
||||
filterRecentVisitsByPermissions,
|
||||
type RecentVisit,
|
||||
} from "../../src/lib/recentVisits";
|
||||
import { resolveSingleCompanyState } from "../../src/features/company/singleCompany";
|
||||
|
||||
const REGISTERED_MANAGEMENT_PATHS = [
|
||||
"/system/settings",
|
||||
"/people",
|
||||
"/people/stats",
|
||||
"/people/:employeeId",
|
||||
"/organization",
|
||||
"/system/users",
|
||||
"/system/departments",
|
||||
"/system/approval-rules",
|
||||
"/system/workflow-definitions",
|
||||
"/system/expense-standards",
|
||||
"/system/reimbursement-templates",
|
||||
"/system/workspace",
|
||||
"/audit-logs",
|
||||
"/system/master-data",
|
||||
"/system/payment-accounts",
|
||||
"/system/notification-channels",
|
||||
"/system/feature-flags",
|
||||
"/system/announcements",
|
||||
"/system/cutover",
|
||||
"/performance",
|
||||
];
|
||||
|
||||
test("管理路由注册表覆盖路径、父级、权限和聚合入口合同", () => {
|
||||
expect(MANAGEMENT_ROUTES.map((route) => route.path)).toEqual(REGISTERED_MANAGEMENT_PATHS);
|
||||
expect(new Set(MANAGEMENT_ROUTES.map((route) => route.key)).size).toBe(MANAGEMENT_ROUTES.length);
|
||||
|
||||
const keys = new Set(MANAGEMENT_ROUTES.map((route) => route.key));
|
||||
const routesByKey = new Map(MANAGEMENT_ROUTES.map((route) => [route.key, route]));
|
||||
for (const route of MANAGEMENT_ROUTES) {
|
||||
expect(route.title.trim()).not.toBe("");
|
||||
expect(route.anyOf.length).toBeGreaterThan(0);
|
||||
if (route.parentKey) {
|
||||
expect(keys.has(route.parentKey)).toBe(true);
|
||||
}
|
||||
if (route.showOnLanding) {
|
||||
expect(route.group).not.toBeNull();
|
||||
expect(route.icon.trim()).not.toBe("");
|
||||
expect(route.description.trim()).not.toBe("");
|
||||
}
|
||||
|
||||
const ancestors = new Set<string>();
|
||||
let cursor = route;
|
||||
while (cursor.parentKey) {
|
||||
expect(ancestors.has(cursor.key)).toBe(false);
|
||||
ancestors.add(cursor.key);
|
||||
cursor = routesByKey.get(cursor.parentKey)!;
|
||||
}
|
||||
}
|
||||
|
||||
const expectedRootPermissions = new Set(
|
||||
MANAGEMENT_ROUTES.filter((route) => route.showOnLanding).flatMap((route) => route.anyOf),
|
||||
);
|
||||
expect(new Set(MANAGEMENT_ROOT_PERMISSIONS)).toEqual(expectedRootPermissions);
|
||||
});
|
||||
|
||||
test("企业银行账户与人员档案按当前权限解析管理父链", () => {
|
||||
const paymentPermissions = ["payment.account.read"];
|
||||
expect(resolveManagementRoute("/system/payment-accounts", paymentPermissions)?.title).toBe("企业银行账户");
|
||||
expect(isManagementPath("/system/payment-accounts", paymentPermissions)).toBe(true);
|
||||
expect(resolveManagementBreadcrumbs("/system/payment-accounts", paymentPermissions)).toEqual([
|
||||
{ key: "management", title: "管理", href: "/system/settings" },
|
||||
{ key: "payment_accounts", title: "企业银行账户", href: null },
|
||||
]);
|
||||
|
||||
const peoplePermissions = ["system.users"];
|
||||
expect(getManagementRoutePermissions("employee_profile")).toEqual(peoplePermissions);
|
||||
expect(resolveManagementBreadcrumbs("/people/synthetic-employee", peoplePermissions)).toEqual([
|
||||
{ key: "management", title: "管理", href: "/system/settings" },
|
||||
{ key: "people", title: "人员", href: "/people" },
|
||||
{ key: "employee_profile", title: "员工档案", href: null },
|
||||
]);
|
||||
});
|
||||
|
||||
test("无权限路径不披露管理标题或面包屑", () => {
|
||||
expect(isKnownManagementPath("/system/payment-accounts")).toBe(true);
|
||||
expect(resolveManagementRoute("/system/payment-accounts", [])).toBeNull();
|
||||
expect(resolveManagementBreadcrumbs("/system/payment-accounts", [])).toEqual([]);
|
||||
expect(resolvePageMeta("/system/payment-accounts", [])).toEqual({ title: "管理", group: "管理" });
|
||||
expect(getVisibleManagementItems([])).toEqual([]);
|
||||
});
|
||||
|
||||
test("最近访问在权限撤销后移除受限管理标题和链接", () => {
|
||||
const visits: RecentVisit[] = [
|
||||
{
|
||||
path: "/system/payment-accounts",
|
||||
title: "企业银行账户",
|
||||
visitedAt: "2026-08-03T00:00:00.000Z",
|
||||
},
|
||||
{
|
||||
path: "/reimbursements",
|
||||
title: "报销",
|
||||
visitedAt: "2026-08-02T00:00:00.000Z",
|
||||
},
|
||||
];
|
||||
|
||||
expect(filterRecentVisitsByPermissions(visits, ["payment.account.read"])).toEqual(visits);
|
||||
expect(filterRecentVisitsByPermissions(visits, [])).toEqual([visits[1]]);
|
||||
});
|
||||
|
||||
test("单公司状态对零个、一个和多个启用公司采用明确且保守的分支", () => {
|
||||
expect(resolveSingleCompanyState([])).toEqual({ kind: "empty" });
|
||||
expect(resolveSingleCompanyState([{ id: "company-1" }])).toEqual({
|
||||
kind: "single",
|
||||
company: { id: "company-1" },
|
||||
});
|
||||
expect(resolveSingleCompanyState([{ id: "company-1" }, { id: "company-2" }])).toEqual({
|
||||
kind: "multiple",
|
||||
companies: [{ id: "company-1" }, { id: "company-2" }],
|
||||
});
|
||||
});
|
||||
@@ -432,6 +432,7 @@ type MockVerificationRecord = {
|
||||
};
|
||||
|
||||
type Wave4MockOptions = {
|
||||
legalEntities?: Array<(typeof legalEntities)[number]>;
|
||||
detailVerification?: MockVerificationRecord | null;
|
||||
verificationRecords?: MockVerificationRecord[];
|
||||
triggerVerificationError?: Record<string, unknown>;
|
||||
@@ -924,7 +925,17 @@ async function mockWave4Apis(page: Page, options: Wave4MockOptions = {}) {
|
||||
}
|
||||
|
||||
if (pathName === "/api/master-data/legal-entities") {
|
||||
await fulfillJson(route, { includeArchived: false, items: legalEntities });
|
||||
await fulfillJson(route, { includeArchived: false, items: options.legalEntities ?? legalEntities });
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/payment-accounts") {
|
||||
await fulfillJson(route, { items: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/bank-institutions") {
|
||||
await fulfillJson(route, { items: [{ id: "bank-wave4", code: "CCB", name: "中国建设银行" }] });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1016,7 +1027,7 @@ const pages = [
|
||||
{ path: "/organization", summary: "organization-summary", proof: "text=研发部", screenshot: "06-organization" },
|
||||
{ path: "/system/workspace", summary: "workspace-settings-summary", proof: "data-testid=workspace-modules-table", screenshot: "07-workspace" },
|
||||
{ path: "/system/users", summary: "users-summary", proof: "data-testid=users-table", screenshot: "08-users" },
|
||||
{ path: "/system/master-data", summary: "master-data-summary", proof: "text=法体列表", screenshot: "09-master-data" },
|
||||
{ path: "/system/master-data", summary: "master-data-summary", proof: "text=公司信息", screenshot: "09-master-data" },
|
||||
{ path: "/system/notification-channels", summary: "notification-channels-summary", proof: "data-testid=notification-channels-table", screenshot: "10-notification-channels" },
|
||||
{ path: "/system/settings", summary: "system-settings-summary", proof: "text=管理", screenshot: "11-system-settings" },
|
||||
{ path: "/system/feature-flags", summary: "feature-flags-summary", proof: "data-testid=feature-flags-table", screenshot: "12-feature-flags" },
|
||||
@@ -1066,6 +1077,47 @@ test("PC 财务列表使用明确动作入口", async ({ page }) => {
|
||||
await expect(page.getByTestId("invoice-export-csv")).toBeVisible();
|
||||
});
|
||||
|
||||
test("管理子页共享面包屑、侧栏高亮并自动关联唯一公司", async ({ page }) => {
|
||||
await mockWave4Apis(page);
|
||||
await page.setViewportSize({ width: 1440, height: 960 });
|
||||
await login(page);
|
||||
|
||||
await page.goto("/system/payment-accounts");
|
||||
|
||||
const breadcrumbs = page.getByTestId("topbar-management-breadcrumbs");
|
||||
await expect(breadcrumbs).toContainText("管理");
|
||||
await expect(breadcrumbs).toContainText("企业银行账户");
|
||||
await expect(breadcrumbs.getByRole("link", { name: "管理" })).toHaveAttribute("href", "/system/settings");
|
||||
await expect(page.getByTestId("payment-account-company")).toHaveText("海音达科技");
|
||||
await expect(page.locator("#payment-account-legal-entity")).toHaveCount(0);
|
||||
await expect(page.locator('a.nav-link-item[aria-label="管理"]')).toHaveClass(/nav-link-item--active/);
|
||||
});
|
||||
|
||||
test("企业银行账户在缺少公司信息时给出可执行入口", async ({ page }) => {
|
||||
await mockWave4Apis(page, { legalEntities: [] });
|
||||
await login(page);
|
||||
await page.goto("/system/payment-accounts");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "先完善公司信息" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "完善公司信息" })).toHaveAttribute("href", "/system/master-data");
|
||||
await expect(page.getByTestId("payment-account-company")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("企业银行账户在异常多公司状态下停止自动关联", async ({ page }) => {
|
||||
await mockWave4Apis(page, {
|
||||
legalEntities: [
|
||||
legalEntities[0],
|
||||
{ ...legalEntities[0], id: "legal-wave4-extra", code: "HYD-EXTRA", name: "合成重复公司" },
|
||||
],
|
||||
});
|
||||
await login(page);
|
||||
await page.goto("/system/payment-accounts");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "公司信息需要处理" })).toBeVisible();
|
||||
await expect(page.getByText("系统不会猜测付款账户应关联哪一条")).toBeVisible();
|
||||
await expect(page.getByTestId("payment-account-company")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("审批规则负责人本人兜底默认财务并兼容财务负责人", async ({ page }) => {
|
||||
await mockWave4Apis(page);
|
||||
await page.setViewportSize({ width: 1440, height: 960 });
|
||||
|
||||
在新工单中引用
屏蔽一个用户