[\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(/
([\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, "太阳"), />落陷);
+ // 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);
+ }
+});
diff --git a/frontend/tests/chart-view-varga-dignity.test.ts b/frontend/tests/chart-view-varga-dignity.test.ts
new file mode 100644
index 00000000..24c5e217
--- /dev/null
+++ b/frontend/tests/chart-view-varga-dignity.test.ts
@@ -0,0 +1,24 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { vargaDignity, vargaMeaning } from "../src/lib/chart-view-varga.ts";
+
+// The tables are pinned to the engine by tests/test_chart_page_vedic_tables_contract.py;
+// this pins the precedence (exalted, own, debilitated) the Python test mirrors.
+test("sign dignity follows the engine's precedence and node own signs", () => {
+ assert.equal(vargaDignity("Sun", "Aries"), "exalted");
+ assert.equal(vargaDignity("Sun", "Leo"), "own");
+ assert.equal(vargaDignity("Sun", "Libra"), "debilitated");
+ assert.equal(vargaDignity("Mercury", "Virgo"), "exalted", "exaltation wins over own sign");
+ assert.equal(vargaDignity("Rahu", "Virgo"), "own");
+ assert.equal(vargaDignity("Ketu", "Pisces"), "own");
+ assert.equal(vargaDignity("Rahu", "Taurus"), null, "the engine has no node exaltation");
+ assert.equal(vargaDignity("Moon", "Leo"), null);
+});
+
+test("varga meanings come from the engine table including D40, D45 and D60", () => {
+ assert.equal(vargaMeaning(9), "配偶/灵性");
+ assert.equal(vargaMeaning(45), "全面判断");
+ assert.equal(vargaMeaning(60), "前世业力");
+ assert.equal(vargaMeaning(81), null);
+});
diff --git a/frontend/tests/fixtures/chart-d1-table-baseline.html b/frontend/tests/fixtures/chart-d1-table-baseline.html
new file mode 100644
index 00000000..6966afd8
--- /dev/null
+++ b/frontend/tests/fixtures/chart-d1-table-baseline.html
@@ -0,0 +1 @@
+D1 · 本命盘| 行星 | 黄道经度 | 速度 | 宫位 | 星宿 | 宿主 | pada | 状态 |
|---|
| As 上升 As | 狮子 29°23′ | 顺 | 1宫 | — | — | — | — |
|---|
| ☉ 太阳 Su | 双子 1°32′ | 顺 | 11宫 | Mrigashira | ♂︎ | 3 | 入友(Friendly Sign) |
|---|
| ☽ 月亮 Mo | 水瓶 18°38′ | 顺 | 7宫 | Shatabhisha | ☊ | 4 | 入友(Friendly Sign) |
|---|
| ♂︎ 火星 Ma | 双鱼 18°31′ | 顺 | 8宫 | Revati | ☿ | 1 | 极友(Great Friend) |
|---|
| ☿ 水星 Me | 金牛 12°50′ | 顺 | 10宫 | Rohini | ☽ | 1 | 极友(Great Friend) |
|---|
| ♃ 木星 Ju | 双子 23°32′ | 顺 | 11宫 | Punarvasu | ♃ | 2 | 中性(Neutral) |
|---|
| ♀︎ 金星 Ve | 白羊 26°06′ | 顺 | 9宫 | Bharani | ♀︎ | 4 | 入友(Friendly Sign) |
|---|
| ♄ 土星 Sa | 摩羯 1°46′ | 逆 | 6宫 | Uttara Ashadha | ☉ | 2 | 入庙(Own Sign) · 逆行 |
|---|
| ☊ 罗睺 Ra | 摩羯 17°26′ | 逆 | 6宫 | Shravana | ☽ | 3 | 中性(Neutral) · 逆行 |
|---|
| ☋ 计都 Ke | 巨蟹 17°26′ | 逆 | 12宫 | Ashlesha | ☿ | 1 | 极敌(Great Enemy) · 逆行 |
|---|
\ No newline at end of file
diff --git a/tests/test_chart_page_vedic_tables_contract.py b/tests/test_chart_page_vedic_tables_contract.py
new file mode 100644
index 00000000..a3473f2d
--- /dev/null
+++ b/tests/test_chart_page_vedic_tables_contract.py
@@ -0,0 +1,58 @@
+"""The chart page's dignity and varga-meaning tables must be the engine's.
+
+`frontend/src/lib/vedic-chart-tables.json` is read by the chart-page mapper so
+the divisional tables can show 入旺 / 自宫 / 落陷 without a second hand-written
+rule set. This test pins it to the constants `jyotish_engine._get_dignity_level`
+uses and to `divisional_charts_extended.VargaType`.
+"""
+
+from __future__ import annotations
+
+import json
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT / "scripts"))
+
+import divisional_charts_extended # noqa: E402
+import jyotish_engine # noqa: E402
+
+TABLES = json.loads((ROOT / "frontend/src/lib/vedic-chart-tables.json").read_text(encoding="utf-8"))
+PLANETS = ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu"]
+
+
+def _table_dignity(planet: str, sign: str) -> str | None:
+ """Same precedence as `vargaDignity` in frontend/src/lib/chart-view-varga.ts."""
+ if TABLES["exaltation"].get(planet) == sign:
+ return "EXALTED"
+ if TABLES["signLords"].get(sign) == planet or TABLES["nodeOwnSign"].get(planet) == sign:
+ return "OWN_SIGN"
+ if TABLES["debilitation"].get(planet) == sign:
+ return "DEBILITATED"
+ return None
+
+
+def test_tables_equal_engine_constants() -> None:
+ assert TABLES["signLords"] == jyotish_engine.SIGN_LORDS
+ assert TABLES["exaltation"] == jyotish_engine.EXALTATION
+ assert TABLES["debilitation"] == jyotish_engine.DEBILITATION
+
+
+def test_every_planet_sign_pair_matches_engine_dignity_level() -> None:
+ for planet in PLANETS:
+ for sign in jyotish_engine.SIGN_LORDS:
+ # No degree and no chart context: the varga table shows the sign
+ # dignity only (no moolatrikona band, no neecha bhanga, no friendship).
+ engine = jyotish_engine._get_dignity_level(planet, sign)
+ expected = engine if engine in {"EXALTED", "OWN_SIGN", "DEBILITATED"} else None
+ if engine == "NEECHA_BHANGA":
+ expected = "DEBILITATED"
+ assert _table_dignity(planet, sign) == expected, (planet, sign, engine)
+
+
+def test_varga_meanings_equal_engine_varga_types() -> None:
+ engine = {str(v.division): v.meaning for v in divisional_charts_extended.VargaType if v.division <= 60}
+ assert TABLES["vargaMeaning"] == engine
+ for division in ("40", "45", "60"):
+ assert division in TABLES["vargaMeaning"]