mirror of https://github.com/garrytan/gstack.git
test(make-pdf): pin zero-truncation invariant, typography floor, centering math
Global img cap pinned as a regex invariant (the figure-scoped-cap regression class); typography floor (12pt body, 56pt cover, 12pt TOC); .page-wide must NOT carry min-height/flex (the phantom-landscape-page regression class); centering margin math verified both ways (2400×1000 image → 1.38in, 2050×600 viewBox diagram → 1.93in, page-filling directive block → no margin). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
9397e7f847
commit
4c11fe4e61
|
|
@ -17,7 +17,9 @@ import {
|
|||
const silent = { warn: () => {} };
|
||||
|
||||
// 6.5in content box → threshold = 6.5 × 96 × 2.5 = 1560 CSS px.
|
||||
const OPTS = { contentWidthIn: 6.5, ...silent };
|
||||
// Letter landscape content box: 9in wide × 6.5in tall.
|
||||
const LANDSCAPE = { contentWIn: 9, contentHIn: 6.5 };
|
||||
const OPTS = { contentWidthIn: 6.5, landscape: LANDSCAPE, ...silent };
|
||||
|
||||
function img(attrs: string): string {
|
||||
return `<p><img ${attrs}></p>`;
|
||||
|
|
@ -131,20 +133,34 @@ describe("auto-landscape: negative cases (the load-bearing ones)", () => {
|
|||
});
|
||||
|
||||
describe("auto-landscape: positive cases", () => {
|
||||
test("wide + alt hint + over threshold promotes and wraps", () => {
|
||||
test("wide + alt hint + over threshold promotes, wraps, and vertically centers", () => {
|
||||
const warnings: string[] = [];
|
||||
const r = applyImagePolicy(
|
||||
img(`src="x" alt="architecture diagram" data-gstack-px-width="2400" data-gstack-px-height="1000"`),
|
||||
{ contentWidthIn: 6.5, warn: (m) => warnings.push(m) },
|
||||
{ contentWidthIn: 6.5, landscape: LANDSCAPE, warn: (m) => warnings.push(m) },
|
||||
);
|
||||
expect(r.hasLandscape).toBe(true);
|
||||
// placed height = 9in × (1000/2400) = 3.75in → margin-top = (6.5−3.75)/2 ≈ 1.38in
|
||||
expect(r.html).toContain('<div class="page-wide" style="margin-top: 1.38in"><img');
|
||||
expect(r.html).not.toContain("<p><img");
|
||||
expect(warnings[0]).toContain("landscape");
|
||||
});
|
||||
|
||||
test("directive-forced tall block that fills the page gets no centering margin", () => {
|
||||
// aspect 0.9 → placed height 9×0.9 = 8.1in > 6.5in box → margin clamps to 0
|
||||
const r = applyImagePolicy(
|
||||
img(`src="x" data-gstack-page="landscape" data-gstack-px-width="1000" data-gstack-px-height="900"`),
|
||||
OPTS,
|
||||
);
|
||||
expect(r.hasLandscape).toBe(true);
|
||||
expect(r.html).toContain('<div class="page-wide"><img');
|
||||
expect(r.html).not.toContain("<p><img");
|
||||
expect(warnings[0]).toContain("landscape");
|
||||
expect(r.html).not.toContain("margin-top");
|
||||
});
|
||||
test("page=landscape forces promotion regardless of size", () => {
|
||||
const r = applyImagePolicy(img(`src="x" data-gstack-page="landscape"`), OPTS);
|
||||
expect(r.hasLandscape).toBe(true);
|
||||
// no intrinsic dims → no centering guess, top placement
|
||||
expect(r.html).toContain('<div class="page-wide"><img');
|
||||
});
|
||||
test("alt hint matches whole words only", () => {
|
||||
const r = applyImagePolicy(
|
||||
|
|
@ -159,10 +175,11 @@ describe("auto-landscape: diagram figures", () => {
|
|||
const fig = (svgAttrs: string, figAttrs = "") =>
|
||||
`<figure class="diagram" role="img" aria-label="d"${figAttrs}>\n<svg ${svgAttrs}><g/></svg>\n</figure>`;
|
||||
|
||||
test("wide diagram via viewBox promotes (provenance automatic, no alt needed)", () => {
|
||||
test("wide diagram via viewBox promotes and centers (provenance automatic, no alt needed)", () => {
|
||||
const r = applyImagePolicy(fig(`width="100%" viewBox="0 0 2050 600"`), OPTS);
|
||||
expect(r.hasLandscape).toBe(true);
|
||||
expect(r.html).toContain('<div class="page-wide"><figure');
|
||||
// placed height = 9 × 600/2050 ≈ 2.63in → margin-top = (6.5−2.63)/2 ≈ 1.93in
|
||||
expect(r.html).toContain('<div class="page-wide" style="margin-top: 1.93in"><figure');
|
||||
});
|
||||
test("normal flowchart stays portrait", () => {
|
||||
const r = applyImagePolicy(fig(`width="100%" viewBox="0 0 800 400"`), OPTS);
|
||||
|
|
|
|||
|
|
@ -327,6 +327,33 @@ describe("printCss", () => {
|
|||
expect(css).toMatch(/@bottom-center\s*\{\s*content:\s*counter\(page\)/);
|
||||
});
|
||||
|
||||
// Zero image truncation, ever: the cap must be a GLOBAL img rule. Markdown
|
||||
// images render as <p><img> (no figure), so a figure-scoped cap alone lets
|
||||
// wide screenshots run off the page edge — the exact regression this pins.
|
||||
test("emits a global img max-width cap (zero truncation invariant)", () => {
|
||||
const css = printCss();
|
||||
expect(css).toMatch(/(^|\n)img\s*\{\s*max-width:\s*100%;\s*height:\s*auto;\s*\}/);
|
||||
});
|
||||
|
||||
test("typography floor: body 12pt, poster cover, readable TOC", () => {
|
||||
const css = printCss({ cover: true, toc: true });
|
||||
expect(css).toContain("font-size: 12pt"); // body
|
||||
expect(css).toMatch(/\.cover h1\.cover-title\s*\{[^}]*font-size:\s*56pt/);
|
||||
expect(css).toMatch(/\.cover \.cover-meta\s*\{[^}]*font-size:\s*13pt/);
|
||||
expect(css).toMatch(/\.toc li\s*\{[^}]*font-size:\s*12pt/);
|
||||
});
|
||||
|
||||
test("page-wide carries the named page and NO height/flex centering", () => {
|
||||
const css = printCss();
|
||||
expect(css).toMatch(/\.page-wide\s*\{[^}]*page:\s*wide/);
|
||||
// Centering is computed by image-policy as an inline margin-top. CSS
|
||||
// flex/min-height centering fragments into phantom empty landscape pages
|
||||
// in Chromium — this pins the regression (landscape-gate: 5 pages for 3
|
||||
// promotions, bisected to min-height at any value).
|
||||
expect(css).not.toMatch(/\.page-wide\s*\{[^}]*min-height/);
|
||||
expect(css).not.toMatch(/\.page-wide\s*\{[^}]*flex/);
|
||||
});
|
||||
|
||||
test("font stacks include Liberation Sans adjacent to Helvetica", () => {
|
||||
const css = printCss({ confidential: true });
|
||||
// Body stack
|
||||
|
|
|
|||
Loading…
Reference in New Issue