import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import React from "react"; import { PersonalReportPage } from "../src/components/personal-report/personal-report-page"; import { SidebarProvider } from "../src/components/ui/sidebar"; import { assembleReportFactTables } from "../src/lib/report-fact-tables"; import { containsHan } from "../src/lib/report-language"; import { createClientLifecycleHarness } from "./react-client-lifecycle-test-support"; // BUG-1126: the reader and the fact tables were sibling elements keyed by the // same language, so switching editions left the first edition's reader on // screen and appended the new one below it. Real reports always carry fact // tables, so the whole page is rendered with them. Fixtures are real engine // output for a fictional birth (AGENTS §7-4). Object.assign(globalThis, { React }); const engine = JSON.parse(readFileSync(new URL("./fixtures/report-density-fictional-engine.json", import.meta.url), "utf8")); const en = JSON.parse(readFileSync(new URL("./fixtures/report-reader-main-fictional-en.json", import.meta.url), "utf8")); const zh = JSON.parse(readFileSync(new URL("./fixtures/report-reader-main-fictional-zh-pair.json", import.meta.url), "utf8")); const REPORT_ID = "00000000-0000-4000-8000-000000001126"; const BODY = { report: { id: REPORT_ID, status: "ready", createdAt: "2026-09-30T00:00:00.000Z" }, englishEdition: "ready", longformMarkdown: zh.markdown, longformMarkdownEn: en.markdown, factTables: assembleReportFactTables(engine), factTablesEn: assembleReportFactTables(engine, "en"), }; type Node = ReturnType["elements"]>[number]; const hasClass = (node: Node, name: string) => (node.getAttribute("class") ?? "").split(/\s+/).includes(name); async function openReader(initialLanguage: "zh" | "en") { const h = createClientLifecycleHarness(); const location = { href: `https://staging.example/reports/${REPORT_ID}${initialLanguage === "en" ? "?lang=en" : ""}` }; const history = { state: null, replaceState: (_state: unknown, _title: string, url: URL) => { location.href = String(url); } }; Object.assign(globalThis.window as object, { location, history }); Object.assign(globalThis.document as object, { hidden: false, querySelector: () => null, querySelectorAll: () => [] }); const originalFetch = globalThis.fetch; // Lazy sections and the chapter jump schedule frames on the global; the harness only has them on window. const frameWindow = globalThis.window as unknown as { requestAnimationFrame: typeof requestAnimationFrame; cancelAnimationFrame: typeof cancelAnimationFrame }; const originalFrames = { request: globalThis.requestAnimationFrame, cancel: globalThis.cancelAnimationFrame }; Object.assign(globalThis, { requestAnimationFrame: frameWindow.requestAnimationFrame, cancelAnimationFrame: frameWindow.cancelAnimationFrame }); globalThis.fetch = (async () => new Response(JSON.stringify(BODY), { status: 200, headers: { "content-type": "application/json" } })) as typeof fetch; await h.render(); for (let i = 0; i < 5 && !h.elements().some(node => hasClass(node, "personal-report-md-layout")); i += 1) await h.idle(); const read = () => { const layouts = h.elements().filter(node => hasClass(node, "personal-report-md-layout")); const toc = h.elements().find(node => hasClass(node, "personal-report-toc-desktop")); const pressed = h.elements().find(node => node.tagName === "BUTTON" && node.getAttribute("aria-pressed") === "true"); // The article only: the 「到底部」 jump beside it is app chrome and stays Chinese (English edition decision 6). const articles = h.elements().filter(node => hasClass(node, "personal-report-md-article")); return { layouts: layouts.length, reader: articles.map(node => node.text).join(""), toc: toc?.childNodes[0]?.text, pressed: pressed?.text, url: location.href }; }; const pick = async (label: string) => { const button = h.elements().find(node => node.tagName === "BUTTON" && node.text === label); assert.ok(button, `${label} switch button`); await h.event(button); await h.idle(); }; const close = async () => { globalThis.fetch = originalFetch; Object.assign(globalThis, { requestAnimationFrame: originalFrames.request, cancelAnimationFrame: originalFrames.cancel }); await h.close(); }; return { h, read, pick, close }; } test("switching editions replaces the reader, fact tables and all (BUG-1126)", async () => { const page = await openReader("zh"); try { let state = page.read(); assert.equal(state.layouts, 1); assert.equal(state.toc, "目录"); assert.equal(state.pressed, "中文"); await page.pick("English"); state = page.read(); assert.equal(state.pressed, "English"); assert.match(state.url, /\?lang=en$/); assert.equal(state.layouts, 1, "the Chinese reader must not stay on screen"); assert.equal(state.toc, "Contents"); assert.equal(containsHan(state.reader), false, "the English reader carries no Chinese"); await page.pick("中文"); state = page.read(); assert.equal(state.pressed, "中文"); assert.equal(state.layouts, 1, "the English reader must not stay on screen"); assert.equal(state.toc, "目录"); assert.equal(page.h.errors.length, 0); } finally { await page.close(); } }); test("a report opened in English switches back to Chinese (BUG-1126)", async () => { const page = await openReader("en"); try { let state = page.read(); assert.equal(state.layouts, 1); assert.equal(state.toc, "Contents"); await page.pick("中文"); state = page.read(); assert.equal(state.layouts, 1); assert.equal(state.toc, "目录"); assert.ok(containsHan(state.reader)); assert.doesNotMatch(state.url, /lang=en/); } finally { await page.close(); } });