import { expect, test, type Page, type Route, type TestInfo } from "@playwright/test"; function envelope(data: unknown) { return { requestId: "mock-r101-mobile", 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-r101", employeeNo: "E101", name: "陈工程", phone: "13800000000", email: "employee@hyinda.com", departmentId: "dept-r101", directManagerId: null, status: "active", notificationPushEnabled: true, roleCodes: ["employee"], profileCompleteness: { hasEmergencyContact: true, hasBankAccount: true, hasAvatar: false, hasIdNumber: false, score: 50, }, wecomBinding: null, }; const leaveType = { id: "leave-type-annual", code: "annual", name: "年假", description: null, enabled: true, requiresAttachment: false, attachmentMimeTypes: [], sortOrder: 1, policy: { id: "leave-policy-annual", annualQuotaDays: 15, enabled: true, description: null, }, createdAt: "2026-05-06T00:00:00.000Z", updatedAt: "2026-05-06T00:00:00.000Z", }; function navigationItem(key: string, href: string, label: string, icon: string) { return { key, href, label, icon, placement: "main", permissionCodes: [], featureFlagKey: null }; } function reimbursementItem(status = "draft") { return { id: "reimbursement-r101-draft", formNumber: "BX20260506001", status, reimbursementDate: "2026-05-06", totalAmount: "168.30", description: "客户拜访交通费", payment: { paymentVoucherNo: null, paymentDate: null, paymentChannel: null, paidAt: null, paidBy: null, }, owner: { id: me.id, employeeNo: me.employeeNo, name: me.name }, submitter: { id: me.id, employeeNo: me.employeeNo, name: me.name }, project: null, pendingWorkflow: null, createdAt: "2026-05-06T09:00:00.000Z", updatedAt: "2026-05-06T09:10:00.000Z", }; } const leaveRequest = { id: "leave-r101-draft", ownerId: me.id, departmentId: me.departmentId, status: "draft", startDate: "2026-05-08", endDate: "2026-05-08", startSession: "am", endSession: "pm", requestedDays: 1, reason: "个人事务", attachment: null, leaveType: { id: leaveType.id, code: leaveType.code, name: leaveType.name, requiresAttachment: false, attachmentMimeTypes: [], }, owner: { id: me.id, employeeNo: me.employeeNo, name: me.name }, createdAt: "2026-05-06T09:00:00.000Z", updatedAt: "2026-05-06T09:10:00.000Z", }; async function mockMobileApis(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: "ww-r101", 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-r101-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-r101", persisted: true, profile: "small_team_internal", orgMode: "single_company", primaryEntryMode: "wecom", 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-r101", persisted: true, profile: "small_team_internal" }, greeting: { name: me.name }, cards: [ { key: "pending_tasks", title: "待处理", value: 0, href: "/workflow/tasks", severity: "normal" }, { key: "my_applications", title: "我的申请", value: 2, href: "/reimbursements", severity: "normal" }, ], pendingTasks: { count: 0, href: "/workflow/tasks" }, myApplications: [ { domain: "reimbursement", count: 1, href: "/reimbursements" }, { domain: "leave", count: 1, href: "/leave-requests" }, ], riskSummary: null, announcements: [], 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: 0, 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-r101", name: "研发部" }, manager: null, }, }); return; } if (path === "/api/reimbursements" && request.method() === "GET") { const status = url.searchParams.get("status") ?? "draft"; const items = status === "draft" ? [reimbursementItem(status)] : []; await fulfillJson(route, { page: 1, pageSize: 20, total: items.length, scope: "own", items, }); return; } if (path === "/api/reimbursements" && request.method() === "POST") { await fulfillJson(route, { id: "reimbursement-r101-created", formNumber: null, submitterId: me.id, ownerId: me.id, reimbursementDate: "2026-05-06", status: "draft", description: null, projectId: null, project: null, totalAmount: "0", linkedTravelRequestId: null, linkedTravelRequest: null, items: [], }); return; } if (path === "/api/leave-types") { await fulfillJson(route, { items: [leaveType] }); return; } if (path === "/api/leave-balances") { await fulfillJson(route, { employeeId: me.id, items: [ { leaveType, policy: leaveType.policy, granted: 15, adjusted: 0, reserved: 1, used: 2, available: 12, periodStart: "2026-01-01", periodEnd: "2026-12-31", }, ], }); return; } if (path === "/api/leave-requests") { await fulfillJson(route, { page: 1, pageSize: 20, total: 1, scope: "own", items: [leaveRequest], }); return; } if (path === "/api/notifications") { await fulfillJson(route, { items: [], total: 0, page: 1, pageSize: 5, unreadCount: 0, }); return; } if (path === "/api/workflow/tasks") { await fulfillJson(route, { items: [], total: 0, page: 1, pageSize: 20 }); 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 attachScreenshot(page: Page, testInfo: TestInfo, name: string) { await testInfo.attach(name, { body: await page.screenshot({ fullPage: true }), contentType: "image/png", }); } async function assertNoMobileDesignRegression(page: Page) { await expect(page.getByText("先处理这几件事")).toHaveCount(0); await expect(page.getByText("清爽", { exact: true })).toHaveCount(0); await expect(page.locator("table:visible")).toHaveCount(0); const darkControls = await page.evaluate(() => { function isVisible(element: Element) { const rect = element.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; } return 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)"; }); }); expect(darkControls).toEqual([]); } test.describe("R101 mobile WIP closeout", () => { test.beforeEach(async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await mockMobileApis(page); await login(page); }); test("移动首页和我的页收口", async ({ page }, testInfo) => { await page.goto("/mobile"); await expect(page.getByRole("heading", { name: "今天" })).toBeVisible(); await expect(page.getByTestId("mobile-today-focus")).toBeVisible(); await expect(page.locator("aside")).toHaveCount(0); await assertNoMobileDesignRegression(page); await attachScreenshot(page, testInfo, "r101-mobile-home.png"); await page.goto("/mobile/me"); await expect(page.getByRole("heading", { name: "我的" })).toBeVisible(); await expect(page.getByTestId("mobile-more-links")).toBeVisible(); await expect(page.getByText("低频模块放在这里")).toHaveCount(0); await assertNoMobileDesignRegression(page); await attachScreenshot(page, testInfo, "r101-mobile-me.png"); }); test("移动报销页使用卡片入口", async ({ page }, testInfo) => { await page.goto("/mobile/reimbursements"); await expect(page.getByRole("heading", { name: "报销" })).toBeVisible(); await expect(page.getByTestId("mobile-reimbursements-create")).toBeVisible(); await expect(page.getByTestId("mobile-reimbursements-list")).toBeVisible(); const card = page.getByTestId("mobile-reimbursement-card").first(); await expect(card).toBeVisible(); await expect(card.locator("a").first()).toBeVisible(); await expect(page.getByRole("link", { name: "继续编辑" })).toHaveCount(0); await assertNoMobileDesignRegression(page); await attachScreenshot(page, testInfo, "r101-mobile-reimbursements.png"); }); test("移动请假页默认只看记录并提示企微主办", async ({ page }, testInfo) => { await page.goto("/mobile/leave"); await expect(page.locator("h1", { hasText: "请假记录" })).toBeVisible(); await expect(page.getByText("企业微信主办")).toBeVisible(); await expect(page.getByTestId("mobile-leave-records")).toBeVisible(); await expect(page.getByTestId("mobile-leave-record-card")).toBeVisible(); await expect(page.getByTestId("mobile-leave-view-form")).toHaveCount(0); await expect(page.getByTestId("mobile-leave-form")).toHaveCount(0); await expect(page.getByTestId("mobile-leave-record-card")).toBeVisible(); await expect(page.getByRole("button", { name: "编辑" })).toHaveCount(0); await expect(page.getByRole("button", { name: "提交应急审批" })).toHaveCount(0); await assertNoMobileDesignRegression(page); await attachScreenshot(page, testInfo, "r101-mobile-leave.png"); }); });