Files
Jyotisha/frontend/tests/membership-navigation-contract.test.ts
T
Jesse_Chen dc6598d7e4 fix(web): keep the settings dialog one size and move billing into it (BUG-554)
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>
2026-09-06 18:29:51 +08:00

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\);/);
});