tscircuit/ti
ti tscircuit library
- Version
- 1.0.198
- License
- unset
- Stars
- 0
system-block-ui/src/rendering/export-pdf.test.ts
import { describe, expect, test } from "bun:test";
import type { jsPDF } from "jspdf";
import {
calculateSchematicPdfPageLayout,
normalizeSvgPaintForPdf,
normalizeSvgTextForPdf,
renderSchematicPdfSheetOverlay,
} from "./export-pdf";
import {
SCHEMATIC_SVG_HEIGHT,
SCHEMATIC_SVG_WIDTH,
} from "./schematic-page-size";
describe("schematic PDF page layout", () => {
test("matches the schematic canvas to A4 landscape", () => {
expect(SCHEMATIC_SVG_HEIGHT).toBe(990);
expect(SCHEMATIC_SVG_WIDTH / SCHEMATIC_SVG_HEIGHT).toBeCloseTo(
297 / 210,
3,
);
});
test("uses an entire A4 landscape page", () => {
const layout = calculateSchematicPdfPageLayout({
pageWidthMm: 297,
pageHeightMm: 210,
});
expect(layout).toEqual({ x: 0, y: 0, width: 297, height: 210 });
});
test("uses the full page for custom page dimensions", () => {
const layout = calculateSchematicPdfPageLayout({
pageWidthMm: 180,
pageHeightMm: 320,
});
expect(layout).toEqual({ x: 0, y: 0, width: 180, height: 320 });
});
test("rejects invalid page dimensions", () => {
expect(() =>
calculateSchematicPdfPageLayout({
pageWidthMm: 0,
pageHeightMm: 210,
}),
).toThrow("pageWidthMm must be a positive finite number");
expect(() =>
calculateSchematicPdfPageLayout({
pageWidthMm: 297,
pageHeightMm: Number.NaN,
}),
).toThrow("pageHeightMm must be a positive finite number");
});
});
describe("schematic PDF text normalization", () => {
const createTextElement = (entries: [string, string][]) => {
const attributes = new Map(entries);
return {
attributes,
element: {
getAttribute: (name: string) => attributes.get(name) ?? null,
hasAttribute: (name: string) => attributes.has(name),
setAttribute: (name: string, value: string) =>
attributes.set(name, value),
removeAttribute: (name: string) => attributes.delete(name),
},
};
};
test("maps baselines and preserves the original text width", () => {
const centered = createTextElement([
["class", "net-label-text sch-net-label-text"],
["dominant-baseline", "central"],
["font-size", "12px"],
["style", "font-size:12px"],
]);
const svg = {
querySelectorAll: () => [centered.element],
} as unknown as Element;
normalizeSvgTextForPdf(svg);
expect(centered.attributes.get("alignment-baseline")).toBe("central");
expect(centered.attributes.get("font-family")).toBe("LiberationSans");
expect(centered.attributes.get("font-size")).toBe("12px");
expect(centered.attributes.get("dy")).toBe("0.06em");
expect(centered.attributes.get("style")).toContain(
"font-family:LiberationSans",
);
});
test("moves text away from top and bottom anchors", () => {
const belowAnchor = createTextElement([["dominant-baseline", "hanging"]]);
const aboveAnchor = createTextElement([
["dominant-baseline", "ideographic"],
]);
const explicitOffset = createTextElement([
["dominant-baseline", "hanging"],
["dy", "1em"],
]);
const centered = createTextElement([["dominant-baseline", "middle"]]);
const svg = {
querySelectorAll: () => [
belowAnchor.element,
aboveAnchor.element,
explicitOffset.element,
centered.element,
],
} as unknown as Element;
normalizeSvgTextForPdf(svg);
expect(belowAnchor.attributes.get("dy")).toBe("0.12em");
expect(aboveAnchor.attributes.get("dy")).toBe("-0.12em");
expect(explicitOffset.attributes.get("dy")).toBe("1em");
expect(centered.attributes.get("dy")).toBe("0.12em");
});
test("removes the svg2pdf-incompatible reference designator halo", () => {
const reference = createTextElement([
["class", "sch-component-name sch-component-text"],
["stroke", "rgb(245, 241, 237)"],
["stroke-width", "0.5px"],
["paint-order", "stroke"],
["fill", "rgb(15, 15, 15)"],
]);
const svg = {
querySelectorAll: () => [reference.element],
} as unknown as Element;
normalizeSvgTextForPdf(svg);
expect(reference.attributes.get("stroke")).toBe("none");
expect(reference.attributes.has("stroke-width")).toBe(false);
expect(reference.attributes.has("paint-order")).toBe(false);
expect(reference.attributes.get("fill")).toBe("rgb(15, 15, 15)");
});
});
describe("schematic PDF paint normalization", () => {
const createElement = (
entries: [string, string][],
descendants: unknown[] = [],
) => {
const attributes = new Map(entries);
return {
attributes,
element: {
getAttribute: (name: string) => attributes.get(name) ?? null,
setAttribute: (name: string, value: string) =>
attributes.set(name, value),
querySelectorAll: () => descendants,
},
};
};
test("keeps custom-symbol stems visible in svg2pdf", () => {
const stem = createElement([
["stroke", "rgba(132, 0, 0)"],
["fill", "rgba(245, 241, 237, 0.5)"],
["style", "color:rgba(15, 15, 15);opacity:1"],
]);
const svg = createElement([], [stem.element]);
normalizeSvgPaintForPdf(svg.element as unknown as Element);
expect(stem.attributes.get("stroke")).toBe("rgb(132, 0, 0)");
expect(stem.attributes.get("fill")).toBe("rgba(245, 241, 237, 0.5)");
expect(stem.attributes.get("style")).toBe(
"color:rgb(15, 15, 15);opacity:1",
);
});
});
describe("schematic PDF sheet overlay", () => {
test("renders the title on the left and compact page count on the right", () => {
const calls: Array<{
text: string;
x: number;
y: number;
options?: { align?: string };
}> = [];
const backdrops: Array<[number, number, number, number, string]> = [];
const pdf = {
getTextWidth: (text: string) => text.length,
setFont: () => pdf,
setFontSize: () => pdf,
setFillColor: () => pdf,
setTextColor: () => pdf,
rect: (
x: number,
y: number,
width: number,
height: number,
style: string,
) => {
backdrops.push([x, y, width, height, style]);
return pdf;
},
text: (
text: string,
x: number,
y: number,
options?: { align?: string },
) => {
calls.push({ text, x, y, options });
return pdf;
},
} as unknown as jsPDF;
renderSchematicPdfSheetOverlay({
pdf,
title: "Power supply",
pageNumber: 1,
pageCount: 6,
pageWidthMm: 297,
});
expect(calls).toEqual([
{ text: "Power supply", x: 16, y: 16, options: undefined },
{ text: "1/6", x: 281, y: 16, options: { align: "right" } },
]);
expect(backdrops).toEqual([
[15, 11.5, 14, 5.5, "F"],
[277, 11.5, 5, 5.5, "F"],
]);
});
test("truncates a title before it can collide with the page count", () => {
const calls: string[] = [];
const pdf = {
getTextWidth: (text: string) => text.length,
setFont: () => pdf,
setFontSize: () => pdf,
setFillColor: () => pdf,
setTextColor: () => pdf,
rect: () => pdf,
text: (text: string) => {
calls.push(text);
return pdf;
},
} as unknown as jsPDF;
renderSchematicPdfSheetOverlay({
pdf,
title: "A very long schematic sheet name",
pageNumber: 10,
pageCount: 12,
pageWidthMm: 49,
});
expect(calls).toEqual(["A ver...", "10/12"]);
});
});