Independent Staging Quality Gate / validate (push) Successful in 12m25s
Independent Staging Quality Gate / publish (push) Successful in 2m6s
Independent Staging Quality Gate / validate (pull_request) Successful in 14m58s
Independent Staging Quality Gate / publish (pull_request) Skipped
148 lines
6.6 KiB
TypeScript
148 lines
6.6 KiB
TypeScript
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<string, unknown>;
|
||
varga_full: Record<string, unknown>;
|
||
chara: Record<string, unknown>;
|
||
western: Record<string, unknown>;
|
||
};
|
||
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<string, unknown> | null = null): Promise<ChartViewOk> {
|
||
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, />星盘</);
|
||
assert.match(markup, />基础信息</);
|
||
assert.match(markup, />大运</);
|
||
assert.match(markup, />西洋盘</);
|
||
assert.match(markup, />七政四余</);
|
||
assert.doesNotMatch(markup, /正在加载|骨架|spinner/i);
|
||
if (tab.id === "vedic") assert.match(markup, /D1/);
|
||
if (tab.id === "basics") assert.match(markup, /词条式释义/);
|
||
if (tab.id === "dasha") {
|
||
assert.match(markup, /Vimshottari/);
|
||
assert.match(markup, /Chara Dasha(kn_rao 变体)/);
|
||
assert.match(markup, /只有两条同时指向同一段时间才算证据/);
|
||
}
|
||
if (tab.id === "western") assert.match(markup, /西洋盘这一栏要等回归黄道端点上线/);
|
||
if (tab.id === "qizheng") {
|
||
assert.match(markup, /七政四余这一栏要等宿度端点上线/);
|
||
assert.match(markup, /角宿/);
|
||
assert.match(markup, /计都派别/);
|
||
assert.match(markup, /庙旺/);
|
||
}
|
||
}
|
||
});
|
||
|
||
test("unavailable western and qizheng tabs stay explanatory", async () => {
|
||
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("<SidebarHeader"), sidebar.indexOf("</SidebarHeader>"));
|
||
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);
|
||
});
|