Files
Jyotisha/frontend/tests/chart-bhava.test.tsx
T

116 lines
5.5 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 { 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<string, unknown> = bhavaGolden.bhava_chalit) {
const calls: Array<{ path: string; body: Record<string, unknown> }> = [];
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<string, unknown> = {}) {
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, /<caption>Bhava · Sripati<\/caption>/);
assert.match(markup, /<caption>宫头与宫界<\/caption>/);
assert.equal((markup.match(/class="is-shifted"/g) ?? []).length, shifted.length);
assert.equal((markup.match(/<td class="chart-page-planet-mid">是<\/td>/g) ?? []).length, shifted.length);
});
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));
});