Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
128 lines
6.5 KiB
TypeScript
128 lines
6.5 KiB
TypeScript
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, /定格/);
|
||
});
|