Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
169 lines
10 KiB
TypeScript
169 lines
10 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
import React from "react";
|
|
|
|
import { BirthSkyEntry } from "../src/components/birth-sky/birth-sky-entry";
|
|
import { ChartSymbolHelp, TOUCH_BUBBLE_MS, placeBubble } from "../src/components/chart-page/chart-symbol-help";
|
|
import { VedicChartSvg } from "../src/components/personal-report/vedic-chart-svg";
|
|
import { assembleChartView } from "../src/lib/chart-view-load";
|
|
import type { ChartViewOk } from "../src/lib/chart-view-contract";
|
|
import { goldenSky } from "./birth-sky-test-support";
|
|
import { createClientLifecycleHarness } from "./react-client-lifecycle-test-support";
|
|
|
|
// Product feedback 2026-09-29 evening (BUG-1100..1103).
|
|
Object.assign(globalThis, { React });
|
|
const read = (path: string) => readFileSync(new URL(path, import.meta.url), "utf8");
|
|
const golden = JSON.parse(read("./fixtures/chart-view-golden.json"));
|
|
|
|
async function d1() {
|
|
const result = await assembleChartView({
|
|
userId: "synthetic", accountId: "synthetic", profileFingerprint: "fixture-v1",
|
|
profile: { name: "Synthetic", date: "1990-06-15", time: "12:00", placeLabel: "Synthetic", latitude: 39.9042, longitude: 116.4074, timezoneOffset: 8, timezoneId: "Asia/Shanghai", ayanamsa: "raman", birthTimeStatus: "confirmed" },
|
|
asOf: "2026-09-15", layers: ["varga"],
|
|
postEngine: async (path) => ({ status: "ok", payload: path === "/api/chart" ? golden.chart : golden.varga_full }),
|
|
});
|
|
return (result.body as ChartViewOk).vedic.vargas.find((varga) => varga.id === "D1")!.chart;
|
|
}
|
|
|
|
test("the bubble sits over the mark, flips below near the top and never leaves the stage (BUG-1101)", () => {
|
|
const stage = { left: 100, top: 50, width: 440 };
|
|
const middle = placeBubble({ left: 300, top: 250, width: 20, height: 20 }, stage);
|
|
assert.deepEqual(middle, { x: 210, y: 200, below: false });
|
|
const top = placeBubble({ left: 300, top: 60, width: 20, height: 20 }, stage);
|
|
assert.equal(top.below, true);
|
|
assert.equal(top.y, 30, "below the mark: its bottom edge");
|
|
assert.equal(placeBubble({ left: 102, top: 250, width: 10, height: 10 }, stage).x, 118, "clamped from the left edge");
|
|
assert.equal(placeBubble({ left: 530, top: 250, width: 10, height: 10 }, stage).x, 322, "clamped from the right edge");
|
|
});
|
|
|
|
test("hover shows a bubble at the mark; a tap keeps it past pointer-out and closes after 3s; no hint line (BUG-1101)", async () => {
|
|
const chart = await d1();
|
|
const h = createClientLifecycleHarness();
|
|
try {
|
|
await h.render(<ChartSymbolHelp resetKey="D1"><VedicChartSvg chart={chart} ariaLabel="测试盘" interactive /></ChartSymbolHelp>);
|
|
const stage = h.elements().find((node) => node.getAttribute("class") === "chart-page-symbol-help")!;
|
|
const bubble = () => h.elements().find((node) => node.getAttribute("role") === "tooltip");
|
|
const asc = h.elements().find((node) => node.getAttribute("data-chart-help") === "As · 上升")!;
|
|
assert.equal(bubble()?.text, undefined);
|
|
assert.ok(!h.container.text.includes("停在或轻点"), "the hint line is gone");
|
|
await h.event(stage, "onPointerOver", { target: asc, pointerType: "mouse" });
|
|
assert.equal(bubble()?.text, "As · 上升");
|
|
await h.event(stage, "onPointerOut", { target: asc, relatedTarget: null, pointerType: "mouse" });
|
|
assert.equal(bubble()?.text, undefined);
|
|
// Touch: pointer-over is ignored, the tap (pointer-up) opens, pointer-out and leave do not close.
|
|
await h.event(stage, "onPointerOver", { target: asc, pointerType: "touch" });
|
|
assert.equal(bubble()?.text, undefined);
|
|
await h.event(stage, "onPointerUp", { target: asc, pointerType: "touch" });
|
|
assert.equal(bubble()?.text, "As · 上升");
|
|
await h.event(stage, "onPointerOut", { target: asc, relatedTarget: null, pointerType: "touch" });
|
|
await h.event(stage, "onPointerLeave", { pointerType: "touch" });
|
|
assert.equal(bubble()?.text, "As · 上升");
|
|
await new Promise((resolve) => setTimeout(resolve, TOUCH_BUBBLE_MS + 50));
|
|
await h.idle();
|
|
assert.equal(bubble()?.text, undefined, "closes by itself");
|
|
assert.deepEqual(h.errors, []);
|
|
} finally { await h.close(); }
|
|
});
|
|
|
|
test("the chart card hugs the drawing on desktop (BUG-1100)", () => {
|
|
const css = read("../src/app/globals.css");
|
|
assert.match(css, /\.chart-page-vedic-stage \{ width: 100%; max-width: calc\(440px \+ 2 \* var\(--space-3\) \+ 2px\); \}/);
|
|
assert.doesNotMatch(read("../src/components/chart-page/chart-symbol-help.tsx"), /停在或轻点盘内标记可查看说明/);
|
|
});
|
|
|
|
test("the sky button shows while the cover paints and opens the moment it lands (BUG-1102)", async () => {
|
|
const h = createClientLifecycleHarness();
|
|
try {
|
|
await h.render(<BirthSkyEntry cover={null} />);
|
|
const button = h.elements().find((node) => node.tagName === "BUTTON")!;
|
|
assert.match(button.text, /那一刻的天空/);
|
|
await h.event(button);
|
|
// Remembered, nothing opened yet and nothing spinning.
|
|
assert.ok(!h.container.text.includes("正在加载"));
|
|
const cover = { key: "self:fp", sky: goldenSky("day"), blob: new Blob([], { type: "image/png" }), url: "blob:birth-sky", serif: false };
|
|
await h.render(<BirthSkyEntry cover={cover} />);
|
|
await h.idle();
|
|
assert.deepEqual(h.errors, []);
|
|
} finally { await h.close(); }
|
|
const view = read("../src/components/chart-page/chart-page-view.tsx");
|
|
assert.match(view, /\(birthSky \|\| birthSkyPending\) \? <BirthSkyEntry cover=\{birthSky \?\? null\} \/>/);
|
|
const hook = read("../src/hooks/use-birth-sky-cover.ts");
|
|
assert.match(hook, /pending: key !== null && current === null && settledKey !== key/);
|
|
});
|
|
|
|
test("report centre card: spaced from the header, title says what, button says do (BUG-1103)", () => {
|
|
const centre = read("../src/components/personal-report/personal-report-center.tsx");
|
|
assert.match(centre, /<h2 id="report-center-create-title">完整本命报告<\/h2>/);
|
|
assert.doesNotMatch(centre, /<h2[^>]*>生成个人报告/);
|
|
assert.match(read("../src/app/globals.css"), /\.report-center-create \{\n width: min\(900px, 100%\);\n(?: \/\*[\s\S]*?\*\/\n)? margin: var\(--space-10\) auto var\(--space-8\);/);
|
|
});
|
|
|
|
// Phone check 2026-09-30 (BUG-1106..1107).
|
|
test("opening a report shows one waiting screen, the same from the route and from the reader (BUG-1106)", () => {
|
|
const route = read("../src/app/(app)/reports/[reportId]/loading.tsx");
|
|
assert.match(route, /return <ReportOpening \/>;/);
|
|
assert.doesNotMatch(route, /InlineSpinner|<main/);
|
|
const page = read("../src/components/personal-report/personal-report-page.tsx");
|
|
assert.match(page, /if \(state\.phase === "loading"\) return <ReportOpening \/>;/);
|
|
const opening = read("../src/components/personal-report/report-opening.tsx");
|
|
assert.match(opening, /<SecondaryHeader title="个人报告" \/>/);
|
|
});
|
|
|
|
test("on phones the reader bar is one row and the export sheet leaves room for the list (BUG-1107)", () => {
|
|
const css = read("../src/app/globals.css");
|
|
const phone = css.slice(css.indexOf(" .personal-report-back-label { display: none; }") - 800, css.indexOf(" .personal-report-back-label { display: none; }") + 400);
|
|
assert.match(phone, /\.personal-report-action-end \{ flex-wrap: nowrap;/);
|
|
assert.match(read("../src/components/personal-report/report-actions.tsx"), /aria-label="返回报告中心"[\s\S]*<span className="personal-report-back-label">返回报告中心<\/span>/);
|
|
// The export sheet was redesigned the same night (two steps, no presets); its own tests are in report-export-drawer.test.tsx.
|
|
const drawer = read("../src/components/personal-report/report-export-drawer.tsx");
|
|
assert.match(drawer, /aria-label="关闭导出" title="关闭"><X aria-hidden="true" \/>/);
|
|
assert.doesNotMatch(drawer, /report-export-presets|常用|全部分盘|仅核对表/);
|
|
});
|
|
|
|
test("switching editions keeps the reader on the same chapter (BUG-1108)", () => {
|
|
const page = read("../src/components/personal-report/personal-report-page.tsx");
|
|
assert.match(page, /const chapter = currentChapterIndex\(\);\n setLanguage\(next\);/);
|
|
assert.match(page, /setDrawnThrough\(chapter\);/);
|
|
assert.match(page, /drawnThrough=\{drawnThrough\}/);
|
|
const view = read("../src/components/personal-report/personal-report-markdown-view.tsx");
|
|
assert.match(view, /drawn=\{drawnThrough !== null && chapter <= drawnThrough\}/);
|
|
assert.match(view, /useState\(section\.eager \|\| drawn\)/);
|
|
});
|
|
|
|
// Product feedback 2026-09-30 01:00.
|
|
test("the home no longer says an unfinished rectification is waiting in history", async () => {
|
|
const { rectificationEntryHint } = await import("../src/components/starter-home");
|
|
assert.equal(rectificationEntryHint(true), null);
|
|
assert.doesNotMatch(read("../src/components/starter-home.tsx"), /上次那次校正还没完成/);
|
|
});
|
|
|
|
test("chart-type cells show the whole meaning line (two lines, CJK-safe line height)", () => {
|
|
const css = read("../src/app/globals.css");
|
|
const small = css.slice(css.indexOf(".chart-page-type-cell small {"), css.indexOf("}", css.indexOf(".chart-page-type-cell small {")));
|
|
assert.match(small, /line-height: 1\.4/);
|
|
assert.match(small, /-webkit-line-clamp: 2/);
|
|
assert.doesNotMatch(small, /white-space: nowrap/);
|
|
});
|
|
|
|
test("chart, ephemeris and report waits show the orbit, the sentence only for screen readers", () => {
|
|
const orbit = read("../src/components/orbit-waiting.tsx");
|
|
assert.match(orbit, /<span className="sr-only">\{label\}<\/span>/);
|
|
assert.match(orbit, /app-loading-orbit/);
|
|
assert.match(read("../src/components/secondary-page-shell.tsx"), /<OrbitWaiting className="secondary-page-waiting" label=\{waiting\} \/>/);
|
|
// 原值: /<OrbitWaiting className="chart-page-skeleton-copy" label=\{copy\} \/>/ (orbit logo over the empty chart)
|
|
// 新值: no orbit over the chart; the sentence is an sr-only span inside the role=status stage
|
|
// 原因: 产品 2026-09-30「星盘的加载动画太丑了」,选「宫位依次点亮」——空盘本身就是等待动画
|
|
const chartWait = read("../src/components/chart-page/chart-page-skeleton.tsx");
|
|
assert.doesNotMatch(chartWait, /OrbitWaiting/);
|
|
assert.match(chartWait, /<span className="sr-only">\{copy\}<\/span>/);
|
|
assert.doesNotMatch(read("../src/components/chart-page/chart-page-skeleton.tsx"), /<p className="chart-page-note/);
|
|
});
|
|
|
|
test("the sky button carries a drawn constellation, not the sparkle", () => {
|
|
const entry = read("../src/components/birth-sky/birth-sky-entry.tsx");
|
|
assert.doesNotMatch(entry, /Sparkles/);
|
|
assert.match(entry, /<ConstellationIcon \/>/);
|
|
assert.equal((entry.match(/<circle /g) ?? []).length, 4);
|
|
});
|