Files
Jyotisha/frontend/tests/ephemeris-page.test.tsx
T
Jesse_ChenandClaude Opus 5.5 2981da6fbc fix(people,ephemeris): style /people and make the ephemeris readable (BUG-1039)
- /people had zero CSS rules for its people-archive classes. Add the
  approved layout: list -> detail below 1024px (data-view, ?person= history
  entry so Back returns to the list), two columns at 1024px and above;
  avatar rows with the current person highlighted, time cards with a
  rectified state, readable ayanamsa name, three quick actions; delete moves
  to the bottom of the edit view with unchanged confirm/count logic.
- Ephemeris: Chinese-first panchanga names from the brief's table only
  (panchanga-names-zh.ts, three listed aliases, unknown names stay
  Sanskrit), coloured 吉/中/凶 tags, N吉/M凶 counts and a fixed-rule day
  summary (pure function, no model call); the header ask button becomes a
  bottom hint card; transit rows show glyph, 「行星 在 星座」, house line and
  degree; the Lahiri note names the subject's own ayanamsa.
- API, engine and database unchanged; /people and /ephemeris stay Static.
- DESIGN, VOICE, BUG_HISTORY, CHANGELOG, BLOCKED, PROGRESS and a manual
  checklist updated. Two existing assertions changed, with 原值/新值/原因.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-09-26 06:58:41 +08:00

