修正门禁测试定位与动画测量(09-17-company-loan-principal-repayment)

保留原断言、容差及运行配置;两份完整测试定向 61/61 通过,补充可复用规约及失败门禁记录。提交后对固定干净候选重跑 verify-candidate。真实回单、人员与独立业务验收仍待核实;本轮不推送、不部署。
这个提交包含在:
2026-10-08 09:49:20 +08:00
父节点 1dd840655b
当前提交 4c37fceeb4
共修改 4 个文件,包含 34 行新增和 4 行删除
@@ -159,6 +159,7 @@ and verification contracts. Do not copy broad aesthetic guidance from
- At the narrowest viewport where the fixed sidebar appears, the main detail column is at least `560px`, the action column is at least `280px`, and the document has no horizontal overflow beyond a `2px` rendering tolerance. - At the narrowest viewport where the fixed sidebar appears, the main detail column is at least `560px`, the action column is at least `280px`, and the document has no horizontal overflow beyond a `2px` rendering tolerance.
- Business identifiers and normal company-time date strings use DOM line-count or geometry assertions; `position: sticky` alone is not proof that the layout is usable. - Business identifiers and normal company-time date strings use DOM line-count or geometry assertions; `position: sticky` alone is not proof that the layout is usable.
- A Playwright spec writes screenshots only to its own current output directory. Never reuse an archived Trellis task's evidence directory as a live test target. - A Playwright spec writes screenshots only to its own current output directory. Never reuse an archived Trellis task's evidence directory as a live test target.
- When comparing alignment inside an animated ancestor (for example `.page-scaffold`'s `page-reveal`), read all relevant `getBoundingClientRect()` values in one synchronous browser `evaluate`/`evaluateAll` callback. Sequential `boundingBox()` calls, or calls grouped with `Promise.all`, can observe different animation frames. Keep the original alignment assertions and pixel tolerances; do not hide the race with sleeps, retries or disabled page animations.
### 4. Validation & Error Matrix ### 4. Validation & Error Matrix
@@ -997,7 +998,9 @@ point and prevents each route from inventing a different title/action layout.
hidden unless `leave.manage`; attendance correction forms are hidden unless hidden unless `leave.manage`; attendance correction forms are hidden unless
`attendance.manage`. Labels must say `OA 应急` / `提交应急审批` / `attendance.manage`. Labels must say `OA 应急` / `提交应急审批` /
`提交应急修正` instead of generic `提交`. `提交应急修正` instead of generic `提交`.
- When a page repeats the same title in the page header and a content section, - Title assertions should use an exact heading role within the intended panel;
broad `getByText("票据详情")` can also match transient loading copy. When a
page repeats the same title in the page header and a content section,
Playwright assertions should target the page header (`h1` or the header test Playwright assertions should target the page header (`h1` or the header test
id) rather than `getByRole("heading", { name })`, which becomes ambiguous. id) rather than `getByRole("heading", { name })`, which becomes ambiguous.
- `个人设置` stays in the topbar account menu. Notifications stay in the topbar - `个人设置` stays in the topbar account menu. Notifications stay in the topbar
@@ -1,3 +1,24 @@
## 2026-10-08 测试稳定性集中检查与新候选门禁
用户已批准集中检查、必要测试修正、本地提交及对新干净候选运行正式门禁。本轮不推送、不部署、不查询生产、不执行远端备份操作;任务仍为进行中。
- [x] 检查两份失败测试、此前修正的审批测试,以及邻近布局用例:严格同排对齐修正为同一次浏览器执行取值;其余宽高/横向比较、带足够间隔的上下顺序及非同类动画场景不机械改写。检查详情标题与加载提示,当前需修正的仍为下方已验证的两处。
- [x] 保留两份测试全部原有断言和运行配置;没有新增用例、修改业务实现、增加容差、超时或重试。被测输入未变,复用下方受管定向 61/61 通过结果,不为本次文档更新重复运行。
- [x] 将同次几何取值、精确标题定位两条已验证的规则补入既有前端规约;按任务、治理、流程合同、文档注册及 diff 检查后,本地提交两份测试、规约和本记录。
- [ ] 对新提交执行 `bash scripts/qa/run-release-gate.sh verify-candidate`,保留两次失败回执,新结果写入独立的 `artifacts/release-gate/` 目录。此处为提交前执行计划,不提前宣称通过;候选冻结后不再修改受版本控制的记录,最终状态以该候选的正式回执为准。
- 真实建行样本、官方查询材料和编号对应关系、合格复核人员及独立业务验收继续待外部核实;正式本地门禁不替代这些条件。
## 2026-10-08 第二轮正式门禁及新增测试稳定性修正
用户确认后,仅将此前的审批按钮测量修正和任务记录本地提交为 `1dd84065`,工作区干净;对该固定候选执行 `bash scripts/qa/run-release-gate.sh verify-candidate`。未推送、部署、查询生产或执行远端备份操作。
- 正式回执 `artifacts/release-gate/loan-1dd84065-20261008/release-result.json` 为 **FAIL,不具备发布资格**。static 和 full-local 的前 11 个阶段通过,服务端单测 4480 通过、1 项原有跳过;网页 370/372 通过、2 失败、0 跳过、0 重试。默认关闭的 7 个可选服务端专项不计通过。上一轮失败的审批按钮用例已在本轮完整回归中通过。
- 两个新增失败来自既有测试:`r92-mobile-invoice-usability.spec.ts:335` 的宽泛文字定位同时命中标题与“加载票据详情…”提示,上传步骤尚未执行;`system-action-confirmation.spec.ts:835` 顺序读取动画中两个按钮的位置,中心差约 3.69px,超过原有 2px 上限。此次候选仅改动另一份审批测试及记录,没有直接修改这两个用例或业务页面。
- 本轮仅局部修正两份测试:票据标题改用精确的 heading 定位;两个布局坐标改为同一次浏览器执行读取。保留中心差小于 2px、按钮顺序、侧栏宽度和批量上传计数等原有断言,不修改业务代码、不增大容差或跳过用例。
- [x] 官方受管 `retained-local` 验证这两个完整文件:61/61 通过、0 跳过、0 重试,包含原失败用例、批量上传计数及 1280/1440/1920 布局断言;运行中源码/仓库未变,环境和自有容器/运行目录清理通过。结果位于 `artifacts/qa-flow/loan-gate-followup-1dd84065-20261008/`,不替代两轮正式失败回执。
- 定向入口:`QA_FULL_REPORT_DIR=artifacts/qa-flow/loan-gate-followup-1dd84065-20261008 QA_RETAINED_SPECS='r92-mobile-invoice-usability.spec.ts system-action-confirmation.spec.ts' bash scripts/qa/run-web-gates.sh retained-local`;另显式指定本轮专用浏览器副本和受控 BIFF8 Python。任务校验、治理、流程合同及 `git diff --check` 通过。
- 正式运行全程源码/仓库未变,隔离容器及运行目录清理通过。新修正与本记录尚未提交,后续仍需固定新的已提交候选重跑正式门禁;真实建行材料、人员核实和独立业务验收继续待完成。
## 2026-10-08 正式本地发布门禁及测量时序修正 ## 2026-10-08 正式本地发布门禁及测量时序修正
用户同意对固定干净候选 `7a48a1e7` 运行正式本地门禁;执行 `bash scripts/qa/run-release-gate.sh verify-candidate`,未推送、部署、查询生产或执行远端备份切换。专用浏览器经官方安装及受控复制,Chromium/WebKit 原生隔离检查通过。原始回执保留在 `artifacts/release-gate/loan-7a48a1e7-20261008/release-result.json`,结论 **FAIL,不具备发布资格**,不以针对性重验覆盖它。 用户同意对固定干净候选 `7a48a1e7` 运行正式本地门禁;执行 `bash scripts/qa/run-release-gate.sh verify-candidate`,未推送、部署、查询生产或执行远端备份切换。专用浏览器经官方安装及受控复制,Chromium/WebKit 原生隔离检查通过。原始回执保留在 `artifacts/release-gate/loan-7a48a1e7-20261008/release-result.json`,结论 **FAIL,不具备发布资格**,不以针对性重验覆盖它。
@@ -332,7 +332,7 @@ test.describe("R92 手机端票据与报销可用性", () => {
await expect(page.getByTestId("invoice-pool-table")).toBeHidden(); await expect(page.getByTestId("invoice-pool-table")).toBeHidden();
await page.getByTestId("invoice-mobile-detail-button").click(); await page.getByTestId("invoice-mobile-detail-button").click();
await expect(page.getByTestId("invoice-detail-panel")).toBeVisible(); await expect(page.getByTestId("invoice-detail-panel")).toBeVisible();
await expect(page.getByText("票据详情")).toBeVisible(); await expect(page.getByRole("heading", { name: "票据详情", exact: true })).toBeVisible();
await attachScreenshot(page, testInfo, "r97-invoice-pool-mobile.png"); await attachScreenshot(page, testInfo, "r97-invoice-pool-mobile.png");
await page.goto("/invoice-pool"); await page.goto("/invoice-pool");
@@ -830,8 +830,14 @@ test.describe("报销付款交接一致性", () => {
} }
widths.push((await panel.locator("../..").boundingBox())!.width); widths.push((await panel.locator("../..").boundingBox())!.width);
const row = panel.getByTestId("detail-inline-actions"); const row = panel.getByTestId("detail-inline-actions");
const primary = await row.locator("a, button").first().boundingBox(); const measuredActions = row.locator("a, button").first()
const more = await row.getByRole("button", { name: "更多", exact: true }).boundingBox(); .or(row.getByRole("button", { name: "更多", exact: true }));
await expect(measuredActions).toHaveCount(2);
// Both positions must come from the same animation frame.
const [primary, more] = await measuredActions.evaluateAll((elements) => elements.map((element) => {
const { x, y, width, height } = element.getBoundingClientRect();
return { x, y, width, height };
}));
expect(Math.abs(primary!.y + primary!.height / 2 - more!.y - more!.height / 2)).toBeLessThan(2); expect(Math.abs(primary!.y + primary!.height / 2 - more!.y - more!.height / 2)).toBeLessThan(2);
expect(more!.x).toBeGreaterThan(primary!.x + primary!.width); expect(more!.x).toBeGreaterThan(primary!.x + primary!.width);
await row.getByRole("button", { name: "更多", exact: true }).click(); await row.getByRole("button", { name: "更多", exact: true }).click();