Files
Jyotisha/frontend/tests/report-sky-cover.test.ts
T
Jesse_ChenandClaude Opus 5.5 a9a917d50c feat(report): birth-sky report cover bound to the report's birth fingerprint (F1–F3)
Product decision A (TASK-birth-sky-followup-20260928):
- every generation path writes subject.birthFingerprint (stored-row
  fingerprint, same function as the chart page's profileFingerprint);
  never into the evidence bundle the model reads
- GET /api/reports/:id compares on the server and returns only
  report.coverSubject (subject id or null); legacy reports never get one
- report page shows the night cover above the body, paper cover alone on
  the first printed A4 page; silent on any failure, no waiting state
- TS / JSON schema / Python contract accept the optional field
- BLOCKED T5 struck through; DESIGN §17, CHANGELOG, checklist, PROGRESS

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
2026-09-28 21:47:44 +08:00

82 lines
4.3 KiB
TypeScript

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 \? <ReportSkyCover subjectId=\{state\.coverSubject\} \/> : null\}\s*<PersonalReportMarkdownView/);
assert.match(page, /const ReportSkyCover = dynamic\(\s*\(\) => import\("@\/components\/birth-sky\/report-sky-cover"\)/);
// No buttons, no placeholder, no waiting state.
assert.doesNotMatch(component, /<Button|<button|Spinner|skeleton|正在/i);
assert.match(component, /if \(!images\) return null;/, "nothing renders before both pictures are painted");
});
test("loader paints night for the screen and paper for print with the same sky", async () => {
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;/);
});