Files
Jyotisha/frontend/tests/chart-type-picker.test.tsx
T
jesse-uxandGrok (agent loop) b7ec8e78ed
Independent Staging Quality Gate / validate (push) Failing after 9m20s
Independent Staging Quality Gate / publish (push) Skipped
星盘页换成组件库:页签、盘型条、可拖分栏和换盘动效(#107)。
四个页签用下划线,盘型条用分段并可以横滑,全部盘型从底部打开。桌面上盘和参数可以拖宽,窄屏上下排。行星表仍跨满整行。换盘型时星体移到新位置;减少动态效果只淡入。第一次进场不重播。

自测:tsc 退出码 0;frontend 下 npm run lint 0 error(122 警告);定向测试 188 通过、0 失败、0 取消;npm run build 后 / 仍是 Static;首屏 JS gzip-9 为 640131,相对 d25dfdbe 记下的 639127 多 0.16%。没有跑全量 npm test、Python 快速门和 test:db。

Co-Authored-By: Grok (agent loop)
2026-10-25 04:02:14 +08:00

129 lines
6.9 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 React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import test from "node:test";
import { ChartTypePicker, ChartTypeSheetBody } 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";
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 lists every chart and choosing one is wired to close", () => {
const source = readFileSync(new URL("../src/components/chart-page/chart-type-picker.tsx", import.meta.url), "utf8");
// 原值:生命周期宿主打开原生 <dialog>,点格子和 Esc 关闭
// 新值:源码用 Sheet;格子静态渲染 23 项。选中后 setSheetOpen(false) 再 onSelect
// 原因:K3 改用 Sheet。这个测试宿主的 portal 挂不进 body,不能假装点开过浮层。
assert.match(source, /<Sheet open=\{sheetOpen\}/);
assert.match(source, /side="bottom"/);
assert.match(source, /CHART_VIEW_COPY\.chartTypeSheetTitle/);
assert.match(source, /CHART_VIEW_COPY\.chartTypeSheetClose/);
assert.match(source, /setSheetOpen\(false\);\s*onSelect\(id\);/);
assert.doesNotMatch(source, /<dialog|<button|ToggleGroup|role="tablist"/);
const markup = renderToStaticMarkup(React.createElement(ChartTypeSheetBody, { current: "D1", onSelect: () => {} }));
const cells = [...markup.matchAll(/class="([^"]*chart-page-type-cell[^"]*)"/g)];
assert.equal(cells.length, 23);
assert.match(markup, /aria-pressed="true"/);
assert.match(markup, /前世业力/);
assert.match(markup, /以月亮为第 1 宫/);
assert.match(markup, /星盘类型|本命/);
assert.match(markup, />本命</);
});
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);
});