import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { detectPrintRestriction, isPrintSupported, printPersonalReport, safeReportFilename, } from "../src/lib/client-report-export.ts"; const exportSource = readFileSync( new URL("../src/lib/client-report-export.ts", import.meta.url), "utf8", ); const pageSource = readFileSync( new URL("../src/components/personal-report/personal-report-page.tsx", import.meta.url), "utf8", ); const documentViewSource = readFileSync( new URL("../src/components/personal-report/personal-report-document-view.tsx", import.meta.url), "utf8", ); test("safeReportFilename strips path, query and control characters", () => { assert.equal(safeReportFilename("../../etc/passwd?x=1&y=2"), "jyotisha-report-etc-passwd-x-1-y-2"); assert.equal(safeReportFilename("a bd|e:f"), "jyotisha-report-a-b-c-d-e-f"); assert.equal(safeReportFilename(""), "jyotisha-report"); assert.equal(safeReportFilename(null as unknown as string), "jyotisha-report"); const cleaned = safeReportFilename("abcdefghijklmnopqrstuvwxyz0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZ-extra"); assert.ok(cleaned.startsWith("jyotisha-report-")); assert.ok(cleaned.length <= "jyotisha-report-".length + 64); assert.doesNotMatch(cleaned, /[^a-zA-Z0-9_-]/); }); test("detectPrintRestriction flags WeChat in-app browser and nothing else", () => { assert.equal(detectPrintRestriction("Mozilla/5.0 MicroMessenger/8.0.49").restricted, true); assert.equal(detectPrintRestriction("Mozilla/5.0 (iPhone) Safari").restricted, false); assert.equal(detectPrintRestriction(undefined).restricted, false); assert.match(detectPrintRestriction("MicroMessenger").message ?? "", /系统浏览器/); }); test("isPrintSupported requires a usable window.print", () => { assert.equal(isPrintSupported(), false); (globalThis as Record).window = { print: () => undefined }; assert.equal(isPrintSupported(), true); delete (globalThis as Record).window; }); test("printPersonalReport waits for fonts, then calls window.print exactly once", async () => { const calls: string[] = []; let resolveFonts: (() => void) | undefined; const fontsReady = new Promise((resolve) => { resolveFonts = resolve; }); const fakeDocument = { fonts: { ready: fontsReady }, title: "original-title" }; (globalThis as Record).window = { print: () => { calls.push("print"); // Browsers derive the PDF filename from document.title at print time. assert.equal(fakeDocument.title, "safe-filename-title"); }, }; (globalThis as Record).document = fakeDocument; const printPromise = printPersonalReport({ title: "safe-filename-title" }); // Fonts not ready yet: print must not have fired. assert.deepEqual(calls, []); resolveFonts?.(); await printPromise; assert.deepEqual(calls, ["print"]); // Original title restored afterwards. assert.equal(fakeDocument.title, "original-title"); delete (globalThis as Record).window; delete (globalThis as Record).document; }); test("printPersonalReport is a no-op without window", async () => { // window is already deleted; must not throw. await printPersonalReport({ title: "x" }); }); test("client export never touches a server PDF pipeline", () => { assert.doesNotMatch(exportSource, /api\/report_artifact/); assert.doesNotMatch(exportSource, /html2canvas|jsPDF|jspdf|playwright|puppeteer|chromium/i); assert.doesNotMatch(exportSource, /canvas|base64/i); assert.doesNotMatch(exportSource, /\bfetch\s*\(/); assert.match(exportSource, /window\.print/); assert.match(exportSource, /document\.fonts/); }); test("native print uses the same validated structured DOM for stored v1 and current v2", () => { assert.match(pageSource, /safeParseReportDocument\(json\.reportDocument\)/); assert.match(pageSource, //); assert.match(documentViewSource, /isReportDocumentV2\(document\)/); assert.match(documentViewSource, /