import { expect, test, type Page, type Route } from "@playwright/test"; import { ADMIN, EMPLOYEE, login } from "./helpers/auth"; // These APIs are page-level fixtures; a service worker must not bypass them. test.use({ serviceWorkers: "block" }); // Use the same scenario clock in the simulated API and browser. Wall-clock // passage must not turn the pending-acknowledgement case into an overdue one. const SCENARIO_NOW = new Date("2026-05-28T03:00:00.000Z"); type MockUserKey = "admin" | "employee"; type MockAnnouncement = { id: string; title: string; summary: string; linkUrl: string | null; startsAt: string | null; endsAt: string | null; enabled: boolean; priority: number; category: "general" | "policy"; requiresAcknowledgement: boolean; acknowledgementStatement: string | null; acknowledgementDueAt: string | null; createdAt: string | null; updatedAt: string | null; acknowledgedEmployeeIds: Set; }; function envelope(data: unknown) { return { requestId: "mock-policy-ack", data }; } async function fulfillJson(route: Route, data: unknown, status = 200) { await route.fulfill({ status, contentType: "application/json", body: JSON.stringify(envelope(data)), }); } function buildMe(user: MockUserKey) { if (user === "admin") { return { id: "admin-policy-ack", employeeNo: "A001", name: "王顺利", phone: "mock-phone-admin", email: "admin@example.test", departmentId: "dept-admin", directManagerId: null, status: "active", notificationPushEnabled: true, roleCodes: ["admin"], profileCompleteness: { hasEmergencyContact: true, hasBankAccount: true, hasAvatar: false, hasIdNumber: false, score: 60 }, wecomBinding: null, }; } return { id: "employee-policy-ack", employeeNo: "E001", name: "陈工程", phone: "mock-phone-employee", email: "employee@example.test", departmentId: "dept-employee", directManagerId: null, status: "active", notificationPushEnabled: true, roleCodes: ["employee"], profileCompleteness: { hasEmergencyContact: true, hasBankAccount: true, hasAvatar: false, hasIdNumber: false, score: 60 }, wecomBinding: null, }; } function mapAnnouncementForUser(item: MockAnnouncement, currentUser: MockUserKey) { const me = buildMe(currentUser); const acknowledged = item.acknowledgedEmployeeIds.has(me.id); const overdue = Boolean(item.acknowledgementDueAt && new Date(item.acknowledgementDueAt).getTime() < SCENARIO_NOW.getTime()); return { ...item, acknowledgedEmployeeIds: undefined, acknowledgement: { status: !item.requiresAcknowledgement ? "not_required" : acknowledged ? "acknowledged" : overdue ? "overdue" : "pending", acknowledgedAt: acknowledged ? "2026-05-28T03:00:00.000Z" : null, overdue: !acknowledged && overdue, }, }; } function mapAnnouncementForAdmin(item: MockAnnouncement) { const employees = [ { id: "employee-policy-ack", employeeNo: "E001", name: "陈工程", status: "pending" }, { id: "employee-policy-ack-2", employeeNo: "E002", name: "员工二", status: "pending" }, ]; const pendingEmployees = item.requiresAcknowledgement ? employees.filter((employee) => !item.acknowledgedEmployeeIds.has(employee.id)) : []; return { ...item, acknowledgedEmployeeIds: undefined, acknowledgementSummary: { required: item.requiresAcknowledgement, totalActiveEmployees: item.requiresAcknowledgement ? employees.length : 0, acknowledgedCount: item.requiresAcknowledgement ? employees.length - pendingEmployees.length : 0, pendingCount: pendingEmployees.length, overdueCount: 0, }, pendingEmployees, }; } async function mockPolicyAckApis(page: Page) { let currentUser: MockUserKey = "employee"; const announcements: MockAnnouncement[] = []; 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/capabilities") { await fulfillJson(route, { emailDelivery: { ready: true, statusReasons: [] }, emailLoginCode: { enabled: true }, passwordReset: { enabled: true }, }); return; } if (path === "/api/auth/sso/providers") { await fulfillJson(route, []); return; } if (path === "/api/auth/wecom/config") { await fulfillJson(route, { enabled: false, entryEnabled: false, oauthEnabled: false, status: "disabled", statusReasons: [] }); return; } if (path === "/api/auth/login") { const payload = request.postDataJSON() as { identifier?: string }; currentUser = payload.identifier === ADMIN.identifier ? "admin" : "employee"; await route.fulfill({ status: 200, contentType: "application/json", headers: { "set-cookie": "has_session=1; Path=/; SameSite=Lax" }, body: JSON.stringify(envelope({ accessToken: `mock-policy-${currentUser}`, expiresIn: 900, mustChangePassword: false })), }); return; } if (path === "/api/auth/refresh") { await fulfillJson(route, { accessToken: `mock-policy-${currentUser}`, expiresIn: 900, mustChangePassword: false }); return; } if (path === "/api/me") { await fulfillJson(route, buildMe(currentUser)); return; } if (path === "/api/system/navigation") { await fulfillJson(route, { workspace: { id: "workspace-policy-ack", persisted: true, profile: "small_team_internal", orgMode: "single_company", primaryEntryMode: "password", notificationStrategy: "in_app", devicePriority: "desktop", moduleVisibility: {}, createdAt: null, updatedAt: null, updatedBy: null, }, sections: { main: [{ key: "announcements", href: "/announcements", label: "公告", icon: "campaign", placement: "main", permissionCodes: [], featureFlagKey: null }], admin: currentUser === "admin" ? [{ key: "announcementsAdmin", href: "/system/announcements", label: "公告管理", icon: "campaign", placement: "admin", permissionCodes: ["system.settings"], featureFlagKey: null }] : [], backup: [], }, recommendedRolePresets: [], }); return; } if (path === "/api/portal/home") { await fulfillJson(route, { workspace: { id: "workspace-policy-ack", persisted: true, profile: "small_team_internal" }, greeting: { name: buildMe(currentUser).name }, cards: [], pendingTasks: { count: 0, href: "/workflow/tasks" }, myApplications: [], riskSummary: null, announcements: [], quickLinks: [], todayPresence: null, teamPresence: null, weeklyReport: null, weeklySummary: null, todoSummary: null, projectSummary: null, peopleLifecycle: null, adoptionSummary: null, recentActivities: [], teamStatusBoard: [], }); return; } if (path === "/api/notifications") { await fulfillJson(route, { items: [], unreadCount: 0, page: 1, pageSize: 20, total: 0 }); return; } if (path === "/api/announcements" && request.method() === "GET") { const manage = url.searchParams.get("manage") === "1"; await fulfillJson(route, { items: announcements.map((item) => manage ? mapAnnouncementForAdmin(item) : mapAnnouncementForUser(item, currentUser)), }); return; } if (path === "/api/announcements" && request.method() === "POST") { const payload = request.postDataJSON() as { title: string; summary: string; linkUrl?: string | null; priority?: number; category?: "general" | "policy"; requiresAcknowledgement?: boolean; acknowledgementStatement?: string | null; acknowledgementDueAt?: string | null; }; const created: MockAnnouncement = { id: `policy-ann-${announcements.length + 1}`, title: payload.title, summary: payload.summary, linkUrl: payload.linkUrl ?? null, startsAt: null, endsAt: null, enabled: true, priority: payload.priority ?? 100, category: payload.category ?? "general", requiresAcknowledgement: Boolean(payload.requiresAcknowledgement), acknowledgementStatement: payload.acknowledgementStatement ?? null, acknowledgementDueAt: payload.acknowledgementDueAt ?? null, createdAt: "2026-05-28T02:00:00.000Z", updatedAt: "2026-05-28T02:00:00.000Z", acknowledgedEmployeeIds: new Set(), }; announcements.push(created); await fulfillJson(route, { item: mapAnnouncementForAdmin(created) }, 201); return; } if (path.endsWith("/acknowledge") && request.method() === "POST") { const segments = path.split("/"); const id = segments[segments.length - 2]; const announcement = announcements.find((item) => item.id === id); if (announcement) { announcement.acknowledgedEmployeeIds.add(buildMe(currentUser).id); } await fulfillJson(route, { item: { id: "ack-1", announcementId: id, status: "acknowledged", acknowledgedAt: "2026-05-28T03:00:00.000Z" } }, 201); return; } await fulfillJson(route, {}); }); } async function clearBrowserSession(page: Page) { await page.context().clearCookies(); await page.evaluate(() => { window.localStorage.clear(); window.sessionStorage.clear(); }); } for (const scenario of [ { title: "制度签收公告:管理员创建、员工签收、管理员看到统计变化", dueDate: "2026-06-03", status: "待签收", action: "确认签收" }, { title: "制度签收公告:逾期员工补签后管理员统计同步变化", dueDate: "2026-05-27", status: "已逾期", action: "逾期补签" }, ]) { test(scenario.title, async ({ page }) => { await page.clock.setFixedTime(SCENARIO_NOW); await mockPolicyAckApis(page); const title = `自动化制度签收 ${scenario.dueDate}`; await login(page, ADMIN); await page.goto("/system/announcements"); await page.getByRole("tab", { name: "制度签收" }).click(); await page.getByPlaceholder("例如:费用报销制度签收").fill(title); await page.getByPlaceholder("写清楚适用范围、入口和处理要求。").fill("请全员阅读并确认制度签收流程。"); await page.locator('input[type="date"]').fill(scenario.dueDate); await page.getByRole("button", { name: "创建制度签收公告" }).click(); await expect(page.getByText(title)).toBeVisible(); await expect(page.getByText("已签 0/2 · 待签 2")).toBeVisible(); await clearBrowserSession(page); await login(page, EMPLOYEE); await page.goto("/announcements"); await expect(page.getByText(title)).toBeVisible(); await expect(page.getByText(scenario.status, { exact: true })).toBeVisible(); await page.getByLabel("我已阅读并理解该制度内容,确认按公司制度执行。").check(); await page.getByRole("button", { name: scenario.action, exact: true }).click(); await expect(page.getByText("已签收")).toBeVisible(); await expect(page.getByText(/完成本人签收/)).toBeVisible(); await clearBrowserSession(page); await login(page, ADMIN); await page.goto("/system/announcements"); await expect(page.getByText(title)).toBeVisible(); await expect(page.getByText("已签 1/2 · 待签 1")).toBeVisible(); }); }