Files
Jyotisha/frontend/tests/membership-page.test.ts
T
jesse-ux 6c748d86b0
Independent Staging Quality Gate / validate (push) Failing after 8m53s
Independent Staging Quality Gate / publish (push) Skipped
fix(ui): 44px coarse hits and align CSS breakpoints
Touch-screen message actions get a 44x44 hit once gap and bottom margin open to 20px; fine-pointer geometry stays 27x34. Tablet CSS cuts at 1023 to match sidebarViewportForWidth. Report centre/reader join the 767 mobile cut; TOC keeps 860. Occupies BUG-695/696/697.
2026-09-15 12:46:59 +08:00

146 lines
8.3 KiB
TypeScript
Raw 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/);
// 原值: 会员栅格 min-width 640 / max-width 639,与校正的 640/641 差 1px。
// 新值: 统一为 640/641。
// 原因: BUG-697 噪音收敛,白名单只留一组内容级切点。
assert.match(globalStyles, /@media \(min-width:\s*641px\)[\s\S]*\.membership-plan-grid[\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*640px\)[\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/);
});