Files
Jyotisha/frontend/tests/secondary-page-entry.test.ts
T
jesse-ux fabe0114dd
Independent Staging Quality Gate / validate (push) Successful in 10m5s
Independent Staging Quality Gate / publish (push) Successful in 3m23s
fix(chart): keep subject-scoped failures visible and follow the new routes
A 401 for the current person was stored, then hidden because the subject stamp only happened on success. Session, report, and birth contracts now follow the subject query and the shared birth lookup. Admin reset fixtures can no longer insert a retired self mirror.
2026-09-25 04:51:49 +08:00

215 lines
8.6 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
hrefFromLinkTarget,
interceptAppNavigation,
isStaleClientBuild,
navigateAppPath,
readHealthGitCommit,
rememberServerBuildCommit,
resetAppNavigationForTests,
setAppNavigationHostForTests,
setClientBuildCommitForTests,
} from "../src/lib/app-navigation.ts";
import {
peekChartPage,
peekEphemerisPage,
peekReportsPage,
prefetchSecondaryPage,
refreshChartPage,
refreshEphemerisPage,
refreshReportsPage,
resetSecondaryPageDataForTests,
writeChartPage,
} from "../src/lib/secondary-page-data.ts";
import { cssDeclarations } from "./css-contract-test-support.ts";
const chartView = readFileSync(new URL("../src/components/chart-page/chart-page-view.tsx", import.meta.url), "utf8");
const ephemerisPage = readFileSync(new URL("../src/components/ephemeris/ephemeris-page.tsx", import.meta.url), "utf8");
const reportsCenter = readFileSync(new URL("../src/components/personal-report/personal-report-center.tsx", import.meta.url), "utf8");
const sidebar = readFileSync(new URL("../src/components/app-sidebar.tsx", import.meta.url), "utf8");
const layout = readFileSync(new URL("../src/app/layout.tsx", import.meta.url), "utf8");
const dockerfile = readFileSync(new URL("../../deploy/railway-web.Dockerfile", import.meta.url), "utf8");
const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const chartHook = readFileSync(new URL("../src/hooks/use-chart-page.ts", import.meta.url), "utf8");
function clickEvent(overrides: Partial<{
button: number;
metaKey: boolean;
ctrlKey: boolean;
shiftKey: boolean;
altKey: boolean;
defaultPrevented: boolean;
}> = {}) {
let defaultPrevented = overrides.defaultPrevented ?? false;
return {
button: overrides.button ?? 0,
metaKey: overrides.metaKey ?? false,
ctrlKey: overrides.ctrlKey ?? false,
shiftKey: overrides.shiftKey ?? false,
altKey: overrides.altKey ?? false,
get defaultPrevented() {
return defaultPrevented;
},
preventDefault() {
defaultPrevented = true;
},
};
}
test("the three entry pages import the shared SecondaryPageShell", () => {
for (const source of [chartView, ephemerisPage, reportsCenter]) {
assert.match(source, /from "@\/components\/secondary-page-shell"/);
assert.match(source, /<SecondaryPageShell/);
}
assert.doesNotMatch(chartView, /<SecondaryHeader title="星盘"/);
assert.doesNotMatch(ephemerisPage, /<SecondaryHeader/);
assert.doesNotMatch(reportsCenter, /<SecondaryHeader/);
// 原值:星盘也禁止 skeleton;新值:仅星盘允许复用盘位骨架;原因:2026-09-24 D1。
assert.match(chartView, /ChartPageSkeleton/);
assert.doesNotMatch(chartView, /InlineSpinner|正在加载/i);
assert.doesNotMatch(ephemerisPage, /InlineSpinner|正在加载|skeleton/i);
assert.doesNotMatch(reportsCenter, /正在读取报告|正在加载/);
});
test("the secondary-page body is a stable remaining-viewport cell", () => {
const page = cssDeclarations(".secondary-page", globalStyles);
assert.match(page, /display:\s*grid/);
assert.match(page, /min-height:\s*0/);
const waitingParent = cssDeclarations(".secondary-page:has(> .secondary-page-waiting)", globalStyles);
assert.match(waitingParent, /align-content:\s*center/);
const waiting = cssDeclarations(".secondary-page-waiting", globalStyles);
assert.match(waiting, /text-align:\s*center/);
assert.doesNotMatch(waiting, /spinner|skeleton/i);
});
test("a warm chart cache does not go through a null waiting state", async () => {
resetSecondaryPageDataForTests();
assert.equal(peekChartPage(), null);
writeChartPage({
kind: "view",
view: { status: "chart_unavailable", billed: false, message: "这一张盘还没拿到。" },
});
const warm = peekChartPage();
assert.equal(warm?.kind, "view");
assert.notEqual(warm, null);
const secondMountView = peekChartPage()?.kind === "view" ? peekChartPage() : null;
assert.equal(secondMountView?.kind, "view");
assert.match(chartHook, /useState<ChartViewResponse \| null>\(\(\) => viewMatchingSubject\(subjectId\)\)/);
});
test("chart / ephemeris / reports caches share one inflight fetch and keep the last result", async () => {
resetSecondaryPageDataForTests();
const originalFetch = globalThis.fetch;
let chartCalls = 0;
let ephemerisCalls = 0;
let reportsCalls = 0;
globalThis.fetch = (async (input: RequestInfo | URL) => {
const url = String(input);
if (url.includes("/api/chart-view")) {
chartCalls += 1;
return new Response("{}", { status: 401 });
}
if (url.includes("/api/ephemeris")) {
ephemerisCalls += 1;
return new Response("{}", { status: 401 });
}
if (url.includes("/api/reports")) {
reportsCalls += 1;
return new Response("{}", { status: 401 });
}
throw new Error(`unexpected fetch ${url}`);
}) as typeof fetch;
try {
prefetchSecondaryPage("/chart");
prefetchSecondaryPage("/ephemeris");
prefetchSecondaryPage("/reports");
const [chart, ephemeris, reports] = await Promise.all([
refreshChartPage(),
refreshEphemerisPage(),
refreshReportsPage(),
]);
assert.equal(chart.kind, "unauthenticated");
assert.equal(ephemeris.kind, "unauthorized");
assert.equal(reports.kind, "unauthorized");
assert.equal(chartCalls, 1);
assert.equal(ephemerisCalls, 1);
assert.equal(reportsCalls, 1);
assert.equal(peekChartPage()?.kind, "unauthenticated");
assert.equal(peekEphemerisPage()?.kind, "unauthorized");
assert.equal(peekReportsPage()?.kind, "unauthorized");
} finally {
globalThis.fetch = originalFetch;
resetSecondaryPageDataForTests();
}
});
test("sidebar prefetches the three entries on pointerenter and pointerdown", () => {
assert.match(sidebar, /prefetchSecondaryPage\(href\)/);
assert.match(sidebar, /onPointerEnter=\{\(\) => prefetchSecondaryPage\(href\)\}/);
assert.match(sidebar, /onPointerDown=\{\(\) => prefetchSecondaryPage\(href\)\}/);
assert.match(sidebar, /from "@\/lib\/secondary-page-data"/);
});
test("version mismatch navigates with location.assign; a match keeps the client router", () => {
resetAppNavigationForTests();
const assigned: string[] = [];
setAppNavigationHostForTests({ assign: (href) => assigned.push(href) });
setClientBuildCommitForTests("old-sha");
rememberServerBuildCommit("new-sha");
assert.equal(isStaleClientBuild(), true);
const staleClick = clickEvent();
interceptAppNavigation(staleClick, "/chart");
assert.equal(staleClick.defaultPrevented, true);
assert.deepEqual(assigned, ["/chart"]);
assigned.length = 0;
navigateAppPath("/ephemeris", (href) => assigned.push(`client:${href}`));
assert.deepEqual(assigned, ["/ephemeris"]);
assigned.length = 0;
rememberServerBuildCommit("old-sha");
assert.equal(isStaleClientBuild(), false);
const freshClick = clickEvent();
interceptAppNavigation(freshClick, "/reports");
assert.equal(freshClick.defaultPrevented, false);
assert.equal(assigned.length, 0);
navigateAppPath("/reports", (href) => assigned.push(`client:${href}`));
assert.deepEqual(assigned, ["client:/reports"]);
assigned.length = 0;
rememberServerBuildCommit(null);
interceptAppNavigation(clickEvent(), "/chart");
assert.equal(assigned.length, 0);
resetAppNavigationForTests();
});
test("modified clicks and health-shaped payloads stay out of the hard-nav path", () => {
resetAppNavigationForTests();
const assigned: string[] = [];
setAppNavigationHostForTests({ assign: (href) => assigned.push(href) });
setClientBuildCommitForTests("old-sha");
rememberServerBuildCommit("new-sha");
interceptAppNavigation(clickEvent({ metaKey: true }), "/chart");
interceptAppNavigation(clickEvent({ button: 1 }), "/chart");
interceptAppNavigation(clickEvent(), "https://example.com");
assert.equal(assigned.length, 0);
assert.equal(readHealthGitCommit({ deployment: { gitCommit: "abc" } }), "abc");
assert.equal(readHealthGitCommit({ status: "ok" }), null);
assert.equal(hrefFromLinkTarget("/chart"), "/chart");
assert.equal(hrefFromLinkTarget({ pathname: "/reports", search: "?x=1" }), "/reports?x=1");
resetAppNavigationForTests();
});
test("the root layout checks health on visibility, not on a timer", () => {
const guard = readFileSync(new URL("../src/components/stale-build-guard.tsx", import.meta.url), "utf8");
assert.match(layout, /StaleBuildGuard/);
assert.match(guard, /visibilitychange/);
assert.match(guard, /syncBuildCommitFromHealth/);
assert.doesNotMatch(guard, /setInterval|setTimeout/);
assert.match(dockerfile, /NEXT_PUBLIC_GIT_COMMIT=\$\{NEXT_DEPLOYMENT_ID\}/);
});