dc6598d7e4
The four account panes now share a fixed frame, chart profiles open as list then detail, and membership lives in the homepage dialog instead of a separate page. Co-authored-by: Cursor <cursoragent@cursor.com>
75 lines
4.1 KiB
TypeScript
75 lines
4.1 KiB
TypeScript
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("account and packages are requested concurrently on mount", () => {
|
|
assert.match(hookSource, /await Promise\.allSettled\(\[fetchAccountData\(\), fetchPackages\(\)\]\);/);
|
|
assert.doesNotMatch(hookSource, /await fetchAccountData\(\);\s*await fetchPackages\(\)/);
|
|
assert.doesNotMatch(hookSource, /Promise\.all\(\[/);
|
|
});
|
|
|
|
test("a failing bootstrap request cannot hide the other section", () => {
|
|
assert.match(hookSource, /setAccountError\(caught instanceof Error/);
|
|
assert.match(hookSource, /setPackagesError\("套餐支付暂时不可用,请稍后重试"\)/);
|
|
// 原值: 摘要在整页 hero,失败时显示「正在读取…」
|
|
// 新值: 弹窗打开时账户已在内存,摘要直接渲染;套餐区失败只出 packagesError
|
|
// 原因: 决策 8,不得出现「正在加载」
|
|
assert.match(panelSource, /余额 \{credits\} 点/);
|
|
assert.match(panelSource, /\{packagesError && <p className="form-error" role="alert">\{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"\);/);
|
|
assert.equal(countMatches(hookSource, /window\.location\.assign\("\/login"\)/g), 4);
|
|
});
|
|
|
|
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 仅 4 次 assign("/login"),面板 0 次
|
|
// 原因: 去掉 goBack 的 assign("/") 后硬跳转只剩认证
|
|
assert.equal(countMatches(hookSource, /window\.location\./g), 4);
|
|
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\);/);
|
|
});
|