// 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('', '');
if (european) html = kind === 'cv'
? html.replace(/(
)[^<]+(<\/h1>)/, '$1S01 Åse Émilie Müller Žák Łódź Sørensen$2')
: html.replace(/()[^<]+(<\/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; });