Files
open-design/e2e/tests/artifact-render-parity.test.ts
lefarcenandGitHub 1c1ecae470 fix(web): preserve HTML preview state across edit handoffs (#7592)
* fix(web): preserve HTML preview state across edit handoffs

* test(web): align reload race coverage with URL handoff

* test(e2e): target canonical active preview frame

* fix(preview): complete URL edit handoff parity

* fix(e2e): classify preview round-trip parity

* fix(preview): close edit handoff readiness gaps

* fix(preview): close remaining handoff regressions

* refactor(preview): centralize runtime state contract

* test(preview): isolate parity audit runtime
2026-08-31 01:32:49 +00:00

118 lines
4.1 KiB
TypeScript

import type { Page } from '@playwright/test';
import { describe, expect, it, vi } from 'vitest';
import { PNG } from 'pngjs';
import {
ACTIVE_ARTIFACT_PREVIEW_SELECTOR,
settledActiveArtifactPreview,
} from '../lib/playwright/artifact-preview.ts';
import {
classifyPixelParity,
combinePixelParityClassifications,
comparePngBuffers,
} from '../lib/playwright/artifact-render-parity.ts';
describe('artifact render parity', () => {
it('reports byte-identical pixels as exact', () => {
const png = solidPng(3, 2, [20, 40, 60, 255]);
const comparison = comparePngBuffers(png, png);
expect(comparison.exactDiffPixels).toBe(0);
expect(comparison.perceptualDiffPixels).toBe(0);
expect(classifyPixelParity({
comparison,
actualSelfDriftRatio: 0,
expectedSelfDriftRatio: 0,
maxPerceptualDiffRatio: 0.001,
maxSelfDriftRatio: 0.001,
})).toBe('exact');
});
it('counts changed pixels and emits a same-sized diff', () => {
const before = solidPng(2, 2, [255, 255, 255, 255]);
const after = solidPng(2, 2, [255, 255, 255, 255]);
const decoded = PNG.sync.read(after);
decoded.data.set([0, 0, 0, 255], 0);
const comparison = comparePngBuffers(before, PNG.sync.write(decoded));
expect(comparison.exactDiffPixels).toBe(1);
expect(comparison.perceptualDiffPixels).toBe(1);
expect(comparison.perceptualDiffRatio).toBe(0.25);
const diff = PNG.sync.read(comparison.diffPng);
expect([diff.width, diff.height]).toEqual([2, 2]);
});
it('treats a viewport-size change as a measurable pixel difference', () => {
const smaller = solidPng(2, 2, [255, 255, 255, 255]);
const taller = solidPng(2, 3, [255, 255, 255, 255]);
const comparison = comparePngBuffers(smaller, taller);
expect(comparison.actualHeight).toBe(2);
expect(comparison.expectedHeight).toBe(3);
expect(comparison.perceptualDiffPixels).toBe(2);
expect(comparison.perceptualDiffRatio).toBeCloseTo(1 / 3);
});
it('does not call two dynamic captures a transport regression', () => {
expect(classifyPixelParity({
comparison: { exactDiffPixels: 100, perceptualDiffRatio: 0.25 },
actualSelfDriftRatio: 0.02,
expectedSelfDriftRatio: 0,
maxPerceptualDiffRatio: 0.001,
maxSelfDriftRatio: 0.001,
})).toBe('unstable');
});
it('reports a differing Edit-to-URL round trip when srcDoc entry is exact', () => {
const beforeEdit = solidPng(2, 2, [255, 255, 255, 255]);
const inEdit = beforeEdit;
const afterEdit = solidPng(2, 2, [0, 0, 0, 255]);
const thresholds = {
actualSelfDriftRatio: 0,
expectedSelfDriftRatio: 0,
maxPerceptualDiffRatio: 0.001,
maxSelfDriftRatio: 0.001,
};
const entryStatus = classifyPixelParity({
comparison: comparePngBuffers(beforeEdit, inEdit),
...thresholds,
});
const roundTripStatus = classifyPixelParity({
comparison: comparePngBuffers(beforeEdit, afterEdit),
...thresholds,
});
expect(entryStatus).toBe('exact');
expect(roundTripStatus).toBe('different');
expect(combinePixelParityClassifications(entryStatus, roundTripStatus)).toBe('different');
});
it('times out instead of capturing an iframe whose visual handoff never completes', async () => {
const waitFor = vi.fn().mockRejectedValue(
new Error('Timed out waiting for data-od-handoff-pending to clear'),
);
const locator = { waitFor };
const page = {
locator: vi.fn((selector: string) => {
expect(selector).toBe(ACTIVE_ARTIFACT_PREVIEW_SELECTOR);
return { first: () => locator };
}),
} as unknown as Page;
await expect(settledActiveArtifactPreview(page, 50)).rejects.toThrow(
'Timed out waiting for data-od-handoff-pending to clear',
);
expect(waitFor).toHaveBeenCalledWith({ state: 'visible', timeout: 250 });
});
});
function solidPng(width: number, height: number, rgba: [number, number, number, number]): Buffer {
const png = new PNG({ width, height });
for (let offset = 0; offset < png.data.length; offset += 4) {
png.data.set(rgba, offset);
}
return PNG.sync.write(png);
}