import assert from "node:assert/strict"; import { existsSync, readFileSync } from "node:fs"; import test from "node:test"; import { cssDeclarations } from "./css-contract-test-support.ts"; import { homeSurface as homePageSource } from "./home-surface.ts"; const projectFile = (path: string) => new URL(`../${path}`, import.meta.url); const readProjectFile = (path: string) => readFileSync(projectFile(path), "utf8"); const globalStyles = readProjectFile("src/app/globals.css"); const onboardingPaywallSource = readProjectFile("src/components/onboarding-redeem-paywall.tsx"); const pageSource = readProjectFile("src/app/membership/page.tsx"); const ordersPageSource = readProjectFile("src/app/membership/orders/page.tsx"); const tabsSource = readProjectFile("src/components/ui/tabs.tsx"); const membershipLib = readProjectFile("src/lib/membership.ts"); const cssBlock = (selector: string) => cssDeclarations(selector, globalStyles); test("membership page exists as a client component with a loading boundary", () => { assert.equal(existsSync(projectFile("src/app/membership/page.tsx")), true); assert.match(pageSource, /"use client"/); assert.match(pageSource, / { assert.match(pageSource, /fetch\("\/api\/account"/); assert.match(pageSource, /fetch\("\/api\/payment\/packages"/); assert.doesNotMatch(pageSource, /\/api\/payment\/orders|fetchOrders/); assert.match(pageSource, /fetch\("\/api\/payment\/epay\/create"/); assert.match(pageSource, /fetch\(`\/api\/payment\/epay\/status\?orderNo=/); assert.match(pageSource, /fetch\("\/api\/redeem"/); }); test("creates orders with productId and opens the signed cashier URL safely", () => { assert.match(pageSource, /JSON\.stringify\(\{ productId \}\)/); assert.match(pageSource, /window\.open\(payload\.payUrl, "_blank", "noopener,noreferrer"\)/); assert.doesNotMatch(pageSource, /document\.createElement\("form"\)|payload\.submitUrl|payload\.fields/); assert.match(pageSource, /if \(!paymentOrder \|\| paymentOrder\.status !== "pending"\) return;/); }); test("terminal payment failures stop polling and offer re-pay with the same product", () => { assert.match(pageSource, /\["failed", "closed", "cancelled"\]\.includes\(payload\.status\)/); assert.match(pageSource, /payload\.grantStatus === "failed"/); assert.match(pageSource, /status !== "pending"\) return;/); assert.match(pageSource, /status: paid \? "paid" : failed \? "failed" : "pending"/); assert.match(pageSource, /paymentOrder\.status === "failed"/); assert.match(pageSource, /支付失败/); assert.match(pageSource, /createPayment\(paymentOrder\.productId\)/); assert.match(pageSource, /重新支付/); }); test("failed status beats paid and non-terminal statuses keep polling", () => { assert.match(pageSource, /const paid = payload\.status === "paid" && !failed;/); assert.match(pageSource, /status: paid \? "paid" : failed \? "failed" : "pending"/); assert.match(pageSource, /if \(paid\) \{/); assert.doesNotMatch(pageSource, /status: paid \? "paid" : failed \? "failed" : payload\.status/); assert.doesNotMatch(pageSource, /payload\.status === "paid" \? "paid"/); }); test("polls order status and refreshes only the balance on paid", () => { assert.match(pageSource, /const paid = payload\.status === "paid" && !failed;/); assert.match(pageSource, /if \(paid\) \{/); assert.match(pageSource, /status: paid \? "paid" : failed \? "failed" : "pending"/); assert.match(pageSource, /fetchAccountData\(\)/); assert.match(pageSource, /notifyBalanceChanged\(refreshed\?\.credits/); assert.doesNotMatch(pageSource, /fetchOrders/); }); test("keeps the selected product and allows retry on payment failure", () => { assert.match(pageSource, /setSelectedProductId\(productId\)/); assert.match(pageSource, /setPaymentError\(caught instanceof Error \? caught\.message : "创建支付失败"\)/); assert.match(pageSource, /finally \{\s*setPayingProductId\(null\);/); assert.match(pageSource, /if \(payingProductId\) return;/); assert.match(pageSource, /createPayment\(selectedProductId\)/); assert.match(pageSource, /membership-payment-error/); assert.doesNotMatch(pageSource, /setPayingProductId\(null\)[\s\S]{0,40}setPaymentOrder\(null\)/); }); test("membership never opens the redeem dialog from the URL", () => { assert.match(pageSource, /const \[redeemOpen, setRedeemOpen\] = useState\(false\)/); assert.doesNotMatch(pageSource, /searchParams\.get\("redeem"\)|searchParams\.delete\("redeem"\)|redeem:\s*true/); assert.match(pageSource, /onClick=\{\(\) => setRedeemOpen\(true\)\}/); }); test("returning prefers history.back and keeps browser back semantics", () => { assert.match(pageSource, /window\.history\.back\(\)/); assert.match(pageSource, /window\.location\.assign\("\/"\)/); assert.match(pageSource, /onClick=\{goBack\}/); }); test("home refreshes the account whenever it is shown again", () => { const pageShow = homePageSource.slice( homePageSource.indexOf("const onPageShow"), homePageSource.indexOf("const onOnline"), ); assert.match(pageShow, /void refreshAccount\(\)/); assert.doesNotMatch(pageShow, /event\.persisted/); }); test("login redirects keep the loading screen instead of flashing the account error page", () => { assert.match(homePageSource, /class LoginRedirectError extends Error/); assert.match(homePageSource, /window\.location\.replace\("\/login"\)/); assert.match(homePageSource, /if \(caught instanceof LoginRedirectError\) \{\n\s*redirectedToLogin = true;\n\s*return;/); assert.match(homePageSource, /if \(redirectedToLogin\) return;/); }); test("homepage composer does not render notice copy", () => { assert.doesNotMatch(homePageSource, /composer-notice/); }); test("trims the redeem code without changing its case", () => { assert.match(pageSource, /const code = redeemCode\.trim\(\);/); assert.doesNotMatch(pageSource, /redeemCode\.(?:toUpperCase|toLowerCase)\(\)/); assert.doesNotMatch(pageSource, /normalizeRedeemCode/); assert.match(pageSource, /JSON\.stringify\(\{ code \}\)/); }); test("redeem success reports awarded credits, latest balance and completes", () => { assert.match(pageSource, /awardedCredits/); assert.match(pageSource, /本次到账/); assert.match(pageSource, /最新余额/); assert.match(pageSource, /setRedeemDone\(true\)/); assert.match(pageSource, /setRedeemCode\(""\)/); assert.match(pageSource, /const refreshed = await fetchAccountData\(\);/); assert.match(pageSource, /notifyBalanceChanged\(refreshed\?\.credits \?\? result\.credits\)/); assert.match(pageSource, /完成/); assert.match(pageSource, /onClick=\{closeRedeem\}/); }); test("supports plan hints without rendering source notices", () => { assert.match(pageSource, /searchParams\.get\("plan"\)/); assert.doesNotMatch(pageSource, /searchParams\.get\("source"\)|membershipSourceNotice|sourceNotice/); assert.doesNotMatch(pageSource, /membership-notice|已为你定位到/); assert.doesNotMatch(globalStyles, /\.membership-notice/); assert.doesNotMatch(pageSource, /体验 \/ 月卡 \/ 年卡见下方套餐|用于咨询与生时校正等服务/); }); test("membership section shows fixed trial/monthly/yearly plans with monthly recommended", () => { assert.match(pageSource, /selectMembershipPlans\(paymentPackages\)/); assert.match(pageSource, /planAlias\(product\)/); assert.match(pageSource, /alias === "monthly"/); assert.match(pageSource, /membership-plan-card--recommended/); assert.match(membershipLib, /trial: "体验"/); assert.match(membershipLib, /monthly: "月卡"/); assert.match(membershipLib, /yearly: "年卡"/); assert.match(pageSource, /会员套餐/); assert.match(pageSource, /点数充值/); assert.match(pageSource, /id="membership-plans-tab"/); assert.match(pageSource, /id="membership-credits-tab"/); assert.match(pageSource, //); assert.match(pageSource, /]*value="membership"/); assert.match(pageSource, /]*value="credits"/); assert.match(pageSource, /]*value="membership"/); assert.match(pageSource, /]*value="credits"/); }); test("plan cards carry price, duration, audience, entitlements, purchase and rules", () => { assert.match(pageSource, /formatPrice\(product\.priceCents, product\.currency\)/); assert.match(pageSource, /formatProductDuration\(product\)/); assert.match(pageSource, /productAudience\(product\)/); assert.match(pageSource, /entitlementLabel\(entitlement\)/); assert.match(pageSource, /
/); assert.match(pageSource, /详细规则<\/summary>/); // 原值: className={recommended ? "button-primary membership-buy" : "button-secondary membership-buy"} // 新值: Button className="membership-buy" + variant default|outline // 原因: 会员购买走共用 Button;登录/引导仍用 .button-primary 类(page.tsx 冻结与破坏确认合同) assert.match(pageSource, /import \{ Button \} from "@\/components\/ui\/button"/); assert.match(pageSource, /className="membership-buy"/); assert.match(pageSource, /variant=\{recommended \? "default" : "outline"\}/); }); test("renewal semantics map the active subscription to the matching plan", () => { assert.match(pageSource, /const currentProductCode = activeSubscription\?\.status === "active"/); assert.match(pageSource, /currentProductCode === product\.code/); assert.match(pageSource, /isCurrent \? "续费" : product\.productType === "trial" \? "立即开通" : "立即购买"/); assert.match(pageSource, /当前套餐/); }); test("membership and credits use the local Base UI tabs wrapper without manual segment state", () => { assert.match(pageSource, /import \{ Tabs, TabsContent, TabsList, TabsTrigger \} from "@\/components\/ui\/tabs"/); assert.doesNotMatch(pageSource, /activeSegment|setActiveSegment/); assert.match(tabsSource, /import \{ Tabs as TabsPrimitive \} from "@base-ui\/react\/tabs"/); assert.match(tabsSource, /TabsPrimitive\.Root/); assert.match(tabsSource, /TabsPrimitive\.List/); assert.match(tabsSource, /TabsPrimitive\.Tab/); assert.match(tabsSource, /TabsPrimitive\.Panel/); assert.match(pageSource, /const plans = selectMembershipPlans\(paymentPackages\);/); assert.match(pageSource, /const creditPacks = paymentPackages\.filter\(\(product\) => product\.productType === "credit_pack"\);/); assert.match(pageSource, /className="membership-plan-grid"/); assert.match(pageSource, /className="membership-credit-grid"/); assert.doesNotMatch(pageSource, /creditPacks\.map[\.\s\S]{0,120}membership-plan-card/); }); test("plan param forces the membership segment and highlights the matching card", () => { assert.match(pageSource, /const highlighted = alias === highlightedPlan;/); assert.match(pageSource, /membership-plan-card--highlighted/); assert.match(pageSource, /aria-current=\{highlighted \? "true" : undefined\}/); assert.match(pageSource, /highlightedCardRef/); assert.match(pageSource, /scrollIntoView\(\{ behavior: "smooth", block: "center" \}\)/); assert.match(pageSource, /card\.focus\(\{ preventScroll: true \}\)/); assert.doesNotMatch(pageSource, /setActiveSegment\(\"credits\"\)[\s\S]{0,80}highlightedPlan/); }); test("page main title is 套餐与会员", () => { assert.match(pageSource, /

套餐与会员<\/h1>/); assert.doesNotMatch(pageSource, /

会员与点数<\/h1>/); }); test("payment errors sit in a common position above the visible segment", () => { const errorAt = pageSource.indexOf("membership-payment-error"); const plansPanelAt = pageSource.indexOf(''); assert.notEqual(errorAt, -1, "missing common payment error surface"); assert.notEqual(plansPanelAt, -1, "missing plans panel"); assert.ok(errorAt < plansPanelAt, "payment error must render before the segment panels"); assert.doesNotMatch(pageSource, /

\{paymentError\}<\/p>/); }); test("summary stays on the membership page and links to the independent orders page", () => { assert.match(pageSource, /会员状态/); assert.match(pageSource, /当前余额/); assert.match(pageSource, /membership-summary-card/); assert.match(pageSource, /href="\/membership\/orders"[^>]*>订单记录<\/Link>/); assert.doesNotMatch(pageSource, /membership-order-row|orderStatusLabel/); }); test("independent orders page preserves loading, empty, error, refresh and list states", () => { assert.equal(existsSync(projectFile("src/app/membership/orders/page.tsx")), true); assert.match(ordersPageSource, /fetch\("\/api\/payment\/orders"/); assert.match(ordersPageSource, /response\.status === 401[\s\S]*window\.location\.assign\("\/login"\)/); assert.match(ordersPageSource, /正在加载订单…/); assert.match(ordersPageSource, /订单记录暂时不可用,请稍后刷新。/); assert.match(ordersPageSource, /暂无订单。/); assert.match(ordersPageSource, /onClick=\{\(\) => void fetchOrders\(\)\}>刷新/); assert.match(ordersPageSource, /membership-order-row/); assert.match(ordersPageSource, /orderStatusLabel\(order\.status\)/); assert.match(ordersPageSource, /formatPrice\(order\.moneyCents, order\.currency\)/); assert.match(membershipLib, /paid: "已支付"/); }); test("orders return replaces its history entry so membership back cannot reopen orders", () => { assert.match(ordersPageSource, / { assert.match(homePageSource, /!starterHomeVisible/); assert.match(homePageSource, /account\.credits > 0/); assert.match(homePageSource, /account\.activeSubscription\?\.status === "active"/); assert.match(homePageSource, /setOnboardingPaywallOpen\(true\)/); assert.match(homePageSource, / { // The close button only sits beside the title when the header carries the shared // flex class. A bespoke wrapper leaves it stacked under the heading on mobile. assert.match(onboardingPaywallSource, /

/); assert.match(onboardingPaywallSource, /<\/header>/); assert.doesNotMatch(onboardingPaywallSource, /className="dialog-header"/); assert.match(onboardingPaywallSource, /className="account-modal-overlay"/); assert.match(onboardingPaywallSource, /className="account-modal paywall-modal"/); assert.match(onboardingPaywallSource, /className="dialog-close"/); }); test("every static class the onboarding paywall renders has a rule in globals.css", () => { // Guards the whole defect class: a class name with no matching selector renders // as an unstyled block and silently breaks the layout. const used = new Set( [...onboardingPaywallSource.matchAll(/className="([^"{}]+)"/g)] .flatMap((hit) => hit[1].split(/\s+/)) .filter(Boolean), ); assert.ok(used.size >= 12, `expected the paywall to carry its usual classes, saw ${used.size}`); const undefinedClasses = [...used].filter((name) => ( !new RegExp(`\\.${name}(?![\\w-])`).test(globalStyles) )); assert.deepEqual(undefinedClasses, []); }); test("payment and plan copy points users to the separate orders page", () => { assert.match(pageSource, /本页会自动检查支付状态并刷新余额。/); assert.match(pageSource, /「订单记录」页面/); assert.doesNotMatch(pageSource, /本页会自动刷新余额与订单记录|本页「订单记录」|用于咨询、报告与生时校正等服务/); }); test("membership hero stays content-led without decorative orbit artwork", () => { assert.doesNotMatch(pageSource, /membership-celestial-visual|membership-orbit|membership-celestial-mark/); assert.doesNotMatch(globalStyles, /membership-celestial-visual|membership-orbit|membership-celestial-mark/); assert.match(cssBlock(".membership-hero"), /background:\s*var\(--color-canvas-soft\)/); }); test("redeem modal surface contains title, balance, input, redeem, rules and close", () => { assert.match(pageSource, /id="membership-redeem-title"/); assert.match(pageSource, /

兑换点数<\/h2>/); assert.match(pageSource, /className="redeem-balance"/); assert.match(pageSource, /id="membership-redeem-code"/); assert.match(pageSource, /立即兑换/); assert.match(pageSource, /兑换规则<\/summary>/); assert.match(pageSource, /aria-label="关闭"/); }); test("redeem modal reuses the existing account dialog classes", () => { assert.match(pageSource, /className="account-modal-overlay"/); assert.match(pageSource, /className="account-modal membership-redeem"/); assert.match(pageSource, /className="account-modal-header"/); assert.match(pageSource, /className="dialog-close"/); assert.match(pageSource, /className="redeem-form"/); assert.match(pageSource, /className="form-error" role="alert"/); assert.match(pageSource, /className="form-success" role="status"/); // 原值: className="button-primary" // 新值: