T1 scripts/birth_sky.py: real alt/az of the seven visible grahas (swe.calc_ut + swe.azalt, geometric altitude, azimuth converted from swe's south origin to north-east) and Yale BSC5 stars V<=4.0 (IAU 1976 precession); phase day / twilight_morning / twilight_evening / night. Thin POST /api/birth_sky registration in do_POST (no new handler method, no __new__). T2 GET /api/birth-sky?subject=: birth truth read server side through loadSubjectBirth + prepareChartViewProfile; zod contract; no billing, no writes, no birth data in the response. T3/T4 lib/birth-sky: sentence table, pure Canvas 2D drawBirthSky (night / paper palettes from DESIGN tokens, east on the left, serif headline with 1.5 s font wait), PNG export via navigator.share or Blob + a.download. Chart page prefetches and paints the cover once the chart is ok; the header button appears only when the PNG is ready. Dialog is a lazy chunk. T7 DESIGN §17, VOICE, CONTEXT glossary, CHANGELOG, real-device checklist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0199rbQDTsUbCVw84wc8BTFe
80 lines
3.5 KiB
TypeScript
80 lines
3.5 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
|
|
import { BIRTH_SKY_FILENAME, saveBirthSkyImage } from "../src/lib/birth-sky/export.ts";
|
|
import { waitForHeadingSerif } from "../src/lib/birth-sky/render.ts";
|
|
|
|
const exportSource = readFileSync(new URL("../src/lib/birth-sky/export.ts", import.meta.url), "utf8");
|
|
|
|
function environment(share: "ok" | "cancel" | "fail" | "unsupported") {
|
|
const downloads: string[] = [];
|
|
const shared: ShareData[] = [];
|
|
const nav = share === "unsupported"
|
|
? { canShare: () => false, share: async () => { throw new Error("must not share"); } }
|
|
: {
|
|
canShare: (data: ShareData) => Array.isArray(data.files) && data.files.length === 1,
|
|
share: async (data: ShareData) => {
|
|
shared.push(data);
|
|
if (share === "cancel") throw Object.assign(new Error("dismissed"), { name: "AbortError" });
|
|
if (share === "fail") throw Object.assign(new Error("denied"), { name: "NotAllowedError" });
|
|
},
|
|
};
|
|
return {
|
|
downloads,
|
|
shared,
|
|
env: {
|
|
navigator: nav,
|
|
makeFile: (blob: Blob, name: string) => new File([blob], name, { type: "image/png" }),
|
|
download: (_blob: Blob, name: string) => { downloads.push(name); },
|
|
},
|
|
};
|
|
}
|
|
|
|
const png = new Blob([new Uint8Array([137, 80, 78, 71])], { type: "image/png" });
|
|
|
|
test("the file name carries no name and no date", () => {
|
|
assert.equal(BIRTH_SKY_FILENAME, "jyotisha-sky.png");
|
|
assert.match(exportSource, /toBlob\([\s\S]*"image\/png"\)/);
|
|
});
|
|
|
|
test("share-capable browsers get the system share sheet with the PNG file", async () => {
|
|
const { env, downloads, shared } = environment("ok");
|
|
assert.equal(await saveBirthSkyImage(png, env), "shared");
|
|
assert.equal(shared.length, 1);
|
|
const file = shared[0]!.files![0]!;
|
|
assert.equal(file.name, "jyotisha-sky.png");
|
|
assert.equal(file.type, "image/png");
|
|
assert.deepEqual(downloads, []);
|
|
});
|
|
|
|
test("without file sharing it downloads through Blob + a.download", async () => {
|
|
const { env, downloads } = environment("unsupported");
|
|
assert.equal(await saveBirthSkyImage(png, env), "downloaded");
|
|
assert.deepEqual(downloads, ["jyotisha-sky.png"]);
|
|
const bare = environment("unsupported");
|
|
assert.equal(await saveBirthSkyImage(png, { ...bare.env, navigator: null }), "downloaded");
|
|
assert.match(exportSource, /anchor\.download = name/);
|
|
assert.match(exportSource, /URL\.revokeObjectURL/);
|
|
});
|
|
|
|
test("a dismissed share sheet is not an error and does not fall back to a download", async () => {
|
|
const { env, downloads } = environment("cancel");
|
|
assert.equal(await saveBirthSkyImage(png, env), "cancelled");
|
|
assert.deepEqual(downloads, []);
|
|
const failing = environment("fail");
|
|
assert.equal(await saveBirthSkyImage(png, failing.env), "downloaded");
|
|
assert.deepEqual(failing.downloads, ["jyotisha-sky.png"]);
|
|
});
|
|
|
|
test("the heading serif wait gives up after its budget instead of hanging", async () => {
|
|
const never = { load: () => new Promise<unknown>(() => {}) };
|
|
const started = Date.now();
|
|
assert.equal(await waitForHeadingSerif("那一刻", never, 30), false);
|
|
assert.ok(Date.now() - started < 1_000);
|
|
assert.equal(await waitForHeadingSerif("那一刻", { load: async () => [{}] }, 30), true);
|
|
assert.equal(await waitForHeadingSerif("那一刻", { load: async () => [] }, 30), false);
|
|
assert.equal(await waitForHeadingSerif("那一刻", { load: async () => { throw new Error("x"); } }, 30), false);
|
|
assert.equal(await waitForHeadingSerif("那一刻", null, 30), false);
|
|
});
|