Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
89 lines
5.0 KiB
TypeScript
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/);
|
|
});
|