Add bounded chart loading, per-layer retry states, and shared SVG skeletons. Add report block downloads with SVG, localized metadata, and inline deletion. Record verification and retain CRLF export, full-build, and controlled-device acceptance blockers. User authorized staging delivery with these gaps documented. Co-Authored-By: Claude Code <noreply@anthropic.com>
176 lines
12 KiB
TypeScript
176 lines
12 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import React, { useLayoutEffect } from "react";
|
|
import test from "node:test";
|
|
import { useChartPage } from "../src/hooks/use-chart-page.ts";
|
|
import { assembleChartView } from "../src/lib/chart-view-load.ts";
|
|
import type { ChartViewLayer, EngineCallResult } from "../src/lib/chart-view-engine.ts";
|
|
import type { ChartViewOk } from "../src/lib/chart-view-contract.ts";
|
|
import { CHART_VIEW_CLIENT_TIMEOUT_MS, CHART_VIEW_COPY } from "../src/lib/chart-view-labels.ts";
|
|
import { peekChartPage, pinChartSnapshotIdentity, prefetchSecondaryPage, resetSecondaryPageDataForTests, writeChartPage } from "../src/lib/secondary-page-data.ts";
|
|
import { createClientLifecycleHarness } from "./react-client-lifecycle-test-support.ts";
|
|
|
|
const golden = JSON.parse(readFileSync(new URL("./fixtures/chart-view-golden.json", import.meta.url), "utf8"));
|
|
const profile = { name: "Synthetic", date: "1990-06-15", time: "12:00", placeLabel: "Synthetic", latitude: 39.9042, longitude: 116.4074, timezoneOffset: 8, timezoneId: "Asia/Shanghai", ayanamsa: "raman", birthTimeStatus: "confirmed" };
|
|
async function packet(layer?: ChartViewLayer, failure?: EngineCallResult) {
|
|
return assembleChartView({ userId: "synthetic", accountId: "synthetic", profileFingerprint: "fixture-v1", profile, asOf: "2026-09-15", layers: layer ? [layer] : [], postEngine: async (path) => {
|
|
if (path === "/api/chart") return { status: "ok", payload: golden.chart };
|
|
if (failure) return failure;
|
|
const payload = path === "/api/varga_full" ? golden.varga_full : path === "/api/dasha/chara" ? golden.chara : golden.western;
|
|
return { status: "ok", payload };
|
|
} });
|
|
}
|
|
function response(result: Awaited<ReturnType<typeof packet>>) { return new Response(JSON.stringify(result.body), { status: result.httpStatus }); }
|
|
function deferred<T>() { let resolve!: (value: T) => void; let reject!: (reason: unknown) => void; const promise = new Promise<T>((a, b) => { resolve = a; reject = b; }); return { promise, resolve, reject }; }
|
|
async function harness(run: (h: ReturnType<typeof createClientLifecycleHarness>, current: () => ReturnType<typeof useChartPage>, assigned: string[]) => Promise<void>) {
|
|
resetSecondaryPageDataForTests();
|
|
const originalFetch = globalThis.fetch;
|
|
const originalTimeout = AbortSignal.timeout;
|
|
const h = createClientLifecycleHarness();
|
|
let value!: ReturnType<typeof useChartPage>;
|
|
const assigned: string[] = [];
|
|
Object.assign(window, { location: { assign: (url: string) => { assert.equal(value.view?.status, "unauthenticated"); assigned.push(url); } } });
|
|
function Probe() { const state = useChartPage(); useLayoutEffect(() => { value = state; }); return null; }
|
|
const mount = h.render.bind(h);
|
|
h.render = () => mount(React.createElement(Probe));
|
|
try { await run(h, () => value, assigned); assert.deepEqual(h.errors, []); }
|
|
finally { await h.close(); globalThis.fetch = originalFetch; AbortSignal.timeout = originalTimeout; resetSecondaryPageDataForTests(); }
|
|
}
|
|
|
|
test("chart first request times out with client_timeout and timedOut copy", async () => harness(async (h, state) => {
|
|
const deadline = new AbortController();
|
|
AbortSignal.timeout = (ms) => { assert.equal(ms, CHART_VIEW_CLIENT_TIMEOUT_MS); return deadline.signal; };
|
|
globalThis.fetch = async (_url, init) => new Promise((_resolve, reject) => init?.signal?.addEventListener("abort", () => reject(init.signal!.reason), { once: true }));
|
|
await h.render(null);
|
|
assert.equal(state().view, null);
|
|
await h.update(() => deadline.abort(new DOMException("synthetic", "TimeoutError")));
|
|
assert.equal(state().view?.status, "client_timeout");
|
|
const view = state().view;
|
|
assert.equal(view?.status !== "ok" && view?.message, CHART_VIEW_COPY.timedOut);
|
|
}));
|
|
|
|
test("sidebar prefetch shares the same bounded request with the mounted hook", async () => harness(async (h, state) => {
|
|
const deadline = new AbortController(); let calls = 0;
|
|
AbortSignal.timeout = (ms) => { assert.equal(ms, 25_000); return deadline.signal; };
|
|
globalThis.fetch = async (_url, init) => { calls++; return new Promise((_resolve, reject) => init?.signal?.addEventListener("abort", () => reject(init.signal!.reason))); };
|
|
prefetchSecondaryPage("/chart");
|
|
await h.render(null);
|
|
await h.update(() => deadline.abort(new DOMException("synthetic", "TimeoutError")));
|
|
assert.equal(calls, 1); assert.equal(state().view?.status, "client_timeout");
|
|
}));
|
|
|
|
test("chart cache populated during failed fetch is painted instead of remaining null", async () => harness(async (h, state) => {
|
|
const request = deferred<Response>(); globalThis.fetch = () => request.promise;
|
|
await h.render(null);
|
|
const result = await packet();
|
|
writeChartPage({ kind: "view", view: result.body });
|
|
await h.update(() => request.reject(new TypeError("synthetic network failure")));
|
|
assert.deepEqual(state().view, result.body);
|
|
}));
|
|
|
|
for (const cached of [false, true]) test(`chart 401 ${cached ? "cache" : "response"} paints unauthenticated before redirect`, async () => harness(async (h, state, assigned) => {
|
|
globalThis.fetch = async () => new Response("{}", { status: 401 });
|
|
if (cached) writeChartPage({ kind: "unauthenticated" });
|
|
await h.render(null);
|
|
assert.equal(state().view?.status, "unauthenticated"); assert.deepEqual(assigned, ["/login"]);
|
|
}));
|
|
|
|
for (const layer of ["varga", "chara", "western", "qizheng"] as const) test(`real assemble to fetch to hook preserves ${layer} engine 429`, async () => harness(async (h, state) => {
|
|
const initial = await packet();
|
|
const failed = await packet(layer, { status: "busy", path: "/api/synthetic", elapsedMs: 1, httpStatus: 429 });
|
|
globalThis.fetch = async (url) => response(String(url).includes("layers=") ? failed : initial);
|
|
await h.render(null);
|
|
await h.update(() => state().requestLayer(layer));
|
|
assert.equal(state().layerFailures.get(layer), "engine_busy");
|
|
assert.equal(state().pendingLayers.has(layer), false);
|
|
assert.deepEqual(state().view, initial.body);
|
|
}));
|
|
|
|
test("layer catch fails unavailable and a retry clears failure while pending then succeeds", async () => harness(async (h, state) => {
|
|
const initial = await packet(); const good = await packet("varga"); const retry = deferred<Response>(); let calls = 0;
|
|
globalThis.fetch = async (url) => {
|
|
if (!String(url).includes("layers=")) return response(initial);
|
|
calls++; if (calls === 1) throw new TypeError("synthetic"); return retry.promise;
|
|
};
|
|
await h.render(null); await h.update(() => state().requestLayer("varga"));
|
|
assert.equal(state().layerFailures.get("varga"), "unavailable");
|
|
await h.update(() => { state().requestLayer("varga"); state().requestLayer("varga"); });
|
|
assert.equal(state().layerFailures.has("varga"), false); assert.equal(state().pendingLayers.has("varga"), true); assert.equal(calls, 2);
|
|
await h.update(() => retry.resolve(response(good)));
|
|
assert.equal(state().pendingLayers.has("varga"), false); assert.equal(state().layerFailures.size, 0);
|
|
assert.ok((state().view as ChartViewOk).vedic.vargas.some((v) => v.id === "D9"));
|
|
}));
|
|
|
|
test("BFF rate_limited stays distinct from engine_busy", async () => harness(async (h, state) => {
|
|
const initial = await packet(); globalThis.fetch = async (url) => String(url).includes("layers=") ? new Response(JSON.stringify({ status: "rate_limited", billed: false, message: CHART_VIEW_COPY.rateLimited }), { status: 429 }) : response(initial);
|
|
await h.render(null); await h.update(() => state().requestLayer("varga"));
|
|
assert.equal(state().layerFailures.get("varga"), "rate_limited");
|
|
}));
|
|
|
|
test("layer timeout while reading response JSON remains client_timeout", async () => harness(async (h, state) => {
|
|
const initial = await packet(); const deadline = new AbortController();
|
|
globalThis.fetch = async (url) => String(url).includes("layers=") ? { status: 200, json: () => new Promise((_resolve, reject) => deadline.signal.addEventListener("abort", () => reject(new DOMException("synthetic", "AbortError")))) } as Response : response(initial);
|
|
await h.render(null);
|
|
AbortSignal.timeout = () => deadline.signal;
|
|
await h.update(() => state().requestLayer("varga"));
|
|
await h.update(() => deadline.abort(new DOMException("synthetic", "TimeoutError")));
|
|
assert.equal(state().layerFailures.get("varga"), "client_timeout");
|
|
}));
|
|
|
|
test("out-of-order successful layers preserve previously loaded data", async () => harness(async (h, state) => {
|
|
const initial = await packet(); const varga = await packet("varga"); const western = await packet("western"); const v = deferred<Response>(); const w = deferred<Response>();
|
|
globalThis.fetch = async (url) => String(url).includes("varga") ? v.promise : String(url).includes("western") ? w.promise : response(initial);
|
|
await h.render(null); await h.update(() => { state().requestLayer("varga"); state().requestLayer("western"); });
|
|
await h.update(() => w.resolve(response(western))); await h.update(() => v.resolve(response(varga)));
|
|
const view = state().view as ChartViewOk;
|
|
assert.equal(view.western.status, "ok"); assert.ok(view.vedic.vargas.some((x) => x.id === "D9"));
|
|
}));
|
|
|
|
test("late warm D1 refresh preserves the layer that completed after mounting", async () => harness(async (h, state) => {
|
|
const initial = await packet(); const western = await packet("western"); const refresh = deferred<Response>();
|
|
writeChartPage({ kind: "view", view: initial.body });
|
|
globalThis.fetch = async (url) => String(url).includes("layers=") ? response(western) : refresh.promise;
|
|
await h.render(null); await h.update(() => state().requestLayer("western"));
|
|
assert.equal((state().view as ChartViewOk).western.status, "ok");
|
|
await h.update(() => refresh.resolve(response(initial)));
|
|
assert.equal((state().view as ChartViewOk).western.status, "ok");
|
|
}));
|
|
|
|
test("HTTP 429 without a valid body remains engine_busy", async () => harness(async (h, state) => {
|
|
const initial = await packet(); globalThis.fetch = async (url) => String(url).includes("layers=") ? new Response("busy", { status: 429 }) : response(initial);
|
|
await h.render(null); await h.update(() => state().requestLayer("western"));
|
|
assert.equal(state().layerFailures.get("western"), "engine_busy");
|
|
}));
|
|
|
|
test("an obsolete first fetch failure cannot overwrite a new profile snapshot", async () => harness(async (h, state) => {
|
|
const old = deferred<Response>(); const initial = await packet();
|
|
const fresh = { ...initial, body: { ...initial.body, profileFingerprint: "fixture-v2" } };
|
|
let calls = 0;
|
|
globalThis.fetch = async () => ++calls === 1 ? old.promise : response(fresh);
|
|
await h.render(null);
|
|
pinChartSnapshotIdentity({ accountId: "synthetic", fingerprint: "fixture-v2" });
|
|
await h.update(() => old.reject(new DOMException("synthetic", "TimeoutError")));
|
|
assert.equal(state().view?.profileFingerprint, "fixture-v2");
|
|
assert.equal(peekChartPage()?.kind === "view" && peekChartPage()?.kind, "view");
|
|
assert.equal(calls, 2);
|
|
}));
|
|
|
|
test("layer 401 paints failure before redirect and never becomes a layer wait", async () => harness(async (h, state, assigned) => {
|
|
const initial = await packet(); globalThis.fetch = async (url) => String(url).includes("layers=") ? new Response(null, { status: 401 }) : response(initial);
|
|
await h.render(null); await h.update(() => state().requestLayer("western"));
|
|
assert.deepEqual(assigned, ["/login"]); assert.equal(state().view?.status, "unauthenticated");
|
|
assert.equal(state().pendingLayers.size, 0);
|
|
}));
|
|
|
|
test("profile pin invalidation ignores an older layer completion", async () => harness(async (h, state) => {
|
|
const initial = await packet(); const old = await packet("varga"); const request = deferred<Response>();
|
|
globalThis.fetch = async (url) => String(url).includes("layers=") ? request.promise : response(initial);
|
|
await h.render(null); await h.update(() => state().requestLayer("varga"));
|
|
pinChartSnapshotIdentity({ accountId: "synthetic", fingerprint: "fixture-v2" });
|
|
const fresh = { ...initial.body, profileFingerprint: "fixture-v2" };
|
|
writeChartPage({ kind: "view", view: fresh });
|
|
await h.update(() => request.resolve(response(old)));
|
|
assert.deepEqual(peekChartPage(), { kind: "view", view: fresh });
|
|
assert.equal(state().layerFailures.size, 0);
|
|
}));
|