Files
Jyotisha/frontend/tests/personal-report-export.test.ts
2026-08-15 05:10:37 +08:00

101 lines
4.3 KiB
TypeScript

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 b<c>d|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<string, unknown>).window = { print: () => undefined };
assert.equal(isPrintSupported(), true);
delete (globalThis as Record<string, unknown>).window;
});
test("printPersonalReport waits for fonts, then calls window.print exactly once", async () => {
const calls: string[] = [];
let resolveFonts: (() => void) | undefined;
const fontsReady = new Promise<void>((resolve) => {
resolveFonts = resolve;
});
const fakeDocument = { fonts: { ready: fontsReady }, title: "original-title" };
(globalThis as Record<string, unknown>).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<string, unknown>).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<string, unknown>).window;
delete (globalThis as Record<string, unknown>).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, /<PersonalReportDocumentView document=\{document\} \/>/);
assert.match(documentViewSource, /isReportDocumentV2\(document\)/);
assert.match(documentViewSource, /<VedicChartSvg chart=\{structuredChart\(chart\)\}/);
assert.doesNotMatch(documentViewSource, /dangerouslySetInnerHTML|srcDoc|<iframe/i);
assert.doesNotMatch(documentViewSource, /modelHtml|modelCss|modelSvg/i);
assert.match(exportSource, /window\.print/);
});