Files
Jyotisha/frontend/tests/ephemeris-page-lifecycle.test.tsx
T
jesse-uxandClaude Code 4d801e53c3
Independent Staging Quality Gate / validate (push) Successful in 16m28s
Independent Staging Quality Gate / publish (push) Successful in 3m33s
fix: integrate people archive, report reader and western chart corrections
Validate on Linux Node 22 and PostgreSQL 17: 3894 frontend tests and 64 database tests pass, with no removed test names or new failures. Preserve static Home, bounded gzip, assertion-change records and manual acceptance gaps.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-25 20:41:16 +08:00

214 lines
9.4 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import React from "react";
import test from "node:test";
import { bindCurrentSubjectAccount, resetSubjectCatalogForTests } from "../src/lib/current-subject.ts";
import { EphemerisPage } from "../src/components/ephemeris/ephemeris-page.tsx";
import { SidebarProvider } from "../src/components/ui/sidebar.tsx";
import { EPHEMERIS_COPY, panchangaFactorsFromEngine, transitsFromEngine } from "../src/lib/ephemeris-view.ts";
import { parseEphemerisOkResponse, type EphemerisOkResponse } from "../src/lib/ephemeris-contract.ts";
import { peekEphemerisPage, resetSecondaryPageDataForTests, writeEphemerisPage } from "../src/lib/secondary-page-data.ts";
import { createClientLifecycleHarness } from "./react-client-lifecycle-test-support.ts";
Object.assign(globalThis, { React });
const panchangaGolden = JSON.parse(readFileSync(new URL("./fixtures/ephemeris-panchanga-range.golden.json", import.meta.url), "utf8"));
const natalGolden = JSON.parse(readFileSync(new URL("./fixtures/ephemeris-chart-natal.golden.json", import.meta.url), "utf8"));
const transit15 = JSON.parse(readFileSync(new URL("./fixtures/ephemeris-chart-transit-2026-09-15.golden.json", import.meta.url), "utf8"));
const transit14 = JSON.parse(readFileSync(new URL("./fixtures/ephemeris-chart-transit-2026-09-14.golden.json", import.meta.url), "utf8"));
function payloadFor(date: "2026-09-14" | "2026-09-15"): EphemerisOkResponse {
const transits = transitsFromEngine({
transitChart: date === "2026-09-14" ? transit14 : transit15,
natalChart: natalGolden,
});
const parsed = parseEphemerisOkResponse({
status: "ok",
date,
today: "2026-09-15",
timezoneId: "Asia/Shanghai",
panchanga: {
available: true,
ayanamsaNote: "lahiri_not_account",
factors: panchangaFactorsFromEngine(panchangaGolden, date),
},
transits: {
available: true,
natalRelativeAvailable: true,
natalRelativeNote: "shown",
ayanamsa: "raman",
bodies: transits.bodies,
},
events: { status: "unavailable" },
});
assert.ok(parsed);
return parsed;
}
function asDate(payload: EphemerisOkResponse, date: string): EphemerisOkResponse {
return { ...payload, date };
}
function json(payload: EphemerisOkResponse) {
return new Response(JSON.stringify(payload), { status: 200, headers: { "content-type": "application/json" } });
}
function deferred<T>() {
let resolve!: (value: T) => void;
const promise = new Promise<T>((next) => { resolve = next; });
return { promise, resolve };
}
async function withPage(run: (h: ReturnType<typeof createClientLifecycleHarness>) => Promise<void>) {
resetSecondaryPageDataForTests();
resetSubjectCatalogForTests();
bindCurrentSubjectAccount("fictional-account", []);
const originalFetch = globalThis.fetch;
const h = createClientLifecycleHarness();
Object.assign(window, { location: { assign() {}, pathname: "/ephemeris", search: "" } });
try {
await run(h);
assert.deepEqual(h.errors, []);
} finally {
await h.close();
globalThis.fetch = originalFetch;
resetSecondaryPageDataForTests();
}
}
function click(h: ReturnType<typeof createClientLifecycleHarness>, label: string) {
const node = h.elements().find((element) => element.getAttribute("aria-label") === label);
assert.ok(node, label);
return h.event(node);
}
test("warm ephemeris mount reuses the cached date without any engine request", async () => withPage(async (h) => {
writeEphemerisPage({ kind: "ready", payload: payloadFor("2026-09-15") });
let requests = 0;
globalThis.fetch = async (input) => {
if (String(input).startsWith("/api/ephemeris")) { requests += 1; throw new Error("warm mount must not fetch"); }
return Response.json({}, { status: 503 });
};
await h.render(React.createElement(SidebarProvider, null, React.createElement(EphemerisPage)));
await h.idle();
assert.match(h.container.text, /Shukla Panchami/);
assert.equal(requests, 0);
}));
test("initial ephemeris fetch failure offers a working retry instead of permanent waiting", async () => withPage(async (h) => {
let requests = 0;
globalThis.fetch = async (input) => {
if (!String(input).startsWith("/api/ephemeris")) return Response.json({}, { status: 503 });
requests += 1;
if (requests === 1) throw new Error("fictional network failure");
return json(payloadFor("2026-09-15"));
};
await h.render(React.createElement(SidebarProvider, null, React.createElement(EphemerisPage)));
await h.idle();
assert.match(h.container.text, new RegExp(EPHEMERIS_COPY.dateFailed));
const retry = h.elements().find((element) => element.tagName === "BUTTON" && element.text === "重试");
assert.ok(retry);
await h.event(retry);
await h.idle();
assert.equal(requests, 2);
assert.match(h.container.text, /Shukla Panchami/);
}));
test("switching to an uncached date keeps the date bar and ignores a late today refresh", async () => withPage(async (h) => {
const today = payloadFor("2026-09-15");
const sixteenth = asDate(payloadFor("2026-09-14"), "2026-09-16");
writeEphemerisPage({ kind: "ready", payload: today });
const refresh = deferred<Response>();
const requested = deferred<Response>();
globalThis.fetch = async (input) => {
const url = String(input);
if (url.includes("date=2026-09-16")) return requested.promise;
return refresh.promise;
};
await h.render(React.createElement(SidebarProvider, null, React.createElement(EphemerisPage)));
await h.idle();
assert.match(h.container.text, /2026年9月15日/);
assert.match(h.container.text, /Shukla Panchami/);
await click(h, EPHEMERIS_COPY.nextDay);
await h.idle();
assert.match(h.container.text, /2026年9月16日/);
assert.match(h.container.text, new RegExp(EPHEMERIS_COPY.panchangaUnavailable));
assert.match(h.container.text, new RegExp(EPHEMERIS_COPY.transitsUnavailable));
assert.match(h.container.text, new RegExp(EPHEMERIS_COPY.eventsPending));
assert.doesNotMatch(h.container.text, /Shukla Panchami|还没接上|正在加载|skeleton/i);
assert.ok(h.elements().some((element) => element.getAttribute("aria-label") === EPHEMERIS_COPY.previousDay));
await h.update(() => refresh.resolve(json(today)));
await h.idle();
assert.match(h.container.text, /2026年9月16日/);
assert.doesNotMatch(h.container.text, /Shukla Panchami/);
await h.update(() => requested.resolve(json(sixteenth)));
await h.idle();
assert.match(h.container.text, /2026年9月16日/);
assert.match(h.container.text, /Shukla Chaturthi/);
assert.doesNotMatch(h.container.text, /Shukla Panchami/);
}));
test("a late response for the previous date cannot overwrite the date now on screen", async () => withPage(async (h) => {
writeEphemerisPage({ kind: "ready", payload: payloadFor("2026-09-15") });
const first = deferred<Response>();
const second = deferred<Response>();
globalThis.fetch = async (input) => {
const url = String(input);
if (url.includes("date=2026-09-16")) return first.promise;
if (url.includes("date=2026-09-17")) return second.promise;
return json(payloadFor("2026-09-15"));
};
await h.render(React.createElement(SidebarProvider, null, React.createElement(EphemerisPage)));
await h.idle();
await click(h, EPHEMERIS_COPY.nextDay);
await h.idle();
await click(h, EPHEMERIS_COPY.nextDay);
await h.idle();
assert.match(h.container.text, /2026年9月17日/);
await h.update(() => first.resolve(json(asDate(payloadFor("2026-09-14"), "2026-09-16"))));
await h.idle();
assert.match(h.container.text, /2026年9月17日/);
assert.match(h.container.text, new RegExp(EPHEMERIS_COPY.panchangaUnavailable));
assert.doesNotMatch(h.container.text, /Shukla Chaturthi/);
}));
test("429 shows the failure sentence and the same date or an arrow retries successfully", async () => withPage(async (h) => {
writeEphemerisPage({ kind: "ready", payload: payloadFor("2026-09-15") });
const sixteenth = asDate(payloadFor("2026-09-14"), "2026-09-16");
let calls = 0;
globalThis.fetch = async (input) => {
const url = String(input);
if (!url.includes("date=2026-09-16")) return json(payloadFor("2026-09-15"));
calls += 1;
if (calls === 1) {
return new Response(JSON.stringify({ status: "unavailable", reason: "rate_limited" }), { status: 429 });
}
return json(sixteenth);
};
await h.render(React.createElement(SidebarProvider, null, React.createElement(EphemerisPage)));
await h.idle();
await click(h, EPHEMERIS_COPY.nextDay);
await h.idle();
assert.equal((h.container.text.match(new RegExp(EPHEMERIS_COPY.dateFailed, "g")) ?? []).length, 3);
assert.match(h.container.text, /2026年9月16日/);
assert.doesNotMatch(h.container.text, /Shukla Panchami|正在加载|skeleton/i);
await click(h, EPHEMERIS_COPY.nextDay);
await h.idle();
assert.equal(calls, 2);
assert.match(h.container.text, /2026年9月16日/);
assert.match(h.container.text, /Shukla Chaturthi/);
assert.doesNotMatch(h.container.text, new RegExp(EPHEMERIS_COPY.dateFailed));
}));
test("warm ephemeris self cache never crosses account binding", () => {
resetSecondaryPageDataForTests(); resetSubjectCatalogForTests();
bindCurrentSubjectAccount("fictional-A", []);
writeEphemerisPage({ kind: "ready", payload: payloadFor("2026-09-15") });
assert.ok(peekEphemerisPage());
bindCurrentSubjectAccount("fictional-B", []);
assert.equal(peekEphemerisPage(), null);
resetSecondaryPageDataForTests(); resetSubjectCatalogForTests();
});