Files
Jyotisha/frontend/tests/ephemeris-page.test.tsx
T
jesse-ux e4e73f56c0
Independent Staging Quality Gate / publish (push) Canceled after 0s
Independent Staging Quality Gate / validate (push) Canceled after 9m33s
fix(web): 四个页面共用一份会话列表,空会话不入列
对话、星盘、星历、报告进同一 (app) 外壳,列表只拉一次。服务端不再列出空咨询;新建复用已有空会话。新标题改成「生时校正 · M月D日」,侧栏副标题用创建时间。
2026-09-17 21:27:40 +08:00

171 lines
8.5 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/(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);
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"));
// 原值: 页尾必须含「这一页是天象本身,不是对你的判断。」
// 新值: 页面不得出现该句
// 原因: 产品 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, /运势|适合|不宜|建议|吉祥日/);
assert.doesNotMatch(pageSource, /InlineSpinner|正在加载|skeleton/i);
assert.equal(ephemerisAskDraft("2026-09-15"), "请按 2026年9月15日 这一天的天象来看。");
assert.match(pageSource, /setComposerDraft\(ephemerisAskDraft/);
});