import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { formatWaitedDuration, POLL_BUDGET_MS, pollIntervalForElapsed, } from "../src/components/personal-report/personal-report-page.tsx"; const pageSource = readFileSync( new URL("../src/components/personal-report/personal-report-page.tsx", import.meta.url), "utf8", ); const centerSource = readFileSync( new URL("../src/components/personal-report/personal-report-center.tsx", import.meta.url), "utf8", ); const hookSource = readFileSync( new URL("../src/hooks/use-visibility-aware-poll.ts", import.meta.url), "utf8", ); test("an exhausted poll budget becomes an explicit state instead of an endless spinner", () => { assert.match(pageSource, /\| \{ phase: "timed-out" \}/, "ReportLoadState carries an explicit timed-out phase"); assert.match(pageSource, /setState\(\(current\) => \(current\.phase === "generating" \? \{ phase: "timed-out" \} : current\)\)/); assert.match(pageSource, /if \(state\.phase === "timed-out"\)/, "the timed-out phase has its own render branch"); // The old silent give-up must be gone. assert.doesNotMatch(pageSource, /MAX_POLLS/); assert.doesNotMatch(pageSource, /polls >= /); }); test("the timed-out branch is honest and offers both resume and the report center", () => { const branchAt = pageSource.indexOf('if (state.phase === "timed-out")'); assert.ok(branchAt >= 0, "timed-out branch must exist"); const branchEnd = pageSource.indexOf('if (state.phase === "unauthorized")'); assert.ok(branchEnd > branchAt, "the timed-out branch must be self-contained"); const branch = pageSource.slice(branchAt, branchEnd); assert.match(branch, /生成时间超出预期/); assert.match(branch, /已等待 \{formatWaitedDuration\(waitedMs\)\}/); assert.match(branch, /页面已暂停自动刷新。/, "we must say polling stopped rather than imply it continues"); assert.match(branch, /报告仍在后台生成/); assert.match(branch, /继续等待/); assert.match(branch, /onClick=\{\(\) => keepWaiting\(\)\}/); assert.match(branch, /href="\/reports"/); assert.match(branch, /返回报告中心/); assert.doesNotMatch(branch, /[!!]/, "copy carries no exclamation marks"); }); test("keepWaiting restarts the wait clock and immediately refetches", () => { const resumeAt = pageSource.indexOf("const keepWaiting = useCallback("); assert.ok(resumeAt >= 0); const resume = pageSource.slice(resumeAt, resumeAt + 320); assert.match(resume, /setWaitStartedAt\(Date\.now\(\)\)/); assert.match(resume, /setWaitedMs\(0\)/); assert.match(resume, /setState\(\{ phase: "generating" \}\)/); assert.match(resume, /void load\(\)/); }); test("the wait budget is an explicit wall clock well beyond the old 120s", () => { assert.equal(POLL_BUDGET_MS, 8 * 60 * 1000); assert.ok(POLL_BUDGET_MS > 120_000, "120s was too short for report generation"); assert.match(pageSource, /export const POLL_BUDGET_MS = 8 \* 60 \* 1000;/); }); test("polling backs off without slowing the first minute", () => { assert.equal(pollIntervalForElapsed(0), 3000); assert.equal(pollIntervalForElapsed(59_999), 3000); assert.equal(pollIntervalForElapsed(60_000), 6000); assert.equal(pollIntervalForElapsed(179_999), 6000); assert.equal(pollIntervalForElapsed(180_000), 10_000); assert.equal(pollIntervalForElapsed(360_000), 15_000); assert.equal(pollIntervalForElapsed(POLL_BUDGET_MS), 15_000); let previous = 0; for (let elapsed = 0; elapsed <= POLL_BUDGET_MS; elapsed += 1000) { const interval = pollIntervalForElapsed(elapsed); assert.ok(interval >= previous, `interval must never shrink at ${elapsed}ms`); assert.ok(interval >= 3000 && interval <= 15_000, `interval stays within 3s..15s at ${elapsed}ms`); previous = interval; } // A full budget must cost far fewer requests than a flat 3s interval would. let requests = 0; for (let elapsed = 0; elapsed < POLL_BUDGET_MS; elapsed += pollIntervalForElapsed(elapsed)) { requests += 1; } assert.ok(requests < POLL_BUDGET_MS / 3000, "backoff must reduce the request count"); assert.ok(requests < 80, `a full wait should stay under 80 requests, got ${requests}`); assert.match(pageSource, /intervalMs: pollIntervalForElapsed\(waitedMs\)/); }); test("elapsed wait is rendered in Simplified Chinese minutes and seconds", () => { assert.equal(formatWaitedDuration(0), "0 秒"); assert.equal(formatWaitedDuration(9_400), "9 秒"); assert.equal(formatWaitedDuration(60_000), "1 分 0 秒"); assert.equal(formatWaitedDuration(130_000), "2 分 10 秒"); assert.equal(formatWaitedDuration(-5), "0 秒"); assert.match(pageSource, /已等待 \{formatWaitedDuration\(waitedMs\)\}/); const generatingAt = pageSource.indexOf('{generating ? "报告正在生成中,请稍候…"'); assert.ok(generatingAt >= 0, "the generating spinner still exists"); assert.match(pageSource.slice(generatingAt, generatingAt + 700), /已等待 \{formatWaitedDuration\(waitedMs\)\}/); }); test("the shared hook pauses on hidden, refreshes on visible and always cleans up", () => { assert.match(hookSource, /export function useVisibilityAwarePoll/); assert.match(hookSource, /document\.hidden/); assert.match(hookSource, /document\.addEventListener\("visibilitychange", handleVisibilityChange\)/); assert.match(hookSource, /document\.removeEventListener\("visibilitychange", handleVisibilityChange\)/); assert.match(hookSource, /if \(document\.hidden\) \{\s*stop\(\);\s*return;\s*\}/); assert.match(hookSource, /if \(refreshOnVisible\) \{\s*pollRef\.current\(\);\s*\}/); assert.match(hookSource, /window\.clearInterval\(timer\)/); // The callback lives in a ref so an inline closure cannot restart the interval every render. assert.match(hookSource, /const pollRef = useRef\(onPoll\)/); assert.match(hookSource, /pollRef\.current = onPoll;/); assert.match(hookSource, /\}, \[enabled, intervalMs, refreshOnVisible\]\);/); // Reusable: no report-specific imports. assert.doesNotMatch(hookSource, /personal-report|\/api\/reports/); }); test("both report surfaces poll through the visibility-aware hook and nothing else", () => { for (const [name, source] of [["page", pageSource], ["center", centerSource]] as const) { assert.match( source, /import \{ useVisibilityAwarePoll \} from "@\/hooks\/use-visibility-aware-poll";/, `${name} imports the shared hook`, ); assert.match(source, /useVisibilityAwarePoll\(\{/, `${name} calls the shared hook`); assert.doesNotMatch(source, /setInterval\(/, `${name} must not hand-roll an interval poll`); assert.doesNotMatch(source, /addEventListener\("visibilitychange"/, `${name} must not duplicate the listener`); } assert.match(centerSource, /enabled: hasGenerating,/); assert.match(centerSource, /intervalMs: LIST_POLL_INTERVAL_MS,/); assert.match(centerSource, /const LIST_POLL_INTERVAL_MS = 3000;/); assert.match(pageSource, /enabled: generating,/); }); test("cancellation guards survive the rewrite and no poll runs outside the generating phase", () => { assert.match(pageSource, /const cancelledRef = useRef\(false\)/); assert.match(pageSource, /if \(cancelledRef\.current\) \{\s*return;\s*\}/); assert.match(pageSource, /cancelledRef\.current = true;/); assert.match(centerSource, /const cancelled = useRef\(false\)/); assert.match(centerSource, /if \(cancelled\.current\) return;/); assert.match(centerSource, /cancelled\.current = true;/); assert.match(pageSource, /const generating = state\.phase === "generating";/); });