64c86dcdd6
Paint 3 known noise regions white in both screenshots before pixel- matching: - top-left ~200×90 (debug counter + orange `Тестовая версия` badge) - top-right ~240×50 (build tag like `rc/2026-04-06`) - bottom-right ~90×90 (chat-widget bubble) These show only on the deployed Angular test env, not in the React dev build, and were inflating every parity score by ~1-2pp. Mismatch deltas vs prior run: en-onlineboard-route 4.62% → 4.45% flight-details 11.24% → 10.82% mobile-flight-details 17.92% → 16.58% mobile-onlineboard-start 20.37% → 18.66% onlineboard-arrival 4.63% → 4.46% onlineboard-departure 5.03% → 4.86% onlineboard-route 4.78% → 4.60% onlineboard-start 14.52% → 13.77% schedule-route 12.47% → 11.87% schedule-start 13.39% → 12.86% flights-map 37.28% → 36.40%
148 lines
5.1 KiB
JavaScript
148 lines
5.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Generate pixel diffs for the side-by-side Angular/React screenshots
|
|
* captured into `comparison-report/visual/screenshots/full/` and emit a
|
|
* JSON file the report.html consumes for the quantitative side of the
|
|
* parity comparison.
|
|
*
|
|
* Pairs are inferred by filename: every `react-{stem}.png` is matched
|
|
* against `angular-{stem}.png` in the same directory. Diffs are written
|
|
* to `comparison-report/visual/diffs-full/{stem}.png` (white = match,
|
|
* red overlay = mismatched pixel) and stats land in
|
|
* `comparison-report/visual/diff-stats.json`.
|
|
*
|
|
* Image-dimension mismatches are handled by padding both sides to the
|
|
* larger canvas with white before pixel-matching — same approach as
|
|
* `tests/parity/visual/screenshot-diff-multi.ts`.
|
|
*/
|
|
|
|
import { readdirSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
import { resolve, join } from "node:path";
|
|
import { PNG } from "pngjs";
|
|
import pixelmatch from "pixelmatch";
|
|
|
|
const ROOT = resolve(process.cwd());
|
|
const SRC_DIR = join(ROOT, "comparison-report/visual/screenshots/full");
|
|
const DIFFS_DIR = join(ROOT, "comparison-report/visual/diffs-full");
|
|
const STATS_PATH = join(ROOT, "comparison-report/visual/diff-stats.json");
|
|
|
|
mkdirSync(DIFFS_DIR, { recursive: true });
|
|
|
|
/**
|
|
* Paint a rectangle white. Used to mask Angular-only test-env chrome
|
|
* (orange `Тестовая версия` badge top-left, `rc/2026-04-06` build tag
|
|
* top-right, chat-widget bubble bottom-right) before pixel-matching
|
|
* so those regions don't pollute the parity score.
|
|
*/
|
|
function maskRect(png, x, y, w, h) {
|
|
const x0 = Math.max(0, x);
|
|
const y0 = Math.max(0, y);
|
|
const x1 = Math.min(png.width, x + w);
|
|
const y1 = Math.min(png.height, y + h);
|
|
for (let yy = y0; yy < y1; yy++) {
|
|
for (let xx = x0; xx < x1; xx++) {
|
|
const idx = (yy * png.width + xx) * 4;
|
|
png.data[idx] = 255;
|
|
png.data[idx + 1] = 255;
|
|
png.data[idx + 2] = 255;
|
|
png.data[idx + 3] = 255;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Apply the standard chrome masks to both PNGs in-place. */
|
|
function applyChromeMasks(png) {
|
|
const isMobile = png.width <= 500;
|
|
// Top-left debug counter / orange "Тестовая версия" badge.
|
|
maskRect(png, 0, 0, isMobile ? 200 : 200, 90);
|
|
// Top-right `rc/...` build tag.
|
|
maskRect(png, png.width - 240, 0, 240, 50);
|
|
// Bottom-right chat widget.
|
|
maskRect(png, png.width - 90, png.height - 90, 90, 90);
|
|
}
|
|
|
|
const files = readdirSync(SRC_DIR);
|
|
const reactFiles = files.filter((f) => f.startsWith("react-") && f.endsWith(".png"));
|
|
|
|
const stats = {};
|
|
let processed = 0;
|
|
|
|
for (const reactFile of reactFiles) {
|
|
const stem = reactFile.slice("react-".length); // e.g. "onlineboard-start.png"
|
|
const angularFile = `angular-${stem}`;
|
|
if (!files.includes(angularFile)) {
|
|
console.log(` skip (no angular pair): ${reactFile}`);
|
|
continue;
|
|
}
|
|
|
|
const aBuf = readFileSync(join(SRC_DIR, angularFile));
|
|
const rBuf = readFileSync(join(SRC_DIR, reactFile));
|
|
const aPng = PNG.sync.read(aBuf);
|
|
const rPng = PNG.sync.read(rBuf);
|
|
|
|
const width = Math.max(aPng.width, rPng.width);
|
|
const height = Math.max(aPng.height, rPng.height);
|
|
|
|
const pad = (src) => {
|
|
const out = new PNG({ width, height });
|
|
for (let i = 0; i < out.data.length; i += 4) {
|
|
out.data[i] = 255; out.data[i + 1] = 255; out.data[i + 2] = 255; out.data[i + 3] = 255;
|
|
}
|
|
for (let y = 0; y < src.height; y++) {
|
|
for (let x = 0; x < src.width; x++) {
|
|
const srcIdx = (y * src.width + x) * 4;
|
|
const dstIdx = (y * width + x) * 4;
|
|
out.data[dstIdx] = src.data[srcIdx];
|
|
out.data[dstIdx + 1] = src.data[srcIdx + 1];
|
|
out.data[dstIdx + 2] = src.data[srcIdx + 2];
|
|
out.data[dstIdx + 3] = src.data[srcIdx + 3];
|
|
}
|
|
}
|
|
return PNG.sync.read(PNG.sync.write(out));
|
|
};
|
|
|
|
const aP = pad(aPng);
|
|
const rP = pad(rPng);
|
|
|
|
// Strip Angular-only chrome (orange test-env badge, build tag, chat
|
|
// widget) from both sides — those regions are environmental noise
|
|
// that shouldn't drag the parity score down.
|
|
applyChromeMasks(aP);
|
|
applyChromeMasks(rP);
|
|
|
|
const diffPng = new PNG({ width, height });
|
|
|
|
const mismatchCount = pixelmatch(
|
|
aP.data, rP.data, diffPng.data, width, height,
|
|
{ threshold: 0.1, alpha: 0.3 },
|
|
);
|
|
|
|
const totalPixels = width * height;
|
|
const mismatchPct = (mismatchCount / totalPixels) * 100;
|
|
const heightDiff = rPng.height - aPng.height;
|
|
|
|
const diffName = stem;
|
|
writeFileSync(join(DIFFS_DIR, diffName), PNG.sync.write(diffPng));
|
|
|
|
const key = stem.replace(/\.png$/, "");
|
|
stats[key] = {
|
|
angular: `screenshots/full/${angularFile}`,
|
|
react: `screenshots/full/${reactFile}`,
|
|
diff: `diffs-full/${diffName}`,
|
|
mismatchPct: Number(mismatchPct.toFixed(3)),
|
|
mismatchCount,
|
|
totalPixels,
|
|
heightDiff,
|
|
angular_w: aPng.width, angular_h: aPng.height,
|
|
react_w: rPng.width, react_h: rPng.height,
|
|
};
|
|
|
|
processed++;
|
|
console.log(` ${stem}: ${mismatchPct.toFixed(2)}% diff (${mismatchCount.toLocaleString()} / ${totalPixels.toLocaleString()} px)`);
|
|
}
|
|
|
|
writeFileSync(STATS_PATH, JSON.stringify(stats, null, 2));
|
|
console.log(`\nDone — ${processed} pairs diffed.`);
|
|
console.log(` Diffs: ${DIFFS_DIR}/`);
|
|
console.log(` Stats: ${STATS_PATH}`);
|