60 lines
4.7 KiB
JavaScript
60 lines
4.7 KiB
JavaScript
// Actual glyph extents must stay inside the first print fragment, including accents.
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const assert = require('node:assert/strict');
|
|
const req = require('node:module').createRequire(path.resolve(__dirname, '../../job-tracker-ui/package.json'));
|
|
const { chromium } = req('playwright');
|
|
const ts = req('typescript');
|
|
const source = fs.readFileSync(path.resolve(__dirname, '../../job-tracker-ui/src/cvPreview.ts'), 'utf8');
|
|
const compiled = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS } }).outputText;
|
|
const copy = { exports: {} }; new Function('exports', 'module', compiled)(copy.exports, copy);
|
|
async function main() {
|
|
const [corpus, target, kind = 'cv'] = process.argv.slice(2);
|
|
assert.ok(['cv', 'letter'].includes(kind));
|
|
assert.ok(corpus && target && !fs.existsSync(target), 'Use native corpus and a fresh output directory');
|
|
fs.mkdirSync(target, { recursive: true });
|
|
const browser = await chromium.launch({ headless: true });
|
|
const page = await browser.newPage({ viewport: { width: 794, height: 1123 }, offline: true, serviceWorkers: 'block' });
|
|
await page.route('**/*', route => route.abort());
|
|
const rows = [];
|
|
try {
|
|
const fixtures = JSON.parse(fs.readFileSync(path.join(corpus, 'measurements.json'), 'utf8')).fixtures.filter(row => row.fixture.startsWith('short-en-'));
|
|
assert.equal(fixtures.length, 9);
|
|
for (const fixture of fixtures) for (const photo of (kind === 'cv' ? [true, false] : [false])) for (const european of [false, true]) {
|
|
assert.match(fixture.fixture, /^short-en-[a-z-]+$/);
|
|
let html = fs.readFileSync(path.join(corpus, fixture.fixture, 'render.html'), 'utf8');
|
|
if (!photo && kind === 'cv') html = html.replace('</head>', '<style>.photo{display:none!important}</style></head>');
|
|
if (european) html = kind === 'cv'
|
|
? html.replace(/(<h1 class="name">)[^<]+(<\/h1>)/, '$1S01 Åse Émilie Müller Žák Łódź Sørensen$2')
|
|
: html.replace(/(<h1>)[^<]+(<\/h1>)/, '$1R01 Åse Émilie Müller Žák Łódź Sørensen$2');
|
|
await page.setContent(copy.exports.previewDocumentHtml(html, 297 * 96 / 25.4));
|
|
await page.waitForFunction(() => document.documentElement.dataset.cvPreviewReady === 'true');
|
|
const measured = await page.evaluate(() => {
|
|
const name = document.querySelector('.name') || document.querySelector('h1'), node = name.firstChild, style = getComputedStyle(name);
|
|
if (node.nodeType !== Node.TEXT_NODE || !/^[SR]01 /.test(node.textContent)) throw new Error('Unexpected synthetic fixture');
|
|
const context = document.createElement('canvas').getContext('2d');
|
|
context.font = `${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
|
|
const clipTop = document.querySelector('[data-cv-preview-flow]').getBoundingClientRect().top;
|
|
const glyphs = [];
|
|
for (let index = 0; index < node.length; index++) {
|
|
const text = node.textContent[index]; if (!text.trim()) continue;
|
|
const range = document.createRange(); range.setStart(node, index); range.setEnd(node, index + 1);
|
|
const rect = range.getClientRects()[0], metrics = context.measureText(text);
|
|
const inkTop = rect.top + metrics.fontBoundingBoxAscent - metrics.actualBoundingBoxAscent;
|
|
glyphs.push({ text, inkTop, clearancePx: inkTop - clipTop });
|
|
}
|
|
return { clipTop, font: context.font, glyphs, minClearancePx: Math.min(...glyphs.map(row => row.clearancePx)) };
|
|
});
|
|
const key = `${fixture.fixture}-${photo ? 'photo' : 'no-photo'}-${european ? 'eu' : 'nb'}`;
|
|
await page.screenshot({ path: path.join(target, key + '.jpg'), type: 'jpeg', quality: 90, clip: { x: 0, y: 0, width: 794, height: 360 } });
|
|
rows.push({ fixture: fixture.fixture, photo, european, pass: measured.minClearancePx >= 0, ...measured });
|
|
}
|
|
assert.equal(rows.length, kind === 'cv' ? 36 : 18);
|
|
const result = { criterion: 'Actual first-page glyph ink cannot extend above the fragment clipping boundary; no epsilon or relaxed tolerance', scope: 'Synthetic layout variants derived from real renderer HTML; display-none photo equivalent and additional European text, not extra API persistence tests', passed: rows.filter(row => row.pass).length, total: rows.length, rows };
|
|
fs.writeFileSync(path.join(target, 'result.json'), JSON.stringify(result, null, 2));
|
|
console.log(`Name accent clearance ${result.passed}/${result.total}; min ${Math.min(...rows.map(row => row.minClearancePx))}px`);
|
|
if (result.passed !== result.total) process.exitCode = 1;
|
|
} finally { await browser.close(); }
|
|
}
|
|
main().catch(error => { console.error(error.message); process.exitCode = 1; });
|