Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
101 lines
6.1 KiB
TypeScript
101 lines
6.1 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(?: \/\*[^\n]*\*\/\n)? margin: var\(--space-6\) auto var\(--space-8\);/);
|
|
});
|