import { test, expect } from "./helpers/fixtures"; import type { Page, Route } from "@playwright/test"; import { ADMIN, EMPLOYEE, login, logout, type E2EUser } from "./helpers/auth"; import { openNavLink } from "./helpers/navigation"; test.use({ serviceWorkers: "block" }); function envelope(data: unknown) { return { requestId: "mock-leave-boundary", data, }; } async function fulfillJson(route: Route, data: unknown, status = 200, headers: Record = {}) { await route.fulfill({ status, contentType: "application/json", headers, body: JSON.stringify(envelope(data)), }); } function buildMe(user: E2EUser) { const isAdmin = user.identifier === ADMIN.identifier; return { id: isAdmin ? "employee-admin" : "employee-user", employeeNo: isAdmin ? "A001" : "E001", name: user.name, phone: "13800000000", email: user.identifier, departmentId: "department-leave", directManagerId: null, status: "active", notificationPushEnabled: true, roleCodes: isAdmin ? ["admin"] : ["employee"], profileCompleteness: { hasEmergencyContact: true, hasBankAccount: true, hasAvatar: false, hasIdNumber: false, score: 50, }, wecomBinding: null, }; } function leaveRequestFor(user: E2EUser) { const me = buildMe(user); return { id: "leave-boundary-1", ownerId: me.id, departmentId: me.departmentId, status: "draft", startDate: "2026-05-31", endDate: "2026-05-31", startSession: "am", endSession: "pm", requestedDays: 1, reason: "企业微信同步记录", attachment: null, leaveType: { id: "leave-type-annual", code: "annual", name: "年假", requiresAttachment: false, attachmentMimeTypes: [], }, owner: { id: me.id, employeeNo: me.employeeNo, name: me.name, }, pendingWorkflow: null, approvalHistory: [], createdAt: "2026-05-30T09:00:00.000Z", updatedAt: "2026-05-30T09:30:00.000Z", }; } async function mockLeaveBoundaryApis(page: Page) { let activeUser = EMPLOYEE; 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: true, entryEnabled: true, oauthEnabled: true, corpId: "ww-leave-boundary", agentId: "1000002", loginUrl: "/api/auth/wecom/login?returnTo=%2Fportal", bindEnabled: true, defaultReturnTo: "/portal", status: "ready", statusReasons: [], }); return; } if (path === "/api/auth/login") { const payload = request.postDataJSON() as { identifier?: string }; activeUser = payload.identifier === ADMIN.identifier ? ADMIN : EMPLOYEE; await fulfillJson( route, { accessToken: `mock-token-${activeUser.employeeNo}`, expiresIn: 900, mustChangePassword: false }, 200, { "set-cookie": "has_session=1; Path=/; SameSite=Strict" }, ); return; } if (path === "/api/auth/refresh") { await fulfillJson(route, { accessToken: `mock-token-${activeUser.employeeNo}`, expiresIn: 900 }); return; } if (path === "/api/auth/logout") { await fulfillJson(route, {}, 200, { "set-cookie": "has_session=; Path=/; Max-Age=0" }); return; } if (path === "/api/me") { await fulfillJson(route, buildMe(activeUser)); return; } if (path === "/api/system/navigation") { await fulfillJson(route, { workspace: { id: "workspace-leave-boundary", persisted: true, profile: "small_team_internal", orgMode: "single_company", primaryEntryMode: "wecom", notificationStrategy: "in_app", devicePriority: "desktop", moduleVisibility: {}, createdAt: null, updatedAt: null, updatedBy: null, }, sections: { main: [ { key: "portal", href: "/portal", label: "工作台", icon: "home_app_logo", placement: "main", permissionCodes: [], featureFlagKey: null }, { key: "expense", href: "/reimbursements", label: "报销", icon: "request_quote", placement: "main", permissionCodes: [], featureFlagKey: null }, { key: "leave", href: "/leave-requests", label: "请假记录", icon: "event_available", placement: "main", permissionCodes: [], featureFlagKey: null }, ], admin: [], backup: [], }, recommendedRolePresets: [], }); return; } if (path === "/api/portal/home") { await fulfillJson(route, { workspace: { id: "workspace-leave-boundary", persisted: true, profile: "small_team_internal" }, greeting: { name: activeUser.name }, cards: [], pendingTasks: { count: 0, href: "/workflow/tasks" }, myApplications: [{ domain: "leave", count: 1, href: "/leave-requests" }], riskSummary: null, announcements: [], quickLinks: [], todayPresence: { date: "2026-05-30", status: "office", label: "在岗", note: null, href: "/team" }, teamPresence: null, weeklyReport: null, weeklySummary: null, todoSummary: null, projectSummary: null, peopleLifecycle: null, adoptionSummary: null, recentActivities: [], teamStatusBoard: [], }); return; } if (path === "/api/attendance/me/today") { await fulfillJson(route, { workDate: "2026-05-30", needClockIn: false, exemptionReason: "wecom_owned", item: null, }); return; } if (path === "/api/notifications") { await fulfillJson(route, { page: 1, pageSize: 10, total: 0, unreadOnly: false, unreadCount: 0, items: [], }); return; } if (path === "/api/leave-types") { await fulfillJson(route, { items: [ { 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-30T00:00:00.000Z", updatedAt: "2026-05-30T00:00:00.000Z", }, ], }); return; } if (path === "/api/leave-balances") { await fulfillJson(route, { employeeId: buildMe(activeUser).id, items: [ { leaveType: { id: "leave-type-annual", code: "annual", name: "年假", description: null, enabled: true, requiresAttachment: false, attachmentMimeTypes: [], sortOrder: 1, policy: null, createdAt: null, updatedAt: null, }, policy: { id: "leave-policy-annual", annualQuotaDays: 15, enabled: true, description: null, }, 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: 10, total: 1, scope: "own", items: [leaveRequestFor(activeUser)], }); return; } await fulfillJson(route, {}); }); } test("E2E 请假边界:员工只看企微主办记录,不显示 OA 日常提交入口", async ({ page }) => { await mockLeaveBoundaryApis(page); await login(page, EMPLOYEE); await (await openNavLink(page, "请假记录")).click(); await expect(page.locator("h1", { hasText: "请假记录" })).toBeVisible(); await expect(page.getByText("企业微信主办请假、补卡、外出和加班")).toBeVisible(); await expect(page.getByRole("heading", { name: "日常请假入口" })).toBeVisible(); await expect(page.locator("#leave-type")).toHaveCount(0); await expect(page.getByRole("button", { name: "创建应急草稿" })).toHaveCount(0); await expect(page.getByRole("button", { name: "提交应急审批" })).toHaveCount(0); await logout(page, EMPLOYEE.name); }); test("E2E 请假边界:管理员保留 OA 应急草稿入口", async ({ page }) => { await mockLeaveBoundaryApis(page); await login(page, ADMIN); await page.goto("/leave-requests"); await expect(page.locator("h1", { hasText: "请假记录" })).toBeVisible(); await expect(page.getByRole("heading", { name: "OA 应急请假草稿" })).toBeVisible(); await expect(page.locator("#leave-type")).toBeVisible(); await expect(page.getByRole("button", { name: "创建应急草稿" })).toBeVisible(); });