import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { classifyReportEnvelope } from "../src/components/personal-report/personal-report-page.tsx"; import type { BirthSkyPaletteName } from "../src/lib/birth-sky/draw.ts"; import { loadReportSkyCover } from "../src/lib/birth-sky/report-cover.ts"; import { goldenSky } from "./birth-sky-test-support.ts"; // Report cover (TASK-birth-sky-followup-20260928 F3). const page = readFileSync(new URL("../src/components/personal-report/personal-report-page.tsx", import.meta.url), "utf8"); const component = readFileSync(new URL("../src/components/birth-sky/report-sky-cover.tsx", import.meta.url), "utf8"); const css = readFileSync(new URL("../src/components/birth-sky/report-sky-cover.css", import.meta.url), "utf8"); function readyEnvelope(coverSubject?: unknown) { return { report: { id: "11111111-1111-4111-8111-111111111111", status: "ready", createdAt: "2026-09-28T00:00:00.000Z", ...(coverSubject === undefined ? {} : { coverSubject }), }, longformMarkdown: "# 报告\n\n正文。", }; } test("matching fingerprint → the ready state carries coverSubject; mismatch or legacy → no cover", () => { const bound = classifyReportEnvelope(200, readyEnvelope("self")); assert.equal(bound.phase, "markdown-ready"); assert.equal(bound.phase === "markdown-ready" && bound.coverSubject, "self"); for (const value of [null, undefined, "", 42]) { const state = classifyReportEnvelope(200, readyEnvelope(value)); assert.equal(state.phase, "markdown-ready"); assert.equal("coverSubject" in state, false, `coverSubject=${String(value)}`); } }); test("the cover sits above the body, lazy, and only when coverSubject is set", () => { assert.match(page, /\{state\.coverSubject \? : null\}\s* import\("@\/components\/birth-sky\/report-sky-cover"\)/); // No buttons, no placeholder, no waiting state. assert.doesNotMatch(component, / { const sky = goldenSky("day"); const painted: BirthSkyPaletteName[] = []; const asked: string[] = []; const pngs = await loadReportSkyCover("self", new AbortController().signal, { fetchSky: async (subjectId) => { asked.push(subjectId); return sky; }, render: async (_sky, palette) => { painted.push(palette); return new Blob([palette]); }, }); assert.deepEqual(asked, ["self"]); assert.deepEqual(painted.sort(), ["night", "paper"]); assert.equal(await pngs?.screen.text(), "night"); assert.equal(await pngs?.print.text(), "paper"); assert.match(pngs?.description ?? "", /^星星都在,只是还看不见。|太阳/); }); test("fetch failure, an empty sky or an abort → no cover, and nothing throws into the report", async () => { const signal = new AbortController().signal; const render = async () => new Blob(["x"]); assert.equal(await loadReportSkyCover("self", signal, { fetchSky: async () => { throw new Error("offline"); }, render }), null); assert.equal(await loadReportSkyCover("self", signal, { fetchSky: async () => null, render }), null); assert.equal(await loadReportSkyCover("self", signal, { fetchSky: async () => goldenSky("day"), render: async () => { throw new Error("canvas"); } }), null); const aborted = new AbortController(); aborted.abort(); assert.equal(await loadReportSkyCover("self", aborted.signal, { fetchSky: async () => goldenSky("day"), render }), null); }); test("print: paper picture alone on the first A4 page; the screen picture is hidden", () => { assert.match(component, /className="report-sky-cover-print" src=\{images\.print\}/); assert.match(css, /\.report-sky-cover-print \{ display: none !important; \}/); const print = css.slice(css.indexOf("@media print")); assert.match(print, /\.report-sky-cover \{[^}]*break-after: page;/); assert.match(print, /\.report-sky-cover-screen \{ display: none !important; \}/); assert.match(print, /\.report-sky-cover-print \{[^}]*display: block !important;/); assert.match(css, /aspect-ratio: 3 \/ 4;/); });