fix(cv): align preview pagination with native PDF

This commit is contained in:
cesnimda
2026-10-07 08:46:43 +02:00
parent 046dc304aa
commit efc9e7bdd4
182 changed files with 16092 additions and 47 deletions
+51
View File
@@ -0,0 +1,51 @@
import { expect, test } from "@playwright/test";
import fs from "node:fs/promises";
import path from "node:path";
import { previewDocumentHtml } from "../src/cvPreview";
const original = path.resolve("../docs/redesign/phase-5/p5-09-readiness/evidence/fidelity");
test("[PRES-C09 C12] original flowing/boundary fixtures retain native PDF page membership", async ({ page }) => {
const baseline = JSON.parse(await fs.readFile(path.join(original, "fidelity-results.json"), "utf8"));
for (const fixture of baseline.fixtures.filter((fixture: { pass: boolean }) => !fixture.pass)) {
await page.setContent(previewDocumentHtml(await fs.readFile(path.join(original, fixture.fixture, "render.html"), "utf8"), 1122.52));
await expect.poll(() => page.evaluate(() => document.documentElement.dataset.cvPreviewReady)).toBe("true");
expect(await page.evaluate(() => Number(document.documentElement.dataset.cvPreviewPages)), fixture.fixture).toBe(fixture.pdfPages);
const membership = await page.evaluate(() => {
const width = Number(document.documentElement.dataset.cvPreviewWidth);
const walker = document.createTreeWalker(document.querySelector(".page")!, NodeFilter.SHOW_TEXT);
const markers: { token: string; page: number }[] = [];
let node;
while ((node = walker.nextNode())) for (const match of (node.textContent || "").matchAll(/\b[SEBPR]\d{2}\b/g)) {
const range = document.createRange();
range.setStart(node, match.index!); range.setEnd(node, match.index! + match[0].length);
markers.push({ token: match[0], page: Math.floor(range.getClientRects()[0].x / width) + 1 });
}
return markers;
});
expect(membership, fixture.fixture).toEqual(fixture.comparisons.map((marker: { token: string; pdfPage: number }) => ({ token: marker.token, page: marker.pdfPage })));
expect(await page.evaluate(() => document.body.innerText)).not.toContain("HIDDEN-MARKER");
}
});
test("[PRES-C06 C09] pagination waits for image layout and publishes one settled measurement", async ({ page }) => {
let releaseImage!: () => void;
const gate = new Promise<void>(resolve => { releaseImage = resolve; });
await page.route("**/pagination-photo.svg", async route => {
await gate;
await route.fulfill({ contentType: "image/svg+xml", body: '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100"><rect width="200" height="100" fill="navy"/></svg>' });
});
const html = '<html><head><style>:root{--cv-page-margin:16mm}body{margin:0}.page{width:210mm;min-height:297mm}@media print{.page{width:auto;min-height:0}}</style></head><body><div class="page"><img alt="Synthetic fixture" src="http://localhost:3300/pagination-photo.svg" style="width:20mm"></div></body></html>';
await page.setContent(previewDocumentHtml(html, 1122.52), { waitUntil: "domcontentloaded" });
expect(await page.evaluate(() => document.documentElement.dataset.cvPreviewReady)).toBeUndefined();
await page.evaluate(() => {
document.documentElement.dataset.readyEvents = "0";
window.addEventListener("cv-preview-ready", () => { document.documentElement.dataset.readyEvents = String(Number(document.documentElement.dataset.readyEvents) + 1); });
});
releaseImage();
await expect.poll(() => page.evaluate(() => document.documentElement.dataset.cvPreviewReady)).toBe("true");
expect(await page.evaluate(() => ({ events: document.documentElement.dataset.readyEvents, pages: document.documentElement.dataset.cvPreviewPages }))).toEqual({ events: "1", pages: "1" });
expect(await page.evaluate(() => document.querySelector("img")!.getBoundingClientRect().height)).toBeCloseTo(10 * 96 / 25.4, 1);
await page.evaluate(() => new Promise<void>(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))));
expect(await page.evaluate(() => document.documentElement.dataset.readyEvents)).toBe("1");
});
+11 -5
View File
@@ -71,13 +71,16 @@ test("[PRES-C06 C08 C09 C12] native preview/PDF fidelity corpus records unchange
await page.goto(`/career/builder/${variant.id}`);
await expect(page.getByTitle("CV preview page 1", { exact: true })).toBeVisible();
const iframe = page.getByTitle("CV preview", { exact: true });
await expect.poll(async () => iframe.evaluate(element => !!(element as HTMLIFrameElement).contentDocument?.querySelector(".name"))).toBe(true);
await expect.poll(async () => iframe.evaluate(element => (element as HTMLIFrameElement).contentDocument?.documentElement?.dataset.cvPreviewReady)).toBe("true");
const measured = await iframe.evaluate(async element => {
const frame = element as HTMLIFrameElement;
const doc = frame.contentDocument!;
await doc.fonts.ready;
await new Promise<void>(resolve => frame.contentWindow!.requestAnimationFrame(() => frame.contentWindow!.requestAnimationFrame(() => resolve())));
const win = frame.contentWindow!;
const geometry = doc.documentElement.dataset;
const physicalWidth = Number(geometry.cvPreviewWidth);
const physicalHeight = Number(geometry.cvPreviewHeight);
const css = (selector: string) => {
const node = doc.querySelector(selector);
if (!node) return null;
@@ -93,12 +96,15 @@ test("[PRES-C06 C08 C09 C12] native preview/PDF fidelity corpus records unchange
for (const match of (node.textContent || "").matchAll(/\b[SEBPR]\d{2}\b/g)) {
const range = doc.createRange();
range.setStart(node, match.index!); range.setEnd(node, match.index! + match[0].length);
const rect = range.getBoundingClientRect();
const rect = range.getClientRects()[0];
const style = win.getComputedStyle(node.parentElement!);
tokens.push({ token: match[0], x: rect.x, y: rect.y, width: rect.width, height: rect.height, fontSize: style.fontSize, fontFamily: style.fontFamily, color: style.color });
// Native screen columns are horizontal. Normalize to the same physical-page
// coordinate system used by the unchanged PDF oracle, retaining raw evidence.
const pageIndex = Math.floor(rect.x / physicalWidth);
tokens.push({ token: match[0], x: rect.x - pageIndex * physicalWidth, y: rect.y + pageIndex * physicalHeight, rawX: rect.x, rawY: rect.y, page: pageIndex + 1, width: rect.width, height: rect.height, fontSize: style.fontSize, fontFamily: style.fontFamily, color: style.color });
}
}
return { width: frame.clientWidth, contentHeight: Math.max(doc.body.scrollHeight, doc.documentElement.scrollHeight), text: doc.body.innerText, tokens, body: css("body"), page: css(".page"), header: css(".header,.hero"), main: css(".main"), sidebar: css(".sidebar"), parsedRules: Array.from(doc.styleSheets).flatMap(sheet => Array.from(sheet.cssRules).map(rule => rule.cssText)), sections: Array.from(doc.querySelectorAll(".section")).map(node => ({ key: node.className, title: node.querySelector("h2")?.textContent, y: node.getBoundingClientRect().y })), effectiveMargin: win.getComputedStyle(doc.documentElement).getPropertyValue("--cv-page-margin"), userAgent: win.navigator.userAgent };
return { width: physicalWidth, contentHeight: Number(geometry.cvPreviewPages) * physicalHeight, usableHeight: Number(geometry.cvPreviewUsableHeight), layoutDurationMs: Number(geometry.cvPreviewDurationMs), text: doc.body.innerText, tokens, body: css("body"), page: css(".page"), header: css(".header,.hero"), main: css(".main"), sidebar: css(".sidebar"), parsedRules: Array.from(doc.styleSheets).flatMap(sheet => Array.from(sheet.cssRules).map(rule => rule.cssText)), sections: Array.from(doc.querySelectorAll(".section")).map(node => ({ key: node.className, title: node.querySelector("h2")?.textContent, fragments: Array.from(node.getClientRects()).map(rect => ({ x: rect.x, y: rect.y, width: rect.width, height: rect.height })) })), effectiveMargin: win.getComputedStyle(doc.documentElement).getPropertyValue("--cv-page-margin"), userAgent: win.navigator.userAgent };
});
expect(measured.text, fixture.key).not.toContain("HIDDEN-MARKER");
const pdf = await page.request.post(`${apiUrl}/cv/variants/${variant.id}/export-pdf`, { headers });
@@ -117,7 +123,7 @@ test("[PRES-C06 C08 C09 C12] native preview/PDF fidelity corpus records unchange
const capture = await browser.newPage({ viewport: { width: Math.ceil(measured.width), height: Math.ceil(height) } });
try {
await capture.setContent(srcDoc!);
await capture.evaluate(async () => { await document.fonts.ready; await new Promise<void>(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))); });
await expect.poll(() => capture.evaluate(() => document.documentElement.dataset.cvPreviewReady)).toBe("true");
await capture.screenshot({ path: path.join(directory, `preview-${index}.png`) });
} finally { await capture.close(); }
}