import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import React from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { PersonalReportDocumentView } from "../src/components/personal-report/personal-report-document-view.tsx"; import { classifyReportEnvelope } from "../src/components/personal-report/personal-report-page.tsx"; import { safeParseReportDocument } from "../src/lib/personal-report-contract.ts"; import type { ReportDocumentV1 } from "../src/lib/personal-report-contract.ts"; Object.assign(globalThis, { React }); const fixturePath = new URL("../../tests/fixtures/personal_report_document.v1.json", import.meta.url); const canonicalFixture = JSON.parse(readFileSync(fixturePath, "utf8")) as ReportDocumentV1; function render(document: ReportDocumentV1): string { return renderToStaticMarkup(React.createElement(PersonalReportDocumentView, { document })); } function withPresentationMode(document: ReportDocumentV1, presentationMode: "default" | "research"): ReportDocumentV1 { return { ...structuredClone(document), presentationMode }; } function withCharts(document: ReportDocumentV1, charts: ReportDocumentV1["charts"]): ReportDocumentV1 { return { ...structuredClone(document), charts }; } test("canonical fixture passes the canonical contract parse", () => { const parsed = safeParseReportDocument(canonicalFixture); assert.equal(parsed.ok, true); if (parsed.ok) { assert.equal(parsed.document.reportId, canonicalFixture.reportId); } }); test("renders sections in the fixed order: cover -> D1 -> summary -> themes -> appendix -> disclaimer", () => { const markup = render(canonicalFixture); const positions: Record = { subject: markup.indexOf(canonicalFixture.subject.displayName), d1: markup.indexOf(canonicalFixture.charts[0].title), summary: markup.indexOf("核心摘要"), theme: markup.indexOf(canonicalFixture.thematicNarrative[0].title), appendix: markup.indexOf("证据附录"), disclaimer: markup.indexOf("声明"), }; for (const key of Object.keys(positions)) { assert.ok(positions[key] >= 0, `${key} must be rendered`); } assert.ok(positions.subject < positions.d1, "cover comes before D1 chart"); assert.ok(positions.d1 < positions.summary, "D1 chart comes before the summary"); assert.ok(positions.summary < positions.theme, "summary comes before themes"); assert.ok(positions.theme < positions.appendix, "themes come before the appendix"); assert.ok(positions.appendix < positions.disclaimer, "appendix comes before the disclaimer"); }); test("Technique Audit Table is never placed before the summary", () => { const markup = render(canonicalFixture); const summaryAt = markup.indexOf("核心摘要"); const auditAt = markup.indexOf("Technique Audit Table"); assert.ok(summaryAt >= 0 && auditAt >= 0, "both summary and audit table render"); assert.ok(auditAt > summaryAt, "audit table must appear after the summary"); }); test("evidence appendix is collapsed by default in default mode and expanded in research mode", () => { const auditText = "MEVG / Global Web Evidence"; const collapsed = render(canonicalFixture); assert.match(collapsed, /personal-report-print-always hidden/, "default mode starts collapsed"); assert.doesNotMatch(collapsed, /personal-report-print-always block/, "default mode must not render expanded"); assert.ok(collapsed.indexOf(auditText) >= 0, "content exists in the DOM but is visually hidden"); const expanded = render(withPresentationMode(canonicalFixture, "research")); assert.match(expanded, /personal-report-print-always block/, "research mode starts expanded"); assert.ok(expanded.indexOf(auditText) >= 0, "research mode renders appendix content"); }); test("expandedByDefault=true expands the appendix even in default mode", () => { const document = structuredClone(canonicalFixture); document.evidenceAppendix.expandedByDefault = true; const markup = render(document); assert.match(markup, /personal-report-print-always block/, "expandedByDefault forces the expanded state"); }); test("D1 SVG renders when real houses exist; D9/D10 are never fabricated", () => { const svgCount = (markup: string) => (markup.match(/ exactly one SVG. const fixtureMarkup = render(canonicalFixture); assert.equal(svgCount(fixtureMarkup), 1); assert.match(fixtureMarkup, /D1/); // D9 with real houses -> rendered as a second SVG. const withD9 = withCharts(canonicalFixture, [ ...canonicalFixture.charts, { id: "D9", title: "D9 九分盘", houses: [{ houseNumber: 1, sign: "白羊座", occupants: ["月亮"] }], planets: [{ name: "月亮", sign: "白羊座", longitudeDegrees: 1.5, houseNumber: 1, retrograde: false }], claimStatus: "single_system_inference", }, ]); assert.equal(svgCount(render(withD9)), 2); // D9 with empty houses must NOT produce a placeholder chart. const withEmptyD9 = withCharts(canonicalFixture, [ ...canonicalFixture.charts, { id: "D9", title: "D9 九分盘", houses: [], claimStatus: "blocked" }, ]); assert.equal(svgCount(render(withEmptyD9)), 1); // No charts at all -> no SVG, no fabricated chart section. const noCharts = render(withCharts(canonicalFixture, [])); assert.equal(svgCount(noCharts), 0); assert.equal(noCharts.indexOf("命盘与基础信息"), -1); }); test("evidenceRefs only produce in-page anchors for ids present in the appendix", () => { const markup = render(canonicalFixture); const knownRef = canonicalFixture.thematicNarrative[0].evidenceRefs.find( (ref) => canonicalFixture.evidenceAppendix.calculationEvidence.some((row) => row.id === ref), ); assert.ok(knownRef, "fixture should contain a resolvable evidenceRef"); assert.match(markup, new RegExp(`href="#evidence-${knownRef}"`)); // No arbitrary URL schemes anywhere. assert.doesNotMatch(markup, /href="(https?:|javascript:|vbscript:|data:|file:)/i); }); test("no dangerous HTML: no script tags, no external resources, no dangerouslySetInnerHTML", async () => { const markup = render(canonicalFixture); assert.doesNotMatch(markup, /