import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; const hookSource = readFileSync(new URL("../src/hooks/use-billing-panel.ts", import.meta.url), "utf8"); const panelSource = readFileSync(new URL("../src/components/billing-panel.tsx", import.meta.url), "utf8"); const billingSource = `${hookSource}\n${panelSource}`; const nextConfig = readFileSync(new URL("../next.config.ts", import.meta.url), "utf8"); function countMatches(source: string, pattern: RegExp) { return source.match(pattern)?.length ?? 0; } test("packages load on mount without a redundant account request", () => { // 原值: 挂载并发 fetchAccountData() 与 fetchPackages(),并锁定 Promise.allSettled。 // 新值: 挂载只执行 fetchPackages(),账户余额来自首页 bootstrap;成功兑换/支付使用接口返回 credits 通过共享事件同步。 // 原因: BUG-1022 / TASK-home-slow-network D5,删除无用的完整 /api/account 请求,避免重复数据库读取。 assert.match(hookSource, /setTimeout\(\(\) => void fetchPackages\(\), 0\)/); assert.doesNotMatch(hookSource, /Promise\.allSettled\(\[fetchAccountData\(\), fetchPackages\(\)\]\)/); assert.doesNotMatch(hookSource, /fetchAccountData/); assert.doesNotMatch(hookSource, /fetch\("\/api\/account"/); assert.match(hookSource, /notifyBalanceChanged\(result\.credits\)/); // 原值: 套餐支付成功契约精确断言 `notifyBalanceChanged(payload.credits)`。 // 新值: 支付成功无条件调用 `notifyBalanceChanged(typeof payload.credits === "number" ? payload.credits : 0)`。 // 原因: 订阅响应可能缺少 credits,仍需广播余额刷新事件;未知载荷使用 0,服务端刷新保持权威。 assert.match(hookSource, /notifyBalanceChanged\(typeof payload\.credits === "number" \? payload\.credits : 0\)/); }); test("a failing packages request cannot hide the account summary", () => { // 原值: 账户与套餐并发加载,各自错误状态均需保留。 // 新值: 账户来自首页 bootstrap;账单挂载只请求套餐,失败仅显示套餐错误,账户摘要仍可见。 // 原因: BUG-1022 / T2 删除重复账户请求,避免为已不存在的请求保留错误契约。 assert.doesNotMatch(hookSource, /setAccountError\(caught instanceof Error/); assert.match(hookSource, /setPackagesError\("套餐支付暂时不可用,请稍后重试"\)/); // 原值: 摘要在整页 hero,失败时显示「正在读取…」 // 新值: 弹窗打开时账户已在内存,摘要直接渲染;套餐区失败只出 packagesError // 原因: 决策 8,不得出现「正在加载」 assert.match(panelSource, /余额 \{credits\} 点/); assert.match(panelSource, /\{packagesError &&

\{packagesError\}<\/p>\}/); assert.doesNotMatch(panelSource, /正在读取/); }); test("membership and orders navigate through next/link instead of a document reload", () => { // 原值: Link 在 /membership 与 /membership/orders 之间 // 新值: 页签切换;旧路径由 next.config redirects 接到 ?settings=billing // 原因: 删除整页 assert.doesNotMatch(billingSource, /import Link from "next\/link"/); assert.doesNotMatch(billingSource, /href="\/membership/); assert.doesNotMatch(billingSource, /window\.location\.assign\("\/membership/); assert.match(nextConfig, /source: "\/membership"/); assert.match(panelSource, /value="orders"/); }); test("401 redirects stay hard navigations so no client state survives the sign-out", () => { assert.match(hookSource, /if \(response\.status === 401\) \{\s*window\.location\.assign\("\/login"\);/); // 原值: 4 次认证硬跳转,包含已删除的挂载账户请求。 // 新值: 3 次,订单、兑换、支付状态仍在 401 时硬跳转登录。 // 原因: 删除重复 /api/account 请求后,认证行为保留在仍实际发起的请求上。 assert.equal(countMatches(hookSource, /window\.location\.assign\("\/login"\)/g), 3); }); test("browser back and the external cashier keep their native behaviour", () => { // 原值: goBack 的 history.back / assign("/") // 新值: 无 goBack;收银台仍 window.open // 原因: 账单在当前页,后退是浏览器原生;BUG-251 要求收银台新标签 assert.doesNotMatch(billingSource, /window\.history\.back\(\)/); assert.match(hookSource, /window\.open\(payload\.payUrl, "_blank", "noopener,noreferrer"\)/); }); test("the hard navigation inventory does not grow", () => { // 原值: 会员页 4 次 window.location. + 订单页 1 次。 // 新值: hook 仅 3 次 assign("/login"),面板 0 次。 // 原因: 删除重复账户请求后,仅订单、兑换、支付状态保留认证硬跳转。 assert.equal(countMatches(hookSource, /window\.location\./g), 3); assert.equal(countMatches(panelSource, /window\.location\./g), 0); }); test("payment polling pauses on a hidden tab and refreshes when it returns", () => { assert.match(hookSource, /if \(!document\.hidden\) startPolling\(\);/); assert.match(hookSource, /document\.addEventListener\("visibilitychange", onVisibilityChange\);/); assert.match(hookSource, /if \(document\.hidden\) \{\s*stopPolling\(\);\s*return;\s*\}/); assert.match(hookSource, /void checkPaymentStatus\(\);\s*startPolling\(\);/); assert.match(hookSource, /window\.setInterval\(\(\) => void checkPaymentStatus\(\), 3000\);/); }); test("polling cleanup releases both the timer and the visibility listener", () => { assert.match(hookSource, /if \(timer\) window\.clearInterval\(timer\);/); assert.match(hookSource, /stopPolling\(\);\s*document\.removeEventListener\("visibilitychange", onVisibilityChange\);/); });