Files
Jyotisha/frontend/tests/report-polling-contract.test.ts
T
Jesse_Chen 9c296f1e3f
Independent Staging Quality Gate / validate (push) Successful in 12m8s
Independent Staging Quality Gate / publish (push) Successful in 14m29s
fix(ux): surface hidden notices, bound report waits, add root boundaries
Framework-level UX fixes found while auditing staging (BUG-216..220).

- chat: route 44 previously discarded composer notices to sonner with
  dedupe, so recovery, cancel and archive feedback is actually visible
  (BUG-216)
- chat: anchor stream auto-scroll to bottom proximity and add a
  jump-to-latest control, so reading history is no longer interrupted
  on every token (BUG-218)
- reports: replace the silent 120s poll cutoff with an explicit
  timed-out state, an 8m budget, stepped backoff and an elapsed
  counter (BUG-217)
- reports: pause polling while the tab is hidden, via a shared hook
- app: add root error, global-error and not-found boundaries (BUG-219)
- admin: add antd SSR style extraction and the React 19 render adapter,
  and move admin-only css out of the global stylesheet (BUG-220)
- membership: run bootstrap fetches concurrently and pause payment
  polling while hidden
- build: configure optimizePackageImports

Verified on top of 2d370f2e: tsc, eslint, next build, and the related
frontend contract suites.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-17 13:14:15 +08:00

148 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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";/);
});