修正 ERP 边界与资产页面一致性(05-30-fix-erp-boundary-asset-consistency)

这个提交包含在:
2026-05-30 23:06:02 +08:00
父节点 16fd9bdf52
当前提交 f299ceeb74
共修改 47 个文件,包含 476 行新增和 1633 行删除
@@ -39,7 +39,7 @@ function buildMe(user: MockUserKey) {
return {
id: "admin-policy-ack",
employeeNo: "A001",
name: "Admin",
name: "王顺利",
phone: "mock-phone-admin",
email: "[email protected]",
departmentId: "dept-admin",
@@ -55,7 +55,7 @@ function buildMe(user: MockUserKey) {
return {
id: "employee-policy-ack",
employeeNo: "E001",
name: "Employee",
name: "陈工程",
phone: "mock-phone-employee",
email: "[email protected]",
departmentId: "dept-employee",
@@ -85,7 +85,7 @@ function mapAnnouncementForUser(item: MockAnnouncement, currentUser: MockUserKey
function mapAnnouncementForAdmin(item: MockAnnouncement) {
const employees = [
{ id: "employee-policy-ack", employeeNo: "E001", name: "Employee", status: "pending" },
{ id: "employee-policy-ack", employeeNo: "E001", name: "陈工程", status: "pending" },
{ id: "employee-policy-ack-2", employeeNo: "E002", name: "员工二", status: "pending" },
];
const pendingEmployees = item.requiresAcknowledgement
@@ -6,7 +6,7 @@ import { TEST_IDS } from "./helpers/testIds";
const ADMIN = {
identifier: "[email protected]",
password: "AdminPassword1234",
name: "Admin",
name: "王顺利",
};
test("E2E 公告与团队状态:公告可见 + 今日状态更新 + Portal 汇总", async ({ page }) => {
+10 -10
查看文件
@@ -2,8 +2,8 @@ import { expect, test, type Locator, type Page } from "@playwright/test";
import { openNavLink } from "./helpers/navigation";
const FINANCE_MGR = { identifier: "[email protected]", password: "FinanceMgrPassword1234", name: "Finance Manager" };
const ADMINISTRATION = { identifier: "[email protected]", password: "AdministrationPassword1234", name: "Administration" };
const FINANCE_MGR = { identifier: "[email protected]", password: "FinanceMgrPassword1234", name: "钱主管", employeeNo: "HYD000005" };
const ADMINISTRATION = { identifier: "[email protected]", password: "AdministrationPassword1234", name: "周行政", employeeNo: "HYD000008" };
async function login(page: Page, user: { identifier: string; password: string }) {
await page.goto("/login");
@@ -130,26 +130,26 @@ test("E2E 资产流转主链路:建卡→领用归还→调拨→盘点", asyn
await page.goto(`/assets?id=${assetId}`);
await expect(page.getByTestId("asset-name")).toHaveValue(assetName, { timeout: 30_000 });
await selectOptionByText(page.getByTestId("asset-movement-assignee"), /Employee/);
await selectOptionByText(page.getByTestId("asset-movement-assignee"), /陈工程/);
await page.getByTestId("asset-movement-note").fill("Playwright 首次领用");
if (await page.getByText(/当前归属人:未分配/).isVisible().catch(() => false)) {
if (await page.getByText(/归属人:-/).isVisible().catch(() => false)) {
await page.getByTestId("asset-assign-submit").click();
} else {
await page.getByTestId("asset-transfer-submit").click();
}
await expect(page.getByText(/当前归属人:Employee/)).toBeVisible({ timeout: 30_000 });
await expect(page.getByText(/归属人:陈工程/)).toBeVisible({ timeout: 30_000 });
await page.getByTestId("asset-return-submit").click();
await expect(page.getByText(/当前归属人:未分配/)).toBeVisible({ timeout: 30_000 });
await expect(page.getByText(/归属人:-/)).toBeVisible({ timeout: 30_000 });
await selectOptionByText(page.getByTestId("asset-movement-assignee"), /Employee/);
await selectOptionByText(page.getByTestId("asset-movement-assignee"), /陈工程/);
await page.getByTestId("asset-assign-submit").click();
await expect(page.getByText(/当前归属人:Employee/)).toBeVisible({ timeout: 30_000 });
await expect(page.getByText(/归属人:陈工程/)).toBeVisible({ timeout: 30_000 });
await selectOptionByText(page.getByTestId("asset-movement-assignee"), /Finance Manager/);
await selectOptionByText(page.getByTestId("asset-movement-assignee"), /钱主管/);
await page.getByTestId("asset-movement-location").fill("深圳办公室 B-02");
await page.getByTestId("asset-transfer-submit").click();
await expect(page.getByText(/当前归属人:Finance Manager/)).toBeVisible({ timeout: 30_000 });
await expect(page.getByText(/归属人:钱主管/)).toBeVisible({ timeout: 30_000 });
await expect(page.getByText(/位置:深圳办公室 B-02/)).toBeVisible({ timeout: 30_000 });
await logout(page, ADMINISTRATION.name);
@@ -5,7 +5,7 @@ import { apiGetData, apiPostData, createAuthenticatedApiClient, login } from "./
const ADMIN = {
identifier: "[email protected]",
password: "AdminPassword1234",
name: "Admin",
name: "王顺利",
};
type FeatureFlagItem = {
+11 -5
查看文件
@@ -8,36 +8,42 @@ export type E2EUser = {
identifier: string;
password: string;
name: string;
employeeNo?: string;
};
export const EMPLOYEE: E2EUser = {
identifier: "[email protected]",
password: "EmployeePassword1234",
name: "Employee",
name: "陈工程",
employeeNo: "HYD000003",
};
export const MANAGER: E2EUser = {
identifier: "[email protected]",
password: "ManagerPassword1234",
name: "Manager",
name: "李研发",
employeeNo: "HYD000002",
};
export const FINANCE: E2EUser = {
identifier: "[email protected]",
password: "FinancePassword1234",
name: "Finance",
name: "赵财务",
employeeNo: "HYD000004",
};
export const FINANCE_MGR: E2EUser = {
identifier: "[email protected]",
password: "FinanceMgrPassword1234",
name: "Finance Manager",
name: "钱主管",
employeeNo: "HYD000005",
};
export const ADMIN: E2EUser = {
identifier: "[email protected]",
password: "AdminPassword1234",
name: "Admin",
name: "王顺利",
employeeNo: "HYD000006",
};
export type MeResponse = {
+1 -1
查看文件
@@ -5,7 +5,7 @@ import { EMPLOYEE, MANAGER, login, type E2EUser } from "./helpers/auth";
const LEAVE_CALENDAR_HR: E2EUser = {
identifier: "[email protected]",
password: "HrPassword1234",
name: "HR",
name: "孙人事",
};
function envelope<T>(data: T) {
+25 -18
查看文件
@@ -5,8 +5,8 @@ import { TEST_IDS } from "./helpers/testIds";
test.use({ serviceWorkers: "block" });
const ADMIN = { identifier: "[email protected]", password: "AdminPassword1234", name: "Admin" };
const MANAGER = { identifier: "[email protected]", password: "ManagerPassword1234", name: "Manager" };
const ADMIN = { identifier: "[email protected]", password: "AdminPassword1234", name: "王顺利", employeeNo: "HYD000006" };
const MANAGER = { identifier: "[email protected]", password: "ManagerPassword1234", name: "李研发", employeeNo: "HYD000002" };
function envelope(data: unknown) {
return {
@@ -148,16 +148,23 @@ function mockNotificationList(page: import("@playwright/test").Page) {
}
function mockMe(page: import("@playwright/test").Page, roleCodes: string[], name: string) {
const employeeNoByName: Record<string, string> = {
王顺利: "HYD000006",
李研发: "HYD000002",
陈工程: "HYD000003",
周行政: "HYD000008",
};
const employeeNo = employeeNoByName[name] ?? "HYD000003";
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",
employeeNo,
name,
phone: "13800000000",
email: `${name.toLowerCase()}@hyinda.com`,
email: `mock-${employeeNo.toLowerCase()}@hyinda.com`,
departmentId: "dept-1",
directManagerId: null,
status: "active",
@@ -260,7 +267,7 @@ function mockPortalHome(page: import("@playwright/test").Page, mode: "employee"
const data = mode === "manager"
? {
workspace: { id: "workspace-1", persisted: true, profile: "small_team_internal" },
greeting: { name: "Manager" },
greeting: { name: "李研发" },
cards: [
{ key: "pending_approvals", title: "待我处理", value: 3, href: "/workflow/tasks", severity: "warning" },
{ key: "team_presence", title: "今日团队状态", value: "6/8 在岗", href: "/team", severity: "warning" },
@@ -298,7 +305,7 @@ function mockPortalHome(page: import("@playwright/test").Page, mode: "employee"
}
: {
workspace: { id: "workspace-1", persisted: true, profile: "small_team_internal" },
greeting: { name: "Employee" },
greeting: { name: "陈工程" },
cards: [
{ key: "my_todos", title: "我的待办", value: 4, href: "/todos", severity: "warning" },
{ key: "today_presence", title: "今日状态", value: "在岗", href: "/team", severity: "normal" },
@@ -325,10 +332,10 @@ function mockPortalHome(page: import("@playwright/test").Page, mode: "employee"
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" },
{ id: "activity-1", type: "leave.submit", actorName: "陈工程", summary: "陈工程 提交了请假单", 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-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: "remote", attendanceClockIn: null, attendanceClockOut: null, onLeave: false, leaveType: null },
],
};
@@ -386,7 +393,7 @@ test("邮箱验证码登录可发送验证码并进入工作台", async ({ page
const verifyPayloads: unknown[] = [];
await mockGuestAuthChrome(page);
await mockMe(page, ["employee"], "Employee");
await mockMe(page, ["employee"], "陈工程");
await page.route("**/api/auth/email-login/code", async (route) => {
codeRequests.push(route.request().postDataJSON());
@@ -507,11 +514,11 @@ test("Portal 员工视角优先显示今日动作顺序", async ({ page }) => {
await mockPortalHome(page, "employee");
await mockAttendanceToday(page);
await mockNotificationList(page);
await mockMe(page, ["employee"], "Employee");
await mockMe(page, ["employee"], "陈工程");
await login(page, EMPLOYEE);
await expect(page.getByTestId("portal-work-summary")).toContainText("今天有 7 件待处理");
await expect(page.getByTestId("portal-work-summary")).toContainText("待处理 7");
await expect(page.getByRole("heading", { name: "今日待处理" })).toBeVisible();
await expect(page.getByRole("link", { name: /待办事项 4 条.*查看待办/ })).toBeVisible();
await expect(page.getByRole("link", { name: /本周周报待填写.*填写周报/ })).toBeVisible();
@@ -527,7 +534,7 @@ test("Portal 管理视角按角色分发待处理和团队卡点", async ({ page
await mockPortalHome(page, "manager");
await mockAttendanceToday(page);
await mockNotificationList(page);
await mockMe(page, ["manager"], "Manager");
await mockMe(page, ["manager"], "李研发");
await login(page, MANAGER);
@@ -552,7 +559,7 @@ test("Portal 新员工只显示轻量欢迎入口,不再显示快速上手清
await mockPortalHome(page, "employee");
await mockAttendanceToday(page);
await mockNotificationList(page);
await mockMe(page, ["employee"], "Employee");
await mockMe(page, ["employee"], "陈工程");
await login(page, EMPLOYEE);
@@ -584,7 +591,7 @@ test("个人设置默认只展示账号通知和企业微信,不展示权限 c
await mockPortalHome(page, "employee");
await mockAttendanceToday(page);
await mockNotificationList(page);
await mockMe(page, ["employee"], "Employee");
await mockMe(page, ["employee"], "陈工程");
await login(page, EMPLOYEE);
await page.goto("/settings/profile");
@@ -646,7 +653,7 @@ test("Portal bootstrap refresh 首次瞬时失败后重试成功仍保留登录
await mockPortalHome(page, "employee");
await mockAttendanceToday(page);
await mockNotificationList(page);
await mockMe(page, ["employee"], "Employee");
await mockMe(page, ["employee"], "陈工程");
await page.goto("/portal");
@@ -661,7 +668,7 @@ test("Portal 域管理角色也能看到团队卡点", async ({ page }) => {
await mockPortalHome(page, "manager");
await mockAttendanceToday(page);
await mockNotificationList(page);
await mockMe(page, ["administration"], "Administration");
await mockMe(page, ["administration"], "周行政");
await login(page, EMPLOYEE);
@@ -676,7 +683,7 @@ test("Portal 管理提醒正常时不显示空壳", async ({ page }) => {
await mockPortalHome(page, "manager");
await mockAttendanceToday(page);
await mockNotificationList(page);
await mockMe(page, ["admin"], "Admin");
await mockMe(page, ["admin"], "王顺利");
await login(page, ADMIN);
@@ -697,7 +704,7 @@ test("企微 WebView 下默认进入移动工作台", async ({ page }) => {
await mockPortalHome(page, "employee");
await mockAttendanceToday(page);
await mockNotificationList(page);
await mockMe(page, ["employee"], "Employee");
await mockMe(page, ["employee"], "陈工程");
await page.goto("/login");
await page.getByTestId(TEST_IDS.login.identifier).fill(EMPLOYEE.identifier);
+1 -1
查看文件
@@ -5,7 +5,7 @@ import { apiGetData, apiPostData, createAuthenticatedApiClient, login } from "./
const ADMIN = {
identifier: "[email protected]",
password: "AdminPassword1234",
name: "Admin",
name: "王顺利",
};
type FeatureFlagItem = {
@@ -15,7 +15,7 @@ async function fulfillJson(route: Route, data: unknown, status = 200) {
const me = {
id: "employee-r100",
employeeNo: "E100",
name: "Employee",
name: "陈工程",
phone: "13800000000",
email: "[email protected]",
departmentId: "dept-r100",
@@ -15,7 +15,7 @@ async function fulfillJson(route: Route, data: unknown, status = 200) {
const me = {
id: "employee-r101",
employeeNo: "E101",
name: "Employee",
name: "陈工程",
phone: "13800000000",
email: "[email protected]",
departmentId: "dept-r101",
@@ -15,7 +15,7 @@ async function fulfillJson(route: Route, data: unknown, status = 200) {
const me = {
id: "employee-r102",
employeeNo: "E102",
name: "Employee",
name: "陈工程",
phone: "13800000000",
email: "[email protected]",
departmentId: "dept-r102",
@@ -23,8 +23,8 @@ function screenshotPath(fileName: string) {
const me = {
id: "employee-r103",
employeeNo: "E103",
name: "Employee",
employeeNo: "HYD000103",
name: "陈工程",
phone: "13800000000",
email: "[email protected]",
departmentId: "dept-r103",
@@ -37,8 +37,8 @@ async function attachScreenshot(page: Page, testInfo: TestInfo, name: string) {
const mockMe = {
id: "employee-mock-1",
employeeNo: "M001",
name: "Manager",
employeeNo: "HYD000002",
name: "李研发",
phone: "13800000000",
email: "[email protected]",
departmentId: "dept-1",
@@ -105,8 +105,8 @@ const pendingTaskDetail = {
reimbursementDate: "2026-05-05",
totalAmount: "1683.00",
description: "R89 移动审批验证",
owner: { id: "employee-mock-2", employeeNo: "E002", name: "Employee" },
submitter: { id: "employee-mock-2", employeeNo: "E002", name: "Employee" },
owner: { id: "employee-mock-2", employeeNo: "HYD000003", name: "陈工程" },
submitter: { id: "employee-mock-2", employeeNo: "HYD000003", name: "陈工程" },
},
leaveRequest: null,
purchaseRequest: null,
@@ -60,8 +60,8 @@ async function mockAuthenticatedShell(page: Page) {
await page.route((url) => url.pathname === "/api/me", (route) =>
fulfillJson(route, {
id: "employee-mock-1",
employeeNo: "E001",
name: "Smoke Employee",
employeeNo: "HYD000003",
name: "陈工程",
phone: "13800000000",
email: "[email protected]",
departmentId: "dept-1",
@@ -113,7 +113,7 @@ async function mockAuthenticatedShell(page: Page) {
await page.route((url) => url.pathname === "/api/portal/home", (route) =>
fulfillJson(route, {
workspace: { id: "workspace-1", persisted: true, profile: "small_team_internal" },
greeting: { name: "Smoke Employee" },
greeting: { name: "陈工程" },
cards: [],
pendingTasks: { count: 0, href: "/todos" },
myApplications: [],
@@ -149,8 +149,8 @@ function reimbursementListItem() {
paidAt: null,
paidBy: null,
},
owner: { id: "employee-mock-1", employeeNo: "E001", name: "Smoke Employee" },
submitter: { id: "employee-mock-1", employeeNo: "E001", name: "Smoke Employee" },
owner: { id: "employee-mock-1", employeeNo: "HYD000003", name: "陈工程" },
submitter: { id: "employee-mock-1", employeeNo: "HYD000003", name: "陈工程" },
project: null,
pendingWorkflow: null,
createdAt: null,
@@ -197,8 +197,8 @@ const invoiceListItem = {
mimeType: "application/pdf",
fileSize: 128,
verification: null,
owner: { id: "employee-mock-1", employeeNo: "E001", name: "Smoke Employee" },
uploadedBy: { id: "employee-mock-1", employeeNo: "E001", name: "Smoke Employee" },
owner: { id: "employee-mock-1", employeeNo: "HYD000003", name: "陈工程" },
uploadedBy: { id: "employee-mock-1", employeeNo: "HYD000003", name: "陈工程" },
createdAt: null,
updatedAt: null,
};
+8 -4
查看文件
@@ -7,7 +7,8 @@ import { EMPLOYEE, FINANCE, login, type E2EUser } from "./helpers/auth";
const ADMIN: E2EUser = {
identifier: "[email protected]",
password: "AdminPassword1234",
name: "Admin",
name: "王顺利",
employeeNo: "HYD000006",
};
const SCREENSHOT_DIR = path.resolve(process.cwd(), "../output/playwright/portal-task-first-redesign");
@@ -25,9 +26,10 @@ function envelope<T>(data: T) {
}
function portalPayload(mode: "employee" | "admin" | "finance") {
const displayName = mode === "employee" ? "陈工程" : mode === "finance" ? "赵财务" : "王顺利";
const base = {
workspace: { id: "workspace-visual", persisted: true, profile: "small_team_internal" },
greeting: { name: mode === "employee" ? "Employee" : mode === "finance" ? "Finance" : "Admin" },
greeting: { name: displayName },
myApplications: [{ domain: "reimbursement", count: 1, href: "/reimbursements" }],
riskSummary: null,
announcements: [],
@@ -134,13 +136,15 @@ async function mockVisualPortal(page: Page, mode: "employee" | "admin" | "financ
return;
}
if (pathName === "/api/me") {
const displayName = mode === "employee" ? "陈工程" : mode === "finance" ? "赵财务" : "王顺利";
const employeeNo = mode === "employee" ? "HYD000003" : mode === "finance" ? "HYD000004" : "HYD000006";
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(envelope({
id: `visual-${mode}`,
employeeNo: "VISUAL",
name: mode === "employee" ? "Employee" : mode === "finance" ? "Finance" : "Admin",
employeeNo,
name: displayName,
email: `${mode}@hyinda.com`,
roleCodes: mode === "employee" ? ["employee"] : mode === "finance" ? ["finance"] : ["admin"],
notificationPushEnabled: true,
+5 -5
查看文件
@@ -19,7 +19,7 @@ async function fulfillJson(route: Route, data: unknown, status = 200) {
const visualMe = {
id: "employee-r96",
employeeNo: "R96001",
name: "Employee",
name: "陈工程",
phone: "13800000000",
email: "[email protected]",
departmentId: "dept-r96",
@@ -261,12 +261,12 @@ async function mockVisualApis(page: Page) {
owner: {
id: "employee-r96",
employeeNo: "R96001",
name: "Employee",
name: "陈工程",
},
submitter: {
id: "employee-r96",
employeeNo: "R96001",
name: "Employee",
name: "陈工程",
},
project: null,
pendingWorkflow: {
@@ -300,12 +300,12 @@ async function mockVisualApis(page: Page) {
owner: {
id: "employee-r96",
employeeNo: "R96001",
name: "Employee",
name: "陈工程",
},
submitter: {
id: "employee-r96",
employeeNo: "R96001",
name: "Employee",
name: "陈工程",
},
project: null,
pendingWorkflow: null,
@@ -21,7 +21,7 @@ async function fulfillJson<T>(route: Route, data: T) {
const mockMe = {
id: "employee-confirm-manager",
employeeNo: "M-confirm",
name: "Manager",
name: "李研发",
email: "[email protected]",
phone: null,
roleCodes: ["manager"],
@@ -98,7 +98,7 @@ const pendingTaskDetail = {
reason: "R7E 审批动作确认",
attachment: null,
leaveType: { id: "annual", code: "annual", name: "年假" },
owner: { id: "employee-confirm-applicant", employeeNo: "E-confirm", name: "Employee" },
owner: { id: "employee-confirm-applicant", employeeNo: "E-confirm", name: "陈工程" },
},
purchaseRequest: null,
travelRequest: null,
@@ -21,7 +21,7 @@ async function fulfillJson<T>(route: Route, data: T) {
const manager = {
id: "manager-wecom-boundary",
employeeNo: "M-WECOM",
name: "Manager",
name: "李研发",
email: "[email protected]",
phone: null,
roleCodes: ["manager"],