fix: R27-5 R27-6 收口本地回归基线与E2E适配

这个提交包含在:
2026-03-22 21:13:03 +08:00
父节点 312200ec1b
当前提交 15d0473c0b
共修改 8 个文件,包含 148 行新增和 18 行删除
+3 -1
查看文件
@@ -1,5 +1,7 @@
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" };
@@ -125,7 +127,7 @@ test("E2E 资产流转主链路:建卡→领用归还→调拨→盘点", asyn
await logout(page, ADMINISTRATION.name);
await login(page, FINANCE_MGR);
await page.getByRole("link", { name: "资产盘点" }).click();
await (await openNavLink(page, "资产盘点")).click();
await page.getByTestId("inventory-create").click();
const inventoryRow = page.getByTestId("inventory-row").first();
+24
查看文件
@@ -0,0 +1,24 @@
import { expect, type Locator, type Page } from "@playwright/test";
export async function openBackupModulesIfNeeded(page: Page) {
const summary = page.locator("summary").filter({ hasText: "备用模块" }).first();
if (await summary.count()) {
const details = summary.locator("xpath=ancestor::details[1]");
const isOpen = await details.evaluate((node) => (node as HTMLDetailsElement).open);
if (!isOpen) {
await summary.click();
await expect(details).toHaveAttribute("open", "");
}
}
}
export async function openNavLink(page: Page, name: string | RegExp): Promise<Locator> {
const link = page.getByRole("link", { name }).first();
if (await link.isVisible().catch(() => false)) {
return link;
}
await openBackupModulesIfNeeded(page);
await expect(link).toBeVisible();
return link;
}
+2
查看文件
@@ -204,6 +204,8 @@ test("Portal 管理视角重排正确,备用模块默认折叠", async ({ page
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")),
);
+6 -4
查看文件
@@ -1,5 +1,7 @@
import { expect, test, type Page } from "@playwright/test";
import { openNavLink } from "./helpers/navigation";
const EMPLOYEE = { identifier: "[email protected]", password: "EmployeePassword1234", name: "Employee" };
const MANAGER = { identifier: "[email protected]", password: "ManagerPassword1234", name: "Manager" };
const FINANCE = { identifier: "[email protected]", password: "FinancePassword1234", name: "Finance" };
@@ -30,7 +32,7 @@ test("E2E 主链路:登录→报销→待办审批→票据OCR→支付→登
// 1) employee: create reimbursement
await login(page, EMPLOYEE);
await page.getByRole("link", { name: "我的报销" }).click();
await page.locator("nav").getByRole("link", { name: "我的报销", exact: true }).click();
const createResponsePromise = page.waitForResponse((response) => {
const url = new URL(response.url());
return response.request().method() === "POST" && url.pathname === "/api/reimbursements" && response.ok();
@@ -47,7 +49,7 @@ test("E2E 主链路:登录→报销→待办审批→票据OCR→支付→登
const reimbursementSummaryText = await page.getByTestId("reimbursement-summary").innerText();
// 2) employee: upload invoice and wait OCR
await page.getByRole("link", { name: "票据中心" }).click();
await (await openNavLink(page, "票据中心")).click();
const marker = `MOCK_E2E_${Date.now()}_${Math.random().toString(16).slice(2)}`;
await page.getByTestId("invoice-upload-file").setInputFiles({
name: `e2e-${Date.now()}.png`,
@@ -87,7 +89,7 @@ test("E2E 主链路:登录→报销→待办审批→票据OCR→支付→登
await logout(page, EMPLOYEE.name);
await login(page, MANAGER);
await page.getByRole("link", { name: "待我处理" }).click();
await (await openNavLink(page, "待我处理")).click();
await expect(page.getByTestId("workflow-tasks-table")).toBeVisible();
const managerTaskRow = page.getByTestId("workflow-task-row").filter({ hasText: reimbursementId }).first();
await expect(managerTaskRow).toBeVisible();
@@ -99,7 +101,7 @@ test("E2E 主链路:登录→报销→待办审批→票据OCR→支付→登
// 6) finance approve + pay
await login(page, FINANCE);
await page.getByRole("link", { name: "待我处理" }).click();
await (await openNavLink(page, "待我处理")).click();
const financeTaskRow = page.getByTestId("workflow-task-row").filter({ hasText: reimbursementId }).first();
await expect(financeTaskRow).toBeVisible();
await financeTaskRow.getByTestId("workflow-task-detail-link").click();
+5 -3
查看文件
@@ -1,5 +1,7 @@
import { expect, test, type Locator, type Page } from "@playwright/test";
import { openNavLink } from "./helpers/navigation";
const EMPLOYEE = { identifier: "[email protected]", password: "EmployeePassword1234", name: "Employee" };
const MANAGER = { identifier: "[email protected]", password: "ManagerPassword1234", name: "Manager" };
const FINANCE_MGR = { identifier: "[email protected]", password: "FinanceMgrPassword1234", name: "Finance Manager" };
@@ -45,7 +47,7 @@ test("E2E 采购主链路:员工提单→经理审批→财务归档", async (
const title = `Playwright 采购 ${Date.now()}`;
await login(page, EMPLOYEE);
await page.getByRole("link", { name: "采购申请", exact: true }).first().click();
await (await openNavLink(page, "采购申请")).click();
await page.getByTestId("purchase-requests-create").click();
await expect(page.getByTestId("purchase-title")).toBeVisible();
@@ -74,7 +76,7 @@ test("E2E 采购主链路:员工提单→经理审批→财务归档", async (
await logout(page, EMPLOYEE.name);
await login(page, MANAGER);
await page.getByRole("link", { name: "待我处理" }).click();
await (await openNavLink(page, "待我处理")).click();
const managerTaskRow = page.getByTestId("workflow-task-row").filter({ hasText: currentTaskId }).first();
await expect(managerTaskRow).toBeVisible({ timeout: 30_000 });
await managerTaskRow.getByTestId("workflow-task-detail-link").click();
@@ -85,7 +87,7 @@ test("E2E 采购主链路:员工提单→经理审批→财务归档", async (
await logout(page, MANAGER.name);
await login(page, FINANCE_MGR);
await page.getByRole("link", { name: "采购申请", exact: true }).first().click();
await (await openNavLink(page, "采购申请")).click();
await page.getByRole("button", { name: "全范围" }).click();
await page.getByRole("combobox").first().selectOption("approved");
const purchaseRow = page.getByRole("row").filter({ hasText: title }).first();
+2 -2
查看文件
@@ -99,8 +99,8 @@ test("E2E 项目工作台:沟通回填 + 收口面板 + Portal 摘要", async
await expect(page.getByTestId("project-closure-panel")).toContainText("确认交付/验收记录齐全");
await page.goto("/portal");
await expect(page.getByText(/项目摘要:/)).toContainText("今日待跟进");
await expect(page.getByText(/项目摘要:/)).toContainText("待收口项目");
await expect(page.getByRole("heading", { name: "今日管理重点" })).toBeVisible();
await expect(page.getByRole("link", { name: /待跟进 \/ 待收口项目/ })).toBeVisible();
await adminClient.dispose();
});
@@ -1,8 +1,8 @@
ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes
R27-0,执行合同建档与范围冻结,为企业微信正式接入与首批交互优化建立执行合同并冻结本轮范围,Issue CSV 校验通过 且范围锁定为企微入口 登录绑定 Portal 导航与测试文档,python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-03-22_19-06-31-r27-wecom-entry-and-ux-polish.csv,manual,DONE,DONE,DONE,issues/2026-03-22_19-06-31-r27-wecom-entry-and-ux-polish.csv,[],none
R27-1,企业微信接入配置与入口链路收口,扩展 wecom config 返回状态与原因 将 WECOM_ENTRY_ENABLED 纳入正式入口判断并保持现有 login bind callback 主链路不变,GET /api/auth/wecom/config 返回 decision complete 状态结构 且未绑定回退与白名单 returnTo 逻辑零回退,cd server && npm test,manual,DONE,DONE,DONE,server/src/routes/wecomAuth.js|client/src/api/auth.ts|server/tests/unit/routes/wecomAuth.test.js,"[""R27-0""]",不新增新路由 仅扩展 config 返回
R27-2,登录页与个人绑定体验优化,重构 LoginPage 与 ProfilePage 让企微入口 绑定状态 回退提示更产品化,普通浏览器与企微场景 CTA 分层明确 wecom query 提示可读 个人设置页可清晰展示接入与绑定状态,cd client && npm run typecheck && npm run build,manual,DONE,DONE,DONE,client/src/pages/LoginPage.tsx|client/src/pages/ProfilePage.tsx|client/src/api/auth.ts,"[""R27-1""]",不新增页面 继续复用现有登录页与个人设置页
R27-3,Portal 首屏信息架构重排,重排 Portal 首屏卡片与摘要层级 让员工与管理员优先看到今日动作与关键提醒,员工与管理员首屏主卡片顺序符合规划 观测摘要对普通员工弱化 公告降为第二阅读层,cd client && npm run typecheck && npm run build,manual,DONE,DONE,DONE,client/src/pages/PortalHomePage.tsx,"[""R27-1""]",仅做信息架构与文案层级优化 不改后端 portal payload 结构
R27-4,导航 Topbar 与备用模块弱化,优化 NavLinks Sidebar Topbar 的分区展示与企微入口提示 弱化备用模块存在感,主导航稳定为 9 项 管理入口单独分组 备用模块默认折叠 顶栏在企微 WebView 下显示入口提示,cd client && npm run typecheck && npm run build,manual,DONE,DONE,DONE,client/src/components/navigation/NavLinks.tsx|client/src/components/navigation/Sidebar.tsx|client/src/components/navigation/Topbar.tsx,"[""R27-3""]",不改后端导航接口 以前端分组与展示优化为主
R27-5,自动化测试与文档补强,补齐 wecom 后端单测 登录与 Portal 交互 E2E 并同步管理员与员工文档,新增或扩展测试覆盖 wecom config 登录提示 Portal 顺序与导航弱化 文档写清配置与使用方式,cd server && npm test && cd ../client && npm run test:e2e,manual,DOING,TODO,TODO,server/tests/unit/routes/wecomAuth.test.js|client/tests/e2e/wecom-entry.spec.ts|client/tests/e2e/login-portal-ux.spec.ts|docs/admin-handbook.md|docs/employee-quickstart.md,"[""R27-2"",""R27-3"",""R27-4""]",新增 wecom-entry/login-portal-ux 用例已通过;client 全量 test:e2e 被既有 assets-flow 与 confidential-boundary 失败阻塞
R27-6,试点验收与上线清单收口,完成本地验证 部署抽检 文档清单同步与 git 提交 形成可进入小范围企微试点的版本,关键单测 类型检查 构建 E2E 通过 且接入清单可供管理员逐项配置实施,cd server && npm test && cd ../client && npm run typecheck && npm run build && npm run test:e2e,manual,DOING,TODO,TODO,deploy/trialrun/checklist.md|deploy/trialrun/runbook.md|issues/2026-03-22_19-06-31-r27-wecom-entry-and-ux-polish.csv,"[""R27-5""]",本地 server 全量单测 250/250 通过;client typecheck/build 通过;全量 E2E 仍受既有失败阻塞
ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes
R27-0,执行合同建档与范围冻结,为企业微信正式接入与首批交互优化建立执行合同并冻结本轮范围,Issue CSV 校验通过 且范围锁定为企微入口 登录绑定 Portal 导航与测试文档,python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-03-22_19-06-31-r27-wecom-entry-and-ux-polish.csv,manual,DONE,DONE,DONE,issues/2026-03-22_19-06-31-r27-wecom-entry-and-ux-polish.csv,[],none
R27-1,企业微信接入配置与入口链路收口,扩展 wecom config 返回状态与原因 将 WECOM_ENTRY_ENABLED 纳入正式入口判断并保持现有 login bind callback 主链路不变,GET /api/auth/wecom/config 返回 decision complete 状态结构 且未绑定回退与白名单 returnTo 逻辑零回退,cd server && npm test,manual,DONE,DONE,DONE,server/src/routes/wecomAuth.js|client/src/api/auth.ts|server/tests/unit/routes/wecomAuth.test.js,"[""R27-0""]",不新增新路由 仅扩展 config 返回
R27-2,登录页与个人绑定体验优化,重构 LoginPage 与 ProfilePage 让企微入口 绑定状态 回退提示更产品化,普通浏览器与企微场景 CTA 分层明确 wecom query 提示可读 个人设置页可清晰展示接入与绑定状态,cd client && npm run typecheck && npm run build,manual,DONE,DONE,DONE,client/src/pages/LoginPage.tsx|client/src/pages/ProfilePage.tsx|client/src/api/auth.ts,"[""R27-1""]",不新增页面 继续复用现有登录页与个人设置页
R27-3,Portal 首屏信息架构重排,重排 Portal 首屏卡片与摘要层级 让员工与管理员优先看到今日动作与关键提醒,员工与管理员首屏主卡片顺序符合规划 观测摘要对普通员工弱化 公告降为第二阅读层,cd client && npm run typecheck && npm run build,manual,DONE,DONE,DONE,client/src/pages/PortalHomePage.tsx,"[""R27-1""]",仅做信息架构与文案层级优化 不改后端 portal payload 结构
R27-4,导航 Topbar 与备用模块弱化,优化 NavLinks Sidebar Topbar 的分区展示与企微入口提示 弱化备用模块存在感,主导航稳定为 9 项 管理入口单独分组 备用模块默认折叠 顶栏在企微 WebView 下显示入口提示,cd client && npm run typecheck && npm run build,manual,DONE,DONE,DONE,client/src/components/navigation/NavLinks.tsx|client/src/components/navigation/Sidebar.tsx|client/src/components/navigation/Topbar.tsx,"[""R27-3""]",不改后端导航接口 以前端分组与展示优化为主
R27-5,自动化测试与文档补强,补齐 wecom 后端单测 登录与 Portal 交互 E2E 并同步管理员与员工文档,新增或扩展测试覆盖 wecom config 登录提示 Portal 顺序与导航弱化 文档写清配置与使用方式,cd server && npm test && cd ../client && npm run test:e2e,manual,DONE,DONE,DONE,server/tests/unit/routes/wecomAuth.test.js|client/tests/e2e/wecom-entry.spec.ts|client/tests/e2e/login-portal-ux.spec.ts|client/tests/e2e/helpers/navigation.ts|client/tests/e2e/assets-flow.spec.ts|client/tests/e2e/main-flow.spec.ts|client/tests/e2e/procurement-flow.spec.ts|client/tests/e2e/project-workbench.spec.ts|docs/admin-handbook.md|docs/employee-quickstart.md,"[""R27-2"",""R27-3"",""R27-4""]",新增 wecom-entry/login-portal-ux 用例与全量 14 个 Playwright 用例均已通过;备用模块折叠后的既有 E2E 已同步适配
R27-6,试点验收与上线清单收口,完成本地验证 部署抽检 文档清单同步与 git 提交 形成可进入小范围企微试点的版本,关键单测 类型检查 构建 E2E 通过 且接入清单可供管理员逐项配置实施,cd server && npm test && cd ../client && npm run typecheck && npm run build && npm run test:e2e,manual,DOING,DOING,DOING,deploy/trialrun/checklist.md|deploy/trialrun/runbook.md|issues/2026-03-22_19-06-31-r27-wecom-entry-and-ux-polish.csv|server/src/scripts/seed-dev.js,"[""R27-5""]",本地 server 单测 250/250 通过;prisma validate 通过;workflow-actions 与 regression-all 均通过;等待部署 trialrun 与线上抽检后关单
1 ID Title Description Acceptance Test_Method Tools Dev_Status Review1_Status Regression_Status Files Dependencies Notes
2 R27-0 执行合同建档与范围冻结 为企业微信正式接入与首批交互优化建立执行合同并冻结本轮范围 Issue CSV 校验通过 且范围锁定为企微入口 登录绑定 Portal 导航与测试文档 python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-03-22_19-06-31-r27-wecom-entry-and-ux-polish.csv manual DONE DONE DONE issues/2026-03-22_19-06-31-r27-wecom-entry-and-ux-polish.csv [] none
3 R27-1 企业微信接入配置与入口链路收口 扩展 wecom config 返回状态与原因 将 WECOM_ENTRY_ENABLED 纳入正式入口判断并保持现有 login bind callback 主链路不变 GET /api/auth/wecom/config 返回 decision complete 状态结构 且未绑定回退与白名单 returnTo 逻辑零回退 cd server && npm test manual DONE DONE DONE server/src/routes/wecomAuth.js|client/src/api/auth.ts|server/tests/unit/routes/wecomAuth.test.js ["R27-0"] 不新增新路由 仅扩展 config 返回
4 R27-2 登录页与个人绑定体验优化 重构 LoginPage 与 ProfilePage 让企微入口 绑定状态 回退提示更产品化 普通浏览器与企微场景 CTA 分层明确 wecom query 提示可读 个人设置页可清晰展示接入与绑定状态 cd client && npm run typecheck && npm run build manual DONE DONE DONE client/src/pages/LoginPage.tsx|client/src/pages/ProfilePage.tsx|client/src/api/auth.ts ["R27-1"] 不新增页面 继续复用现有登录页与个人设置页
5 R27-3 Portal 首屏信息架构重排 重排 Portal 首屏卡片与摘要层级 让员工与管理员优先看到今日动作与关键提醒 员工与管理员首屏主卡片顺序符合规划 观测摘要对普通员工弱化 公告降为第二阅读层 cd client && npm run typecheck && npm run build manual DONE DONE DONE client/src/pages/PortalHomePage.tsx ["R27-1"] 仅做信息架构与文案层级优化 不改后端 portal payload 结构
6 R27-4 导航 Topbar 与备用模块弱化 优化 NavLinks Sidebar Topbar 的分区展示与企微入口提示 弱化备用模块存在感 主导航稳定为 9 项 管理入口单独分组 备用模块默认折叠 顶栏在企微 WebView 下显示入口提示 cd client && npm run typecheck && npm run build manual DONE DONE DONE client/src/components/navigation/NavLinks.tsx|client/src/components/navigation/Sidebar.tsx|client/src/components/navigation/Topbar.tsx ["R27-3"] 不改后端导航接口 以前端分组与展示优化为主
7 R27-5 自动化测试与文档补强 补齐 wecom 后端单测 登录与 Portal 交互 E2E 并同步管理员与员工文档 新增或扩展测试覆盖 wecom config 登录提示 Portal 顺序与导航弱化 文档写清配置与使用方式 cd server && npm test && cd ../client && npm run test:e2e manual DOING DONE TODO DONE TODO DONE server/tests/unit/routes/wecomAuth.test.js|client/tests/e2e/wecom-entry.spec.ts|client/tests/e2e/login-portal-ux.spec.ts|docs/admin-handbook.md|docs/employee-quickstart.md server/tests/unit/routes/wecomAuth.test.js|client/tests/e2e/wecom-entry.spec.ts|client/tests/e2e/login-portal-ux.spec.ts|client/tests/e2e/helpers/navigation.ts|client/tests/e2e/assets-flow.spec.ts|client/tests/e2e/main-flow.spec.ts|client/tests/e2e/procurement-flow.spec.ts|client/tests/e2e/project-workbench.spec.ts|docs/admin-handbook.md|docs/employee-quickstart.md ["R27-2","R27-3","R27-4"] 新增 wecom-entry/login-portal-ux 用例已通过;client 全量 test:e2e 被既有 assets-flow 与 confidential-boundary 失败阻塞 新增 wecom-entry/login-portal-ux 用例与全量 14 个 Playwright 用例均已通过;备用模块折叠后的既有 E2E 已同步适配
8 R27-6 试点验收与上线清单收口 完成本地验证 部署抽检 文档清单同步与 git 提交 形成可进入小范围企微试点的版本 关键单测 类型检查 构建 E2E 通过 且接入清单可供管理员逐项配置实施 cd server && npm test && cd ../client && npm run typecheck && npm run build && npm run test:e2e manual DOING TODO DOING TODO DOING deploy/trialrun/checklist.md|deploy/trialrun/runbook.md|issues/2026-03-22_19-06-31-r27-wecom-entry-and-ux-polish.csv deploy/trialrun/checklist.md|deploy/trialrun/runbook.md|issues/2026-03-22_19-06-31-r27-wecom-entry-and-ux-polish.csv|server/src/scripts/seed-dev.js ["R27-5"] 本地 server 全量单测 250/250 通过;client typecheck/build 通过;全量 E2E 仍受既有失败阻塞 本地 server 单测 250/250 通过;prisma validate 通过;workflow-actions 与 regression-all 均通过;等待部署 trialrun 与线上抽检后关单
+98
查看文件
@@ -257,6 +257,79 @@ async function ensureHolidayCalendar(prisma, payload) {
return calendar;
}
async function ensureFeatureFlag(prisma, payload) {
const now = new Date();
const existing = await prisma.featureFlag.findUnique({
where: { flag_key: payload.flag_key },
select: { id: true, version: true },
});
if (existing) {
return prisma.featureFlag.update({
where: { id: existing.id },
data: {
override_mode: payload.override_mode,
rollout_type: payload.rollout_type,
department_ids: payload.department_ids,
note: payload.note,
version: existing.version + 1,
updated_at: now,
},
});
}
return prisma.featureFlag.create({
data: {
...payload,
created_at: now,
updated_at: now,
},
});
}
async function ensureWorkspaceSetting(prisma) {
const current = await prisma.workspaceSetting.findFirst({ select: { id: true } });
const data = {
profile: "small_team_internal",
org_mode: "single_company",
primary_entry_mode: "password",
notification_strategy: "in_app",
device_priority: "desktop",
module_visibility_json: {
portal: "main",
team: "main",
leaveRequests: "main",
reimbursements: "main",
assets: "main",
weeklyReports: "main",
todos: "main",
announcements: "main",
workspace: "admin",
people: "admin",
workflowDefinitions: "admin",
systemSettings: "admin",
featureFlags: "admin",
auditLogs: "admin",
purchaseRequests: "admin",
travelRequests: "admin",
budgets: "admin",
paymentBatches: "admin",
apOperations: "admin",
cutover: "admin",
erpIntegrations: "admin",
},
};
if (current) {
return prisma.workspaceSetting.update({
where: { id: current.id },
data,
});
}
return prisma.workspaceSetting.create({ data });
}
async function ensureEmployee(prisma, payload) {
const now = new Date();
const passwordHash = await hashPassword(payload.password);
@@ -378,6 +451,31 @@ async function main() {
taxNo: null,
});
await ensureWorkspaceSetting(prisma);
for (const flagKey of [
"portal_home_v1",
"announcements_v1",
"global_search_v1",
"presence_board_v1",
"weekly_report_v1",
"employee_lifecycle_v1",
"todo_board_v1",
"project_domain_v1",
"project_linking_v1",
"project_communication_log_v1",
"project_closure_checklist_v1",
"confidential_doc_refs_v1",
]) {
await ensureFeatureFlag(prisma, {
flag_key: flagKey,
override_mode: "force_on",
rollout_type: "global",
department_ids: [],
note: "dev default: force_on",
});
}
const ceo = await ensureEmployee(prisma, {
employeeNo: process.env.SEED_CEO_EMPLOYEE_NO ?? "E0001",
name: "CEO",