Homepage no longer waits on /api/onboarding for six starter questions. Natal answers show the spoken layer first; the Level 2 skeleton sits in a collapsed 完整分析 block. Wide tables scroll sideways on a phone. Form inputs are 16px so iOS does not zoom on focus.
159 lines
8.9 KiB
TypeScript
159 lines
8.9 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
import { homeSurface as pageSource } from "./home-surface.ts";
|
|
const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
|
|
const appSidebarSource = readFileSync(new URL("../src/components/app-sidebar.tsx", import.meta.url), "utf8");
|
|
|
|
function sourceBetween(source: string, startMarker: string, endMarker: string) {
|
|
const start = source.indexOf(startMarker);
|
|
const end = source.indexOf(endMarker, start);
|
|
assert.notEqual(start, -1);
|
|
assert.notEqual(end, -1);
|
|
return source.slice(start, end);
|
|
}
|
|
|
|
test("completed account initialization switches directly to the home cards", () => {
|
|
assert.doesNotMatch(pageSource, /setOnboardingJustCompleted\(true\)/);
|
|
assert.doesNotMatch(pageSource, /!profileComplete \|\| onboardingJustCompleted/);
|
|
});
|
|
|
|
test("profiles without a usable birth minute still start daily fortune from the homepage card", () => {
|
|
// 原值: 无可用分钟时主题卡换 generalGuidedJyotishTopics,边界句写在「从一个主题开始」小标题。
|
|
// 新值: 主题卡下线;无分钟账号只剩问候语、今日运势卡、生时校正卡。
|
|
// 原因: 2026-09-15 产品决定删除低价值入口,不做替代。
|
|
assert.match(pageSource, /GENERAL_NO_MINUTE_DAILY_FORTUNE_QUESTION/);
|
|
assert.match(pageSource, /startSuggestedConsultation\(\s*dailyStarlanguageQuestion,\s*"timing",\s*"daily_starlanguage"/);
|
|
assert.doesNotMatch(pageSource, /personalChartAvailable \? "daily_starlanguage" : null/);
|
|
assert.doesNotMatch(pageSource, /starter-themes-heading|starter-theme-accordion|starterSuggestions/);
|
|
});
|
|
|
|
test("homepage daily card starts a consultation instead of filling the composer", () => {
|
|
// 原值: 同时锁今日星语卡与主题卡 `guided_topic` 点击。
|
|
// 新值: 只覆盖今日星语卡。
|
|
// 原因: 主题卡入口已下线,`guided_topic` 不再有发出点。
|
|
const starterSend = sourceBetween(
|
|
pageSource,
|
|
"async function startSuggestedConsultation(",
|
|
"function startDailyStarlanguageConsultation",
|
|
);
|
|
|
|
assert.match(pageSource, /onClick=\{startDailyStarlanguageConsultation\}/);
|
|
assert.doesNotMatch(pageSource, /"guided_topic"/);
|
|
assert.doesNotMatch(pageSource, /draftDailyStarlanguageQuestion/);
|
|
assert.doesNotMatch(pageSource, /onClick=\{\(\) => chooseSuggestedQuestion\(item\.text, item\.theme\)\}/);
|
|
assert.match(starterSend, /await startNewChat\(\)/);
|
|
assert.match(starterSend, /await send\(trimmed, theme, entrypoint, null, targetSession\.id/);
|
|
assert.doesNotMatch(starterSend, /setDraft\(/);
|
|
assert.doesNotMatch(starterSend, /composerInput\.current\?\.focus/);
|
|
});
|
|
|
|
|
|
test("keeps session history clickable while another session is answering", () => {
|
|
// Given: the page-owned selection callback and the app sidebar session action.
|
|
const selectSession = pageSource.match(/function selectSession\(sessionId: string\) \{([\s\S]*?)\n \}/);
|
|
|
|
// When: request-time navigation constraints are inspected.
|
|
// Then: pending request state cannot disable read-only session switching.
|
|
assert.ok(selectSession);
|
|
assert.doesNotMatch(selectSession[1], /pendingSessionId|isLoading|cancellationPending|creatingSession/);
|
|
assert.match(appSidebarSource, /onSelectSession\(session\.id\)/);
|
|
assert.doesNotMatch(appSidebarSource, /pendingSession|isLoading|cancellationPending|requestPending/);
|
|
});
|
|
|
|
test("sizes the model popup to its content with responsive bounds", () => {
|
|
// Given: the model selector popup styles.
|
|
const popupStyles = sourceBetween(globalStyles, ".model-selector-popup {", "}\n.model-selector-popup[data-starting-style]");
|
|
|
|
// When: its responsive width is inspected.
|
|
// Then: short labels stay compact while long content remains viewport-safe.
|
|
assert.match(popupStyles, /width:\s*max-content/);
|
|
assert.match(popupStyles, /min-width:\s*180px/);
|
|
assert.match(popupStyles, /max-width:\s*min\(420px,/);
|
|
});
|
|
|
|
test("centers the credit value with its icon", () => {
|
|
// Given: the credit value styles next to the existing flex-centered icon.
|
|
const creditValueStyles = sourceBetween(globalStyles, ".credit-button span {", "}\n.credit-button .credit-icon");
|
|
|
|
// When: the value line box is inspected.
|
|
// Then: it participates in flex centering without extra line-height drift.
|
|
assert.match(creditValueStyles, /display:\s*inline-flex/);
|
|
assert.match(creditValueStyles, /align-items:\s*center/);
|
|
assert.match(creditValueStyles, /line-height:\s*1/);
|
|
});
|
|
|
|
test("routes account actions through a menu and focused dialogs", () => {
|
|
// Given: the account surface state and entry-point handlers.
|
|
// When: the page source is inspected for independent menu and dialog routes.
|
|
// Then: profile and logout stay focused dialogs; billing opens in the settings pane.
|
|
assert.match(pageSource, /const \[accountMenuOpen, setAccountMenuOpen\] = useState\(false\)/);
|
|
assert.match(pageSource, /const \[activeAccountDialog, setActiveAccountDialog\] = useState<AccountDialog \| null>\(null\)/);
|
|
assert.match(pageSource, /openAccountDialog\("profile"/);
|
|
assert.match(pageSource, /openAccountDialog\("logout"/);
|
|
assert.doesNotMatch(pageSource, /type AccountDialog = "profile" \| "redeem" \| "logout"/);
|
|
assert.doesNotMatch(pageSource, /openAccountDialog\("redeem"/);
|
|
assert.match(appSidebarSource, /<Menu\.Popup className="account-menu-popup"/);
|
|
assert.doesNotMatch(appSidebarSource, /<Menu\.Popup className="account-menu"/);
|
|
assert.match(appSidebarSource, /<Menu\.Root open=\{accountMenuOpen\} onOpenChange=\{onAccountMenuOpenChange\} modal=\{false\}>/);
|
|
});
|
|
|
|
test("membership entries navigate without auto-opening redeem", () => {
|
|
// Given: the account menu, credit control and insufficient-balance paths.
|
|
// 原值: membershipHref 跳 /membership,不打开 redeem 弹窗
|
|
// 新值: 三处都打开 billing 分区,仍不打开独立兑换弹窗
|
|
// 原因: 兑换码是 billing 页签,不是独立 dialog
|
|
assert.match(pageSource, /openAccountDialog\("billing", \{ source: "account-menu" \}\)/);
|
|
assert.match(pageSource, /openAccountDialog\("billing", \{ returnTarget: event\.currentTarget, source: "credits" \}\)/);
|
|
assert.match(pageSource, /openAccountDialog\("billing", \{ source: "insufficient-credits" \}\)/);
|
|
assert.doesNotMatch(pageSource, /activeAccountDialog === "redeem"/);
|
|
assert.doesNotMatch(pageSource, /paymentEnabled|paymentPackages|paymentOrder|payingPackageId/);
|
|
assert.doesNotMatch(pageSource, /redeemCode|redeeming|redeemError|redeemMessage/);
|
|
});
|
|
|
|
test("removes the monolithic account sheet", () => {
|
|
// Given: the former sheet implementation names.
|
|
// When: the page and global styles are inspected.
|
|
// Then: no right-side account sheet remains.
|
|
assert.doesNotMatch(pageSource, /profile-overlay|profile-dialog|openAccount\(/);
|
|
assert.doesNotMatch(globalStyles, /\.profile-overlay|\.profile-dialog/);
|
|
});
|
|
|
|
test("removes the user-facing admin button from the chat page", () => {
|
|
// Given: the administrator-only route and the new account menu.
|
|
// Then: code management is no longer surfaced to users on the chat page.
|
|
assert.doesNotMatch(appSidebarSource, /adminUrl|account\.isAdmin|后台管理/);
|
|
assert.doesNotMatch(pageSource, /account\.isAdmin && account\.adminUrl/);
|
|
assert.doesNotMatch(pageSource, /className="admin-button"/);
|
|
assert.doesNotMatch(pageSource, /ShieldCheck/);
|
|
});
|
|
|
|
test("keeps the empty starter home at the top instead of auto-scrolling", () => {
|
|
// Former lock: a page-level effect starting `if (starterHomeVisible) return;` that called
|
|
// `container.scrollTo`. The follow now lives in useConversationScrollAnchor (BUG-478); the page
|
|
// switches it off for the starter home through the hook's `active` argument instead.
|
|
assert.match(pageSource, /useConversationScrollAnchor\(\n\s*conversation,\n\s*!rectificationSurfaceOpen && !starterHomeVisible,/);
|
|
const anchorSource = readFileSync(new URL("../src/hooks/use-conversation-scroll-anchor.ts", import.meta.url), "utf8");
|
|
const followEffect = anchorSource.slice(anchorSource.indexOf("// Follow: while anchored"));
|
|
assert.match(followEffect, /if \(!active \|\| !element\) return;/);
|
|
assert.ok(followEffect.indexOf("if (!active || !element) return;") < followEffect.indexOf("element.scrollTop = element.scrollHeight"));
|
|
assert.doesNotMatch(pageSource, /container\.scrollTo\(/);
|
|
assert.match(pageSource, /ref=\{conversation\} className=\{`conversation/);
|
|
assert.doesNotMatch(pageSource, /conversationEnd|scrollIntoView/);
|
|
});
|
|
|
|
test("does not submit the composer while an IME composition is active", () => {
|
|
const keyHandler = sourceBetween(
|
|
pageSource,
|
|
"function handleComposerKeyDown",
|
|
"\n\n if (!hydrated",
|
|
);
|
|
|
|
assert.match(keyHandler, /if \(event\.nativeEvent\.isComposing\) return/);
|
|
assert.match(keyHandler, /event\.currentTarget\.form\?\.requestSubmit\(\)/);
|
|
});
|
|
|
|
test("announces conversation errors to assistive technology", () => {
|
|
assert.match(pageSource, /<p className="error-message" role="alert">\{activeError\}<\/p>/);
|
|
});
|