新增 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
167 lines
8.2 KiB
TypeScript
167 lines
8.2 KiB
TypeScript
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/);
|
||
});
|