import { expect, test } from "@playwright/test"; import { EMPLOYEE, login } from "./helpers/auth"; import { TEST_IDS } from "./helpers/testIds"; const ADMIN = { identifier: "admin@hyinda.com", password: "AdminPassword1234", name: "Admin" }; function envelope(data: unknown) { return { requestId: "mock-request", data, }; } function mockMe(page: import("@playwright/test").Page, roleCodes: string[], name: string) { return page.route("**/api/me", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope({ id: "employee-mock-1", employeeNo: "E001", name, phone: "13800000000", email: `${name.toLowerCase()}@hyinda.com`, departmentId: "dept-1", directManagerId: null, status: "active", notificationPushEnabled: true, roleCodes, profileCompleteness: { hasEmergencyContact: true, hasBankAccount: true, hasAvatar: false, hasIdNumber: false, score: 50, }, wecomBinding: null, })), }); }); } function mockNavigation(page: import("@playwright/test").Page, mode: "employee" | "manager") { return page.route("**/api/system/navigation", async (route) => { const sections = mode === "manager" ? { main: [ { key: "portal", href: "/portal", label: "Portal", icon: "home_app_logo", placement: "main", permissionCodes: [], featureFlagKey: null }, { key: "team", href: "/team", label: "团队", icon: "groups", placement: "main", permissionCodes: [], featureFlagKey: null }, { key: "leave", href: "/leave-requests", label: "请假", icon: "event_available", placement: "main", permissionCodes: [], featureFlagKey: null }, { key: "expense", href: "/reimbursements", label: "报销", icon: "request_quote", placement: "main", permissionCodes: [], featureFlagKey: null }, { key: "assets", href: "/assets", label: "资产", icon: "inventory_2", placement: "main", permissionCodes: [], featureFlagKey: null }, { key: "weekly", href: "/weekly-reports", label: "周报", icon: "article", placement: "main", permissionCodes: [], featureFlagKey: null }, { key: "todos", href: "/todos", label: "待办", icon: "check_circle", placement: "main", permissionCodes: [], featureFlagKey: null }, { key: "announcements", href: "/announcements", label: "公告", icon: "campaign", placement: "main", permissionCodes: [], featureFlagKey: null }, { key: "projects", href: "/projects", label: "项目", icon: "workspaces", placement: "main", permissionCodes: [], featureFlagKey: null }, ], admin: [ { key: "workspace", href: "/system/workspace", label: "工作区设置", icon: "tune", placement: "admin", permissionCodes: [], featureFlagKey: null }, { key: "people", href: "/people", label: "人员管理", icon: "group", placement: "admin", permissionCodes: [], featureFlagKey: null }, { key: "workflow", href: "/system/workflow-definitions", label: "流程定义", icon: "schema", placement: "admin", permissionCodes: [], featureFlagKey: null }, { key: "settings", href: "/system/settings", label: "系统设置", icon: "settings", placement: "admin", permissionCodes: [], featureFlagKey: null }, { key: "feature-flags", href: "/system/feature-flags", label: "功能开关", icon: "toggle_on", placement: "admin", permissionCodes: [], featureFlagKey: null }, { key: "audit", href: "/audit-logs", label: "审计日志", icon: "manage_search", placement: "admin", permissionCodes: [], featureFlagKey: null }, ], backup: [ { key: "purchase", href: "/purchase-requests", label: "采购申请", icon: "shopping_cart", placement: "main", permissionCodes: [], featureFlagKey: null }, { key: "travel", href: "/travel-requests", label: "差旅申请", icon: "flight", placement: "main", permissionCodes: [], featureFlagKey: null }, ], } : { main: [ { key: "portal", href: "/portal", label: "Portal", icon: "home_app_logo", placement: "main", permissionCodes: [], featureFlagKey: null }, { key: "team", href: "/team", label: "团队", icon: "groups", placement: "main", permissionCodes: [], featureFlagKey: null }, { key: "leave", href: "/leave-requests", label: "请假", icon: "event_available", placement: "main", permissionCodes: [], featureFlagKey: null }, { key: "expense", href: "/reimbursements", label: "报销", icon: "request_quote", placement: "main", permissionCodes: [], featureFlagKey: null }, { key: "assets", href: "/assets", label: "资产", icon: "inventory_2", placement: "main", permissionCodes: [], featureFlagKey: null }, { key: "weekly", href: "/weekly-reports", label: "周报", icon: "article", placement: "main", permissionCodes: [], featureFlagKey: null }, { key: "todos", href: "/todos", label: "待办", icon: "check_circle", placement: "main", permissionCodes: [], featureFlagKey: null }, { key: "announcements", href: "/announcements", label: "公告", icon: "campaign", placement: "main", permissionCodes: [], featureFlagKey: null }, { key: "projects", href: "/projects", label: "项目", icon: "workspaces", placement: "main", permissionCodes: [], featureFlagKey: null }, ], admin: [], backup: [], }; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope({ workspace: { id: "workspace-1", persisted: true, profile: "small_team_internal", orgMode: "single_company", primaryEntryMode: "password", notificationStrategy: "in_app", devicePriority: "desktop", moduleVisibility: {}, createdAt: null, updatedAt: null, updatedBy: null, }, sections, recommendedRolePresets: [], })), }); }); } function mockAttendanceToday(page: import("@playwright/test").Page) { return page.route("**/api/attendance/me/today", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope({ workDate: "2026-03-22", needClockIn: true, exemptionReason: null, item: null, })), }); }); } function mockPortalHome(page: import("@playwright/test").Page, mode: "employee" | "manager") { return page.route("**/api/portal/home", async (route) => { const data = mode === "manager" ? { workspace: { id: "workspace-1", persisted: true, profile: "small_team_internal" }, greeting: { name: "Manager" }, cards: [ { key: "pending_approvals", title: "审批待办", value: 3, href: "/workflow/tasks", severity: "warning" }, { key: "team_presence", title: "今日团队状态", value: "6/8 在岗", href: "/team", severity: "warning" }, { key: "weekly_missing", title: "未提交周报", value: 2, href: "/weekly-reports", severity: "warning" }, { key: "weekly_help", title: "需协助员工", value: 1, href: "/weekly-reports", severity: "high" }, { key: "people_lifecycle", title: "入/离职待办", value: 1, href: "/people", severity: "warning" }, { key: "asset_return_pending", title: "资产待归还", value: 1, href: "/people", severity: "warning" }, { key: "project_milestones", title: "逾期里程碑", value: 2, href: "/projects", severity: "warning" }, { key: "project_risks", title: "项目风险", value: 1, href: "/projects", severity: "high" }, ], pendingTasks: { count: 3, href: "/workflow/tasks" }, myApplications: [ { domain: "reimbursement", count: 1, href: "/reimbursements" }, { domain: "leave", count: 0, href: "/leave-requests" }, ], riskSummary: { openCount: 1, href: "/ap-operations" }, announcements: [{ id: "announcement-1", title: "试点公告", summary: "今天统一从 Portal 进入。", href: null }], quickLinks: [{ key: "projects", title: "项目", href: "/projects" }], todayPresence: null, teamPresence: { date: "2026-03-22", href: "/team", office: 6, remote: 1, out: 0, travel: 1, leave: 1, unspecified: 1, total: 8 }, weeklyReport: null, weeklySummary: { weekStart: "2026-03-16", submittedCount: 6, missingCount: 2, needsHelpCount: 1, href: "/weekly-reports" }, todoSummary: { openCount: 4, highPriorityCount: 1, href: "/todos" }, projectSummary: { myCount: 5, overdueMilestonesCount: 2, highPriorityTaskCount: 2, openRiskCount: 1, followUpDueTodayCount: 2, closurePendingProjectCount: 1, href: "/projects" }, peopleLifecycle: { onboardingCount: 1, offboardingCount: 0, assetReturnPendingCount: 1, href: "/people" }, adoptionSummary: { activeEmployeeCount: 8, login7dCount: 8, weeklySubmittedCount: 6, weeklyMissingCount: 2, presenceUpdatedCount: 7, presenceMissingCount: 1, announcementCount: 1, login7dRate: 100, weeklySubmissionRate: 75, presenceCoverageRate: 87.5, hints: ["本周仍有 2 人未提交周报"] }, recentActivities: [ { id: "activity-1", type: "reimbursement.submit", actorName: "张三", summary: "张三 提交了报销单", createdAt: "2026-03-22T09:00:00.000Z", href: "/reimbursements/reimbursement-1" }, { id: "activity-2", type: "attendance.clock_in", actorName: "李四", summary: "李四 完成了上班打卡", createdAt: "2026-03-22T08:50:00.000Z", href: "/attendance" }, ], teamStatusBoard: [ { employeeId: "employee-1", name: "张三", avatarUrl: null, presenceStatus: "office", attendanceClockIn: "2026-03-22T00:55:00.000Z", attendanceClockOut: null, onLeave: false, leaveType: null }, { employeeId: "employee-2", name: "李四", avatarUrl: null, presenceStatus: "leave", attendanceClockIn: null, attendanceClockOut: null, onLeave: true, leaveType: "年假" }, ], } : { workspace: { id: "workspace-1", persisted: true, profile: "small_team_internal" }, greeting: { name: "Employee" }, cards: [ { key: "my_todos", title: "我的待办", value: 4, href: "/todos", severity: "warning" }, { key: "today_presence", title: "今日状态", value: "在岗", href: "/team", severity: "normal" }, { key: "my_weekly_report", title: "本周周报", value: "待填写", href: "/weekly-reports", severity: "warning" }, { key: "my_projects", title: "我的项目", value: 2, href: "/projects", severity: "warning" }, { key: "my_leave", title: "我的请假", value: 1, href: "/leave-requests", severity: "normal" }, { key: "my_reimbursements", title: "我的报销", value: 2, href: "/reimbursements", severity: "normal" }, { key: "my_assets", title: "我的资产", value: 1, href: "/assets", severity: "normal" }, ], pendingTasks: { count: 0, href: "/workflow/tasks" }, myApplications: [ { domain: "reimbursement", count: 2, href: "/reimbursements" }, { domain: "leave", count: 1, href: "/leave-requests" }, ], riskSummary: null, announcements: [{ id: "announcement-1", title: "本周安排", summary: "本周统一回填周报。", href: null }], quickLinks: [{ key: "weekly", title: "周报", href: "/weekly-reports" }], todayPresence: { date: "2026-03-22", status: "office", label: "在岗", note: "今天在公司办公", href: "/team" }, teamPresence: null, weeklyReport: { weekStart: "2026-03-16", status: "draft", needsHelp: false, href: "/weekly-reports" }, weeklySummary: null, todoSummary: { openCount: 4, highPriorityCount: 1, href: "/todos" }, projectSummary: { myCount: 2, overdueMilestonesCount: 1, highPriorityTaskCount: 1, openRiskCount: 0, followUpDueTodayCount: 1, closurePendingProjectCount: 0, href: "/projects" }, peopleLifecycle: null, adoptionSummary: null, recentActivities: [ { id: "activity-1", type: "leave.submit", actorName: "Employee", summary: "Employee 提交了请假单", createdAt: "2026-03-22T09:00:00.000Z", href: "/leave-requests" }, ], teamStatusBoard: [ { employeeId: "employee-1", name: "Employee", avatarUrl: null, presenceStatus: "office", attendanceClockIn: "2026-03-22T00:55:00.000Z", attendanceClockOut: null, onLeave: false, leaveType: null }, { employeeId: "employee-2", name: "同事", avatarUrl: null, presenceStatus: "remote", attendanceClockIn: null, attendanceClockOut: null, onLeave: false, leaveType: null }, ], }; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope(data)), }); }); } test("Portal 员工视角优先显示今日动作顺序", async ({ page }) => { await mockNavigation(page, "employee"); await mockPortalHome(page, "employee"); await mockAttendanceToday(page); await mockMe(page, ["employee"], "Employee"); await login(page, EMPLOYEE); await expect(page.locator('[aria-label^="portal-card-"]').first()).toBeVisible(); await expect(page.locator('[aria-label^="portal-card-"]').first()).toBeVisible(); const labels = await page.locator('[aria-label^="portal-card-"]').evaluateAll((nodes) => nodes.map((node) => node.getAttribute("aria-label")), ); expect(labels).toEqual([ "portal-card-my_todos", "portal-card-today_presence", "portal-card-my_weekly_report", "portal-card-my_projects", ]); await expect(page.getByText("我的申请与资产")).toBeVisible(); await expect(page.getByRole("heading", { name: "公告" })).toBeVisible(); await expect(page.getByRole("heading", { name: "快捷操作栏" })).toBeVisible(); await expect(page.getByRole("heading", { name: "团队全员状态" })).toBeVisible(); await expect(page.getByRole("heading", { name: "今日待办聚合" })).toBeVisible(); await expect(page.getByRole("heading", { name: "近期动态流" })).toBeVisible(); }); test("Portal 管理视角重排正确,备用模块默认折叠", async ({ page }) => { await mockNavigation(page, "manager"); await mockPortalHome(page, "manager"); await mockAttendanceToday(page); await mockMe(page, ["admin"], "Admin"); await login(page, ADMIN); await expect(page.locator('[aria-label^="portal-card-"]').first()).toBeVisible(); const labels = await page.locator('[aria-label^="portal-card-"]').evaluateAll((nodes) => nodes.map((node) => node.getAttribute("aria-label")), ); expect(labels).toEqual([ "portal-card-pending_approvals", "portal-card-team_presence", "portal-card-weekly_missing", "portal-card-weekly_help", ]); await expect(page.getByText("今日管理重点")).toBeVisible(); await expect(page.getByText("试点观测摘要")).toBeVisible(); await expect(page.getByRole("link", { name: "采购申请" })).not.toBeVisible(); await page.locator("summary").filter({ hasText: "备用模块" }).click(); await expect(page.getByRole("link", { name: "采购申请" })).toBeVisible(); }); test("Portal 新员工引导先显示欢迎弹窗,再显示快速上手清单", async ({ page }) => { await page.addInitScript(() => { window.localStorage.setItem("hyb_oa_show_welcome", "1"); window.localStorage.setItem("hyb_oa_onboarding_active", "1"); window.localStorage.removeItem("hyb_oa_getting_started_dismissed"); }); await mockNavigation(page, "employee"); await mockPortalHome(page, "employee"); await mockAttendanceToday(page); await mockMe(page, ["employee"], "Employee"); await login(page, EMPLOYEE); await expect(page.getByTestId(TEST_IDS.onboarding.welcomeDialog)).toBeVisible(); await page.getByTestId(TEST_IDS.onboarding.welcomeClose).click(); await expect(page.getByTestId(TEST_IDS.onboarding.welcomeDialog)).not.toBeVisible(); await expect(page.getByTestId(TEST_IDS.onboarding.gettingStartedCard)).toBeVisible(); await page.getByTestId(TEST_IDS.onboarding.gettingStartedDismiss).click(); await expect(page.getByTestId(TEST_IDS.onboarding.gettingStartedCard)).not.toBeVisible(); }); test("Portal 域管理角色也进入经理视图", async ({ page }) => { await mockNavigation(page, "manager"); await mockPortalHome(page, "manager"); await mockAttendanceToday(page); await mockMe(page, ["administration"], "Administration"); await login(page, EMPLOYEE); await expect(page.getByText("今日管理重点")).toBeVisible(); await expect(page.locator('[aria-label="portal-card-pending_approvals"]')).toBeVisible(); await expect(page.locator('[aria-label="portal-card-team_presence"]')).toBeVisible(); }); test("企微 WebView 下顶栏显示入口提示", async ({ page }) => { await page.addInitScript(() => { Object.defineProperty(window.navigator, "userAgent", { configurable: true, value: "Mozilla/5.0 wxwork/4.1.20 MicroMessenger/7.0.1", }); }); await mockNavigation(page, "employee"); await mockPortalHome(page, "employee"); await mockAttendanceToday(page); await mockMe(page, ["employee"], "Employee"); await login(page, EMPLOYEE); await expect(page.getByTestId("topbar-wecom-entry-badge")).toBeVisible(); });