import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import React from "react"; import { renderToStaticMarkup } from "react-dom/server"; import test from "node:test"; import { ChartPageView } from "../src/components/chart-page/chart-page-view.tsx"; import { assembleChartView } from "../src/lib/chart-view-load.ts"; import { CHART_VIEW_TABS, type ChartViewOk } from "../src/lib/chart-view-contract.ts"; import { cssDeclarations } from "./css-contract-test-support.ts"; Object.assign(globalThis, { React }); const golden = JSON.parse( readFileSync(new URL("./fixtures/chart-view-golden.json", import.meta.url), "utf8"), ) as { chart: Record; varga_full: Record; chara: Record; western: Record; }; const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8"); const sidebar = readFileSync(new URL("../src/components/app-sidebar.tsx", import.meta.url), "utf8"); const vedicTab = readFileSync(new URL("../src/components/chart-page/chart-vedic-tab.tsx", import.meta.url), "utf8"); const vedicSvg = readFileSync(new URL("../src/components/personal-report/vedic-chart-svg.tsx", import.meta.url), "utf8"); const profile = { name: "示例", date: "1990-06-15", time: "12:00", placeLabel: "北京", latitude: 39.9042, longitude: 116.4074, timezoneOffset: 8, timezoneId: "Asia/Shanghai", ayanamsa: "raman", birthTimeStatus: "confirmed", }; async function okView(western: Record | null = null): Promise { const result = await assembleChartView({ userId: "user-1", profile, asOf: "2026-09-15", postEngine: async (path) => { if (path === "/api/chart") return golden.chart; if (path === "/api/varga_full") return golden.varga_full; if (path === "/api/dasha/chara") return golden.chara; if (path === "/api/western") return western; return null; }, }); assert.equal(result.body.status, "ok"); return result.body as ChartViewOk; } test("the five tabs render and can each be selected", async () => { const view = await okView(); for (const tab of CHART_VIEW_TABS) { const markup = renderToStaticMarkup(React.createElement(ChartPageView, { view, initialTab: tab.id })); assert.match(markup, /直接计算 · 打开即有 · 不消耗点数/); assert.match(markup, />星盘基础信息大运西洋盘七政四余 { const view = await okView(); const western = renderToStaticMarkup(React.createElement(ChartPageView, { view, initialTab: "western" })); const qizheng = renderToStaticMarkup(React.createElement(ChartPageView, { view, initialTab: "qizheng" })); assert.doesNotMatch(western, /报错|出错了|500/); assert.doesNotMatch(qizheng, /报错|出错了|500/); assert.match(western, /不能换算/); assert.match(qizheng, /不能换算/); }); test("a tropical packet draws the western wheel instead of the unavailable copy", async () => { const view = await okView(golden.western); const markup = renderToStaticMarkup(React.createElement(ChartPageView, { view, initialTab: "western" })); assert.match(markup, /回归黄道本命盘/); assert.doesNotMatch(markup, /西洋盘这一栏要等回归黄道端点上线/); }); test("the North Indian svg is imported in place and its export signature is unchanged", () => { assert.match(vedicTab, /from "@\/components\/personal-report\/vedic-chart-svg"/); assert.match(vedicSvg, /export function VedicChartSvg/); assert.match(vedicSvg, /export const NorthIndianChartSvg = VedicChartSvg/); assert.doesNotMatch(vedicTab, /chart-page\/vedic-chart-svg/); }); test("sidebar adds 星盘 and 星历 after 新建对话 without renaming 我的报告", () => { const header = sidebar.slice(sidebar.indexOf("")); const newChat = header.indexOf("新建对话"); const chart = header.indexOf(">星盘<"); const ephemeris = header.indexOf(">星历<"); const reports = header.indexOf("我的报告"); assert.ok(newChat >= 0 && chart > newChat && ephemeris > chart && reports > ephemeris); assert.match(sidebar, /leaveChat\("\/chart"\)/); assert.match(sidebar, /leaveChat\("\/ephemeris"\)/); assert.match(sidebar, /tooltip="星盘"/); assert.match(sidebar, /tooltip="星历"/); }); test("mobile width keeps two centre lines and moves the full parameter card below the chart", () => { assert.match( globalStyles, /@media \(max-width: 767px\) \{\r?\n \.chart-page-hero h1[\s\S]*\.chart-page-center-compact \{ display: block; \}/, ); assert.match( globalStyles, /@media \(max-width: 767px\) \{\r?\n \.chart-page-hero h1[\s\S]*\.chart-page-center-extra \{ display: none; \}/, ); assert.match( globalStyles, /@media \(max-width: 767px\) \{\r?\n \.chart-page-hero h1[\s\S]*\.chart-page-params-below \{ display: block; \}/, ); assert.match(cssDeclarations(".chart-page-tab", globalStyles), /min-height:\s*44px/); assert.match(cssDeclarations(".chart-page-chip", globalStyles), /min-height:\s*44px/); assert.match(cssDeclarations(".chart-page-back", globalStyles), /min-height:\s*44px/); assert.match(globalStyles, /\.chart-page-vedic-stage \.personal-report-chart-svg text \{ font-size: 16px; \}/); }); test("an incomplete profile page has no loading animation", () => { const markup = renderToStaticMarkup(React.createElement(ChartPageView, { view: { status: "birth_profile_incomplete", billed: false, message: "还没有可用来排盘的出生资料。" }, })); assert.match(markup, /还没有可用来排盘的出生资料/); assert.doesNotMatch(markup, /正在加载|InlineSpinner|skeleton/i); });