import { expect, test, type Page, type Route, type TestInfo } from "@playwright/test"; function envelope(data: unknown) { return { requestId: "mock-r100-design", data }; } async function fulfillJson(route: Route, data: unknown, status = 200) { await route.fulfill({ status, contentType: "application/json", body: JSON.stringify(envelope(data)), }); } const me = { id: "employee-r100", employeeNo: "E100", name: "陈工程", phone: "13800000000", email: "employee@hyinda.com", departmentId: "dept-r100", 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 mockDesignApis(page: Page) { await page.route((url) => url.pathname.startsWith("/api/"), async (route) => { const url = new URL(route.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: "ww-r100", agentId: "1000002", loginUrl: "/api/auth/wecom/login?returnTo=%2Fmobile", bindEnabled: true, defaultReturnTo: "/mobile", 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-r100-token", expiresIn: 900, mustChangePassword: false })), }); return; } if (path === "/api/me") { await fulfillJson(route, me); return; } if (path === "/api/system/navigation") { await fulfillJson(route, { workspace: { id: "workspace-r100", persisted: true, profile: "small_team_internal", orgMode: "single_company", primaryEntryMode: "password", notificationStrategy: "in_app", devicePriority: "mobile", moduleVisibility: {}, createdAt: null, updatedAt: null, updatedBy: null, }, sections: { main: [ navigationItem("portal", "/portal", "工作台", "home_app_logo"), navigationItem("expense", "/reimbursements", "报销", "request_quote"), navigationItem("leave", "/leave-requests", "请假记录", "event_available"), navigationItem("invoice", "/invoice-pool", "票据", "receipt_long"), ], admin: [], backup: [], }, recommendedRolePresets: [], }); return; } if (path === "/api/portal/home") { await fulfillJson(route, { workspace: { id: "workspace-r100", persisted: true, profile: "small_team_internal" }, greeting: { name: me.name }, cards: [ { key: "pending_tasks", title: "待处理", value: 2, href: "/workflow/tasks", severity: "warning" }, { key: "my_applications", title: "我的申请", value: 1, href: "/reimbursements", severity: "normal" }, ], pendingTasks: { count: 2, href: "/workflow/tasks" }, myApplications: [ { domain: "reimbursement", count: 1, href: "/reimbursements" }, { domain: "leave", count: 0, href: "/leave-requests" }, ], riskSummary: null, announcements: [{ id: "ann-r100", title: "试运行提醒", summary: "今天处理待办。", href: null }], quickLinks: [{ key: "capture", 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: 2, 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.startsWith("/api/employees/") && path.endsWith("/profile")) { await fulfillJson(route, { employee: { ...me, avatarUrl: null, department: { id: "dept-r100", name: "研发部" }, manager: null, }, }); return; } if (path === "/api/workflow/tasks") { await fulfillJson(route, { items: [], total: 0, page: 1, pageSize: 20, }); return; } if (path === "/api/notifications") { await fulfillJson(route, { items: [], total: 0, page: 1, pageSize: 5, unreadCount: 0, }); return; } await fulfillJson(route, {}); }); } async function login(page: Page) { 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/); } async function collectDesignDiagnostics(page: Page) { return page.evaluate(() => { function isVisible(element: Element) { const rect = element.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; } const bodyText = document.body.innerText; const badCopy = ["先处理这几件事", "清爽"].filter((phrase) => bodyText.includes(phrase)); const routeLikeText = bodyText .split("\n") .map((line) => line.trim()) .filter((line) => /^\/[a-z0-9-]+/.test(line)) .slice(0, 10); const visibleTables = Array.from(document.querySelectorAll("table")).filter(isVisible).length; const darkControls = Array.from(document.querySelectorAll("a, button, [role='button'], [role='tab']")) .filter(isVisible) .map((element) => { const styles = window.getComputedStyle(element); return { text: (element.textContent ?? "").trim().replace(/\s+/g, " ").slice(0, 40), backgroundColor: styles.backgroundColor, color: styles.color, }; }) .filter((item) => { const darkBackgrounds = new Set(["rgb(0, 0, 0)", "rgb(24, 24, 27)", "rgb(15, 23, 42)"]); return darkBackgrounds.has(item.backgroundColor) && item.color === "rgb(255, 255, 255)"; }); return { url: window.location.pathname, badCopy, routeLikeText, visibleTables, darkControls, }; }); } async function attachDiagnostics(page: Page, testInfo: TestInfo, name: string) { const diagnostics = await collectDesignDiagnostics(page); await testInfo.attach(`${name}.json`, { body: JSON.stringify(diagnostics, null, 2), contentType: "application/json", }); await testInfo.attach(`${name}.png`, { body: await page.screenshot({ fullPage: true }), contentType: "image/png", }); } test.describe("R100 design baseline diagnostics", () => { test("手机首页和我的页可生成视觉诊断", async ({ page }, testInfo) => { await mockDesignApis(page); await page.setViewportSize({ width: 390, height: 844 }); await login(page); await page.goto("/mobile"); await expect(page.getByRole("heading", { name: "今天" })).toBeVisible(); await expect(page.getByTestId("mobile-today-focus")).toBeVisible(); await attachDiagnostics(page, testInfo, "r100-mobile-home"); await page.goto("/mobile/me"); await expect(page.getByRole("heading", { name: "我的" })).toBeVisible(); await expect(page.getByTestId("mobile-more-links")).toBeVisible(); await attachDiagnostics(page, testInfo, "r100-mobile-me"); }); test("PC 工作台可生成视觉诊断", async ({ page }, testInfo) => { await mockDesignApis(page); await page.setViewportSize({ width: 1440, height: 960 }); await login(page); await expect(page.getByTestId("topbar-profile-trigger")).toBeVisible(); await expect(page.locator("aside")).toBeVisible(); await attachDiagnostics(page, testInfo, "r100-pc-portal"); }); });