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>
250 lines
12 KiB
TypeScript
250 lines
12 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, VargaTable } from "../src/components/chart-page/chart-vedic-tab.tsx";
|
||
import { assembleChartView } from "../src/lib/chart-view-load.ts";
|
||
import { chartViewOkSchema, chartViewVargaSchema, type ChartViewOk, type ChartViewVarga } from "../src/lib/chart-view-contract.ts";
|
||
import { buildChartView } from "../src/lib/chart-view-mapper.ts";
|
||
|
||
Object.assign(globalThis, { React });
|
||
|
||
// Real engine output (see the fixture's sourceCommand); nothing here is hand-shaped.
|
||
const golden = JSON.parse(
|
||
readFileSync(new URL("./fixtures/chart-view-golden.json", import.meta.url), "utf8"),
|
||
) as {
|
||
chart: Record<string, unknown>;
|
||
varga_full: { result: Record<string, { ascendant: Record<string, unknown>; planets: Record<string, Record<string, unknown>> }> };
|
||
chara: Record<string, unknown>;
|
||
};
|
||
|
||
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 vargaView(): Promise<ChartViewOk> {
|
||
const result = await assembleChartView({
|
||
userId: "user-1",
|
||
profile,
|
||
asOf: "2026-09-15",
|
||
layers: ["varga"],
|
||
postEngine: async (path) => {
|
||
if (path === "/api/chart") return { status: "ok", payload: golden.chart };
|
||
if (path === "/api/varga_full") return { status: "ok", payload: golden.varga_full as unknown as Record<string, unknown> };
|
||
return { status: "http_error", path, elapsedMs: 1, httpStatus: 500 };
|
||
},
|
||
});
|
||
assert.equal(result.body.status, "ok");
|
||
return result.body as ChartViewOk;
|
||
}
|
||
|
||
function render(view: ChartViewOk, vargaId: string): string {
|
||
return renderToStaticMarkup(React.createElement(ChartVedicTab, { view, vargaId, onSelectVarga: () => {} }));
|
||
}
|
||
|
||
/** The table row whose header cell names this body. */
|
||
function row(markup: string, label: string): string {
|
||
const tbody = markup.match(/<tbody>([\s\S]*?)<\/tbody>/)?.[1] ?? "";
|
||
const hit = [...tbody.matchAll(/<tr>([\s\S]*?)<\/tr>/g)].map((match) => match[1]!).find((cells) => {
|
||
const header = cells.match(/<th scope="row">([\s\S]*?)<\/th>/)?.[1] ?? "";
|
||
return header.includes(label);
|
||
});
|
||
assert.ok(hit, `no row for ${label}`);
|
||
return hit.replace(/<!-- -->/g, "");
|
||
}
|
||
|
||
function degreeMinutes(degree: number): string {
|
||
const whole = Math.floor(degree);
|
||
const minutes = Math.floor((degree - whole) * 60);
|
||
return `${whole}°${String(minutes).padStart(2, "0")}′`;
|
||
}
|
||
|
||
// BUG-1076: the D9 caption changed but every row stayed D1.
|
||
test("a divisional chart's table carries that chart's signs, degrees and houses", async () => {
|
||
const view = await vargaView();
|
||
const d9 = golden.varga_full.result.D9_Navamsa!;
|
||
const d1Markup = render(view, "D1");
|
||
const d9Markup = render(view, "D9");
|
||
|
||
const sunD9 = row(d9Markup, "太阳");
|
||
const moonD9 = row(d9Markup, "月亮");
|
||
assert.match(sunD9, /天秤/, "D9 Sun is in Libra");
|
||
assert.ok(sunD9.includes(degreeMinutes(Number(d9.planets.Sun!.degree))), "D9 Sun degree is the in-varga degree");
|
||
assert.match(moonD9, /双鱼/, "D9 Moon is in Pisces");
|
||
assert.match(moonD9, />4宫</, "D9 Moon house counts from the D9 ascendant");
|
||
assert.notEqual(sunD9, row(d1Markup, "太阳"));
|
||
assert.notEqual(moonD9, row(d1Markup, "月亮"));
|
||
|
||
const ascD9 = row(d9Markup, "上升");
|
||
assert.match(ascD9, /射手/, "D9 ascendant is Sagittarius");
|
||
});
|
||
|
||
test("the D1 table is byte-for-byte the table the page drew before BUG-1076", async () => {
|
||
const view = await vargaView();
|
||
const markup = render(view, "D1");
|
||
const table = markup.match(/<div class="chart-page-planet-table-wrap">[\s\S]*?<\/table><\/div>/)?.[0];
|
||
// Rendered from the baseline commit 5dc977ef with the same golden.
|
||
const baseline = readFileSync(new URL("./fixtures/chart-d1-table-baseline.html", import.meta.url), "utf8");
|
||
assert.equal(table, baseline);
|
||
});
|
||
|
||
test("divisional rows carry engine dignity and vargottama, and drop nakshatra columns", async () => {
|
||
const view = await vargaView();
|
||
const d9 = view.vedic.vargas.find((item) => item.id === "D9");
|
||
assert.ok(d9?.rows);
|
||
const saturn = d9.rows.find((item) => item.name === "Saturn");
|
||
// D1 Saturn is in Capricorn and D9 Saturn is in Capricorn: own sign, vargottama.
|
||
assert.equal(saturn?.sign, "Capricorn");
|
||
assert.equal(saturn?.dignity, "own");
|
||
assert.equal(saturn?.vargottama, true);
|
||
const sun = d9.rows.find((item) => item.name === "Sun");
|
||
assert.equal(sun?.sign, "Libra");
|
||
assert.equal(sun?.dignity, "debilitated");
|
||
assert.equal(sun?.vargottama, false);
|
||
const markup = render(view, "D9");
|
||
const header = markup.match(/<thead>([\s\S]*?)<\/thead>/)?.[1] ?? "";
|
||
for (const column of ["行星", "分盘星座", "宫位", "本命星座", "庙旺", "同位"]) assert.match(header, new RegExp(`>${column}<`));
|
||
assert.doesNotMatch(header, /星宿|宿主|pada/);
|
||
assert.match(row(markup, "土星"), />自宫<[\s\S]*>同位</);
|
||
assert.match(row(markup, "太阳"), />落陷</);
|
||
assert.match(row(markup, "上升"), />不适用</, "the ascendant has no sign dignity");
|
||
// Every divisional chart the engine returned has its own rows, D1 has none.
|
||
for (const varga of view.vedic.vargas) {
|
||
if (varga.id === "D1") assert.equal(varga.rows, undefined);
|
||
else assert.equal(varga.rows?.length, 9, varga.id);
|
||
}
|
||
});
|
||
|
||
function tableMarkup(varga: ChartViewVarga): string {
|
||
return renderToStaticMarkup(React.createElement(VargaTable, { varga }));
|
||
}
|
||
|
||
function assertColumns(markup: string, dignity: boolean, vargottama: boolean) {
|
||
const header = markup.match(/<thead>([\s\S]*?)<\/thead>/)?.[1] ?? "";
|
||
assert.equal(header.includes(">庙旺<"), dignity);
|
||
assert.equal(header.includes(">同位<"), vargottama);
|
||
const count = 4 + Number(dignity) + Number(vargottama);
|
||
assert.equal((header.match(/<th /g) ?? []).length, count);
|
||
const body = markup.match(/<tbody>([\s\S]*?)<\/tbody>/)?.[1] ?? "";
|
||
const rows = [...body.matchAll(/<tr>([\s\S]*?)<\/tr>/g)];
|
||
assert.equal(rows.length, 10, "the ascendant and all nine planets remain");
|
||
for (const [, cells] of rows) assert.equal((cells!.match(/<(?:th|td)\b/g) ?? []).length, count);
|
||
}
|
||
|
||
test("server capabilities retain dignity for every Dn and vargottama only for D9", async () => {
|
||
const view = await vargaView();
|
||
for (const varga of view.vedic.vargas) {
|
||
if (varga.id === "D1") {
|
||
assert.equal(varga.supportsDignity, undefined);
|
||
assert.equal(varga.supportsVargottama, undefined);
|
||
continue;
|
||
}
|
||
assert.equal(varga.supportsDignity, true, varga.id);
|
||
assert.equal(varga.supportsVargottama, varga.id === "D9", varga.id);
|
||
assertColumns(tableMarkup(varga), true, varga.id === "D9");
|
||
}
|
||
});
|
||
|
||
test("capability columns are independent and absent declarations hide whole columns", async () => {
|
||
const view = await vargaView();
|
||
const source = view.vedic.vargas.find((varga) => varga.id === "D9")!;
|
||
for (const dignity of [undefined, false, true]) {
|
||
for (const vargottama of [undefined, false, true]) {
|
||
const copy = structuredClone(source);
|
||
delete copy.supportsDignity;
|
||
delete copy.supportsVargottama;
|
||
if (dignity !== undefined) copy.supportsDignity = dignity;
|
||
if (vargottama !== undefined) copy.supportsVargottama = vargottama;
|
||
assertColumns(tableMarkup(chartViewVargaSchema.parse(copy)), dignity === true, vargottama === true);
|
||
}
|
||
}
|
||
});
|
||
|
||
test("valid false and null results never turn supported columns off", async () => {
|
||
const view = await vargaView();
|
||
const copy = structuredClone(view.vedic.vargas.find((varga) => varga.id === "D9")!);
|
||
for (const item of copy.rows!) {
|
||
item.dignity = null;
|
||
item.vargottama = false;
|
||
}
|
||
copy.ascendant!.vargottama = false;
|
||
const markup = tableMarkup(chartViewVargaSchema.parse(copy));
|
||
assertColumns(markup, true, true);
|
||
assert.doesNotMatch(markup.match(/<tbody>([\s\S]*?)<\/tbody>/)?.[1] ?? "", />同位<|>入旺<|>自宫<|>落陷</);
|
||
assert.match(row(markup, "上升"), />不适用</);
|
||
});
|
||
|
||
test("capability schema rejects wrong types and direct cache rendering requires strict true", async () => {
|
||
const view = await vargaView();
|
||
const source = view.vedic.vargas.find((varga) => varga.id === "D9")!;
|
||
for (const field of ["supportsDignity", "supportsVargottama"] as const) {
|
||
for (const value of [null, "true", "false", 1, 0, [], {}]) {
|
||
const copy = structuredClone(source);
|
||
Object.assign(copy, { [field]: value });
|
||
const parsed = chartViewVargaSchema.safeParse(copy);
|
||
assert.equal(parsed.success, false, `${field} must not coerce ${JSON.stringify(value)}`);
|
||
if (!parsed.success) assert.ok(parsed.error.issues.some((issue) => issue.path[0] === field && issue.code === "invalid_type"));
|
||
assertColumns(tableMarkup(copy), field !== "supportsDignity", field !== "supportsVargottama");
|
||
}
|
||
}
|
||
});
|
||
|
||
test("missing or invalid source positions are not manufactured as negative same-sign results", () => {
|
||
const chart = structuredClone(golden.chart);
|
||
const varga = structuredClone(golden.varga_full);
|
||
const d9 = varga.result.D9_Navamsa!;
|
||
delete d9.planets.Sun!.sign;
|
||
d9.planets.Moon!.sign = "not-a-sign";
|
||
const view = buildChartView({ chart, varga: varga as unknown as Record<string, unknown>, chara: null, western: null, profile, asOf: "2026-09-15" });
|
||
const projected = view.vedic.vargas.find((item) => item.id === "D9")!;
|
||
assert.equal(projected.supportsDignity, true);
|
||
assert.equal(projected.supportsVargottama, true);
|
||
assert.equal(projected.rows!.some((item) => item.name === "Sun" || item.name === "Moon"), false);
|
||
// Destroy only the source positions on a golden copy, rather than making a
|
||
// response shape. With no valid result source no capability is advertised.
|
||
for (const planet of Object.values(d9.planets)) delete planet.sign;
|
||
delete (chart.ascendant as Record<string, unknown>).sign;
|
||
const empty = buildChartView({ chart, varga: varga as unknown as Record<string, unknown>, chara: null, western: null, profile, asOf: "2026-09-15" });
|
||
const emptyD9 = empty.vedic.vargas.find((item) => item.id === "D9")!;
|
||
assert.deepEqual(emptyD9.rows, []);
|
||
assert.equal(emptyD9.ascendant, undefined);
|
||
assert.equal(emptyD9.supportsDignity, false);
|
||
assert.equal(emptyD9.supportsVargottama, false);
|
||
});
|
||
|
||
test("view projection omits engine branding but old profile fields still parse and raw metadata stays intact", async () => {
|
||
const before = structuredClone(golden);
|
||
const view = await vargaView();
|
||
assert.equal("engineName" in view.profile, false);
|
||
const legacy = structuredClone(view);
|
||
Object.assign(legacy.profile, { engineName: "Swiss Ephemeris(本站)" });
|
||
assert.equal("engineName" in chartViewOkSchema.parse(legacy).profile, false);
|
||
assert.deepEqual(golden, before, "the mapper never strips or changes the raw engine packet");
|
||
});
|
||
|
||
test("divisional house cells do not override their header's left alignment", async () => {
|
||
const view = await vargaView();
|
||
for (const varga of view.vedic.vargas.filter((item) => item.id !== "D1")) {
|
||
const markup = tableMarkup(varga);
|
||
const rows = [...markup.matchAll(/<tr>([\s\S]*?)<\/tr>/g)].slice(1);
|
||
assert.equal(rows.length, 10);
|
||
for (const [, cells] of rows) {
|
||
const tds = [...cells!.matchAll(/<td([^>]*)>([\s\S]*?)<\/td>/g)];
|
||
assert.equal(tds[1]![1], "", "house cells inherit the same alignment and padding as the header");
|
||
assert.match(tds[1]![2]!, /^\d+宫$/);
|
||
assert.match(tds[3]![1]!, /chart-page-planet-mid/, "the dignity column's deliberate centering remains");
|
||
}
|
||
}
|
||
});
|