test(e2e): sample overlay styles on pane document-blocks

UI-07 reads every active-pane document-block after the real overlay
fetch. Pure must stay unmarked; manual/all may share amber on the
same human hunk. UI-10 waits for history to auto-close.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-13 18:06:53 +00:00
co-authored by Cursor
parent 1b020790da
commit 20b6092cf0
2 changed files with 88 additions and 35 deletions
+83 -35
View File
@@ -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');
});