Hide engine branding, gate supported divisional columns, align house cells, and add accessible symbol help. Reduce desktop chart width and stabilize the waiting slot. Validated with 78 browser checks and 87 focused frontend tests. Full suite retains 29 identical baseline loader failures; no new failures. Co-Authored-By: Claude Code <noreply@anthropic.com>
131 lines
6.3 KiB
TypeScript
131 lines
6.3 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, /<dl class="chart-page-params-table">/);
|
|
assert.match(markup, /<dt>岁差<\/dt>/);
|
|
assert.match(markup, /<dt>交点<\/dt>/);
|
|
assert.doesNotMatch(markup, /Swiss Ephemeris|>引擎</);
|
|
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);
|
|
const body = markup.match(/<tbody>([\s\S]*?)<\/tbody>/)![1]!;
|
|
const rows = [...body.matchAll(/<tr\b[^>]*>([\s\S]*?)<\/tr>/g)];
|
|
assert.equal(rows.length, 9);
|
|
for (const [, cells] of rows) {
|
|
const tds = [...cells!.matchAll(/<td([^>]*)>([\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));
|
|
});
|