Files
Jyotisha/frontend/tests/ephemeris-page.test.tsx
T
Jesse_ChenandClaude Fable 5.1 fb77c86585 test(ui): 侧栏只读模式、共享外壳与列表缓存的合同回归
新增 8 条:`sidebar-data-cache.test.ts`(命中不重拉 / 过期重拉一次 / 写操作后
拿到新标题并逐条锁住五个写路径 / 双账户不串 / 只存内存 / 401 清空)、
`sidebar-state.test.ts` +2(收起后重挂仍收起,含三种降级;移动端不读不写)、
`sidebar-contract.test.ts` +1(只读模式只少三样)、`chart-page-view.test.tsx` +1
(`(secondary)` layout 恰好挂一份只读侧栏,数据 hook 无写方法)。

改写 9 处既有断言,每处带「原值 / 新值 / 原因」三栏注释,均未削弱:
`window.location.assign(path)` → `<SidebarMenuLink href>` 并追加反向断言;
`onOpenReports` / `useRouter` 改成 doesNotMatch;`SecondaryShell` → `SecondaryHeader`;
导航顺序改在 `NAV_PAGES` 常量里量;两个 render 辅助改为裹 `SidebarProvider`
(provider 上移到 layout);三处源码路径跟随路由组移动。

测试总数 3391 → 3399,失败清单与基线逐条一致(47 条均为无 Docker 的既有缺口)。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
2026-09-16 11:23:11 +00:00

167 lines
8.2 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,
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/(secondary)/ephemeris/page.tsx`
// 原因:四个次级路由移进 `(secondary)` 路由组共享一份外壳(TASK-sidebar-unify D2)。
// 路由组括号不进 URL`/ephemeris` 一字未改;这里改的只是源码位置。
const routePage = readFileSync(new URL("../src/app/(secondary)/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/(secondary)/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/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);
const note = markup.indexOf(EPHEMERIS_COPY.panchangaLahiriNote);
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(EPHEMERIS_COPY.panchangaLahiriNote));
});
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"));
assert.match(markup, new RegExp(EPHEMERIS_COPY.footerNote));
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, /运势|适合|不宜|建议|吉祥日/);
assert.doesNotMatch(pageSource, /InlineSpinner|正在加载|skeleton/i);
assert.equal(ephemerisAskDraft("2026-09-15"), "请按 2026年9月15日 这一天的天象来看。");
assert.match(pageSource, /setComposerDraft\(ephemerisAskDraft/);
});