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 { ChartVedicTab } from "../src/components/chart-page/chart-vedic-tab.tsx"; import { assembleChartView } from "../src/lib/chart-view-load.ts"; import { chartViewOkSchema, type ChartViewOk } from "../src/lib/chart-view-contract.ts"; import { CHART_VIEW_COPY } from "../src/lib/chart-view-labels.ts"; Object.assign(globalThis, { React }); const golden = JSON.parse(readFileSync(new URL("./fixtures/chart-view-golden.json", import.meta.url), "utf8")); // Real `/api/bhava_chalit` response to the body chart-view-load sends (see its sourceCommand). const bhavaGolden = JSON.parse(readFileSync(new URL("./fixtures/chart-view-bhava-golden.json", 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 assemble(bhavaPayload: Record = bhavaGolden.bhava_chalit) { const calls: Array<{ path: string; body: Record }> = []; const result = await assembleChartView({ userId: "user-1", profile, asOf: "2026-09-15", layers: ["bhava"], postEngine: async (path, body) => { calls.push({ path, body }); if (path === "/api/chart") return { status: "ok", payload: golden.chart }; if (path === "/api/bhava_chalit") return { status: "ok", payload: bhavaPayload }; return { status: "http_error", path, elapsedMs: 1, httpStatus: 500 }; }, }); assert.equal(result.body.status, "ok"); return { view: chartViewOkSchema.parse(result.body), calls }; } function render(view: ChartViewOk, props: Record = {}) { return renderToStaticMarkup(React.createElement(ChartVedicTab, { view, vargaId: "BHAVA", onSelectVarga: () => {}, ...props })); } test("the bhava layer asks for Sripati with the birth moment the MC needs (BUG-1077)", async () => { const { calls } = await assemble(); const call = calls.find((item) => item.path === "/api/bhava_chalit"); assert.ok(call); assert.equal(call.body.house_system, "sripati", "the western wheel's house_system P must not leak in"); assert.equal(call.body.mode, "compare"); for (const key of ["year", "month", "day", "hour", "minute", "lat", "lon", "tz", "ayanamsa"]) assert.ok(call.body[key] !== undefined, key); assert.equal(call.body.mc_lon, undefined, "the page never sends a guessed MC"); assert.ok(call.body.planets && call.body.ascendant); }); test("Sripati rows, cusps and the drawn houses follow the engine's Bhava houses", async () => { const { view } = await assemble(); assert.equal(view.bhava.status, "ok"); if (view.bhava.status !== "ok") throw new Error("expected bhava ok"); const engine = bhavaGolden.bhava_chalit.result; assert.equal(view.bhava.rows.length, 9); for (const row of view.bhava.rows) { assert.equal(row.bhavaHouse, engine.bhava_chart[row.name]); assert.equal(row.rashiHouse, engine.rashi_chart[row.name].house); } const shifted = view.bhava.rows.filter((row) => row.shifted).map((row) => row.name).sort(); assert.deepEqual(shifted, engine.shifts.map((item: { planet: string }) => item.planet).sort()); assert.ok(shifted.length > 0, "this chart has at least one body that changes house"); assert.equal(view.bhava.cusps.length, 12); // Not the old equal-house chart: the boundary spans differ. const starts = view.bhava.cusps.map((cusp) => cusp.sandhiDegreeInSign); assert.ok(new Set(starts.map((value) => value.toFixed(2))).size > 1); for (const row of view.bhava.rows) { const house = view.bhava.chart.houses[row.bhavaHouse - 1]!; assert.ok(house.occupantGlyphs?.some((glyph) => glyph.key === row.name.toLowerCase()), `${row.name} drawn in Bhava house ${row.bhavaHouse}`); } const markup = render(view); assert.ok(markup.includes(CHART_VIEW_COPY.bhavaNote)); assert.match(markup, /
/); assert.match(markup, /
岁差<\/dt>/); assert.match(markup, /
交点<\/dt>/); assert.doesNotMatch(markup, /Swiss Ephemeris|>引擎Bhava · Sripati<\/caption>/); assert.match(markup, /宫头与宫界<\/caption>/); assert.equal((markup.match(/class="is-shifted"/g) ?? []).length, shifted.length); assert.equal((markup.match(/是<\/td>/g) ?? []).length, shifted.length); const body = markup.match(/([\s\S]*?)<\/tbody>/)![1]!; const rows = [...body.matchAll(/]*>([\s\S]*?)<\/tr>/g)]; assert.equal(rows.length, 9); for (const [, cells] of rows) { const tds = [...cells!.matchAll(/]*)>([\s\S]*?)<\/td>/g)]; for (const index of [1, 2]) { assert.equal(tds[index]![1], "", "both house columns inherit their header's left alignment"); assert.match(tds[index]![2]!, /^\d+宫$/); } assert.match(tds[3]![1]!, /chart-page-planet-mid/, "the shifted column remains centered"); } }); test("an equal-house placeholder MC or a house-system fallback is never drawn", async () => { const response = bhavaGolden.bhava_chalit; const asc = response.result.ascendant_lon as number; const placeholder = structuredClone(response); placeholder.result.mc_lon = (asc + 270) % 360; const fallback = structuredClone(response); fallback.selected_house_system = "sripati"; fallback.fallback_reason = "placidus requires swisseph plus valid birth date"; for (const payload of [placeholder, fallback]) { const { view } = await assemble(payload); assert.equal(view.bhava.status, "unavailable"); const markup = render(view, { failure: "unavailable" }); assert.doesNotMatch(markup, /宫头与宫界|data-skeleton/); } }); test("the bhava tab waits with the chart skeleton and shows the layer failure copy", async () => { const { view } = await assemble(); const bare = { ...view, bhava: { status: "unavailable" as const, reason: "not_requested", copy: CHART_VIEW_COPY.unavailable } }; assert.match(render(bare, { pending: true }), /data-skeleton="true"/); assert.ok(render(bare, { pending: false, failure: "engine_busy" }).includes(CHART_VIEW_COPY.busy)); });