import { expect, test } from "@playwright/test"; function envelope(data: unknown) { return { requestId: "mock-request", data, }; } function mockCommonAuth(page: import("@playwright/test").Page, options: { mustChangePassword?: boolean; meId?: string; meName?: string; meEmployeeNo?: string; permissions?: string[]; roleCodes?: string[]; }) { const mustChangePassword = options.mustChangePassword === true; const meId = options.meId ?? "employee-admin-1"; const meName = options.meName ?? "Admin"; const meEmployeeNo = options.meEmployeeNo ?? "HYD000001"; const permissions = options.permissions ?? ["system.users", "system.accounts.manage", "system.roles.manage"]; const roleCodes = options.roleCodes ?? ["admin"]; return Promise.all([ page.route("**/api/auth/sso/providers", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope([])), }); }), page.route("**/api/auth/wecom/config", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify( envelope({ enabled: false, entryEnabled: false, oauthEnabled: false, corpId: "", agentId: "", loginUrl: null, bindEnabled: false, defaultReturnTo: "/portal", status: "disabled", statusReasons: ["disabled"], }), ), }); }), page.route("**/api/auth/login", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify( envelope({ accessToken: "test-access-token", expiresIn: 900, mustChangePassword, wecomBindingStatus: null, }), ), }); }), page.route("**/api/me", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify( envelope({ id: meId, employeeNo: meEmployeeNo, name: meName, phone: "13800000000", email: "admin@hyinda.com", departmentId: "dept-1", directManagerId: null, status: "active", notificationPushEnabled: true, roleCodes, permissions, profileCompleteness: { hasEmergencyContact: true, hasBankAccount: true, hasAvatar: false, hasIdNumber: false, score: 50, }, wecomBinding: null, }), ), }); }), page.route("**/api/system/navigation", async (route) => { 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: { main: [{ key: "portal", href: "/portal", label: "工作台", icon: "home_app_logo", placement: "main", permissionCodes: [], featureFlagKey: null }], admin: [{ key: "settings", href: "/system/settings", label: "管理", icon: "settings", placement: "admin", permissionCodes: ["system.users"], featureFlagKey: null }], backup: [], }, recommendedRolePresets: [], }), ), }); }), page.route((url) => url.pathname === "/api/notifications", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify( envelope({ page: 1, pageSize: 10, total: 0, unreadOnly: true, unreadCount: 0, items: [], }), ), }); }), ]); } type MockUser = { id: string; employeeNo: string; name: string; phone: string; email: string; status: "active" | "archived"; departmentId: string; title: string; roleCodes: string[]; createdAt: string; updatedAt: string; }; function buildMockUser(id: string, name: string, roleCodes: string[]): MockUser { return { id, employeeNo: `MOCK-${id}`, name, phone: "00000000000", email: `${id}@example.test`, status: "active", departmentId: "dept-1", title: "员工", roleCodes, createdAt: "2026-07-18T00:00:00.000Z", updatedAt: "2026-07-18T00:00:00.000Z", }; } async function mockUserManagementData( page: import("@playwright/test").Page, items: MockUser[], ) { await Promise.all([ page.route((url) => url.pathname === "/api/users", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope({ page: 1, pageSize: 20, total: items.length, items })), }); }), page.route((url) => url.pathname === "/api/departments", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope({ includeArchived: false, items: [], tree: [] })), }); }), page.route((url) => url.pathname === "/api/employees", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope({ page: 1, pageSize: 200, total: 0, items: [] })), }); }), ]); } async function loginToUserManagement(page: import("@playwright/test").Page) { await page.goto("/login?redirect=%2Fsystem%2Fusers"); await page.getByTestId("login-identifier").fill("operator@example.test"); await page.getByTestId("login-password").fill("SyntheticPassword1234"); await page.getByTestId("login-submit").click(); await expect(page).toHaveURL(/\/system\/users/); } test("E2E 用户开通:新建用户时由系统生成工号和一次性口令", async ({ page }) => { let createPayload: Record | null = null; await mockCommonAuth(page, {}); await page.route("**/api/users?**", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify( envelope({ page: 1, pageSize: 20, total: 0, items: [], }), ), }); }); await page.route("**/api/departments?includeArchived=0", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify( envelope({ includeArchived: false, items: [{ id: "dept-1", name: "行政部", parentId: null, archivedAt: null, createdAt: null, updatedAt: null }], tree: [], }), ), }); }); await page.route("**/api/employees?page=1&pageSize=200&status=active", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify( envelope({ page: 1, pageSize: 200, total: 0, items: [], }), ), }); }); await page.route("**/api/users", async (route) => { if (route.request().method() !== "POST") { await route.fallback(); return; } createPayload = route.request().postDataJSON() as Record; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify( envelope({ id: "user-1", employeeNo: "HYD000123", name: "张三", phone: "13800000001", email: "zhangsan@example.com", status: "active", departmentId: "dept-1", title: "员工", roleCodes: ["employee"], createdAt: "2026-05-04T12:00:00.000Z", updatedAt: "2026-05-04T12:00:00.000Z", oneTimePassword: "ResetA1abc123def456", }), ), }); }); await page.goto("/login?redirect=%2Fsystem%2Fusers"); await page.getByTestId("login-identifier").fill("admin@hyinda.com"); await page.getByTestId("login-password").fill("AdminPassword1234"); await page.getByTestId("login-submit").click(); await expect(page).toHaveURL(/\/system\/users/); await page.getByTestId("users-create-open").click(); await expect(page.getByTestId("users-create-employeeNo")).toHaveCount(0); await expect(page.getByTestId("users-create-password")).toHaveCount(0); await page.getByTestId("users-create-name").fill("张三"); await page.getByTestId("users-create-phone").fill("13800000001"); await page.getByTestId("users-create-email").fill("zhangsan@example.com"); await page.getByTestId("users-create-submit").click(); expect(createPayload).toEqual({ name: "张三", phone: "13800000001", email: "zhangsan@example.com", departmentId: undefined, directManagerId: null, roles: ["employee"], }); await expect(page.getByTestId("users-reset-result")).toContainText("工号=HYD000123"); await expect(page.getByTestId("users-reset-result")).toContainText("ResetA1abc123def456"); }); test("E2E 账号保护:OA 系统管理员只能操作普通账号", async ({ page }) => { const currentId = "system-admin-current"; const ordinary = buildMockUser("ordinary-user", "普通员工", ["employee"]); const protectedAdmin = buildMockUser("protected-admin", "应急账号甲", ["employee", "admin"]); const protectedSystemAdmin = buildMockUser("protected-system-admin", "运维账号乙", ["employee", "system_admin"]); const currentSystemAdmin = buildMockUser(currentId, "当前运维账号", ["employee", "system_admin"]); await mockCommonAuth(page, { meId: currentId, meName: "当前运维账号", roleCodes: ["system_admin"], permissions: ["system.accounts.manage", "system.roles.manage"], }); await mockUserManagementData(page, [ordinary, protectedAdmin, protectedSystemAdmin, currentSystemAdmin]); await loginToUserManagement(page); const ordinaryRow = page.getByTestId("users-row").filter({ hasText: ordinary.name }); await expect(ordinaryRow.getByTestId("users-toggle")).toHaveCount(1); await expect(ordinaryRow.getByTestId("users-reset-password")).toHaveCount(1); await expect(ordinaryRow.getByTestId("users-edit-roles")).toHaveCount(1); for (const user of [protectedAdmin, protectedSystemAdmin, currentSystemAdmin]) { const row = page.getByTestId("users-row").filter({ hasText: user.name }); await expect(row.getByTestId("users-toggle")).toHaveCount(0); await expect(row.getByTestId("users-reset-password")).toHaveCount(0); await expect(row.getByTestId("users-edit-roles")).toHaveCount(0); await expect(row.getByTestId("users-protected-account")).toHaveText("受保护账号"); } }); test("E2E 账号保护:应急超级管理员可操作其他受保护账号但不能停用或改自己的角色", async ({ page }) => { const currentId = "employee-admin-1"; const otherAdmin = buildMockUser("other-admin", "其他应急账号", ["employee", "admin"]); const otherSystemAdmin = buildMockUser("other-system-admin", "其他运维账号", ["employee", "system_admin"]); const currentAdmin = buildMockUser(currentId, "当前应急账号", ["employee", "admin"]); await mockCommonAuth(page, { meId: currentId, meName: currentAdmin.name }); await mockUserManagementData(page, [otherAdmin, otherSystemAdmin, currentAdmin]); await loginToUserManagement(page); for (const user of [otherAdmin, otherSystemAdmin]) { const row = page.getByTestId("users-row").filter({ hasText: user.name }); await expect(row.getByTestId("users-toggle")).toHaveCount(1); await expect(row.getByTestId("users-reset-password")).toHaveCount(1); await expect(row.getByTestId("users-edit-roles")).toHaveCount(1); await expect(row.getByTestId("users-protected-account")).toHaveCount(0); } const currentRow = page.getByTestId("users-row").filter({ hasText: currentAdmin.name }); await expect(currentRow.getByTestId("users-toggle")).toHaveCount(0); await expect(currentRow.getByTestId("users-edit-roles")).toHaveCount(0); await expect(currentRow.getByTestId("users-reset-password")).toHaveCount(1); }); test("E2E 首次登录:一次性口令登录后强制改密", async ({ page }) => { let changePasswordPayload: Record | null = null; await mockCommonAuth(page, { mustChangePassword: true, meName: "新员工", meEmployeeNo: "HYD000123" }); await page.route("**/api/auth/change-password", async (route) => { changePasswordPayload = route.request().postDataJSON() as Record; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify( envelope({ accessToken: "post-change-token", expiresIn: 900, mustChangePassword: false, }), ), }); }); await page.route("**/api/portal/home", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify( envelope({ workspace: { id: "workspace-1", persisted: true, profile: "small_team_internal" }, greeting: { name: "新员工" }, cards: [], pendingTasks: { count: 0, href: "/workflow/tasks" }, myApplications: [], riskSummary: null, announcements: [], quickLinks: [], todayPresence: null, teamPresence: null, weeklyReport: null, weeklySummary: null, todoSummary: { openCount: 0, highPriorityCount: 0, href: "/todos" }, projectSummary: { myCount: 0, overdueMilestonesCount: 0, highPriorityTaskCount: 0, openRiskCount: 0, followUpDueTodayCount: 0, closurePendingProjectCount: 0, href: "/projects" }, peopleLifecycle: null, adoptionSummary: null, recentActivities: [], teamStatusBoard: [], }), ), }); }); await page.goto("/login"); await page.getByTestId("login-identifier").fill("new.user@hyinda.com"); await page.getByTestId("login-password").fill("ResetA1abc123def456"); await page.getByTestId("login-submit").click(); await expect(page).toHaveURL(/\/change-password/); await page.getByTestId("change-password-old").fill("ResetA1abc123def456"); await page.getByTestId("change-password-new").fill("NewPassword1234"); await page.getByLabel("确认新密码").fill("NewPassword1234"); await page.getByTestId("change-password-old").focus(); await page.keyboard.press("Tab"); await expect(page.getByTestId("change-password-new")).toBeFocused(); await page.keyboard.press("Tab"); await expect(page.getByLabel("确认新密码")).toBeFocused(); await page.keyboard.press("Tab"); await expect(page.getByTestId("change-password-submit")).toBeFocused(); await page.getByTestId("change-password-submit").click(); expect(changePasswordPayload).toEqual({ oldPassword: "ResetA1abc123def456", newPassword: "NewPassword1234", }); await expect(page).toHaveURL(/\/portal/); });