diff --git a/web/e2e/document-revision-runtime.spec.ts b/web/e2e/document-revision-runtime.spec.ts index 6f3a98a9..a7f8b5d0 100644 --- a/web/e2e/document-revision-runtime.spec.ts +++ b/web/e2e/document-revision-runtime.spec.ts @@ -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), diff --git a/web/src/features/documentRevision/overlayMode.test.ts b/web/src/features/documentRevision/overlayMode.test.ts index 464330cc..aec05a16 100644 --- a/web/src/features/documentRevision/overlayMode.test.ts +++ b/web/src/features/documentRevision/overlayMode.test.ts @@ -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'); });