219 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { EphemerisView } from "../src/components/ephemeris/ephemeris-page.tsx";
import { SidebarProvider } from "../src/components/ui/sidebar.tsx";
import { parseEphemerisOkResponse, type EphemerisOkResponse } from "../src/lib/ephemeris-contract.ts";
import {
EPHEMERIS_COPY,
ephemerisAskDraft,
ephemerisAyanamsaNote,
panchangaFactorsFromEngine,
shiftEphemerisDate,
transitsFromEngine,
} from "../src/lib/ephemeris-view.ts";
Object.assign(globalThis, { React });
const pageSource = readFileSync(new URL("../src/components/ephemeris/ephemeris-page.tsx", import.meta.url), "utf8");
// 原值:`../src/app/ephemeris/page.tsx`
// 新值:`../src/app/(app)/ephemeris/page.tsx`
// 原因:四个次级路由移进 `(secondary)` 路由组共享一份外壳(TASK-sidebar-unify D2)。
// 路由组括号不进 URL,`/ephemeris` 一字未改;这里改的只是源码位置。
const routePage = readFileSync(new URL("../src/app/(app)/ephemeris/page.tsx", import.meta.url), "utf8");
const css = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const panchangaGolden = JSON.parse(readFileSync(new URL("./fixtures/ephemeris-panchanga-range.golden.json", import.meta.url), "utf8"));
const natalGolden = JSON.parse(readFileSync(new URL("./fixtures/ephemeris-chart-natal.golden.json", import.meta.url), "utf8"));
const transit15 = JSON.parse(readFileSync(new URL("./fixtures/ephemeris-chart-transit-2026-09-15.golden.json", import.meta.url), "utf8"));
const transit14 = JSON.parse(readFileSync(new URL("./fixtures/ephemeris-chart-transit-2026-09-14.golden.json", import.meta.url), "utf8"));
function payloadFor(date: "2026-09-14" | "2026-09-15", events: EphemerisOkResponse["events"] = { status: "unavailable" }): EphemerisOkResponse {
const transits = transitsFromEngine({
transitChart: date === "2026-09-14" ? transit14 : transit15,
natalChart: natalGolden,
});
const parsed = parseEphemerisOkResponse({
status: "ok",
date,
today: "2026-09-15",
timezoneId: "Asia/Shanghai",
panchanga: {
available: true,
ayanamsaNote: "lahiri_not_account",
factors: panchangaFactorsFromEngine(panchangaGolden, date),
},
transits: {
available: true,
natalRelativeAvailable: true,
natalRelativeNote: "shown",
ayanamsa: "raman",
bodies: transits.bodies,
},
events,
});
assert.ok(parsed);
return parsed;
}
/* 原值:直接 `renderToStaticMarkup(<EphemerisView … />)`
新值:外面裹一层 `SidebarProvider`
原因:次级页的 `SidebarProvider` 从每页各一份上移到 `app/(app)/layout.tsx`
(TASK-sidebar-unify D2)。页面组件自己不再自带 provider,而 46px 顶栏里的
`SidebarTrigger` 仍要读它。断言主语一字未改,改的是测试挂载环境。 */
function withSidebarProvider(element: React.ReactElement) {
return renderToStaticMarkup(React.createElement(SidebarProvider, null, element));
}
function render(payload: EphemerisOkResponse | null, date = payload?.date ?? "2026-09-15") {
return withSidebarProvider(React.createElement(EphemerisView, {
date,
today: "2026-09-15",
payload,
onPrevious() {},
onNext() {},
onToday() {},
onSelectDate() {},
onAsk() {},
}));
}
test("the ephemeris page is its own route and never grows page.tsx", () => {
assert.match(routePage, /from "@\/components\/ephemeris\/ephemeris-page"/);
assert.doesNotMatch(readFileSync(new URL("../src/app/(app)/page.tsx", import.meta.url), "utf8"), /ephemeris-page/);
// 原值: assert.doesNotMatch(app-sidebar.tsx, /ephemeris/)
// 新值: 本单文件归属只锁 page.tsx / 本 route,不再锁 sidebar
// 原因: TASK-chart-page-20260915 任务 3 要求 sidebar 同时加「星盘」「星历」入口;文件归属「本单不改 sidebar」不是产品事实「sidebar 不得出现 ephemeris」(BUG-711)
const thisTest = readFileSync(new URL(import.meta.url), "utf8");
assert.doesNotMatch(thisTest, /from "@\/components\/app-sidebar"/);
});
test("date switching replaces panchanga and transits", () => {
const fourteenth = payloadFor("2026-09-14");
const fifteenth = payloadFor("2026-09-15");
assert.notEqual(
fourteenth.panchanga.factors.find((item) => item.key === "tithi")?.value,
fifteenth.panchanga.factors.find((item) => item.key === "tithi")?.value,
);
const moon14 = fourteenth.transits.bodies.find((item) => item.planet === "Moon");
const moon15 = fifteenth.transits.bodies.find((item) => item.planet === "Moon");
assert.notEqual(moon14?.degreeInSign, moon15?.degreeInSign);
const markup14 = render(fourteenth);
const markup15 = render(fifteenth);
assert.match(markup14, /Shukla Chaturthi/);
assert.match(markup15, /Shukla Panchami/);
assert.doesNotMatch(markup15, /Shukla Chaturthi/);
assert.match(markup14, /2026年9月14日/);
assert.match(markup15, /2026年9月15日/);
assert.equal(shiftEphemerisDate("2026-09-15", -1), "2026-09-14");
});
test("unavailable events render a static note, not a blank page", () => {
const markup = render(payloadFor("2026-09-15"));
assert.match(markup, new RegExp(EPHEMERIS_COPY.eventsUnavailable));
assert.match(markup, /Shukla Panchami/);
assert.match(markup, /狮子座/);
assert.doesNotMatch(markup, /spinner|正在加载|skeleton/i);
});
test("the Lahiri note sits under panchanga and not under transits", () => {
const markup = render(payloadFor("2026-09-15"));
const panchanga = markup.indexOf(EPHEMERIS_COPY.panchangaHeading);
// 原值:`EPHEMERIS_COPY.panchangaLahiriNote`(固定句「这一段按 Lahiri 岁差算,和星盘页用的岁差不是同一套。」)
// 新值:`ephemerisAyanamsaNote("raman")`(「五要素按 Lahiri 岁差算,和星盘页的 Raman 不同。」,fixture 的 transits.ayanamsa 是 raman)
// 原因:TASK-people-ephemeris-ui-20260926 D10 改句式,并要求按资料实际岁差填名、不写死;位置断言(五要素之下、行运之上)一字未改。
const lahiriNote = ephemerisAyanamsaNote("raman");
const note = markup.indexOf(lahiriNote);
const transits = markup.indexOf(EPHEMERIS_COPY.transitsHeading);
assert.ok(panchanga >= 0 && note >= 0 && transits >= 0);
assert.ok(panchanga < note && note < transits);
const transitsBlock = markup.slice(transits);
assert.doesNotMatch(transitsBlock, new RegExp(lahiriNote));
});
test("今天 is absent on today and present off it, never disabled", () => {
// 原值:`assert.match(markup, new RegExp(EPHEMERIS_COPY.today))` —— 在
// 「copy stays factual」里对唯一一次渲染(date === today)断言「今天」在场。
// 新值:拆成两个状态各断一次,并加断 `disabled` 不出现。
// 原因:T5.2 验收标准要求「在今日时不出现『今天』按钮,而不是出现一个禁用按钮」。
// 原断言恰好只覆盖 date === today 这一种情况,照搬会要求今日也渲染该按钮,
// 与验收标准直接冲突。新断言覆盖两个状态,比原来严格。
const onToday = render(payloadFor("2026-09-15"));
assert.doesNotMatch(onToday, new RegExp(EPHEMERIS_COPY.today));
assert.doesNotMatch(onToday, /disabled=""/);
const offToday = render(payloadFor("2026-09-14"));
assert.match(offToday, new RegExp(EPHEMERIS_COPY.today));
assert.doesNotMatch(offToday, /disabled=""/);
});
test("mobile CSS turns five panchanga cards into one stacked card", () => {
const markup = render(payloadFor("2026-09-15"));
assert.match(markup, /class="ephemeris-panchanga"/);
assert.equal([...markup.matchAll(/ephemeris-panchanga-item/g)].length, 5);
assert.match(css, /@media \(max-width: 767px\) \{[\s\S]*\.ephemeris-panchanga \{[\s\S]*grid-template-columns:\s*1fr/);
assert.match(css, /@media \(max-width: 767px\) \{[\s\S]*\.ephemeris-panchanga-item \{[\s\S]*grid-template-columns:\s*5\.5rem minmax\(0, 1fr\)/);
assert.match(css, /\.ephemeris-date-button \{[\s\S]*min-height:\s*44px/);
});
test("copy stays factual: no fortune-telling, no spinner, ask prefills the date", () => {
const markup = render(payloadFor("2026-09-15"));
// 原值: 页尾必须含「这一页是天象本身,不是对你的判断。」
// 新值: 页面不得出现该句
// 原因: 产品 2026-09-17 判定为过度提示,与星盘页眉标同一类
assert.doesNotMatch(markup, /天象本身|不是对你的判断/);
assert.doesNotMatch(markup, /ephemeris-outro/);
assert.match(markup, new RegExp(EPHEMERIS_COPY.ask));
assert.match(markup, new RegExp(EPHEMERIS_COPY.previousDay));
assert.match(markup, new RegExp(EPHEMERIS_COPY.nextDay));
// 原值:`assert.doesNotMatch(markup, /运势|适合|不宜|建议|吉祥日/)`,对整页 markup。
// 新值:先去掉底部提示卡那一句问句(EPHEMERIS_COPY.askPrompt),再对其余 markup 断同一组词;另断问句只出现一次。
// 原因:D8 产品定稿的底部提示卡原文是「想问这一天适不适合做某件事?」——这是邀请用户提问的问句,不是页面给出的
// 宜忌结论;五要素、总评、行运、九十天事件里仍不得出现这组词,约束范围没有放宽。
const withoutAskPrompt = markup.replace(EPHEMERIS_COPY.askPrompt, "");
assert.equal(markup.split(EPHEMERIS_COPY.askPrompt).length - 1, 1);
assert.doesNotMatch(withoutAskPrompt, /运势|适合|不宜|建议|吉祥日/);
assert.doesNotMatch(pageSource, /InlineSpinner|正在加载|skeleton/i);
assert.equal(ephemerisAskDraft("2026-09-15"), "请按 2026年9月15日 这一天的天象来看。");
assert.match(pageSource, /setComposerDraft\(ephemerisAskDraft/);
});
test("an uncached date keeps the date bar and replaces the three sections with static sentences", () => {
const markup = withSidebarProvider(React.createElement(EphemerisView, {
date: "2026-09-16",
today: "2026-09-15",
payload: null,
dataState: "pending",
onPrevious() {},
onNext() {},
onToday() {},
onSelectDate() {},
onAsk() {},
}));
assert.match(markup, /2026年9月16日/);
assert.match(markup, new RegExp(EPHEMERIS_COPY.previousDay));
assert.match(markup, new RegExp(EPHEMERIS_COPY.panchangaUnavailable));
assert.match(markup, new RegExp(EPHEMERIS_COPY.transitsUnavailable));
assert.match(markup, new RegExp(EPHEMERIS_COPY.eventsPending));
assert.doesNotMatch(markup, /还没接上|正在加载|skeleton|spinner/i);
});
test("a failed date shows the retry sentence in each section", () => {
const markup = withSidebarProvider(React.createElement(EphemerisView, {
date: "2026-09-16",
today: "2026-09-15",
payload: null,
dataState: "failed",
onPrevious() {},
onNext() {},
onToday() {},
onSelectDate() {},
onAsk() {},
}));
assert.equal((markup.match(new RegExp(EPHEMERIS_COPY.dateFailed, "g")) ?? []).length, 3);
assert.match(markup, /2026年9月16日/);
assert.doesNotMatch(markup, /正在加载|skeleton|spinner/i);
});