文件
hyb-oa/client/tests/e2e/r101-mobile-wip-closeout.spec.ts
T

429 行
14 KiB
TypeScript

import { expect, test, type Page, type Route, type TestInfo } from "@playwright/test";
// Keep API fixtures authoritative across full navigation in the production PWA.
test.use({ serviceWorkers: "block" });
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: "[email protected]",
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("[email protected]");
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", { level: 1, name: "今日待处理", exact: true })).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");
const [refreshResponse, meResponse] = await Promise.all([
page.waitForResponse((response) => new URL(response.url()).pathname === "/api/auth/refresh"),
page.waitForResponse((response) => new URL(response.url()).pathname === "/api/me"),
page.goto("/mobile/me"),
]);
for (const response of [refreshResponse, meResponse]) {
expect(response.status()).toBe(200);
expect(response.fromServiceWorker()).toBe(false);
expect((await response.json()).requestId).toBe("mock-r101-mobile");
}
await expect(page).toHaveURL(/\/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", { level: 1, name: "移动报销", exact: true })).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("企业微信主办", { exact: true })).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");
});
});