Files
Jyotisha/frontend/tests/report-reader-polish.test.tsx
T

89 lines
5.0 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import React from "react";
import { PersonalReportMarkdownView } from "../src/components/personal-report/personal-report-markdown-view";
import { REPORT_EDGE_JUMP_NEAR_END_PX, reportEdgeDirection } from "../src/components/personal-report/report-edge-jump";
import { createClientLifecycleHarness } from "./react-client-lifecycle-test-support";
// TASK-report-reader-polish-20260929 (BUG-1094): generate entry in the page
// body, 到底部 that lands on the real end, two-level TOC, report-only table bands.
Object.assign(globalThis, { React });
const read = (path: string) => readFileSync(new URL(path, import.meta.url), "utf8");
const centre = read("../src/components/personal-report/personal-report-center.tsx");
const globals = read("../src/app/globals.css");
function rule(selector: string): string {
const start = globals.indexOf(`${selector} {`);
assert.notEqual(start, -1, `missing rule: ${selector}`);
return globals.slice(start, globals.indexOf("}", start));
}
test("the report centre has one generate entry, in a card above the archive", () => {
assert.doesNotMatch(centre, /actions=\{<GeneratePersonalReportButton/, "the header button is gone");
assert.equal(centre.match(/<GeneratePersonalReportButton/g)?.length, 1);
const card = centre.indexOf('className="report-center-create"');
const archive = centre.indexOf("<h2>过往的报告</h2>");
assert.ok(card > 0 && archive > card, "generate card comes first, then 过往的报告");
assert.ok(centre.indexOf("<GeneratePersonalReportButton", card) < archive);
assert.doesNotMatch(centre, /报告记录|点击“生成完整报告”/, "old heading and mismatched button name are gone");
assert.match(rule(".report-center-create"), /align-items: center/);
const button = read("../src/components/personal-report/generate-personal-report-button.tsx");
assert.doesNotMatch(button, /variant="outline"|size="sm"/, "the page's one filled action");
});
test("the edge jump flips near the end and never shows both directions", () => {
assert.equal(reportEdgeDirection({ scrollTop: 0, scrollHeight: 10_000, clientHeight: 800 }), "end");
assert.equal(reportEdgeDirection({ scrollTop: 10_000 - 800 - REPORT_EDGE_JUMP_NEAR_END_PX, scrollHeight: 10_000, clientHeight: 800 }), "top");
assert.equal(reportEdgeDirection({ scrollTop: 0, scrollHeight: 600, clientHeight: 800 }), "top", "a short report has no bottom to jump to");
});
test("到底部 mounts every lazy section before it scrolls, in one click", async () => {
const markdown = ["# 报告", "", "## 摘要", "", "开头", "",
...Array.from({ length: 6 }, (_, index) => [`## 第${index + 1}章`, "", `正文${index + 1}`, ""]).flat()].join("\n");
const originalObserver = globalThis.IntersectionObserver;
// Nothing ever intersects: without the jump, lazy sections stay placeholders.
Object.defineProperty(globalThis, "IntersectionObserver", {
configurable: true, writable: true,
value: class { observe() {} disconnect() {} },
});
const h = createClientLifecycleHarness();
try {
const scroller = h.container;
scroller.setAttribute("class", "personal-report-reader");
scroller.scrollHeight = 12_345;
await h.render(<PersonalReportMarkdownView markdown={markdown} />);
assert.ok(!h.container.text.includes("正文6"), "far sections start unmounted");
const buttons = h.elements().filter(node => node.tagName === "BUTTON" && /到底部|回到顶部/.test(node.text));
assert.equal(buttons.length, 1);
assert.equal(buttons[0].text, "到底部");
await h.event(buttons[0]);
assert.ok(h.container.text.includes("正文6"), "every section rendered by the time the scroll runs");
assert.deepEqual(scroller.scrollCalls, [{ top: 12_345 }]);
assert.deepEqual(h.errors, []);
} finally {
await h.close();
Object.defineProperty(globalThis, "IntersectionObserver", { configurable: true, writable: true, value: originalObserver });
}
});
test("the edge jump stays off paper", () => {
const source = read("../src/components/personal-report/report-edge-jump.tsx");
assert.match(source, /className="report-edge-jump personal-report-screen-only"/);
assert.match(rule(".report-edge-jump"), /position: fixed/);
});
test("TOC chapters and sections read as two levels", () => {
assert.match(globals, /\.personal-report-toc li\[data-level="2"\] > a \{ color: var\(--color-ink\); font-weight: 600; \}/);
assert.match(globals, /\.personal-report-toc li\[data-level="3"\] \{ padding-left: var\(--space-5\); \}/);
});
test("zebra bands live on report tables only, never on chat tables", () => {
assert.match(globals, /\.personal-report-table-wrap tbody tr:nth-child\(even\),\n\.personal-report-fact-tables tbody tr:nth-child\(even\) \{/);
assert.match(globals, /\.personal-report-table-wrap thead th,\n\.personal-report-fact-tables thead th \{\n background: color-mix/);
assert.doesNotMatch(globals, /\.markdown-table tbody tr:nth-child\(even\)/);
assert.doesNotMatch(globals, /\.markdown-table thead th \{[^}]*background/);
});