fix(ux): surface hidden notices, bound report waits, add root boundaries
Independent Staging Quality Gate / validate (push) Successful in 12m8s
Independent Staging Quality Gate / publish (push) Successful in 14m29s

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>
This commit is contained in:
Jesse_Chen
2026-08-17 12:52:24 +08:00
parent 2d370f2e9d
commit 9c296f1e3f
23 changed files with 1144 additions and 88 deletions
@@ -0,0 +1,69 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
const registry = readFileSync(new URL("../src/components/admin/admin-antd-registry.tsx", import.meta.url), "utf8");
const adminLayout = readFileSync(new URL("../src/app/admin/layout.tsx", import.meta.url), "utf8");
const rootLayout = readFileSync(new URL("../src/app/layout.tsx", import.meta.url), "utf8");
const globalsCss = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const adminCss = readFileSync(new URL("../src/app/admin/admin.css", import.meta.url), "utf8");
const nextConfig = readFileSync(new URL("../next.config.ts", import.meta.url), "utf8");
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as {
dependencies: Record<string, string>;
};
test("admin antd styles are extracted during server rendering", () => {
assert.match(registry, /^"use client";/);
assert.match(registry, /import \{ StyleProvider, createCache, extractStyle \} from "@ant-design\/cssinjs";/);
assert.match(registry, /useServerInsertedHTML\(\(\) => \{/);
assert.match(registry, /extractStyle\(cache, \{ plain: true, once: true \}\)/);
assert.match(registry, /<StyleProvider cache=\{cache\}>\{children\}<\/StyleProvider>/);
assert.match(registry, /id="antd-cssinjs"/);
assert.match(registry, /data-rc-order="prepend"/);
assert.ok("@ant-design/cssinjs" in packageJson.dependencies, "cssinjs must be a declared dependency");
});
test("the antd registry wraps the admin subtree only", () => {
assert.match(adminLayout, /import \{ AdminAntdRegistry \} from "@\/components\/admin\/admin-antd-registry";/);
assert.match(adminLayout, /<AdminAntdRegistry>\s*<AdminApp>\{children\}<\/AdminApp>\s*<\/AdminAntdRegistry>/);
assert.doesNotMatch(rootLayout, /AdminAntdRegistry|cssinjs|antd/);
});
test("antd runs with the React 19 render patch inside admin", () => {
assert.match(registry, /import \{ unstableSetRender \} from "antd";/);
assert.match(registry, /import \{ createRoot, type Root \} from "react-dom\/client";/);
assert.match(registry, /unstableSetRender\(\(node, container\) => \{/);
assert.match(registry, /target\._reactRoot \?\?= createRoot\(target\);/);
assert.match(registry, /root\.render\(node\);/);
assert.match(registry, /root\.unmount\(\);/);
});
test("admin antd overrides ship only on admin routes", () => {
assert.doesNotMatch(globalsCss, /\.admin-app-shell \.ant-layout-content/);
assert.doesNotMatch(globalsCss, /\.admin-app-shell \.ant-table-wrapper/);
assert.doesNotMatch(globalsCss, /\.admin-text-list|\.admin-loading/);
assert.match(adminCss, /\.admin-app-shell \.ant-layout-content \{ padding: 22px; \}/);
assert.match(adminCss, /\.admin-app-shell \.ant-table-wrapper \.ant-table \{ background: #fbfaf7; \}/);
assert.match(adminCss, /\.admin-text-list \{ max-width: 680px;/);
assert.match(adminCss, /\.admin-loading \{ min-height: 100dvh;/);
assert.match(adminLayout, /import "\.\/admin\.css";/);
assert.doesNotMatch(rootLayout, /admin\.css/);
});
test("the admin shell keeps one antd reset stylesheet", () => {
assert.match(adminLayout, /import "antd\/dist\/reset\.css";/);
assert.doesNotMatch(adminLayout, /@refinedev\/antd\/dist\/reset\.css/);
});
test("barrel-heavy packages are optimized at build time", () => {
assert.match(nextConfig, /optimizePackageImports: \[/);
for (const pkg of ["@ant-design/icons", "@refinedev/antd", "@refinedev/core", "antd", "date-fns", "lucide-react"]) {
assert.ok(nextConfig.includes(`"${pkg}"`), `${pkg} must be listed in optimizePackageImports`);
assert.ok(pkg in packageJson.dependencies, `${pkg} must be a declared dependency`);
}
assert.match(nextConfig, /authInterrupts: true/);
assert.match(nextConfig, /outputFileTracingIncludes: \{/);
assert.match(nextConfig, /"\/api\/consult": \[/);
assert.match(nextConfig, /turbopack: \{ root: repositoryRoot \}/);
assert.match(nextConfig, /output: "standalone"/);
});
@@ -0,0 +1,115 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { noticeTone } from "../src/lib/chat-notice.ts";
import { nextAnchorState } from "../src/hooks/use-conversation-scroll-anchor.ts";
const pageSource = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
const noticeSource = readFileSync(new URL("../src/lib/chat-notice.ts", import.meta.url), "utf8");
const anchorSource = readFileSync(new URL("../src/hooks/use-conversation-scroll-anchor.ts", import.meta.url), "utf8");
function sourceBetween(source: string, startMarker: string, endMarker: string) {
const start = source.indexOf(startMarker);
const end = source.indexOf(endMarker, start);
assert.notEqual(start, -1);
assert.notEqual(end, -1);
return source.slice(start, end);
}
test("routes composer notices to the user instead of discarding them", () => {
// Given: the page no longer keeps the notice in write-only state.
assert.doesNotMatch(pageSource, /const \[, setComposerNotice\] = useState/);
assert.match(pageSource, /import \{ showChatNotice as setComposerNotice \} from "@\/lib\/chat-notice"/);
// Then: every notice reaches the mounted sonner toaster.
assert.match(noticeSource, /import \{ toast \} from "sonner"/);
assert.match(noticeSource, /toast\.success\(message, \{ id: chatNoticeToastId \}\)/);
assert.match(noticeSource, /toast\.error\(message, \{ id: chatNoticeToastId \}\)/);
assert.match(noticeSource, /toast\(message, \{ id: chatNoticeToastId \}\)/);
});
test("clearing a notice dismisses instead of showing an empty toast", () => {
const clearBranch = sourceBetween(noticeSource, "if (!message.trim())", "if (lastNotice === message) return;");
assert.match(clearBranch, /toast\.dismiss\(chatNoticeToastId\)/);
assert.doesNotMatch(clearBranch, /toast\(|toast\.success|toast\.error/);
assert.match(pageSource, /setComposerNotice\(""\)/);
});
test("keeps the recovery poll from stacking repeated notices", () => {
// Given: the recovery loop repeats the same message every 1750ms.
assert.match(pageSource, /timer = window\.setTimeout\(\(\) => void poll\(\), 1_750\)/);
// Then: a stable toast id plus last-message dedupe replaces instead of accumulating.
assert.match(noticeSource, /export const chatNoticeToastId = "chat-notice"/);
assert.match(noticeSource, /if \(lastNotice === message\) return;/);
});
test("assigns notice severity by message intent", () => {
assert.equal(noticeTone("网络已断开,回答仍在后台生成;联网后会自动恢复。"), "info");
assert.equal(noticeTone("回答仍在后台生成,正在自动恢复。"), "info");
assert.equal(noticeTone("正在确认本次咨询请求是否已开始…"), "info");
assert.equal(noticeTone("此前选择的模型已下线,已切换为默认模型。"), "info");
assert.equal(noticeTone("回答已取消;问题仍保留在聊天记录中,可重新发送。"), "info");
assert.equal(noticeTone("回答已恢复。"), "success");
assert.equal(noticeTone("已归档,可在左侧归档中恢复。"), "success");
assert.equal(noticeTone("已停止回答,现有内容已保留,本次点数已退回。"), "success");
assert.equal(noticeTone("删除失败:网络异常"), "error");
assert.equal(noticeTone("重命名同步失败"), "error");
assert.equal(noticeTone("模型服务暂时不可用,当前无法发送问题。"), "error");
assert.equal(noticeTone("后台未找到本次咨询请求,已停止恢复,请重新发送。"), "error");
});
test("anchors the streaming scroll instead of following every token", () => {
const autoScrollEffect = sourceBetween(
pageSource,
"useEffect(() => {\n if (starterHomeVisible) return;",
"profileComplete, starterHomeVisible]);",
);
// Then: streamed tokens only move the viewport while the reader stays anchored.
assert.match(autoScrollEffect, /if \(!conversationAnchor\.anchored\) return/);
const scrollGuard = autoScrollEffect.indexOf("if (!conversationAnchor.anchored) return");
const scrollCall = autoScrollEffect.indexOf("container.scrollTo(");
assert.ok(scrollGuard >= 0 && scrollGuard < scrollCall);
});
test("scrolls to the newest turn on intentional jumps", () => {
// Given: switching sessions resets the anchor through the hook reset key.
assert.match(pageSource, /useConversationScrollAnchor\(\n\s*conversation,\n\s*!rectificationSurfaceOpen && !starterHomeVisible,\n\s*activeSessionId,\n\s*\)/);
assert.match(anchorSource, /const anchored = anchor\.key === resetKey \? anchor\.anchored : true/);
// And: sending a question re-anchors before the optimistic turn renders.
const sendBlock = sourceBetween(pageSource, " updateSession(sessionId, () => userSession);", " setDraft(\"\");");
assert.match(sendBlock, /conversationAnchor\.anchorToLatest\(\)/);
});
test("offers an accessible jump-to-latest control while reading history", () => {
const jumpControl = sourceBetween(pageSource, "{jumpToLatestVisible && (", "</div>\n )}");
assert.match(pageSource, /const jumpToLatestVisible = !rectificationSurfaceOpen[\s\S]*?&& !conversationAnchor\.anchored/);
assert.match(jumpControl, /type="button"/);
assert.match(jumpControl, /aria-label="跳到最新"/);
assert.match(jumpControl, /focus-visible:ring-3/);
assert.match(jumpControl, /min-h-11/);
assert.match(jumpControl, /min-w-11/);
assert.match(jumpControl, /onClick=\{conversationAnchor\.anchorToLatest\}/);
});
test("keeps the scroll listener passive and reduced-motion aware", () => {
assert.match(anchorSource, /addEventListener\("scroll", onScroll, \{ passive: true \}\)/);
assert.match(anchorSource, /frame = window\.requestAnimationFrame\(measure\)/);
assert.match(anchorSource, /window\.matchMedia\("\(prefers-reduced-motion: reduce\)"\)\.matches/);
assert.match(anchorSource, /behavior: reduceMotion \? "auto" : "smooth"/);
});
test("re-anchors once the reader returns to the newest turn", () => {
// Given: the reader is following the stream and scrolls up mid-answer.
assert.equal(nextAnchorState(true, 0, false), true);
assert.equal(nextAnchorState(true, 600, true), false);
// Then: growing content alone never re-anchors, but scrolling back does.
assert.equal(nextAnchorState(false, 600, false), false);
assert.equal(nextAnchorState(false, 40, false), true);
});
@@ -0,0 +1,63 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
const membershipPage = readFileSync(new URL("../src/app/membership/page.tsx", import.meta.url), "utf8");
const ordersPage = readFileSync(new URL("../src/app/membership/orders/page.tsx", import.meta.url), "utf8");
function countMatches(source: string, pattern: RegExp) {
return source.match(pattern)?.length ?? 0;
}
test("account and packages are requested concurrently on mount", () => {
assert.match(membershipPage, /await Promise\.allSettled\(\[fetchAccountData\(\), fetchPackages\(\)\]\);/);
assert.doesNotMatch(membershipPage, /await fetchAccountData\(\);\s*await fetchPackages\(\)/);
assert.doesNotMatch(membershipPage, /Promise\.all\(\[/);
});
test("a failing bootstrap request cannot hide the other section", () => {
assert.match(membershipPage, /setAccountError\(caught instanceof Error/);
assert.match(membershipPage, /setPackagesError\("套餐支付暂时不可用,请稍后重试"\)/);
assert.match(membershipPage, /\{account \? `\$\{account\.credits\} 点` : accountError \|\| "正在读取…"\}/);
assert.match(membershipPage, /\{packagesError && <p className="form-error" role="alert">\{packagesError\}<\/p>\}/);
});
test("membership and orders navigate through next/link instead of a document reload", () => {
assert.match(membershipPage, /import Link from "next\/link"/);
assert.match(membershipPage, /<Link className="membership-orders-entry" href="\/membership\/orders">/);
assert.match(ordersPage, /import Link from "next\/link"/);
assert.match(ordersPage, /<Link className="membership-back" href="\/membership" replace/);
assert.doesNotMatch(membershipPage, /window\.location\.assign\("\/membership/);
assert.doesNotMatch(ordersPage, /window\.location\.assign\("\/membership/);
});
test("401 redirects stay hard navigations so no client state survives the sign-out", () => {
assert.match(membershipPage, /if \(response\.status === 401\) \{\s*window\.location\.assign\("\/login"\);/);
assert.match(ordersPage, /if \(response\.status === 401\) \{\s*window\.location\.assign\("\/login"\);/);
assert.equal(countMatches(membershipPage, /window\.location\.assign\("\/login"\)/g), 3);
assert.equal(countMatches(ordersPage, /window\.location\.assign\("\/login"\)/g), 1);
});
test("browser back and the external cashier keep their native behaviour", () => {
assert.match(membershipPage, /if \(window\.history\.length > 1\) window\.history\.back\(\);/);
assert.match(membershipPage, /else window\.location\.assign\("\/"\);/);
assert.match(membershipPage, /window\.open\(payload\.payUrl, "_blank", "noopener,noreferrer"\)/);
});
test("the hard navigation inventory does not grow", () => {
assert.equal(countMatches(membershipPage, /window\.location\./g), 4);
assert.equal(countMatches(ordersPage, /window\.location\./g), 1);
});
test("payment polling pauses on a hidden tab and refreshes when it returns", () => {
assert.match(membershipPage, /if \(!document\.hidden\) startPolling\(\);/);
assert.match(membershipPage, /document\.addEventListener\("visibilitychange", onVisibilityChange\);/);
assert.match(membershipPage, /if \(document\.hidden\) \{\s*stopPolling\(\);\s*return;\s*\}/);
assert.match(membershipPage, /void checkPaymentStatus\(\);\s*startPolling\(\);/);
assert.match(membershipPage, /window\.setInterval\(\(\) => void checkPaymentStatus\(\), 3000\);/);
});
test("polling cleanup releases both the timer and the visibility listener", () => {
assert.match(membershipPage, /if \(timer\) window\.clearInterval\(timer\);/);
assert.match(membershipPage, /stopPolling\(\);\s*document\.removeEventListener\("visibilitychange", onVisibilityChange\);/);
});
@@ -0,0 +1,147 @@
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";/);
});
@@ -0,0 +1,69 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
const read = (relative: string) => readFileSync(new URL(relative, import.meta.url), "utf8");
const error = read("../src/app/error.tsx");
const globalError = read("../src/app/global-error.tsx");
const notFound = read("../src/app/not-found.tsx");
const chinese = /[\u4e00-\u9fff]/;
test("the app root ships error, global-error and not-found boundaries", () => {
for (const source of [error, globalError, notFound]) {
assert.ok(source.length > 0);
assert.match(source, /export default function \w+\(/);
}
});
test("root error boundary is a client component that can reset and route home", () => {
assert.match(error, /^"use client";/);
assert.match(error, /reset,\s*\}: \{[\s\S]*reset: \(\) => void;/);
assert.match(error, /onClick=\{\(\) => reset\(\)\}/);
assert.match(error, /href="\/"/);
assert.match(error, /error\.digest/);
assert.doesNotMatch(error, /error\.stack/);
});
test("global error boundary replaces the root layout without external styling", () => {
assert.match(globalError, /^"use client";/);
assert.match(globalError, /<html lang="zh-CN">/);
assert.match(globalError, /<body/);
assert.match(globalError, /<\/body>/);
assert.match(globalError, /<\/html>/);
assert.doesNotMatch(globalError, /^import /m);
assert.doesNotMatch(globalError, /className="[a-z-]*(flex|text-|bg-|min-h-)/);
assert.match(globalError, /minHeight: "44px"/);
assert.match(globalError, /onClick=\{\(\) => reset\(\)\}/);
assert.match(globalError, /window\.location\.assign\("\/"\)/);
});
test("root not found stays a server component and links back to the chat home", () => {
assert.doesNotMatch(notFound, /"use client"/);
assert.doesNotMatch(notFound, /useState|useEffect|onClick/);
assert.match(notFound, /404/);
assert.match(notFound, /render=\{<Link href="\/" \/>\}/);
});
test("every boundary announces itself accessibly with one heading", () => {
for (const source of [error, globalError]) {
assert.match(source, /role="alert"/);
}
for (const source of [error, globalError, notFound]) {
assert.equal(source.match(/<h1/g)?.length, 1);
assert.doesNotMatch(source, /<h[3-6]/);
}
});
test("boundary copy is simplified chinese in the product voice", () => {
for (const source of [error, globalError, notFound]) {
const copy = source.match(/>\s*([^<>{}]*[\u4e00-\u9fff][^<>{}]*)\s*</g) ?? [];
assert.ok(copy.length >= 2);
for (const line of copy) {
assert.match(line, chinese);
assert.doesNotMatch(line, /[!]/);
assert.doesNotMatch(line, /\p{Extended_Pictographic}/u);
}
}
});