feat(08-05-interaction-pattern-unification): 统一前端交互与反馈模式
这个提交包含在:
@@ -189,7 +189,7 @@ function mockNavigation(page: import("@playwright/test").Page, mode: "employee"
|
||||
? {
|
||||
main: [
|
||||
{ key: "portal", href: "/portal", label: "工作台", icon: "home_app_logo", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "workflow", href: "/workflow/tasks", label: "待我处理", icon: "fact_check", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "workflow", href: "/workflow/tasks", label: "待我审批", icon: "fact_check", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "expense", href: "/reimbursements", label: "报销", icon: "request_quote", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "purchase", href: "/purchase-requests", label: "采购", icon: "shopping_cart", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "contracts", href: "/contracts-registry", label: "合同", icon: "description", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
@@ -262,14 +262,37 @@ function mockAttendanceToday(page: import("@playwright/test").Page) {
|
||||
});
|
||||
}
|
||||
|
||||
function mockPortalHome(page: import("@playwright/test").Page, mode: "employee" | "manager") {
|
||||
function mockPortalHome(
|
||||
page: import("@playwright/test").Page,
|
||||
mode: "employee" | "manager",
|
||||
options: { failuresBeforeSuccess?: number } = {},
|
||||
) {
|
||||
let requestCount = 0;
|
||||
|
||||
return page.route("**/api/portal/home", async (route) => {
|
||||
requestCount += 1;
|
||||
if (requestCount <= (options.failuresBeforeSuccess ?? 0)) {
|
||||
await route.fulfill({
|
||||
status: 500,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
requestId: "mock-request",
|
||||
error: {
|
||||
code: "SYS_001",
|
||||
message: "GET /api/portal/home failed",
|
||||
details: {},
|
||||
},
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const data = mode === "manager"
|
||||
? {
|
||||
workspace: { id: "workspace-1", persisted: true, profile: "small_team_internal" },
|
||||
greeting: { name: "李研发" },
|
||||
cards: [
|
||||
{ key: "pending_approvals", title: "待我处理", value: 3, href: "/workflow/tasks", severity: "warning" },
|
||||
{ key: "pending_approvals", title: "待我审批", value: 3, href: "/workflow/tasks", severity: "warning" },
|
||||
{ key: "team_presence", title: "今日团队状态", value: "6/8 在岗", href: "/team", severity: "warning" },
|
||||
{ key: "weekly_missing", title: "未提交周报", value: 2, href: "/weekly-reports", severity: "warning" },
|
||||
{ key: "weekly_help", title: "需协助员工", value: 1, href: "/weekly-reports", severity: "high" },
|
||||
@@ -307,7 +330,7 @@ function mockPortalHome(page: import("@playwright/test").Page, mode: "employee"
|
||||
workspace: { id: "workspace-1", persisted: true, profile: "small_team_internal" },
|
||||
greeting: { name: "陈工程" },
|
||||
cards: [
|
||||
{ key: "my_todos", title: "我的待办", value: 4, href: "/todos", severity: "warning" },
|
||||
{ key: "my_todos", title: "我的事项", value: 4, href: "/todos", severity: "warning" },
|
||||
{ key: "today_presence", title: "今日状态", value: "在岗", href: "/team", severity: "normal" },
|
||||
{ key: "my_weekly_report", title: "本周周报", value: "待填写", href: "/weekly-reports", severity: "warning" },
|
||||
{ key: "my_projects", title: "我的项目", value: 2, href: "/projects", severity: "warning" },
|
||||
@@ -524,12 +547,47 @@ test("Portal 员工视角优先显示今日动作顺序", async ({ page }) => {
|
||||
|
||||
await expect(page.getByTestId("portal-work-summary")).toContainText("待处理 7");
|
||||
await expect(page.getByRole("heading", { name: "今日待处理" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: /待办事项 4 条.*查看待办/ })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: /我的事项 4 条.*查看事项/ })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: /本周周报待填写.*填写周报/ })).toBeVisible();
|
||||
await expect(page.getByText("当前焦点")).not.toBeVisible();
|
||||
await expect(page.getByText("下一步", { exact: true })).not.toBeVisible();
|
||||
await expect(page.locator("summary").filter({ hasText: "团队与通知" })).not.toBeVisible();
|
||||
await expect(page.locator("summary").filter({ hasText: "更多入口" })).not.toBeVisible();
|
||||
const portalSectionOrder = await page.getByRole("heading", { name: /今日待处理|常用入口/ }).evaluateAll((headings) =>
|
||||
headings.map((heading) => heading.textContent?.trim()),
|
||||
);
|
||||
expect(portalSectionOrder).toEqual(["今日待处理", "常用入口"]);
|
||||
|
||||
const todoLink = page.getByRole("link", { name: /我的事项 4 条.*查看事项/ });
|
||||
const weeklyLink = page.getByRole("link", { name: /本周周报待填写.*填写周报/ });
|
||||
await todoLink.focus();
|
||||
await expect(todoLink).toBeFocused();
|
||||
await page.keyboard.press("Tab");
|
||||
await expect(weeklyLink).toBeFocused();
|
||||
});
|
||||
|
||||
test("Portal 加载失败持久显示自然中文并可重新加载", async ({ page }) => {
|
||||
await mockPasswordLogin(page);
|
||||
await mockNavigation(page, "employee");
|
||||
await mockPortalHome(page, "employee", { failuresBeforeSuccess: 2 });
|
||||
await mockAttendanceToday(page);
|
||||
await mockNotificationList(page);
|
||||
await mockMe(page, ["employee"], "陈工程");
|
||||
|
||||
await login(page, EMPLOYEE);
|
||||
|
||||
const errorRegion = page.getByTestId("portal-load-error");
|
||||
await expect(errorRegion).toContainText("服务暂不可用,请稍后重试或联系管理员");
|
||||
await expect(errorRegion).not.toContainText("SYS_001");
|
||||
await expect(errorRegion).not.toContainText("/api/portal/home");
|
||||
|
||||
const retryButton = page.getByTestId("portal-load-retry");
|
||||
await retryButton.focus();
|
||||
await expect(retryButton).toBeFocused();
|
||||
await retryButton.click();
|
||||
|
||||
await expect(page.getByRole("heading", { name: "今日待处理" })).toBeVisible();
|
||||
await expect(errorRegion).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Portal 管理视角按角色分发待处理和团队卡点", async ({ page }) => {
|
||||
@@ -543,7 +601,7 @@ test("Portal 管理视角按角色分发待处理和团队卡点", async ({ page
|
||||
await login(page, MANAGER);
|
||||
|
||||
await expect(page.getByRole("heading", { name: "今日待处理" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: /待我处理 3 条.*去处理/ })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: /待我审批 3 条.*去处理/ })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "团队卡点" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: /项目风险未关闭.*处理风险/ })).toBeVisible();
|
||||
await expect(page.locator("summary").filter({ hasText: "团队与通知" })).not.toBeVisible();
|
||||
@@ -653,9 +711,9 @@ test("Portal 新员工只显示轻量欢迎入口,不再显示快速上手清
|
||||
const welcomeDialog = page.getByTestId(TEST_IDS.onboarding.welcomeDialog);
|
||||
await expect(welcomeDialog).toBeVisible();
|
||||
await expect(welcomeDialog.getByRole("heading", { name: "常用入口" })).toBeVisible();
|
||||
await expect(welcomeDialog.getByRole("link")).toHaveCount(3);
|
||||
await expect(welcomeDialog.getByRole("link", { name: /处理待办/ })).toBeVisible();
|
||||
await expect(welcomeDialog.getByRole("link", { name: /提交报销/ })).toBeVisible();
|
||||
await expect(welcomeDialog.getByRole("link")).toHaveCount(2);
|
||||
await expect(welcomeDialog.getByRole("link", { name: /待我审批/ })).toHaveCount(0);
|
||||
await expect(welcomeDialog.getByRole("link", { name: /报销/ })).toBeVisible();
|
||||
await expect(welcomeDialog.getByRole("link", { name: /请假/ })).toBeVisible();
|
||||
await expect(welcomeDialog.getByRole("link", { name: /个人设置/ })).not.toBeVisible();
|
||||
await expect(welcomeDialog.getByRole("link", { name: /通知中心/ })).not.toBeVisible();
|
||||
|
||||
在新工单中引用
屏蔽一个用户