Files
Jyotisha/frontend/tests/report-language-switch-20260930.test.tsx
T
Jesse_ChenandClaude Opus 5.5 d87ec44aab
Independent Staging Quality Gate / validate (push) Successful in 14m44s
Independent Staging Quality Gate / publish (push) Successful in 3m32s
fix(reports): switching editions replaces the reader instead of stacking it (BUG-1126)
The reader and the fact tables were siblings keyed by the same language, so
React left the first edition's reader on screen and appended the new one.
Distinct keys per sibling; whole-page switch test with real engine fact tables.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
2026-09-30 17:13:28 +08:00

112 lines
5.9 KiB
TypeScript

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<ReturnType<typeof createClientLifecycleHarness>["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(<SidebarProvider><PersonalReportPage reportId={REPORT_ID} initialLanguage={initialLanguage} /></SidebarProvider>);
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(); }
});