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=\{过往的报告"); assert.ok(card > 0 && archive > card, "generate card comes first, then 过往的报告"); assert.ok(centre.indexOf(" { 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(); 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/); });