mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
* 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
118 lines
4.1 KiB
TypeScript
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);
|
|
}
|