180 行
7.2 KiB
TypeScript
180 行
7.2 KiB
TypeScript
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<SeededReimbursement> {
|
|
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-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);
|
|
});
|
|
});
|