import assert from "node:assert/strict"; import { 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 membershipLib = readProjectFile("src/lib/membership.ts"); const cssBlock = (selector: string) => cssDeclarations(selector, globalStyles); 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("finished starter questions open the redeem paywall only for accounts without access", () => { 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, / { 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", () => { 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("membership hero stays content-led without decorative orbit artwork", () => { assert.doesNotMatch(globalStyles, /membership-celestial-visual|membership-orbit|membership-celestial-mark/); assert.doesNotMatch(globalStyles, /\.membership-hero|\.membership-page|\.membership-shell/); }); test("desktop uses three columns, tablet two and mobile one with monthly first", () => { assert.match(globalStyles, /\.membership-plan-grid,[\s\S]*\.membership-credit-grid\s*\{[^}]*grid-template-columns:\s*1fr/); assert.match(globalStyles, /@media \(min-width:\s*640px\)[\s\S]*grid-template-columns:\s*repeat\(2, minmax\(0, 1fr\)\)/); assert.match(globalStyles, /@media \(min-width:\s*1024px\)[\s\S]*grid-template-columns:\s*repeat\(3, minmax\(0, 1fr\)\)/); assert.match(globalStyles, /@media \(max-width:\s*639px\)[\s\S]*\.membership-plan-card--recommended\s*\{[^}]*order:\s*-1/); assert.doesNotMatch(cssBlock(".membership-plan-card--recommended"), /grid-column/); }); test("segment tabs use a quiet underline treatment", () => { assert.match(cssBlock(".membership-tabs"), /border-bottom:\s*1px solid var\(--color-border\)/); assert.doesNotMatch(cssBlock(".membership-tabs"), /border-radius|background|box-shadow/); assert.match(cssBlock(".membership-tab[data-active]"), /border-bottom-color:\s*var\(--color-action\)/); assert.match(cssBlock(".membership-tab[data-active]"), /background:\s*transparent/); assert.match(cssBlock(".membership-tab[data-active]"), /box-shadow:\s*none/); assert.match(cssBlock(".membership-plan-card--highlighted"), /outline:\s*2px solid color-mix\(in srgb, var\(--color-action\)/); assert.match(cssBlock(".membership-payment-error"), /background:\s*transparent/); }); test("membership shell is capped within the 1120-1200 range", () => { // 原值: .membership-shell width min(1160px, 100%) // 新值: 页面壳删除;账单装进 .settings-modal 固定尺寸 // 原因: 删除 /membership 整页 assert.doesNotMatch(globalStyles, /\.membership-shell/); assert.match(cssBlock(".settings-modal"), /width:\s*min\(100vw - 32px, 880px\)/); assert.match(cssBlock(".settings-modal"), /height:\s*min\(84dvh, 640px\)/); }); test("membership page reuses the warm parchment / terracotta tokens", () => { assert.match(cssBlock(".membership-plan-card--current"), /border-color:\s*var\(--color-action\)/); assert.match(cssBlock(".membership-status"), /color:\s*var\(--color-action\)/); assert.doesNotMatch(globalStyles, /membership-badge--recommended/); assert.doesNotMatch(globalStyles, /\.membership-title|\.membership-hero/); }); test("membership page scrolls within a fixed viewport and cards stay flat", () => { // 原值: .membership-page height 100dvh overflow-y auto // 新值: 内容区 .settings-dialog-content 独立滚动;卡片仍无阴影 // 原因: 固定尺寸设置弹窗 assert.doesNotMatch(globalStyles, /\.membership-page/); assert.match(cssBlock(".settings-dialog-content"), /overflow-y:\s*auto/); assert.match(cssBlock(".membership-plan-card, .membership-credit-card"), /box-shadow:\s*none/); assert.match(cssBlock(".membership-plan-card--recommended"), /background:\s*var\(--color-canvas\)/); assert.doesNotMatch(globalStyles, /membership-plan-card--recommended::before/); }); test("membership lib keeps helpers and the balance sync contract", () => { assert.match(membershipLib, /export const BALANCE_SYNC_KEY = "jyotisha:balance:updated"/); assert.match(membershipLib, /export const BALANCE_CHANGED_EVENT = "jyotisha:balance:changed"/); assert.match(membershipLib, /export function notifyBalanceChanged\(credits: number\)/); assert.match(membershipLib, /localStorage\.setItem\(BALANCE_SYNC_KEY/); assert.match(membershipLib, /new CustomEvent\(BALANCE_CHANGED_EVENT/); assert.match(membershipLib, /export function formatPrice\(priceCents: number, currency = "CNY"\)/); assert.match(membershipLib, /export function orderStatusLabel/); assert.match(membershipLib, /export function planDisplayLabel/); // 原值: options: { plan?: MembershipPlanAlias } = {},href /membership // 新值: 可带 tab;生成 /?settings=billing&source=… // 原因: 深链接与重定向,不再指向已删页面 assert.match(membershipLib, /export function membershipHref\(/); assert.match(membershipLib, /settings: "billing"/); assert.doesNotMatch(membershipLib, /params\.set\("redeem"/); assert.doesNotMatch(membershipLib, /"\/membership/); });