TASK-birth-sky-polish-20260928. - P1: chart page dialog shows only the picture on a dimmed stage, 「保存图片」 below, icon × in the corner. - P2: BFF returns one server-built `caption` (「1994年5月18日 07:40 · 杭州」); printed under the fact line in all three places; adopted time adds 「 · 校正后」. - P3: convergence 3 s + words 2.5 s + fade 1 s; settled frame still equals the exported cover. - P4: redeem paywall no longer opens when the starter home appears; first send refused for credits opens it, later ones open billing; convergence dialog portalled to <body>. BUG-1080 investigating (Chrome repro rules out the overlay covering the convergence). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
160 lines
7.8 KiB
TypeScript
160 lines
7.8 KiB
TypeScript
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<string, unknown> };
|
|
|
|
function recorder() {
|
|
const calls: Call[] = [];
|
|
const state: Record<string, unknown> = {};
|
|
const target = {} as Record<string, unknown>;
|
|
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<typeof goldenSky>[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<typeof draw>) => 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);
|
|
});
|