修复 r102 普通报销编辑夹具(08-29-r102-reimbursement-fixture-debt)
这个提交包含在:
@@ -0,0 +1 @@
|
|||||||
|
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# 实施清单:修复 r102 普通报销编辑夹具
|
||||||
|
|
||||||
|
## 1. 夹具修正
|
||||||
|
|
||||||
|
- [x] 将目标用例整体改为语义一致的普通报销,不命中任何差旅识别条件。
|
||||||
|
- [x] 保留编辑报销事由与日期、保存后展示更新的原始断言目标。
|
||||||
|
- [x] 不修改生产组件、差旅识别规则或费用类型种子。
|
||||||
|
|
||||||
|
## 2. 验证
|
||||||
|
|
||||||
|
- [x] 完整 r102 从既有 5/6 恢复为 6/6 全绿。
|
||||||
|
- [x] 核对改动仅限合成普通报销夹具,差旅识别/详情生产合同未改。
|
||||||
|
- [x] 运行前端 typecheck、Trellis governance 和 `git diff --check`。
|
||||||
|
|
||||||
|
## 3. 收口
|
||||||
|
|
||||||
|
- [x] 只读检查 diff,确认没有通过改选择器或削弱断言制造假绿。
|
||||||
|
- [ ] 中文提交信息引用 `08-29-r102-reimbursement-fixture-debt`。
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# 修复 r102 普通报销编辑夹具
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
恢复 r102 中“普通报销可编辑报销事由和日期”用例的真实业务前提,让回归重新验证普通报销头部编辑合同,而不是因夹具被识别成差旅报销后通过改选择器制造假绿。
|
||||||
|
|
||||||
|
## Confirmed Facts
|
||||||
|
|
||||||
|
- `r102-reimbursement-main-flow.spec.ts` 的详情夹具把唯一明细费用类型设为 `category: "travel"`。
|
||||||
|
- `isTravelReimbursement` 只要命中 `travelSummary.isTravel`、`linkedTravelRequestId` 或任一明细 `category === "travel"` 即判定为差旅。
|
||||||
|
- 差旅详情向 `ReimbursementHeaderEditor` 传入 `showDescription=false`,按钮显示“编辑报销日期”,且“报销事由”输入框不渲染。
|
||||||
|
- 既有失败与已上线的非参与人只读投影无关;只修改按钮选择器无法恢复用例原本要验证的行为。
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- 仅调整 `r102-reimbursement-main-flow.spec.ts` 中普通报销编辑场景的合成夹具及必要断言。
|
||||||
|
- 复跑完整 r102 和差旅识别相关聚焦回归,确认普通报销与差旅报销合同同时成立。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- 把该用例的合成报销整体改为可信的普通报销场景,费用类型、事由、明细说明和预期文案必须语义一致。
|
||||||
|
- 不通过只改 `category`、只改按钮名称或取消“报销事由”断言制造假绿。
|
||||||
|
- 保留差旅识别规则和差旅详情不重复展示普通“报销事由”的现有产品合同。
|
||||||
|
- 不修改生产业务代码、费用类型种子、权限、状态机或服务端 API。
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] 夹具不再命中 `isTravelReimbursement` 的任一条件,且内容能解释为普通报销。
|
||||||
|
- [x] 用例仍点击“编辑”、修改“报销事由”和报销日期,并断言保存后的展示结果。
|
||||||
|
- [x] 完整 `r102-reimbursement-main-flow.spec.ts` 通过,不再保留既有 5/6 基线。
|
||||||
|
- [x] 差旅识别和差旅头部展示合同未改;完整 r102 中普通报销和管理员只读场景均通过。
|
||||||
|
- [x] 前端 typecheck、相关 Playwright、Trellis governance 与 `git diff --check` 通过。
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- 调整 `isTravelReimbursement` 判定、差旅页面字段或按钮文案。
|
||||||
|
- Task B 的费用类型重命名、票据关联和金额门禁。
|
||||||
|
- 生产部署或生产数据写入。
|
||||||
|
|
||||||
|
## Test Plan
|
||||||
|
|
||||||
|
- `cd client && npx playwright test tests/e2e/r102-reimbursement-main-flow.spec.ts --workers=1 --reporter=list`
|
||||||
|
- 运行现有差旅识别/详情聚焦测试;若仓库没有独立文件,至少由 r102 夹具断言普通报销路径,并核对相关既有套件。
|
||||||
|
- `cd client && npm run typecheck`
|
||||||
|
- `node scripts/qa/check-trellis-governance.mjs`
|
||||||
|
- `git diff --check`
|
||||||
|
|
||||||
|
## Validation Result
|
||||||
|
|
||||||
|
- `npm run typecheck`:PASS。
|
||||||
|
- `E2E_BASE_URL=http://127.0.0.1:5173 npx playwright test tests/e2e/r102-reimbursement-main-flow.spec.ts --workers=1 --reporter=list`:6/6 PASS。
|
||||||
|
- 首次未启动本地服务的运行因 6 个 `ERR_CONNECTION_REFUSED` 失败;启动 Vite 后按相同测试命令复跑全绿,属于测试环境问题,不是产品断言失败。
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"id": "r102-reimbursement-fixture-debt",
|
||||||
|
"name": "r102-reimbursement-fixture-debt",
|
||||||
|
"title": "修复 r102 普通报销编辑夹具",
|
||||||
|
"description": "修复 r102 普通报销头部编辑用例误用 travel 类费用明细导致整个测试前提失效的问题。",
|
||||||
|
"status": "in_progress",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": "client Playwright reimbursement fixture and focused regression only",
|
||||||
|
"package": "client",
|
||||||
|
"priority": "P1",
|
||||||
|
"creator": "shunli",
|
||||||
|
"assignee": "shunli",
|
||||||
|
"createdAt": "2026-08-29",
|
||||||
|
"completedAt": null,
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "main",
|
||||||
|
"worktree_path": null,
|
||||||
|
"commit": null,
|
||||||
|
"pr_url": null,
|
||||||
|
"subtasks": [],
|
||||||
|
"children": [],
|
||||||
|
"parent": null,
|
||||||
|
"relatedFiles": [
|
||||||
|
"client/tests/e2e/r102-reimbursement-main-flow.spec.ts",
|
||||||
|
"client/src/lib/reimbursementTravel.ts",
|
||||||
|
"client/src/components/reimbursements/ReimbursementHeaderEditor.tsx"
|
||||||
|
],
|
||||||
|
"notes": "只修正合成夹具和测试断言,不改变差旅识别或生产页面行为。",
|
||||||
|
"meta": {}
|
||||||
|
}
|
||||||
@@ -51,7 +51,7 @@ function reimbursementListItem() {
|
|||||||
status: "draft",
|
status: "draft",
|
||||||
reimbursementDate: "2026-05-06",
|
reimbursementDate: "2026-05-06",
|
||||||
totalAmount: "168.30",
|
totalAmount: "168.30",
|
||||||
description: "客户拜访交通费",
|
description: "办公室宽带费用",
|
||||||
payment: {
|
payment: {
|
||||||
paymentVoucherNo: null,
|
paymentVoucherNo: null,
|
||||||
paymentDate: null,
|
paymentDate: null,
|
||||||
@@ -81,7 +81,7 @@ function reimbursementDetail(overrides: { reimbursementDate?: string; descriptio
|
|||||||
submittedAt: null,
|
submittedAt: null,
|
||||||
reimbursementDate: overrides.reimbursementDate ?? "2026-05-06",
|
reimbursementDate: overrides.reimbursementDate ?? "2026-05-06",
|
||||||
totalAmount: "168.30",
|
totalAmount: "168.30",
|
||||||
description: overrides.description ?? "客户拜访交通费",
|
description: overrides.description ?? "办公室宽带费用",
|
||||||
projectId: null,
|
projectId: null,
|
||||||
linkedTravelRequestId: null,
|
linkedTravelRequestId: null,
|
||||||
linkedTravelRequest: null,
|
linkedTravelRequest: null,
|
||||||
@@ -91,15 +91,15 @@ function reimbursementDetail(overrides: { reimbursementDate?: string; descriptio
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: itemId,
|
id: itemId,
|
||||||
expenseTypeId: "expense-transport",
|
expenseTypeId: "expense-office-communication",
|
||||||
expenseDate: "2026-05-06",
|
expenseDate: "2026-05-06",
|
||||||
amount: "168.30",
|
amount: "168.30",
|
||||||
description: "客户拜访交通费",
|
description: "办公室宽带费用",
|
||||||
expenseType: {
|
expenseType: {
|
||||||
id: "expense-transport",
|
id: "expense-office-communication",
|
||||||
code: "transport",
|
code: "office_communication",
|
||||||
name: "交通费",
|
name: "通讯费用",
|
||||||
category: "travel",
|
category: "office",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -116,7 +116,7 @@ function reimbursementDetail(overrides: { reimbursementDate?: string; descriptio
|
|||||||
async function mockApis(page: Page) {
|
async function mockApis(page: Page) {
|
||||||
let draftExists = true;
|
let draftExists = true;
|
||||||
let detailReimbursementDate = "2026-05-06";
|
let detailReimbursementDate = "2026-05-06";
|
||||||
let detailDescription = "客户拜访交通费";
|
let detailDescription = "办公室宽带费用";
|
||||||
|
|
||||||
await page.route((url) => url.pathname.startsWith("/api/"), async (route) => {
|
await page.route((url) => url.pathname.startsWith("/api/"), async (route) => {
|
||||||
const request = route.request();
|
const request = route.request();
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户