Files
Jyotisha/frontend/tests/birth-sky-replay-20260929.test.ts
T

128 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
BIRTH_SKY_CONVERGE_MS,
BIRTH_SKY_REVEAL_TOTAL_MS,
birthSkyHoldAtMs,
birthSkyRevealAt,
birthSkyScatter,
} from "../src/lib/birth-sky/converge.ts";
import {
BIRTH_SKY_EXPORT_HEIGHT,
BIRTH_SKY_EXPORT_WIDTH,
drawBirthSky,
type BirthSkyCanvas,
} from "../src/lib/birth-sky/draw.ts";
import { goldenSky } from "./birth-sky-test-support.ts";
/*
* TASK-mobile-chart-and-confirmed-edit-20260929 M4 (product 2026-09-29): the
* chart page's 「那一刻的天空」 replays the convergence on every open, stays on
* the settled frame — which is the poster — and shares from the top right.
* The first-chart reveal keeps fading out (its own tests are unchanged).
*/
const read = (path: string) => readFileSync(new URL(`../${path}`, import.meta.url), "utf8");
const converge = read("src/components/birth-sky/birth-sky-converge.tsx");
const replay = read("src/components/birth-sky/birth-sky-replay.tsx");
const entry = read("src/components/birth-sky/birth-sky-entry.tsx");
const cover = read("src/lib/birth-sky/cover.ts");
const voice = read("docs/VOICE.md");
const design = read("DESIGN.md");
type Call = { method: string; args: unknown[] };
function recorder() {
const calls: Call[] = [];
const state: Record<string, unknown> = {};
const ctx = new Proxy({} as Record<string, unknown>, {
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: [...args, { ...state }] }); };
},
set(_target, key: string, value: unknown) {
state[key] = value;
return true;
},
}) as unknown as BirthSkyCanvas;
return { ctx, calls };
}
test("hold: converges like the reveal, then stops on the settled frame and never fades or ends", () => {
const at = (ms: number) => birthSkyRevealAt(ms, false, "hold");
assert.deepEqual(at(0).frame, birthSkyRevealAt(0, false).frame, "same start as the reveal");
assert.deepEqual(at(1_500).frame, birthSkyRevealAt(1_500, false).frame, "same travel as the reveal");
assert.equal(at(BIRTH_SKY_CONVERGE_MS).held, false, "the words are still coming in");
const holdAt = birthSkyHoldAtMs(false);
assert.equal(holdAt, BIRTH_SKY_CONVERGE_MS + 600);
for (const ms of [holdAt, BIRTH_SKY_REVEAL_TOTAL_MS, BIRTH_SKY_REVEAL_TOTAL_MS * 10]) {
const moment = at(ms);
assert.deepEqual(moment.frame, { converge: 1, chrome: 1, text: 1 }, `settled at ${ms}`);
assert.equal(moment.layer, 1, `no fade at ${ms}`);
assert.equal(moment.done, false, `never closes itself at ${ms}`);
assert.equal(moment.held, true);
}
// The reveal is unchanged: it still fades out and finishes.
assert.equal(birthSkyRevealAt(BIRTH_SKY_REVEAL_TOTAL_MS, false).done, true);
assert.equal(birthSkyRevealAt(BIRTH_SKY_REVEAL_TOTAL_MS, false).held, false);
});
test("hold with reduced motion: no travel, one fade in, then held", () => {
const holdAt = birthSkyHoldAtMs(true);
assert.equal(holdAt, 600);
for (const ms of [0, 300, holdAt, 60_000]) {
assert.deepEqual(birthSkyRevealAt(ms, true, "hold").frame, { converge: 1, chrome: 1, text: 1 });
assert.equal(birthSkyRevealAt(ms, true, "hold").done, false);
}
assert.equal(birthSkyRevealAt(0, true, "hold").layer, 0);
assert.equal(birthSkyRevealAt(holdAt, true, "hold").layer, 1);
assert.equal(birthSkyRevealAt(holdAt, true, "hold").held, true);
assert.equal(birthSkyRevealAt(60_000, true, "hold").layer, 1);
});
test("the held frame is the exported poster, call for call", () => {
for (const phase of ["day", "night", "twilight_evening"] as const) {
const sky = goldenSky(phase);
const options = { palette: "night" as const, width: BIRTH_SKY_EXPORT_WIDTH, height: BIRTH_SKY_EXPORT_HEIGHT };
const poster = recorder();
drawBirthSky(poster.ctx, sky, options);
const held = recorder();
drawBirthSky(held.ctx, sky, options, { ...birthSkyRevealAt(birthSkyHoldAtMs(false), false, "hold").frame, scatter: birthSkyScatter(sky) });
assert.deepEqual(held.calls, poster.calls, phase);
}
// The live replay draws with the serif decision the PNG was painted with.
assert.match(cover, /const \{ blob, serif \} = await paintBirthSkyPng\(sky, "night"\);/);
assert.match(replay, /useMemo\(\(\) => \(\{ sky: cover\.sky, serif: cover\.serif \}\)/);
});
test("replay overlay: a tap skips to the held frame without closing; Esc and × close; held frames stop the clock", () => {
assert.match(converge, /const onPointerDown = mode === "hold" \? skip : finish;/);
const skip = converge.slice(converge.indexOf("const skip = "), converge.indexOf("const onPointerDown"));
assert.doesNotMatch(skip, /onDone|finish\(\)/, "a tap never closes the replay");
assert.match(skip, /closest\("\.birth-sky-controls"\)/, "taps on the controls are theirs");
assert.match(skip, /started = performance\.now\(\) - birthSkyHoldAtMs\(reduced\)/);
assert.match(converge, /else if \(!moment\.held\) frameHandle = requestAnimationFrame\(tick\);/);
assert.match(converge, /if \(last\?\.held\) draw\(last\);/, "a resize repaints the held frame");
assert.match(converge, /layer\.addEventListener\("cancel", cancel\)/);
assert.match(replay, /onDone=\{onClose\}/);
assert.match(replay, /aria-label="关闭"/);
assert.match(replay, /\{controls\}|controls=\{/);
assert.match(converge, /\{controls\}/);
});
test("「分享」 hands out the painted PNG; every open is a fresh replay from a lazy chunk", () => {
assert.match(replay, /export const BIRTH_SKY_SHARE_LABEL = "分享";/);
assert.match(replay, /export const BIRTH_SKY_SHARE_FAILED = "没能分享,再试一次。";/);
assert.match(replay, /await saveBirthSkyImage\(cover\.blob\)/);
assert.match(entry, /dynamic\(\s*\(\) => import\("\.\/birth-sky-replay"\)/);
// 原值:`{open ? <BirthSkyReplay cover={cover} onClose={() => setOpen(false)} /> : null}`
// 新值:`{open ? <BirthSkyReplay cover={cover} onClose={() => setWanted(false)} /> : null}`,其中 open = 点过且封面已画好
// 原因:BUG-1102——按钮在星盘就绪时立即出现,封面还在画时点击被记下、画好即开;每次打开仍重放的断言不变
assert.match(entry, /\{open \? <BirthSkyReplay cover=\{cover\} onClose=\{\(\) => setWanted\(false\)\} \/> : null\}/);
assert.match(entry, /const open = wanted && cover !== null;/);
assert.match(voice, /没能分享,再试一次。/);
assert.match(design, /定格/);
});