import { expect, test, type Browser, type Page } from "@playwright/test"; import { EMPLOYEE, createAuthenticatedApiClient, type AuthenticatedApiClient, type E2EUser, } from "./helpers/auth"; const WECOM_UA = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 wxwork/4.1.20 MicroMessenger/8.0.1 Language/zh_CN"; type SeededReimbursement = { id: string; client: AuthenticatedApiClient; }; async function login(page: Page, user: E2EUser) { await page.goto("/login"); const credentialForm = page.locator("#login-identifier, input[autocomplete='username'], input[placeholder*='邮箱']").first(); const accountLoginButton = page.getByRole("button", { name: "使用账号密码登录" }); for (let attempt = 0; attempt < 3; attempt += 1) { const credentialFormVisible = await credentialForm.isVisible({ timeout: 1_000 }).catch(() => false); if (credentialFormVisible) { break; } const accountLoginVisible = await accountLoginButton.isVisible({ timeout: 3_000 }).catch(() => false); if (!accountLoginVisible) { break; } try { await accountLoginButton.click({ timeout: 5_000 }); } catch (error) { if (attempt >= 2) { throw error; } } } const identifierByTestId = page.getByTestId("login-identifier"); const passwordByTestId = page.getByTestId("login-password"); const submitByTestId = page.getByTestId("login-submit"); const hasTestIdLogin = await identifierByTestId.isVisible({ timeout: 5_000 }).catch(() => false); if (hasTestIdLogin) { await identifierByTestId.fill(user.identifier, { timeout: 10_000 }); await passwordByTestId.fill(user.password, { timeout: 10_000 }); await submitByTestId.click({ timeout: 10_000 }); } else { await credentialForm.fill(user.identifier, { timeout: 10_000 }); await page .locator("#login-password, input[type='password'], input[autocomplete='current-password']") .first() .fill(user.password, { timeout: 10_000 }); await page.getByRole("button", { name: "登录", exact: true }).click({ timeout: 10_000 }); } await page.waitForURL(/\/(portal|mobile)/, { timeout: 30_000 }); } async function newWecomPage(browser: Browser) { const context = await browser.newContext({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true, userAgent: WECOM_UA, locale: "zh-CN", serviceWorkers: "block", }); return { context, page: await context.newPage() }; } async function expectNoPcShell(page: Page) { await expect(page.locator("aside")).toHaveCount(0); await expect(page.getByTestId("global-search-trigger-mobile")).toHaveCount(0); } async function firstEnabledExpenseTypeId(client: AuthenticatedApiClient) { const response = await client.api.get("/api/expense-types?enabled=true"); const payload = await response.json() as { data?: { items?: Array<{ id: string; enabled?: boolean }> } }; const item = payload.data?.items?.find((candidate) => candidate.enabled !== false) ?? payload.data?.items?.[0]; if (!response.ok() || !item?.id) { throw new Error(`无法读取费用类型:HTTP ${response.status()}`); } return item.id; } async function seedDraftReimbursement(): Promise { const client = await createAuthenticatedApiClient(EMPLOYEE); const expenseTypeId = await firstEnabledExpenseTypeId(client); const reimbursementResponse = await client.api.post("/api/reimbursements", { data: { description: `R110 路由分流 ${Date.now()}` }, }); const reimbursementPayload = await reimbursementResponse.json() as { data?: { id?: string } }; if (!reimbursementResponse.ok() || !reimbursementPayload.data?.id) { throw new Error(`创建报销草稿失败:HTTP ${reimbursementResponse.status()}`); } const itemResponse = await client.api.post(`/api/reimbursements/${reimbursementPayload.data.id}/items`, { data: { expenseTypeId, expenseDate: "2026-05-08", amount: "110.00", description: "R110 移动路由分流明细", }, }); if (!itemResponse.ok()) { throw new Error(`创建报销明细失败:HTTP ${itemResponse.status()}`); } return { id: reimbursementPayload.data.id, client }; } test.describe.serial("R110 手机和 PC 路由分流", () => { test.skip(process.env.E2E_MODE === "live", "R110 本地路由专项只在临时本地环境运行"); test("企业微信手机 UA 访问 PC 路由时进入移动页", async ({ browser }) => { const seeded = await seedDraftReimbursement(); const mobile = await newWecomPage(browser); try { await login(mobile.page, EMPLOYEE); await mobile.page.goto("/reimbursements"); await expect(mobile.page).toHaveURL(/\/mobile\/reimbursements$/); await expect(mobile.page.getByTestId("mobile-reimbursements-list")).toBeVisible(); await expectNoPcShell(mobile.page); await mobile.page.goto(`/reimbursements/${seeded.id}`); await expect(mobile.page).toHaveURL(new RegExp(`/mobile/reimbursements/${seeded.id}$`)); await expect(mobile.page.getByTestId("mobile-reimbursement-detail")).toBeVisible(); await expectNoPcShell(mobile.page); await mobile.page.goto(`/reimbursements/${seeded.id}/print`); await expect(mobile.page).toHaveURL(new RegExp(`/mobile/reimbursements/${seeded.id}$`)); await expectNoPcShell(mobile.page); await mobile.page.goto("/leave-requests"); await expect(mobile.page).toHaveURL(/\/mobile\/leave$/); await expect(mobile.page.getByTestId("mobile-leave-records")).toBeVisible(); await expectNoPcShell(mobile.page); await mobile.page.goto("/leave-requests?action=create"); await expect(mobile.page).toHaveURL(/\/mobile\/leave\?view=form$/); await expect(mobile.page.getByTestId("mobile-leave-form")).toBeVisible(); await expectNoPcShell(mobile.page); await mobile.page.goto("/invoice-pool"); await expect(mobile.page).toHaveURL(/\/mobile\/capture$/); await expect(mobile.page.getByTestId("mobile-capture-pdf-file")).toBeAttached(); await expect(mobile.page.getByTestId("mobile-capture-pdf-file")).toHaveAttribute("accept", ".pdf,application/pdf"); await expect(mobile.page.getByTestId("mobile-capture-pdf-file")).not.toHaveAttribute("capture", /.+/); await expectNoPcShell(mobile.page); await mobile.page.goto("/assets"); await expect(mobile.page).toHaveURL(/\/mobile\/assets\/scan$/); await expect(mobile.page.getByTestId("mobile-asset-scan-search")).toBeVisible(); await expectNoPcShell(mobile.page); await mobile.page.goto("/notifications"); await expect(mobile.page).toHaveURL(/\/mobile\/notifications$/); await expect(mobile.page.getByText("通知").first()).toBeVisible(); await expectNoPcShell(mobile.page); await mobile.page.goto("/settings/profile"); await expect(mobile.page).toHaveURL(/\/mobile\/profile$/); await expectNoPcShell(mobile.page); } finally { await seeded.client.api.delete(`/api/reimbursements/${seeded.id}`).catch(() => undefined); await Promise.all([seeded.client.dispose(), mobile.context.close()]); } }); test("桌面浏览器访问 PC 路由时不被移动分流", async ({ page }) => { await login(page, EMPLOYEE); await page.goto("/reimbursements"); await expect(page).toHaveURL(/\/reimbursements$/); await expect(page).not.toHaveURL(/\/mobile\/reimbursements/); await expect(page.locator("aside")).toHaveCount(1); }); });