merge: fix/document-revision-e2e-overlay-selector
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: "UI-07 纯净阅读用 overlay/class*block 找不到真实块"
|
||||
status: in_progress
|
||||
created: 2026-09-13
|
||||
updated: 2026-09-13
|
||||
severity: high
|
||||
---
|
||||
|
||||
# UI-07 纯净阅读用 overlay/class*block 找不到真实块
|
||||
|
||||
## 现象
|
||||
|
||||
第八轮真实 UI(schema 30):
|
||||
|
||||
- UI-00~UI-06 **PASS**(UI-06 `r0→r1` 且硬刷新持久)
|
||||
- UI-07 `captureOverlayFingerprint(mode=pure)` 抛 `no block to sample computed style`
|
||||
|
||||
## 根因分析
|
||||
|
||||
| 层 | 问题 |
|
||||
|---|---|
|
||||
| DOM | `BlockRenderer` 真实块是 `data-testid=document-block` 的 generic wrapper;无 `class` 含 `block` |
|
||||
| overlay | `data-overlay-kind` 仅在 `overlayMode !== 'pure'` 且 hunk 命中时出现;纯净阅读该属性不存在 |
|
||||
| 脚本 | pure 取样 `[data-overlay-kind], [class*="block"]`;manual/all 全页 `[data-overlay-kind]`,未限定活动文档 |
|
||||
|
||||
`ManualChangeOverlay` 只是工具条 hint,不是块装饰。块装饰是同一 wrapper 上的 inline style。
|
||||
|
||||
同一 human hunk 在 manual/all 都是琥珀底,产品不保证两两不同。Checklist:UI-07 无装饰、UI-08 琥珀标记、失败标准是「三 overlay 视觉无差异」,不是强制 manual≠all。
|
||||
|
||||
## 严重度
|
||||
|
||||
`high`
|
||||
|
||||
## 关联整改
|
||||
|
||||
- Short-term: [../ops/short-term/2026-09-13-document-revision-e2e-overlay-selector.md](../ops/short-term/2026-09-13-document-revision-e2e-overlay-selector.md)
|
||||
- 分支: `fix/document-revision-e2e-overlay-selector`
|
||||
|
||||
## 证据
|
||||
|
||||
- Evidence root(已归档、本轮勿提交):`.doc/test-reports/assets/2026-09-13-document-revision-runtime-ui/schema30-ui07-pure-selector-failure/`
|
||||
|
||||
## 开放问题
|
||||
|
||||
- 占槽后真实 UI-07/08/09 重跑;本任务保持 `in_progress`,不得标 `done`
|
||||
+4
-2
@@ -1,11 +1,11 @@
|
||||
# 当前焦点
|
||||
|
||||
**更新**: 2026-09-13
|
||||
**production_ready**: no(DOCREV:schema **30**;第七轮 UI-06 保存选择器未 PATCH;不操作容器)
|
||||
**production_ready**: no(DOCREV:schema **30**;第八轮 UI-07 纯净阅读取样失败;不操作容器)
|
||||
|
||||
## 焦点
|
||||
|
||||
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`)
|
||||
1. **DOCREV-RT**: [E2E 纯净阅读按 document-block 取样 overlay](short-term/2026-09-13-document-revision-e2e-overlay-selector.md)(`fix/document-revision-e2e-overlay-selector`:禁止 class*=block;不重跑 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
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
| 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 |
|
||||
| Cursor-Auto | document-revision-e2e-overlay-selector | fix/document-revision-e2e-overlay-selector | - | document-revision-overlay-selector | in_progress |
|
||||
|
||||
> 新任务开始前登记。`branch` 必须为 `feature/<slug>` 或 `fix/<slug>`,**禁止** `dev` / `test` / `main`。同一 `mutex` 同时仅一个 `in_progress`。
|
||||
|
||||
@@ -65,3 +66,4 @@
|
||||
| 职责伪造与 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) |
|
||||
| UI-07 纯净阅读取样失败审计 | [../audit/2026-09-13-document-revision-overlay-selector.md](../audit/2026-09-13-document-revision-overlay-selector.md) |
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: "E2E:纯净阅读按 document-block 取样 overlay"
|
||||
status: in_progress
|
||||
created: 2026-09-13
|
||||
updated: 2026-09-13
|
||||
owner: Cursor-Auto
|
||||
branch: fix/document-revision-e2e-overlay-selector
|
||||
worktree: "-"
|
||||
mutex: document-revision-overlay-selector
|
||||
---
|
||||
|
||||
# E2E:纯净阅读按 document-block 取样 overlay
|
||||
|
||||
## 目标
|
||||
|
||||
第八轮 UI-07 首错:pure 模式用 `[data-overlay-kind],[class*=block]` 找不到真实块。在 `active-document-pane` 内对全部 `document-block` 取样真实样式/hunk;manual/all 不保证两两不同。不得将本任务标 `done`。
|
||||
|
||||
## 范围
|
||||
|
||||
- `web/e2e/document-revision-runtime.spec.ts`(UI-07 取样;UI-10 历史自动关闭;UI-16 text 选择器)
|
||||
- `web/src/features/documentRevision/overlayMode.test.ts`(同一 human hunk 不保证 pairwise)
|
||||
- 文档:audit / CURRENT
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不新建 policy 抽象;不改 overlay 视觉/业务
|
||||
- 不人为换块制造 manual≠all
|
||||
- 不触碰/提交 `.doc/test-reports/assets`;不操作容器;本轮不重跑 E2E
|
||||
- 不改 FEATURE-MATRIX
|
||||
|
||||
## 步骤
|
||||
|
||||
1. [x] 认领 CURRENT(mutex `document-revision-overlay-selector`)
|
||||
2. [x] UI-07 在 pane `document-block` 上取样
|
||||
3. [x] 按 checklist 校正三模式断言
|
||||
4. [x] UI-10 按抽屉自动关闭比较
|
||||
5. [ ] 占槽后真实 UI 重跑(本轮不做)
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [x] 禁止 `[class*=block]` / 全页 overlay 取样
|
||||
- [x] 纯净阅读必须无装饰;人工/全部必须有真实 hunk 装饰
|
||||
- [x] web tests + `tsc`/build + `git diff --check`
|
||||
- [ ] 真实 UI-07/08/09 PASS 后才可将本任务标 `done`
|
||||
|
||||
## 相关链接
|
||||
|
||||
- Audit: [../../audit/2026-09-13-document-revision-overlay-selector.md](../../audit/2026-09-13-document-revision-overlay-selector.md)
|
||||
- Evidence: [../../test-reports/assets/2026-09-13-document-revision-runtime-ui/schema30-ui07-pure-selector-failure](../../test-reports/assets/2026-09-13-document-revision-runtime-ui/schema30-ui07-pure-selector-failure)
|
||||
- Checklist: [../checklists/2026-09-13-document-revision-runtime-ui-acceptance.md](../checklists/2026-09-13-document-revision-runtime-ui-acceptance.md)
|
||||
- 前序: [2026-09-13-document-revision-e2e-save-selector.md](./2026-09-13-document-revision-e2e-save-selector.md)
|
||||
@@ -422,26 +422,68 @@ async function readVersionBadge(page: Page): Promise<{ v: string; r: string }> {
|
||||
return { v, r };
|
||||
}
|
||||
|
||||
type OverlaySample = {
|
||||
backgroundColor: string;
|
||||
boxShadow: string;
|
||||
className: string;
|
||||
overlayKind: string;
|
||||
blockId: string;
|
||||
};
|
||||
|
||||
function overlayPaneKey(samples: OverlaySample[]): string {
|
||||
return samples
|
||||
.map((s) => `${s.blockId}:${s.backgroundColor}|${s.boxShadow}|${s.overlayKind}`)
|
||||
.join(';');
|
||||
}
|
||||
|
||||
function overlayPaneHasMark(samples: OverlaySample[]): boolean {
|
||||
return samples.some((s) => s.overlayKind !== '' || (s.boxShadow !== '' && s.boxShadow !== 'none'));
|
||||
}
|
||||
|
||||
/** Sample every real pane block. Overlay styles live on the document-block wrapper itself. */
|
||||
async function captureOverlayFingerprint(page: Page, mode: 'pure' | 'manual' | 'all') {
|
||||
await page.locator('select[aria-label="变更显示模式"]').selectOption(mode);
|
||||
await page.waitForTimeout(400);
|
||||
const marked = page.locator('[data-overlay-kind]');
|
||||
const target =
|
||||
mode === 'pure'
|
||||
? page.locator('[data-overlay-kind], [class*="block"]').first()
|
||||
: marked.first();
|
||||
if ((await target.count()) === 0) {
|
||||
fail(`UI overlay mode=${mode}: no block to sample computed style`);
|
||||
const pane = page.getByTestId('active-document-pane');
|
||||
const modeSelect = pane.locator('select[aria-label="变更显示模式"]');
|
||||
const overlayResp = page.waitForResponse((r) => {
|
||||
if (r.request().method() !== 'GET') return false;
|
||||
const pathOnly = sanitizeUrl(r.url());
|
||||
if (mode === 'pure') return pathOnly.includes('/revision-state');
|
||||
if (mode === 'manual') return pathOnly.includes('/manual-overlay');
|
||||
return pathOnly.includes('/diff') || pathOnly.includes('/manual-overlay');
|
||||
}, { timeout: 20_000 });
|
||||
await modeSelect.selectOption(mode);
|
||||
await expect(modeSelect).toHaveValue(mode);
|
||||
await overlayResp;
|
||||
|
||||
const blocks = pane.getByTestId('document-block');
|
||||
await expect(blocks.first()).toBeVisible({ timeout: 15_000 });
|
||||
const marked = pane.locator('[data-testid="document-block"][data-overlay-kind]');
|
||||
if (mode === 'pure') {
|
||||
await expect(marked).toHaveCount(0, { timeout: 15_000 });
|
||||
} else {
|
||||
await expect(marked.first()).toBeVisible({ timeout: 15_000 });
|
||||
}
|
||||
return target.evaluate((el) => {
|
||||
const s = window.getComputedStyle(el as HTMLElement);
|
||||
return {
|
||||
backgroundColor: s.backgroundColor,
|
||||
boxShadow: s.boxShadow,
|
||||
className: (el as HTMLElement).className || '',
|
||||
overlayKind: (el as HTMLElement).getAttribute('data-overlay-kind') || '',
|
||||
};
|
||||
});
|
||||
|
||||
const samples = await blocks.evaluateAll((els) =>
|
||||
els.map((node) => {
|
||||
const el = node as HTMLElement;
|
||||
const s = window.getComputedStyle(el);
|
||||
return {
|
||||
backgroundColor: s.backgroundColor,
|
||||
boxShadow: s.boxShadow,
|
||||
className: el.className || '',
|
||||
overlayKind: el.getAttribute('data-overlay-kind') || '',
|
||||
blockId: el.getAttribute('data-block-id') || '',
|
||||
};
|
||||
}),
|
||||
);
|
||||
if (samples.length === 0) {
|
||||
fail(`UI overlay mode=${mode}: no document-block to sample computed style`);
|
||||
}
|
||||
if (mode !== 'pure' && !samples.some((s) => s.overlayKind)) {
|
||||
fail(`UI overlay mode=${mode}: no document-block overlay to sample computed style`);
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
// ─── post-restart phase (host restarted docker between runs) ───
|
||||
@@ -895,20 +937,28 @@ test.describe.serial('document-revision-runtime UI acceptance', () => {
|
||||
await shot(page, 'UI-08-manual.png');
|
||||
const all = await captureOverlayFingerprint(page, 'all');
|
||||
await shot(page, 'UI-09-all.png');
|
||||
const key = (x: { backgroundColor: string; boxShadow: string; className: string }) =>
|
||||
`${x.backgroundColor}|${x.boxShadow}|${x.className}`;
|
||||
if (key(pure) === key(manual) && key(manual) === key(all)) {
|
||||
fail(`overlay modes not visually distinct: pure=${JSON.stringify(pure)} manual=${JSON.stringify(manual)} all=${JSON.stringify(all)}`);
|
||||
// Checklist: UI-07 无琥珀/蓝底;UI-08 人工改动琥珀标记;失败标准是「三 overlay 视觉无差异」。
|
||||
// 同一 human hunk 在 manual/all 都是琥珀底,产品不保证两两不同,禁止换块制造差异。
|
||||
const pureKey = overlayPaneKey(pure);
|
||||
const manualKey = overlayPaneKey(manual);
|
||||
const allKey = overlayPaneKey(all);
|
||||
if (overlayPaneHasMark(pure)) {
|
||||
fail(`UI-07 纯净阅读 must have no overlay decoration: ${JSON.stringify(pure)}`);
|
||||
}
|
||||
if (key(pure) === key(manual) || key(pure) === key(all) || key(manual) === key(all)) {
|
||||
// Require pairwise difference across the three captures.
|
||||
fail(`each overlay mode must differ: pure=${key(pure)} manual=${key(manual)} all=${key(all)}`);
|
||||
if (!overlayPaneHasMark(manual)) {
|
||||
fail(`UI-08 人工修改 must decorate changed blocks: ${JSON.stringify(manual)}`);
|
||||
}
|
||||
if (!overlayPaneHasMark(all)) {
|
||||
fail(`UI-09 全部变更 must show version/human decorations: ${JSON.stringify(all)}`);
|
||||
}
|
||||
if (pureKey === manualKey || pureKey === allKey) {
|
||||
fail(`pure must be visually distinct from manual/all: pure=${pureKey} manual=${manualKey} all=${allKey}`);
|
||||
}
|
||||
recordPass({
|
||||
id: 'UI-07',
|
||||
name: 'overlay pure/manual/all',
|
||||
input: 'switch three overlay modes; sample computed style',
|
||||
actual: `pure≠manual≠all`,
|
||||
input: 'switch three overlay modes; sample pane document-block styles',
|
||||
actual: `pure unmarked; manual/all marked; manual===all allowed when hunks match`,
|
||||
networkLogs: logs,
|
||||
durationMs: Date.now() - t0,
|
||||
verdict: 'PASS',
|
||||
@@ -937,20 +987,18 @@ test.describe.serial('document-revision-runtime UI acceptance', () => {
|
||||
await history.getByRole('button', { name: '比较 A/B' }).click();
|
||||
const compare = page.getByRole('dialog', { name: '版本比较' });
|
||||
await expect(compare).toBeVisible({ timeout: 20_000 });
|
||||
// VersionHistoryDrawer onSelectCompare closes history before compare mounts.
|
||||
await expect(history).toHaveCount(0, { timeout: 10_000 });
|
||||
await expect(compare.getByText(/\+\d+\s*\/\s*-\d+\s*\/\s*~\d+/)).toBeVisible({ timeout: 15_000 });
|
||||
await shot(page, 'UI-10-history-compare.png');
|
||||
if ((await history.count()) > 0) {
|
||||
await history.getByRole('button', { name: '关闭' }).click();
|
||||
await expect(history).toHaveCount(0, { timeout: 10_000 });
|
||||
}
|
||||
await compare.getByRole('button', { name: '关闭' }).click();
|
||||
await compare.getByRole('button', { name: '关闭', exact: true }).click();
|
||||
await expect(compare).toHaveCount(0, { timeout: 10_000 });
|
||||
await expect(page.getByRole('dialog', { name: '版本历史' })).toHaveCount(0);
|
||||
recordPass({
|
||||
id: 'UI-10',
|
||||
name: 'history + compare',
|
||||
input: 'open 版本历史; pick two V as A/B; click 比较 A/B; close both dialogs',
|
||||
actual: `history entries=${n}; compared ${vA} vs ${vB}; both dialogs closed`,
|
||||
input: 'open 版本历史; pick two V as A/B; click 比较 A/B; history auto-closes; close compare',
|
||||
actual: `history entries=${n}; compared ${vA} vs ${vB}; history auto-closed; compare closed`,
|
||||
networkLogs: logs,
|
||||
durationMs: Date.now() - t0,
|
||||
verdict: 'PASS',
|
||||
@@ -1296,7 +1344,7 @@ test.describe.serial('document-revision-runtime UI acceptance', () => {
|
||||
if (paneId.startsWith('doc-upload')) {
|
||||
fail(`UI-16 must not open doc-upload orphan: ${paneId}`);
|
||||
}
|
||||
await expect(page.locator('text=版本账本未就绪')).toHaveCount(0);
|
||||
await expect(page.getByText('版本账本未就绪')).toHaveCount(0);
|
||||
await expect(page.locator('[data-testid="doc-rev-version"]')).toBeVisible({ timeout: 30_000 });
|
||||
const orphanHits = logs.filter(
|
||||
(l) => l.path.includes('/revision-state') && /\/documents\/doc-upload-/.test(l.path),
|
||||
|
||||
@@ -35,6 +35,11 @@ describe('document revision overlay modes', () => {
|
||||
assert.equal(decorationFor('all', agent), 'agent');
|
||||
});
|
||||
|
||||
it('same human hunk stays amber in manual and all (no pairwise style guarantee)', () => {
|
||||
assert.equal(decorationFor('manual', human), decorationFor('all', human));
|
||||
assert.notEqual(decorationFor('pure', human), decorationFor('manual', human));
|
||||
});
|
||||
|
||||
it('exposes needs-content error code for empty orphan docs', () => {
|
||||
assert.equal(DOC_REV_ERROR.NEEDS_CONTENT, 'DOCUMENT_REVISION_NEEDS_CONTENT');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user