Files
Jyotisha/frontend/tests/membership-page.test.ts
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

143 lines
8.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, /<OnboardingRedeemPaywall/);
assert.match(onboardingPaywallSource, /入门问题已经准备好/);
assert.match(onboardingPaywallSource, /fetch\("\/api\/redeem"/);
assert.match(onboardingPaywallSource, /兑换并继续/);
// 原值: membershipHref("onboarding-paywall") 的 Link
// 新值: onOpenBilling 按钮,关掉付费墙后打开设置弹窗 billing 格
// 原因: 七处入口改回调,对话状态不丢
assert.match(onboardingPaywallSource, /onOpenBilling/);
assert.match(onboardingPaywallSource, /查看套餐与会员/);
assert.doesNotMatch(onboardingPaywallSource, /membershipHref\("onboarding-paywall"\)/);
assert.match(homePageSource, /openAccountDialog\("billing", \{ source: "onboarding-paywall" \}\)/);
});
test("the onboarding paywall reuses the shared account dialog header", () => {
assert.match(onboardingPaywallSource, /<header className="account-modal-header">/);
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/);
});