Files
Jyotisha/frontend/tests/chart-type-picker.test.tsx
T
Jesse_ChenandClaude Opus 5.5 de9d4826c1 #111 第 3 轮:建档不退回加载、右栏能拖宽、清单不迟到、月亮问句与别人的上升句(BUG-1455~1459)
- 建档重读人物名单时保留旧名单,失败也不卡在加载画面(BUG-1455)
- 对话右栏改用自己的拖宽把手,侧栏与右栏都能用方向键(BUG-1456/1457)
- 启程清单胶囊和首页一起出来,读状态时用上次的样子占位(BUG-1458)
- 起步第一问与别人的天空过场读一次已算好的星盘取星座,上升只在不会换座时用(BUG-1459,产品 10-27 同意)
- 补回删减的断言与三栏说明;登录代码改回原样,替身放进测试;删旧校正画面的无用样式
- 首屏 +7.54% 产品已接受,写进 DESIGN

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LutgzyJqNhPpZy1nFsJtN3
2026-10-27 10:24:57 +08:00

160 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 "./install-test-document";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import test from "node:test";
import { ChartTypePicker } from "../src/components/chart-page/chart-type-picker.tsx";
import { ChartVedicTab } from "../src/components/chart-page/chart-vedic-tab.tsx";
import { assembleChartView } from "../src/lib/chart-view-load.ts";
import type { ChartViewOk } from "../src/lib/chart-view-contract.ts";
import { CHART_TYPE_ORDER, CHART_VIEW_COPY, VARGA_CHIP_ORDER } from "../src/lib/chart-view-labels.ts";
import { cssDeclarations } from "./css-contract-test-support.ts";
import { createClientLifecycleHarness } from "./react-client-lifecycle-test-support.ts";
Object.assign(globalThis, { React });
const golden = JSON.parse(readFileSync(new URL("./fixtures/chart-view-golden.json", import.meta.url), "utf8"));
const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const profile = {
name: "示例",
date: "1990-06-15",
time: "12:00",
placeLabel: "北京",
latitude: 39.9042,
longitude: 116.4074,
timezoneOffset: 8,
timezoneId: "Asia/Shanghai",
ayanamsa: "raman",
birthTimeStatus: "confirmed",
};
async function vargaView(birthTimeStatus = "confirmed"): Promise<{ view: ChartViewOk; divisions: unknown }> {
let divisions: unknown = null;
const result = await assembleChartView({
userId: "user-1",
profile: { ...profile, birthTimeStatus },
asOf: "2026-09-15",
layers: ["varga"],
postEngine: async (path, body) => {
if (path === "/api/chart") return { status: "ok", payload: golden.chart };
if (path === "/api/varga_full") { divisions = body.divisions; return { status: "ok", payload: golden.varga_full }; }
return { status: "http_error", path, elapsedMs: 1, httpStatus: 500 };
},
});
assert.equal(result.body.status, "ok");
return { view: result.body as ChartViewOk, divisions };
}
test("the strip lists all 23 chart types in the decided order with a pinned 全部盘型 button", () => {
assert.deepEqual([...CHART_TYPE_ORDER], [
"D1", "D9", "MOON", "BHAVA", "TRANSIT",
"D2", "D3", "D4", "D5", "D6", "D7", "D8", "D10", "D11", "D12",
"D16", "D20", "D24", "D27", "D30", "D40", "D45", "D60",
]);
const markup = renderToStaticMarkup(React.createElement(ChartTypePicker, { current: "D1", onSelect: () => {} }));
// 原值:`<div class="chart-page-type-strip" role="tablist">` 里的原生 button,菜单在条外
// 新值:分段 Tabs(data-variant="default")的 role="tab" 文案,菜单按钮仍带「全部盘型」
// 原因:K3 盘型条改成组件库 Tabs + ScrollArea,不再手写 tablist。
const labels = [...markup.matchAll(/role="tab"[^>]*>([^<]*)</g)].map((match) => match[1]);
assert.deepEqual(labels, ["本命", "D9", "月亮", "Bhava", "行运", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D10", "D11", "D12", "D16", "D20", "D24", "D27", "D30", "D40", "D45", "D60"]);
assert.match(markup, /data-variant="default"/);
assert.match(markup, /aria-label="盘型"/);
assert.match(markup, /aria-selected="true"/);
assert.match(markup, /本命/);
assert.match(markup, /aria-label="全部盘型"/);
assert.match(markup, /aria-haspopup="dialog"/);
const tablistAt = markup.indexOf('role="tablist"');
const menuAt = markup.indexOf('aria-label="全部盘型"');
assert.ok(tablistAt >= 0 && menuAt > tablistAt);
});
test("the strip is one sideways-scrolling line and the sheet is a three-column grid", () => {
const strip = cssDeclarations(".chart-page-type-strip", globalStyles);
assert.match(strip, /flex-wrap:\s*nowrap/);
assert.match(strip, /overflow-x:\s*auto/);
assert.match(strip, /min-width:\s*0/);
assert.match(cssDeclarations(".chart-page-type-grid", globalStyles), /grid-template-columns:\s*repeat\(3, minmax\(0, 1fr\)\)/);
assert.match(cssDeclarations(".chart-page-type-menu", globalStyles), /width:\s*44px/);
// 原值:min-height 56px;新值:64px;原因:2026-09-30 真机截图说明文字被截底,说明行改为最多两行、行高 1.4,格子加高以容纳
assert.match(cssDeclarations(".chart-page-type-cell", globalStyles), /min-height:\s*64px/);
assert.match(cssDeclarations(".chart-page-type-cell small", globalStyles), /line-clamp:\s*2/);
assert.doesNotMatch(globalStyles, /\.chart-page-varga-chips/);
});
test("the type sheet closes when a cell is chosen or Escape is pressed", async () => {
// 原值:生命周期宿主打开原生 <dialog>,点格子和 Esc(onCancel)关闭
// 新值:打开底部 Sheet,点格子调用选择并关掉;再打开后按 Esc 关掉,且不再次选择
// 原因:K3 改用 Sheet。关闭要看挂上的浮层,不再用源码里的 setSheetOpen 代替点按
const h = createClientLifecycleHarness();
const picked: string[] = [];
type Node = ReturnType<typeof h.elements>[number];
const nodes = () => {
const walk = (node: Node): Node[] => [node, ...node.childNodes.flatMap((child) => walk(child))];
return [...walk(h.container), ...[...h.document.body.childNodes].flatMap((child) => walk(child))];
};
const sheet = () => nodes().find((node) => node.getAttribute("data-slot") === "sheet-content");
try {
await h.render(React.createElement(ChartTypePicker, {
current: "D1",
onSelect: (id: string) => picked.push(id),
}));
const menu = nodes().find((node) => node.getAttribute("aria-label") === CHART_VIEW_COPY.chartTypeMenu);
assert.ok(menu);
assert.equal(sheet(), undefined);
await h.event(menu);
assert.ok(sheet());
assert.match(sheet()!.text, /星盘类型/);
const cells = nodes().filter((node) => (node.getAttribute("class") ?? "").includes("chart-page-type-cell"));
assert.equal(cells.length, 23);
assert.match(cells.find((cell) => cell.text.startsWith("D60"))!.text, /前世业力/);
assert.match(cells.find((cell) => cell.text.startsWith("月亮盘"))!.text, /以月亮为第 1 宫/);
assert.equal(cells.find((cell) => cell.text.startsWith("本命"))!.getAttribute("aria-pressed"), "true");
await h.event(cells.find((cell) => cell.text.startsWith("D60"))!);
assert.deepEqual(picked, ["D60"]);
await h.idle();
assert.equal(sheet(), undefined, "choosing a cell closes the sheet");
await h.event(nodes().find((node) => node.getAttribute("aria-label") === CHART_VIEW_COPY.chartTypeMenu)!);
const open = sheet();
assert.ok(open);
await h.event(open, "onKeyDown", { key: "Escape" });
await h.idle();
assert.equal(sheet(), undefined, "Escape closes the sheet");
assert.deepEqual(picked, ["D60"]);
assert.deepEqual(h.errors, []);
} finally {
await h.close();
}
// 原值(365a623fc):删掉了 doesNotMatch(source, /<dialog|<button|ToggleGroup|role="tablist"/)
// 新值:补回这条源码守卫
// 原因:上面的点按测试证明浮层能关,但拦不住有人退回手写原生按钮或对话框;两条守的是不同的事(第 2 轮验收第 4 条)
const source = readFileSync(new URL("../src/components/chart-page/chart-type-picker.tsx", import.meta.url), "utf8");
assert.doesNotMatch(source, /<dialog|<button|ToggleGroup|role="tablist"/);
});
test("the varga layer asks the engine for D40, D45 and D60 and the page draws them", async () => {
const { view, divisions } = await vargaView();
assert.deepEqual(divisions, VARGA_CHIP_ORDER.map((division) => `D${division}`));
for (const id of ["D40", "D45", "D60"]) {
const varga = view.vedic.vargas.find((item) => item.id === id);
assert.ok(varga, id);
assert.equal(varga.rows?.length, 9);
const markup = renderToStaticMarkup(React.createElement(ChartVedicTab, { view, vargaId: id, onSelectVarga: () => {} }));
assert.match(markup, new RegExp(`<caption>${id} · `));
assert.match(markup, new RegExp(`aria-label="${id} 北印度盘`));
}
});
test("the precision hint shows on D40/D45/D60 only while the birth time is not confirmed", async () => {
const reported = (await vargaView("reported")).view;
const confirmed = (await vargaView("confirmed")).view;
const render = (view: ChartViewOk, id: string) => renderToStaticMarkup(React.createElement(ChartVedicTab, { view, vargaId: id, onSelectVarga: () => {} }));
for (const id of ["D40", "D45", "D60"]) {
assert.ok(render(reported, id).includes(CHART_VIEW_COPY.precisionHint), id);
assert.ok(!render(confirmed, id).includes(CHART_VIEW_COPY.precisionHint), id);
}
for (const id of ["D1", "D9", "D30"]) assert.ok(!render(reported, id).includes(CHART_VIEW_COPY.precisionHint), id);
});