import assert from "node:assert/strict"; import test from "node:test"; import { BIRTH_SKY_CENTER, BIRTH_SKY_EXPORT_HEIGHT, BIRTH_SKY_EXPORT_WIDTH, BIRTH_SKY_PALETTES, drawBirthSky, skyPoint, type BirthSkyCanvas, } from "../src/lib/birth-sky/draw.ts"; import { goldenCaption, goldenSky } from "./birth-sky-test-support.ts"; type Call = { method: string; args: unknown[]; state: Record }; function recorder() { const calls: Call[] = []; const state: Record = {}; const target = {} as Record; const ctx = new Proxy(target, { get(_target, key: string) { if (key in state) return state[key]; if (key === "measureText") return (text: string) => ({ width: text.length * 10 }); return (...args: unknown[]) => { calls.push({ method: key, args, state: { ...state } }); }; }, set(_target, key: string, value: unknown) { state[key] = value; return true; }, }) as unknown as BirthSkyCanvas; return { ctx, calls, texts: () => calls.filter((call) => call.method === "fillText").map((call) => String(call.args[0])) }; } function draw(phase: Parameters[0], timeBasis: "adopted" | "reported", palette: "night" | "paper" = "night") { const canvas = recorder(); drawBirthSky(canvas.ctx, goldenSky(phase, timeBasis), { palette, width: BIRTH_SKY_EXPORT_WIDTH, height: BIRTH_SKY_EXPORT_HEIGHT, }); return canvas; } test("export size is 1080 x 1440 (3:4) and the background fills it", () => { assert.equal(BIRTH_SKY_EXPORT_WIDTH, 1080); assert.equal(BIRTH_SKY_EXPORT_HEIGHT, 1440); const { calls } = draw("day", "adopted"); const background = calls.find((call) => call.method === "fillRect"); assert.deepEqual(background?.args, [0, 0, 1080, 1440]); assert.equal(background?.state.fillStyle, BIRTH_SKY_PALETTES.night.background); }); // 原值: no drawn text carries a date, a time, coordinates, a place or a name. // 新值: only the caption line carries the date, time and city; nothing carries coordinates, a district or a name. // 原因: TASK-birth-sky-polish-20260928 P2 — product overturned 「不印出生资料」 for date + time + city. test("only the caption line carries birth data: date, time, city; never coordinates, district or name", () => { for (const phase of ["day", "twilight_morning", "twilight_evening", "night"] as const) { const texts = draw(phase, "reported").texts(); const caption = goldenCaption(phase); assert.ok(texts.includes(caption), caption); const others = texts.filter((text) => text !== caption).join("\n"); assert.ok(texts.length >= 8, others); assert.doesNotMatch(others, /\d{4}|\d{1,2}:\d{2}|杭州|年|月\d|日\d/); const joined = texts.join("\n"); assert.doesNotMatch(joined, /\d+\s*[°′']|\d\s*[NSEW]\b|北纬|东经|UTC|西湖区|浙江/); assert.doesNotMatch(joined, /示例|虚构|王芳/); } const canvas = draw("day", "reported"); const texts = canvas.texts(); const topLabel = canvas.calls.filter((call) => call.method === "fillText" && call.args[2] === 64).map((call) => call.args[0]).join(""); assert.equal(topLabel, "JYOTISHA · 那 一 刻"); assert.ok(texts.includes("星星都在,只是还看不见。")); assert.ok(texts.includes("太阳在东,月亮在脚下,火星正悬在东南")); assert.ok(texts.includes("1994年5月18日 07:40 · 杭州")); assert.ok(texts.includes("仅供自我探索,不替你做决定。")); for (const direction of ["北", "南", "东", "西"]) assert.ok(texts.includes(direction)); }); // 原值: 「按你填写的时间」 for a reported time, nothing for an adopted one. // 新值: the caption sits where the footnote was (y = 712); adopted appends 「 · 校正后」; 「按你填写的时间」 is gone. // 原因: TASK-birth-sky-polish-20260928 P2. test("the caption replaces the footnote; an adopted time adds 「· 校正后」", () => { const reported = draw("day", "reported"); const adopted = draw("day", "adopted"); const at712 = (canvas: ReturnType) => canvas.calls.filter((call) => call.method === "fillText" && call.args[2] === 712).map((call) => call.args[0]); assert.deepEqual(at712(reported), ["1994年5月18日 07:40 · 杭州"]); assert.deepEqual(at712(adopted), ["1994年5月18日 07:40 · 杭州 · 校正后"]); for (const canvas of [reported, adopted]) assert.ok(!canvas.texts().includes("按你填写的时间")); const paper = draw("twilight_evening", "reported", "paper"); const line = paper.calls.find((call) => call.method === "fillText" && call.args[2] === 712); assert.equal(line?.args[0], "1994年5月18日 19:10 · 杭州"); assert.equal(line?.state.fillStyle, BIRTH_SKY_PALETTES.paper.inkSecondary); }); test("east is on the left, north is up", () => { const east = skyPoint(0, 90); const west = skyPoint(0, 270); const north = skyPoint(0, 0); assert.ok(east.x < BIRTH_SKY_CENTER.x); assert.ok(west.x > BIRTH_SKY_CENTER.x); assert.ok(north.y < BIRTH_SKY_CENTER.y); assert.deepEqual(skyPoint(90, 123), BIRTH_SKY_CENTER); // The real Sun of the sample (azimuth ~84.7, east) is painted left of centre. const sun = goldenSky("day").planets.find((planet) => planet.id === "sun")!; const { calls } = draw("day", "adopted"); const sunRing = calls.find((call) => call.method === "arc" && call.args[2] === 6); assert.ok(sunRing, "the Sun ring is drawn"); assert.ok(Number(sunRing.args[0]) < BIRTH_SKY_CENTER.x); assert.equal(sunRing.args[0], skyPoint(sun.altitude, sun.azimuth).x); }); test("bodies below the horizon are not drawn; the Moon of the sample is not named on the disc", () => { const texts = draw("day", "adopted").texts(); assert.ok(!texts.includes("月亮")); assert.ok(!texts.includes("木星")); assert.ok(texts.includes("土星")); assert.ok(texts.includes("火星")); assert.ok(!texts.some((text) => /罗睺|计都/.test(text))); }); test("headline and top label use the heading serif unless it timed out", () => { const headlineFont = (serif: boolean | undefined) => { const canvas = recorder(); drawBirthSky(canvas.ctx, goldenSky("night"), { palette: "night", width: 600, height: 800, ...(serif === undefined ? {} : { serif }) }); const call = canvas.calls.find((item) => item.method === "fillText" && item.args[0] === "那一刻,头顶就是这片天。"); const fact = canvas.calls.find((item) => item.method === "fillText" && String(item.args[0]).startsWith("月亮")); return { headline: String(call?.state.font), fact: String(fact?.state.font) }; }; assert.match(headlineFont(undefined).headline, /^600 30px "Jyotisha Serif SC", /); assert.doesNotMatch(headlineFont(undefined).fact, /Jyotisha Serif SC/); assert.doesNotMatch(headlineFont(false).headline, /Jyotisha Serif SC/); }); test("the paper palette paints ink stars on a light sheet", () => { const { calls } = draw("night", "adopted", "paper"); assert.equal(calls[0]?.method, "save"); const background = calls.find((call) => call.method === "fillRect"); assert.equal(background?.state.fillStyle, "#fbfaf7"); const starFill = calls.find((call) => call.method === "fill"); assert.equal(starFill?.state.fillStyle, BIRTH_SKY_PALETTES.paper.star); }); test("asterism lines only join stars that are above the horizon", () => { const sky = goldenSky("night"); const visible = new Set(sky.stars.map((star) => star.hr)); const { calls } = draw("night", "adopted"); const segments = calls.filter((call) => call.method === "lineTo").length; let expected = 0; for (const line of sky.lines) { for (const stroke of line.strokes) { for (let index = 1; index < stroke.length; index += 1) { if (visible.has(stroke[index - 1]) && visible.has(stroke[index])) expected += 1; } } } assert.ok(expected > 0, "the night sample shows at least one asterism segment"); assert.equal(segments, expected); });