Files
Jyotisha/frontend/tests/membership-navigation-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

64 lines
3.7 KiB
TypeScript

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