merge: fix/document-revision-e2e-save-selector

This commit is contained in:
weichen
2026-09-13 17:47:28 +00:00
7 changed files with 164 additions and 6 deletions
@@ -0,0 +1,45 @@
---
title: "UI-06 全页模糊保存按钮未发出 PATCH"
status: in_progress
created: 2026-09-13
updated: 2026-09-13
severity: high
---
# UI-06 全页模糊保存按钮未发出 PATCH
## 现象
第七轮真实 UI(schema 30):
- 稳定 `document-block` 已成功双击,编辑弹窗出现
- `saveOpenEditor` 使用全页 `getByRole(button, { name: /保存修改|保存/ })`
- 严格模式同时命中 7 个「一键保存到我的私密文档库」与真正的「保存修改」
- **未发送 PATCH**,UI-06 失败
## 根因分析
| 层 | 问题 |
|---|---|
| 聊天栏 | `ReplyActionBar` 保存按钮的可访问名称是 `aria-label="一键保存到我的私密文档库"`,含「保存」 |
| 弹窗 | `BlockEditorModal` 无 `role=dialog` / 稳定 `aria-label`,无法按 dialog 收窄 |
| 脚本 | 全页模糊 `/保存修改\|保存/` 在 Playwright 严格模式命中多个控件 |
块寻址已修好;本轮是 **保存选择器过宽**,不是正文或双击失败。
## 严重度
`high`
## 关联整改
- Short-term: [../ops/short-term/2026-09-13-document-revision-e2e-save-selector.md](../ops/short-term/2026-09-13-document-revision-e2e-save-selector.md)
- 分支: `fix/document-revision-e2e-save-selector`
## 证据
- Evidence root(已归档、本轮勿提交):`.doc/test-reports/assets/2026-09-13-document-revision-runtime-ui/schema30-ui06-save-selector-failure/`
## 开放问题
- 占槽后真实 UI-06/13b 重跑;本任务保持 `in_progress`,不得标 `done`
+4 -2
View File
@@ -1,11 +1,11 @@
# 当前焦点
**更新**: 2026-09-13
**production_ready**: no(DOCREV:schema **30**;第六轮 UI-06 块选择器超时;不操作容器)
**production_ready**: no(DOCREV:schema **30**;第七轮 UI-06 保存选择器未 PATCH;不操作容器)
## 焦点
1. **DOCREV-RT**: [E2E 真实文档块稳定寻址](short-term/2026-09-13-document-revision-e2e-block-selector.md)(`fix/document-revision-e2e-block-selector`:UI-06/13b 不依赖 overlay;不重跑 E2E / 不操作容器;仍 `in_progress`)
1. **DOCREV-RT**: [E2E 块编辑 dialog 内精确保存](short-term/2026-09-13-document-revision-e2e-save-selector.md)(`fix/document-revision-e2e-save-selector`:禁止全页模糊 /保存/;不重跑 E2E / 不操作容器;仍 `in_progress`)
2. **CHAT**: [刷新恢复整改已实施](short-term/2026-09-13-chat-history-refresh-remediation.md)(待合 dev + PI E2E 补录)
3. **CI**: `TEST_DATABASE_URL` + 真实 PI E2E 实录;INTENT 发布 blocker 见 conversation-intent short-term
@@ -23,6 +23,7 @@
| Cursor-Auto | document-revision-e2e-pending-flow | fix/document-revision-e2e-pending-flow | - | pending-report-ui | in_progress |
| Cursor-Auto | document-revision-e2e-reject-routing | fix/document-revision-e2e-reject-routing | - | document-revision-runtime-ui | in_progress |
| Cursor-Auto | document-revision-e2e-block-selector | fix/document-revision-e2e-block-selector | - | document-revision-block-selector | in_progress |
| Cursor-Auto | document-revision-e2e-save-selector | fix/document-revision-e2e-save-selector | - | document-revision-save-selector | in_progress |
> 新任务开始前登记。`branch` 必须为 `feature/<slug>` 或 `fix/<slug>`,**禁止** `dev` / `test` / `main`。同一 `mutex` 同时仅一个 `in_progress`。
@@ -63,3 +64,4 @@
| UI-05b 拒绝路由失败审计 | [../audit/2026-09-13-document-revision-e2e-reject-routing.md](../audit/2026-09-13-document-revision-e2e-reject-routing.md) |
| 职责伪造与 ETag 覆盖审计 | [../audit/2026-09-13-document-revision-duty-etag-overwrite.md](../audit/2026-09-13-document-revision-duty-etag-overwrite.md) |
| UI-06 块选择器失败审计 | [../audit/2026-09-13-document-revision-block-selector.md](../audit/2026-09-13-document-revision-block-selector.md) |
| UI-06 保存选择器失败审计 | [../audit/2026-09-13-document-revision-save-selector.md](../audit/2026-09-13-document-revision-save-selector.md) |
@@ -0,0 +1,51 @@
---
title: "E2E:块编辑 dialog 内精确保存"
status: in_progress
created: 2026-09-13
updated: 2026-09-13
owner: Cursor-Auto
branch: fix/document-revision-e2e-save-selector
worktree: "-"
mutex: document-revision-save-selector
---
# E2E:块编辑 dialog 内精确保存
## 目标
第七轮 UI-06 首错:双击已打开编辑器,但全页模糊「保存」点到聊天私密库按钮,未 PATCH。给 `BlockEditorModal` 可访问 dialog + 稳定 aria-label;保存只在该 dialog 内 `exact: true` 点「保存修改」。不得将本任务标 `done`。
## 范围
- `web/src/lib/blockEditorDialog.ts` + 契约单测
- `web/src/components/doc/BlockEditorModal.tsx`
- `web/e2e/document-revision-runtime.spec.ts`(UI-06/13b 复用;UI-13a dialog=0)
- 文档:audit / CURRENT
## 非目标
- 不改视觉/保存业务
- 不降低已有断言
- 不触碰/提交 `.doc/test-reports/assets`;不操作容器;本轮不重跑 E2E
- 不改 FEATURE-MATRIX
## 步骤
1. [x] 认领 CURRENT(mutex `document-revision-save-selector`)
2. [x] dialog `role` + `aria-label=编辑受控块`
3. [x] E2E 在 dialog 内精确点「保存修改」
4. [x] UI-13a 断言编辑 dialog 不出现
5. [ ] 占槽后真实 UI 重跑(本轮不做)
## 验收标准
- [x] 契约单测钉住 dialog label 与 exact 保存文案
- [x] 禁止全页 `/保存/`
- [x] web tests + `tsc`/build + `git diff --check`
- [ ] 真实 UI-06/13b PASS 后才可将本任务标 `done`
## 相关链接
- Audit: [../../audit/2026-09-13-document-revision-save-selector.md](../../audit/2026-09-13-document-revision-save-selector.md)
- Evidence: [../../test-reports/assets/2026-09-13-document-revision-runtime-ui/schema30-ui06-save-selector-failure](../../test-reports/assets/2026-09-13-document-revision-runtime-ui/schema30-ui06-save-selector-failure)
- 前序: [2026-09-13-document-revision-e2e-block-selector.md](./2026-09-13-document-revision-e2e-block-selector.md)
+14 -3
View File
@@ -2,6 +2,10 @@ import { test, expect, type Page, type BrowserContext, type Response } from '@pl
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import {
BLOCK_EDITOR_DIALOG_LABEL,
BLOCK_EDITOR_SAVE_NAME,
} from '../src/lib/blockEditorDialog';
/**
* Document revision runtime — real UI acceptance (UI-01..UI-16).
@@ -361,18 +365,25 @@ async function closeNamedDialog(page: Page, name: string, timeout = 10_000) {
await expect(dialog).toHaveCount(0, { timeout });
}
function blockEditorDialog(target: Page) {
return target.getByRole('dialog', { name: BLOCK_EDITOR_DIALOG_LABEL });
}
async function openFirstPaneEditorAndFill(target: Page, text: string) {
const pane = target.getByTestId('active-document-pane');
const block = pane.getByTestId('document-block').first();
await block.dblclick({ timeout: 15_000 });
const editor = target.locator('textarea, [contenteditable="true"]').last();
const dialog = blockEditorDialog(target);
await expect(dialog).toBeVisible({ timeout: 10_000 });
const editor = dialog.locator('textarea, [contenteditable="true"]').first();
await expect(editor).toBeVisible({ timeout: 10_000 });
await editor.fill(text);
return editor;
}
async function saveOpenEditor(target: Page) {
await target.getByRole('button', { name: /保存修改|保存/ }).click();
const dialog = blockEditorDialog(target);
await dialog.getByRole('button', { name: BLOCK_EDITOR_SAVE_NAME, exact: true }).click();
}
async function editFirstPaneBlockAndSave(target: Page, text: string) {
@@ -1092,7 +1103,7 @@ test.describe.serial('document-revision-runtime UI acceptance', () => {
await expect(pageB.getByText('双击任意段落就地编辑')).toHaveCount(0);
const pane = pageB.getByTestId('active-document-pane');
await pane.getByTestId('document-block').first().dblclick({ timeout: 15_000 });
await expect(pageB.getByRole('button', { name: /保存修改|保存并关闭/ })).toHaveCount(0);
await expect(blockEditorDialog(pageB)).toHaveCount(0);
await shot(pageB, 'UI-13a-reviewer-readonly.png');
await ctxB.close();
recordPass({
+5 -1
View File
@@ -23,6 +23,7 @@ import {
} from 'lucide-react';
import katex from 'katex';
import { renderInlineMarkdown } from '../../engine/renderMarkdown';
import { BLOCK_EDITOR_DIALOG_LABEL, BLOCK_EDITOR_SAVE_NAME } from '../../lib/blockEditorDialog';
interface BlockEditorModalProps {
block: DocumentBlock;
@@ -216,6 +217,9 @@ export const BlockEditorModal: React.FC<BlockEditorModalProps> = ({ block, onSav
return (
<div
role="dialog"
aria-modal="true"
aria-label={BLOCK_EDITOR_DIALOG_LABEL}
style={{
position: 'fixed',
inset: 0,
@@ -973,7 +977,7 @@ export const BlockEditorModal: React.FC<BlockEditorModalProps> = ({ block, onSav
}}
>
<Check size={14} />
<span>保存修改</span>
<span>{BLOCK_EDITOR_SAVE_NAME}</span>
</button>
</div>
</div>
+28
View File
@@ -0,0 +1,28 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
BLOCK_EDITOR_DIALOG_LABEL,
BLOCK_EDITOR_SAVE_NAME,
blockEditorDialogPolicy,
} from './blockEditorDialog';
describe('blockEditorDialogPolicy', () => {
it('pins an accessible dialog label and exact save name', () => {
const policy = blockEditorDialogPolicy();
assert.equal(policy.role, 'dialog');
assert.equal(policy.name, '编辑受控块');
assert.equal(policy.name, BLOCK_EDITOR_DIALOG_LABEL);
assert.equal(policy.saveButton.name, '保存修改');
assert.equal(policy.saveButton.name, BLOCK_EDITOR_SAVE_NAME);
assert.equal(policy.saveButton.exact, true);
});
it('rejects page-wide fuzzy /保存/ that would hit private-library buttons', () => {
const policy = blockEditorDialogPolicy();
const privateLibrary = '一键保存到我的私密文档库';
assert.match(privateLibrary, policy.forbidPageWideSaveRegex);
assert.equal(policy.saveButton.name === '保存修改', true);
assert.doesNotMatch(policy.saveButton.name, /^一键保存/);
assert.equal(policy.forbidPageWideSaveRegex.source, '保存修改|保存');
});
});
+17
View File
@@ -0,0 +1,17 @@
/** 块编辑弹窗的稳定可访问名称与保存按钮文案(禁止全页模糊 /保存/)。 */
export const BLOCK_EDITOR_DIALOG_LABEL = '编辑受控块';
export const BLOCK_EDITOR_SAVE_NAME = '保存修改';
export function blockEditorDialogPolicy(): {
role: 'dialog';
name: typeof BLOCK_EDITOR_DIALOG_LABEL;
saveButton: { name: typeof BLOCK_EDITOR_SAVE_NAME; exact: true };
forbidPageWideSaveRegex: RegExp;
} {
return {
role: 'dialog',
name: BLOCK_EDITOR_DIALOG_LABEL,
saveButton: { name: BLOCK_EDITOR_SAVE_NAME, exact: true },
forbidPageWideSaveRegex: /保存修改|保存/,
};
}