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: () => {} })); // 原值:`
` 里的原生 button,菜单在条外 // 新值:分段 Tabs(data-variant="default")的 role="tab" 文案,菜单按钮仍带「全部盘型」 // 原因:K3 盘型条改成组件库 Tabs + ScrollArea,不再手写 tablist。 const labels = [...markup.matchAll(/role="tab"[^>]*>([^<]*) 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 () => { // 原值:生命周期宿主打开原生 ,点格子和 Esc(onCancel)关闭 // 新值:打开底部 Sheet,点格子调用选择并关掉;再打开后按 Esc 关掉,且不再次选择 // 原因:K3 改用 Sheet。关闭要看挂上的浮层,不再用源码里的 setSheetOpen 代替点按 const h = createClientLifecycleHarness(); const picked: string[] = []; type Node = ReturnType[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, / { 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(`${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); });