Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
116 lines
5.5 KiB
TypeScript
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));
|
|
});
|