- 建档重读人物名单时保留旧名单,失败也不卡在加载画面(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
160 lines
8.5 KiB
TypeScript
160 lines
8.5 KiB
TypeScript
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);
|
||
});
|