Files
Jyotisha/frontend/tests/secondary-page-entry.test.ts
T

213 lines
8.4 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/);
assert.doesNotMatch(chartView, /InlineSpinner|正在加载|skeleton/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>\(\(\) => viewFromSnapshot\(peekChartPage\(\)\)\)/);
});
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\}/);
});