import { expect, test, type Page, type Route, type TestInfo } from "@playwright/test"; function envelope(data: unknown) { return { requestId: "mock-r96-ui", data }; } async function fulfillJson(route: Route, data: unknown, status = 200) { await route.fulfill({ status, contentType: "application/json", body: JSON.stringify(envelope(data)), }); } const visualMe = { id: "employee-r96", employeeNo: "R96001", name: "Employee", phone: "13800000000", email: "employee@hyinda.com", departmentId: "dept-r96", directManagerId: null, status: "active", notificationPushEnabled: true, roleCodes: ["manager"], profileCompleteness: { hasEmergencyContact: true, hasBankAccount: true, hasAvatar: false, hasIdNumber: false, score: 50, }, wecomBinding: null, }; function navigationItem(key: string, href: string, label: string, icon: string) { return { key, href, label, icon, placement: "main", permissionCodes: [], featureFlagKey: null }; } async function mockVisualApis(page: Page) { await page.route((url) => url.pathname.startsWith("/api/"), async (route) => { const request = route.request(); const url = new URL(request.url()); const path = url.pathname; if (path === "/api/auth/sso/providers") { await fulfillJson(route, []); return; } if (path === "/api/auth/wecom/config") { await fulfillJson(route, { enabled: true, entryEnabled: true, oauthEnabled: true, corpId: "wx-test-corp", agentId: "1000002", loginUrl: "/api/auth/wecom/login?returnTo=%2Fportal", bindEnabled: true, defaultReturnTo: "/portal", status: "ready", statusReasons: [], }); return; } if (path === "/api/auth/login" || path === "/api/auth/refresh") { await route.fulfill({ status: 200, contentType: "application/json", headers: { "set-cookie": "has_session=1; Path=/; SameSite=Lax" }, body: JSON.stringify(envelope({ accessToken: "mock-r96-access-token", expiresIn: 900, mustChangePassword: false })), }); return; } if (path === "/api/me") { await fulfillJson(route, visualMe); return; } if (path === "/api/system/navigation") { await fulfillJson(route, { workspace: { id: "workspace-r96", persisted: true, profile: "small_team_internal", orgMode: "single_company", primaryEntryMode: "password", notificationStrategy: "in_app", devicePriority: "desktop", moduleVisibility: {}, createdAt: null, updatedAt: null, updatedBy: null, }, sections: { main: [ navigationItem("portal", "/portal", "Portal", "home_app_logo"), navigationItem("reimbursements", "/reimbursements", "报销", "request_quote"), navigationItem("approvals", "/workflow/tasks", "审批待办", "fact_check"), navigationItem("invoice", "/invoice-pool", "票据中心", "receipt_long"), ], admin: [], backup: [navigationItem("settings", "/settings/profile", "个人设置", "manage_accounts")], }, recommendedRolePresets: [], }); return; } if (path === "/api/portal/home") { await fulfillJson(route, { workspace: { id: "workspace-r96", persisted: true, profile: "small_team_internal" }, greeting: { name: visualMe.name }, cards: [ { key: "pending_tasks", title: "待处理", value: 1, href: "/workflow/tasks", severity: "warning" }, { key: "my_weekly_report", title: "周报", value: "待填写", href: "/weekly-reports" }, ], pendingTasks: { count: 1, href: "/workflow/tasks" }, myApplications: [{ domain: "reimbursement", count: 2, href: "/reimbursements" }], riskSummary: null, announcements: [{ id: "ann-r96", title: "试运行提醒", summary: "今天先处理待办。", href: null }], quickLinks: [{ key: "invoice", title: "上传票据", href: "/mobile/capture" }], todayPresence: { date: "2026-05-06", status: "office", label: "在岗", note: null, href: "/team" }, teamPresence: null, weeklyReport: { weekStart: "2026-05-04", status: "draft", needsHelp: false, href: "/weekly-reports" }, weeklySummary: null, todoSummary: { openCount: 1, highPriorityCount: 0, href: "/todos" }, projectSummary: null, peopleLifecycle: null, adoptionSummary: null, recentActivities: [], teamStatusBoard: [], }); return; } if (path === "/api/attendance/me/today") { await fulfillJson(route, { workDate: "2026-05-06", needClockIn: true, exemptionReason: null, item: null, }); return; } if (path === "/api/workflow/tasks") { await fulfillJson(route, { items: [ { id: "task-r96", stepName: "经理审批", status: "pending", createdAt: "2026-05-06T09:00:00.000Z", completedAt: null, workflowInstance: { entityType: "reimbursement", entityId: "rb-r96" }, allowedActions: ["approve"], }, ], total: 1, page: 1, pageSize: 3, }); return; } if (path === "/api/notifications") { await fulfillJson(route, { items: [ { id: "notice-r96", title: "审批提醒", body: "有 1 条待办。", targetType: "workflow_task", targetId: "task-r96", readAt: null, createdAt: "2026-05-06T09:10:00.000Z", }, ], total: 1, page: 1, pageSize: 3, }); return; } await fulfillJson(route, {}); }); } async function attachScreenshot(page: Page, testInfo: TestInfo, name: string) { await testInfo.attach(name, { body: await page.screenshot({ fullPage: true }), contentType: "image/png", }); } test.describe("visual shell", () => { test("登录页入口布局基线", async ({ page }, testInfo) => { await mockVisualApis(page); await page.setViewportSize({ width: 1440, height: 1024 }); await page.goto("/login"); await expect(page.getByText("海音达工作台").first()).toBeVisible(); await expect(page.getByRole("heading", { name: "登录工作台" })).toBeVisible(); await expect(page.getByTestId("login-password-primary-panel")).toBeVisible(); await attachScreenshot(page, testInfo, "login-shell.png"); }); test("登录后壳层布局基线", async ({ page }, testInfo) => { await mockVisualApis(page); await page.setViewportSize({ width: 1440, height: 1024 }); await page.goto("/login"); await page.getByTestId("login-identifier").fill("employee@hyinda.com"); await page.getByTestId("login-password").fill("EmployeePassword1234"); await page.getByTestId("login-submit").click(); await expect(page).toHaveURL(/\/portal/); await expect(page.locator("aside")).toBeVisible(); await expect(page.getByTestId("topbar-profile-trigger")).toBeVisible(); await expect(page.locator("aside").getByRole("link", { name: "Portal", exact: true })).toBeVisible(); await expect(page.locator("summary").filter({ hasText: "备用模块" })).toBeVisible(); await attachScreenshot(page, testInfo, "app-shell.png"); }); test("移动首页任务化布局基线", async ({ page }, testInfo) => { await mockVisualApis(page); await page.setViewportSize({ width: 390, height: 844 }); await page.goto("/login"); await page.getByTestId("login-identifier").fill("employee@hyinda.com"); await page.getByTestId("login-password").fill("EmployeePassword1234"); await page.getByTestId("login-submit").click(); await page.goto("/mobile"); await expect(page.getByRole("heading", { name: "今天" })).toBeVisible(); await expect(page.getByTestId("mobile-today-focus")).toBeVisible(); await expect(page.getByTestId("mobile-action-capture")).toBeVisible(); await expect(page.getByTestId("mobile-attendance-card")).toBeVisible(); await attachScreenshot(page, testInfo, "mobile-today.png"); }); test("审批待办手机卡片布局基线", async ({ page }, testInfo) => { await mockVisualApis(page); await page.setViewportSize({ width: 390, height: 844 }); await page.goto("/login"); await page.getByTestId("login-identifier").fill("employee@hyinda.com"); await page.getByTestId("login-password").fill("EmployeePassword1234"); await page.getByTestId("login-submit").click(); await page.goto("/workflow/tasks"); await expect(page.getByTestId("workflow-tasks-mobile-list")).toBeVisible(); await expect(page.getByTestId("workflow-tasks-table")).toBeHidden(); await expect(page.getByRole("link", { name: "详情" })).toBeVisible(); await attachScreenshot(page, testInfo, "r97-workflow-tasks-mobile.png"); }); });