Files
Codex b7d8b9e6ee test: displayed version through a seam; docs:accept --identical for pixel-identical screenshots
Golden frames carried the card version text (about, version banner,
support/export previews), so every beta bump re-accepted up to 20
baselines that had not visually changed. The version now reaches the DOM
and stand requests through displayVersion() (src/card-version.ts); the
golden harness pins window.__HP_VERSION_OVERRIDE__ = '0.0.0-golden'
before the card is created. CARD_VERSION literals stay where the release
contract reads them; cache-busting and the console banner keep the literal.

Docs screenshots: `npm run docs:accept -- --identical` re-captures
locally, compares decoded RGBA pixels with the committed frames inside
Chromium (scripts/png-identical.mjs) and, only when every frame is
identical, refreshes the manifest fingerprints and captureScriptSha256;
bytes stay, any difference refuses with a per-frame count. First run on
this tree: 11/11 identical, manifest refreshed.

Mutants: version-seam-ignores-override, docs-identical-accepts-any-frame.

Issue: #512
User-Visible: no
2026-09-09 18:53:38 +03:00

74 lines
3.9 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Попиксельное сравнение PNG-кадров (#512 §6).
*
* Байты двух PNG с одинаковыми пикселями различаются законно: oxipng в CI
* перепаковывает файл, локальная съёмка — нет. Сравнивать поэтому надо
* декодированные пиксели, а декодирует их тот же Chromium, что и снимал:
* `compareInPage` живёт на странице Playwright и исполняет там же
* `compareDecodedPixels` — чистую функцию над RGBA-буферами, которую тест
* компаратора гоняет без браузера.
*/
/**
* @param {{width:number,height:number,data:Uint8Array|Uint8ClampedArray|number[]}} a
* @param {{width:number,height:number,data:Uint8Array|Uint8ClampedArray|number[]}} b
* @returns {{identical:boolean, differing:number, sizeMismatch:boolean}}
*/
export function compareDecodedPixels(a, b) {
if (!a || !b || a.width !== b.width || a.height !== b.height) {
return { identical: false, differing: Number.POSITIVE_INFINITY, sizeMismatch: true };
}
const total = a.width * a.height;
let differing = 0;
for (let index = 0; index < total; index += 1) {
const at = index * 4;
if (a.data[at] !== b.data[at] || a.data[at + 1] !== b.data[at + 1]
|| a.data[at + 2] !== b.data[at + 2] || a.data[at + 3] !== b.data[at + 3]) differing += 1;
}
return { identical: differing === 0, differing, sizeMismatch: false };
}
/**
* Сравнить одну пару PNG на странице Chromium: декодирование и подсчёт идут
* внутри страницы, наружу выходит только результат — пиксели двух десятков
* кадров через границу evaluate не протащить (#512: «Target crashed» на
* первом же прогоне с сериализацией RGBA).
* @param {import('playwright').Page} page
* @param {Buffer} committed
* @param {Buffer} candidate
*/
export async function compareInPage(page, committed, candidate) {
return page.evaluate(async ({ a, b, compareSource }) => {
const compare = new Function(`return (${compareSource});`)();
const decode = async (base64) => {
const bytes = Uint8Array.from(atob(base64), (char) => char.charCodeAt(0));
const bitmap = await createImageBitmap(new Blob([bytes], { type: 'image/png' }));
const canvas = document.createElement('canvas');
canvas.width = bitmap.width;
canvas.height = bitmap.height;
const context = canvas.getContext('2d', { willReadFrequently: true });
context.drawImage(bitmap, 0, 0);
const { data } = context.getImageData(0, 0, bitmap.width, bitmap.height);
bitmap.close();
return { width: bitmap.width, height: bitmap.height, data };
};
const result = compare(await decode(a), await decode(b));
return { ...result, differing: Number.isFinite(result.differing) ? result.differing : -1 };
}, {
a: Buffer.from(committed).toString('base64'),
b: Buffer.from(candidate).toString('base64'),
compareSource: compareDecodedPixels.toString(),
}).then((result) => ({ ...result, differing: result.differing < 0 ? Number.POSITIVE_INFINITY : result.differing }));
}
/**
* Сравнить пары кадров: `pairs[i] = { id, committed: Buffer, candidate: Buffer }`.
* Компаратор пары инъектируется (в бою — `compareInPage` с Playwright-страницей).
* @returns {Promise<Array<{id:string, identical:boolean, differing:number, sizeMismatch:boolean}>>}
*/
export async function compareFramePairs(pairs, comparePair) {
const results = [];
for (const pair of pairs) results.push({ id: pair.id, ...(await comparePair(pair.committed, pair.candidate)) });
return results;
}