68 lines
4.8 KiB
JavaScript
68 lines
4.8 KiB
JavaScript
// Evidence-only DOM measurement: never changes renderer, goldens or acceptance policy.
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const assert = require('node:assert/strict');
|
|
const crypto = require('node:crypto');
|
|
const req = require('node:module').createRequire(path.resolve(__dirname, '../../job-tracker-ui/package.json'));
|
|
const { chromium } = req('playwright');
|
|
const ts = req('typescript');
|
|
const hash = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
const previewFile = path.resolve(__dirname, '../../job-tracker-ui/src/cvPreview.ts');
|
|
const previewSource = fs.readFileSync(previewFile);
|
|
const compiled = ts.transpileModule(previewSource.toString('utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS } }).outputText;
|
|
const copy = { exports: {} };
|
|
new Function('exports', 'module', compiled)(copy.exports, copy);
|
|
|
|
async function main() {
|
|
const [source, target] = process.argv.slice(2);
|
|
assert.ok(source && target, 'Usage: node measure-baselines.cjs <native corpus> <new output JSON>');
|
|
assert.ok(!fs.existsSync(target), 'Keep earlier measurements immutable');
|
|
const input = fs.readFileSync(path.join(source, 'measurements.json'));
|
|
const fixtures = JSON.parse(input).fixtures;
|
|
assert.equal(fixtures.length, 36);
|
|
const browser = await chromium.launch({ headless: true });
|
|
const page = await browser.newPage({ viewport: { width: 794, height: 1123 }, serviceWorkers: 'block', offline: true });
|
|
await page.route('**/*', route => route.abort());
|
|
const rows = [];
|
|
try {
|
|
for (const fixture of fixtures) {
|
|
assert.match(fixture.fixture, /^(?:short-en|medium-nb|long-en|dense-nb)-[a-z-]+$/);
|
|
const html = fs.readFileSync(path.join(source, fixture.fixture, 'render.html'));
|
|
assert.ok(html.length <= 8 * 1024 * 1024, 'Excessive synthetic HTML');
|
|
await page.setContent(copy.exports.previewDocumentHtml(html.toString('utf8'), 297 * 96 / 25.4));
|
|
await page.waitForFunction(() => document.documentElement.dataset.cvPreviewReady === 'true');
|
|
const measured = await page.evaluate(() => {
|
|
const name = document.querySelector('.name');
|
|
const node = name?.firstChild;
|
|
if (node?.nodeType !== Node.TEXT_NODE || !node.textContent.startsWith('S01 ')) throw new Error('Unexpected synthetic name fixture');
|
|
const range = document.createRange(); range.setStart(node, 0); range.setEnd(node, 3);
|
|
const rect = range.getBoundingClientRect(), style = getComputedStyle(name);
|
|
const sentinel = document.createElement('span');
|
|
sentinel.style.cssText = 'display:inline-block;width:0;height:0;padding:0;margin:0;border:0;vertical-align:baseline';
|
|
name.prepend(sentinel);
|
|
const baseline = sentinel.getBoundingClientRect().top;
|
|
sentinel.remove();
|
|
const after = range.getBoundingClientRect();
|
|
const context = document.createElement('canvas').getContext('2d');
|
|
context.font = `${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
|
|
const metrics = context.measureText('S01');
|
|
return { rangeX: rect.x, rangeTop: rect.y, rangeHeight: rect.height, rangeWidth: rect.width,
|
|
sentinelBaseline: baseline, canvasAscent: metrics.fontBoundingBoxAscent, canvasDescent: metrics.fontBoundingBoxDescent,
|
|
canvasBaseline: rect.y + metrics.fontBoundingBoxAscent,
|
|
sentinelChangedGeometry: rect.x !== after.x || rect.y !== after.y || rect.width !== after.width || rect.height !== after.height,
|
|
font: context.font, fontFamily: style.fontFamily, fontSize: style.fontSize, fontWeight: style.fontWeight,
|
|
fontLoaded: document.fonts.check(context.font, 'S01'), pages: Number(document.documentElement.dataset.cvPreviewPages) };
|
|
});
|
|
const original = fixture.measured.tokens.find(token => token.token === 'S01');
|
|
assert.ok(measured.fontLoaded && !measured.sentinelChangedGeometry);
|
|
assert.ok(Math.abs(measured.sentinelBaseline - measured.canvasBaseline) < 0.01, 'Independent baseline measurements disagree');
|
|
assert.ok(Math.abs(measured.rangeX - original.x) < 0.01 && Math.abs(measured.rangeTop - original.y) < 0.01, 'Preview source/geometry drift');
|
|
assert.equal(measured.pages, fixture.previewPages);
|
|
rows.push({ fixture: fixture.fixture, htmlSha256: hash(html), pdfSha256: hash(fs.readFileSync(path.join(source, fixture.fixture, 'export.pdf'))), ...measured });
|
|
}
|
|
fs.writeFileSync(target, JSON.stringify({ evidenceOnly: true, policy: 'PROPOSED_DIAGNOSTIC_NOT_ORIGINAL_GATE_REPLACEMENT', browser: browser.version(), measurementsSha256: hash(input), previewSourceSha256: hash(previewSource), rows }, null, 2));
|
|
console.log(`Measured ${rows.length} synthetic DOM baselines; sentinel/canvas agree; geometry unchanged`);
|
|
} finally { await browser.close(); }
|
|
}
|
|
main().catch(error => { console.error(error.message); process.exitCode = 1; });
|