汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、PWA/采购事务、错误原因与日期校验及ERP退役。 统一共享预算池锁序和锁后判断,恢复差旅原预算选择,补原因码固定夹具及反向完整性检查。 纳入配套迁移、测试、受管QA、分层验收合同、规约及任务记录;原始证据留在ignored artifacts。 关联任务:09-17-company-loan-principal-repayment、09-17-erp-exclusive-retirement、09-18-reimbursement-periodic-numbering、09-08-reimbursement-payment-closeout、09-18-local-concurrency-product-fixes、09-19-budget-pool-concurrency、09-05-error-explainability-payment-readiness。 预算补审由Claude定向验证2文件928项通过,原4446/28及r6结果仍仅对应各自历史输入;本轮未重跑测试。 完整新候选门禁、R1真实入口接入与12项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
440 行
15 KiB
TypeScript
440 行
15 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
// These synthetic auth routes must also handle the post-change /api/me request.
|
|
test.use({ serviceWorkers: "block" });
|
|
|
|
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: "[email protected]",
|
|
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("[email protected]");
|
|
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<string, unknown> | 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<string, unknown>;
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(
|
|
envelope({
|
|
id: "user-1",
|
|
employeeNo: "HYD000123",
|
|
name: "张三",
|
|
phone: "13800000001",
|
|
email: "[email protected]",
|
|
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("[email protected]");
|
|
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("[email protected]");
|
|
await page.getByTestId("users-create-submit").click();
|
|
|
|
expect(createPayload).toEqual({
|
|
name: "张三",
|
|
phone: "13800000001",
|
|
email: "[email protected]",
|
|
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<string, unknown> | 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<string, unknown>;
|
|
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("[email protected]");
|
|
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/);
|
|
});
|