Fix Monolith and Cobalt Stripe closing-block squeeze; guard Monolith flow notes

When only the closing block moved to page 2, the page-1 deficit was shared across every gap, including the closing block's own gaps, which cannot help page 1. Page 1 then came out a few points short and the totals spilled over with the closing block. Monolith and Cobalt Stripe now squeeze only the page-1 gaps in that case, as Marble and Highlighter already do.

Monolith's flow notes block could start in the last few points of a page and be cut into zero-height lines. It now holds 70 pt of room under its label first, as Marble does.

Adds tests for both cases (they failed before the fix) and notes in the template specs. Fingerprint goldens and thumbnails are unchanged.
This commit is contained in:
2026-10-04 16:46:22 +05:30
parent afbc0aa56f
commit 9e17ba3479
7 changed files with 89 additions and 15 deletions
+19
View File
@@ -339,6 +339,25 @@ describe("closing and footer", () => {
expect(r.pdf.pages.map((p) => p.items.map((i) => i.str).join(" ")).join(" ")).toContain("Note 18");
});
it("when only the closing block moves to page 2 the page-1 gaps are squeezed enough to keep the items and totals on page 1", async () => {
// Row counts where the overflow before the closing block is within the gaps that help page 1 (A4 / Letter).
const cases: Array<["A4" | "LETTER", number]> = [["A4", 7]];
for (const [size, n] of cases) {
const r = await renderFixture({ name: "s", items: n, logo: "none" }, prefsFor(size, { templateId: "cobalt-stripe" }));
const where = (id: string) => {
const pages: number[] = [];
r.result.core.layout.forEach((pg, i) => walk(pg.nodes, (x) => x.id === id && pages.push(i)));
return pages;
};
const rowPages = new Set<number>();
r.result.core.layout.forEach((pg, i) => walk(pg.nodes, (x) => x.id?.startsWith(TABLE_ROW_PREFIX) && rowPages.add(i)));
expect([...rowPages], `${size} n=${n} rows`).toEqual([0]);
expect(where(TOTALS_ID), `${size} n=${n} totals`).toEqual([0]);
expect(where(CLOSING_ID), `${size} n=${n} closing`).toEqual([1]);
expect(r.result.pages).toBe(2);
}
});
it("an address, e-mail and phone that are too long for one row shrink and wrap inside the footer", async () => {
const r = await renderFixture(FIXTURES.find((f) => f.name === "id-tokens")!, prefsFor("A4", { templateId: "cobalt-stripe" }));
expect(r.result.issues.filter((i) => i.severity === "error")).toEqual([]);