Files
Jyotisha/frontend/tests/surface-feedback-20260929.test.tsx
T

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\);/);
});