import { expect, test } from "@playwright/test"; import { answerUiDemoBlock } from "./support/answer-ui-demo"; import { mockAgentApi } from "./support/mock-agent-api"; import { captureReadingGeometry } from "./support/reading-geometry"; for (const width of [1600, 375, 320]) { test(`formal rich reports preserve drafts, map and versioned references at ${width}px`, async ({ page }, testInfo) => { await page.setViewportSize({ width, height: 960 }); await page.emulateMedia({ reducedMotion: "reduce" }); await mockAgentApi(page); // Load Vite's React preamble without mounting a second application. Its // global resize listeners would otherwise survive page.setContent(). await page.route("**/src/main.tsx", (route) => route.fulfill({ contentType: "application/javascript", body: "" })); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.unroute("**/src/main.tsx"); await page.setContent('
'); await page.evaluate(async ({ block, width }) => { const entry = await (await fetch("/src/main.tsx")).text(); const reactUrl = entry.match(/from "([^"\n]*\/react\.js[^"\n]*)"/)?.[1]; const clientUrl = entry.match(/from "([^"\n]*\/react-dom_client\.js[^"\n]*)"/)?.[1]; if (!reactUrl || !clientUrl) throw new Error("Vite React paths unavailable"); const reactModule = await import(reactUrl); const React = reactModule.default ?? reactModule; const clientModule = await import(clientUrl); const { createRoot } = clientModule.default ?? clientModule; const { InvestigationTaskReport } = await import("/src/features/workbench/components/investigation-task-report.tsx"); const { ReadingWorkspaceProvider } = await import("/src/shared/ui/reading-workspace.tsx"); const { useReadingWorkspace } = await import("/src/shared/ui/reading-workspace-context.ts"); const { WorkbenchMainFrame } = await import("/src/features/workbench/workspace/workbench-main-frame.tsx"); const now = "2026-10-20T08:00:00Z"; const report = { revision: 3, execution_generation: 2, published_at: now, report: { title: "供水压力演练核验", current_conclusion: "当前演练数据提示压力变化,仍待现场核验。", conclusion_boundary: "仅覆盖已加载的演练记录。", stage: "final", outcome: "concluded", data_gaps: [], recommendations: ["核对现场位置及对应时段。"], pending_items: [], evidence_refs: ["DEMO-001"], ui_blocks: [ { block, evidence_refs: ["DEMO-001"] }, { block: { version: "answer-ui@future", block_id: "future", fallback_text: "扩展分析摘要:等待下一次现场核验。", spec: {} }, evidence_refs: ["DEMO-001"] }, ], } }; const historical = { ...report, revision: 2, report: { ...report.report, current_conclusion: "历史结论:初次演练核验。" } }; const task = { task_id: "task-ui-demo", title: "供水压力演练核验", summary: "当前报告已发布", execution_generation: 2, lifecycle_status: "completed", status: "completed", updated_at: now, latest_report: report, latest_progress: null, latest_plan: null, actions: [], evidence: [{ evidence_id: "DEMO-001", kind: "synthetic", summary: "保存的演练监测记录,等待现场核验。", source_label: "演练监测记录", observed_at: now, recorded_at: now }], }; const noAction = () => undefined; function Harness() { const reading = useReadingWorkspace(); return React.createElement("div", { style: { "--workbench-agent-current-width": "400px" } }, React.createElement("div", { style: { position: "absolute", inset: 0, left: width >= 1024 ? 400 : 0 } }, React.createElement(WorkbenchMainFrame, { mapContent: React.createElement("div", { "data-map-instance": true }, "演练地图工作区"), timelineContent: null, timelineMode: "compact", timelineBottom: 0, activeArtifact: null, pendingArtifact: null, surfaceMode: "map_only", mapSplitAvailable: width >= 1360, readingSplitAvailable: width >= 1360, onOpenPendingArtifact: noAction, onDestroyPendingArtifact: noAction, onSetArtifactView: noAction, onCollapseArtifact: noAction, onDestroyArtifact: noAction })), React.createElement("aside", { "aria-label": "报告列表", style: { position: "absolute", inset: "0 auto 0 0", width: width >= 1024 ? 400 : "100%", overflow: "auto", padding: 16, display: width < 1024 && reading.active ? "none" : "block", } }, React.createElement(InvestigationTaskReport, { task, onContinue: (discussion) => { Object.assign(window, { quotedReport: discussion }); }, renderAction: noAction, renderEvidence: () => React.createElement("p", null, "原始演练记录"), onLoadReports: async () => ({ items: [report, historical], next_offset: null }), })), ); } createRoot(document.getElementById("reader")).render(React.createElement(ReadingWorkspaceProvider, null, React.createElement(Harness))); }, { block: answerUiDemoBlock, width }); await page.getByLabel("现场位置(必填)").fill("展开前的现场资料"); await expect(page.getByRole("navigation", { name: "报告章节" })).toBeHidden(); const map = page.locator("[data-map-instance]"); await map.evaluate((element) => Object.assign(window, { originalMap: element })); await page.getByRole("button", { name: "展开查看", exact: true }).click(); const workspace = page.getByRole("region", { name: "展开报告工作区", exact: true }); await expect(workspace).toContainText("当前演练数据提示压力变化,仍待现场核验。"); await expect(workspace.getByLabel("现场位置(必填)")).toHaveValue("展开前的现场资料"); await expect(page.getByLabel("现场位置(必填)")).toHaveCount(1); const chapters = workspace.getByRole("navigation", { name: "报告章节" }); await expect(chapters).toBeVisible(); await expect(chapters.getByRole("button", { name: "待办", exact: true })).toHaveCount(0); await chapters.getByRole("button", { name: "依据", exact: true }).focus(); await page.keyboard.press("Enter"); await expect(workspace.getByRole("region", { name: "主要依据", exact: true })).toBeFocused(); await testInfo.attach("chapter-jump-geometry", { body: JSON.stringify(await workspace.evaluate((root) => { const scroll = root.querySelector("[data-reading-scroll]")!; return { scrollTop: scroll.scrollTop, maxScroll: scroll.scrollHeight - scroll.clientHeight, sections: [...root.querySelectorAll(".investigation-report > section")].map((section) => ({ label: section.getAttribute("aria-label"), top: Math.round(section.getBoundingClientRect().top - scroll.getBoundingClientRect().top), })), }; }), null, 2), contentType: "application/json", }); await expect(chapters.getByRole("button", { name: "依据", exact: true })).toHaveAttribute("aria-current", "location"); const navigationTop = (await chapters.boundingBox())!.y; const scrollTop = (await workspace.locator("[data-reading-scroll]").boundingBox())!.y; expect(Math.abs(navigationTop - scrollTop)).toBeLessThan(2); await chapters.getByRole("button", { name: "分析", exact: true }).click(); await expect(workspace.getByRole("region", { name: "交互分析", exact: true })).toBeFocused(); await expect(workspace.getByLabel("现场位置(必填)")).toHaveValue("展开前的现场资料"); await page.evaluate(() => document.fonts.ready); await page.screenshot({ path: `test-results/formal-report-analysis-${width}.png` }); await expect(workspace).toContainText("扩展分析摘要:等待下一次现场核验。"); await workspace.getByRole("button", { name: /查看演练证据/ }).click(); await expect(workspace).toContainText("保存的演练监测记录,等待现场核验。"); if (width >= 1360) { await workspace.getByRole("button", { name: "地图对照" }).click(); await expect(page.locator("#main-workspace")).toHaveAttribute("data-layout-mode", "map_split"); await expect(map).toBeVisible(); const separator = page.getByRole("separator", { name: "调整地图与报告宽度" }); await expect(separator).toHaveAttribute("data-slot", "resizable-handle"); const mapPanel = page.locator("#workspace-map"); const initialWidth = (await mapPanel.boundingBox())!.width; const handle = (await separator.boundingBox())!; await page.mouse.move(handle.x + handle.width / 2, handle.y + handle.height / 2); await expect(separator).toHaveAttribute("data-separator", "hover"); await page.mouse.down(); await expect(separator).toHaveAttribute("data-separator", "active"); await page.mouse.move(handle.x + 100, handle.y + handle.height / 2); await expect.poll(async () => (await mapPanel.boundingBox())!.width).toBeGreaterThan(initialWidth + 90); await page.mouse.up(); await expect.poll(async () => (await mapPanel.boundingBox())!.width).toBeGreaterThan(initialWidth + 90); await separator.focus(); await page.keyboard.press("ArrowLeft"); await expect.poll(async () => (await mapPanel.boundingBox())!.width).toBeLessThan(initialWidth + 90); const resizedWidth = (await mapPanel.boundingBox())!.width; await expect(workspace.getByLabel("现场位置(必填)")).toHaveValue("展开前的现场资料"); await page.screenshot({ path: "test-results/resizable-report-map.png" }); await workspace.getByRole("button", { name: "专注阅读" }).click(); await expect(separator).toBeHidden(); await workspace.getByRole("button", { name: "地图对照" }).click(); await expect.poll(async () => (await mapPanel.boundingBox())!.width).toBeCloseTo(resizedWidth, 0); await workspace.getByRole("button", { name: "专注阅读" }).click(); } else { await workspace.getByRole("button", { name: "查看地图" }).click(); await expect(map).toBeVisible(); await page.getByRole("button", { name: "返回报告", exact: true }).click(); } expect(await map.evaluate((element) => element === (window as unknown as { originalMap: Element }).originalMap)).toBe(true); await workspace.locator("[data-reading-scroll]").evaluate((element) => { element.scrollTop = 0; }); await expect(chapters.getByRole("button", { name: "结论", exact: true })).toHaveAttribute("aria-current", "location"); await page.evaluate(() => document.fonts.ready); const geometry = await captureReadingGeometry(workspace, testInfo); expect(geometry.metricsHeight).toBeLessThan(145); expect(geometry.horizontalOverflow).toBe(false); await page.screenshot({ path: `test-results/formal-report-workspace-${width}.png` }); await workspace.getByRole("button", { name: "展开报告版本" }).click(); await page.keyboard.press("Escape"); await expect(page.getByRole("dialog", { name: "报告版本" })).toHaveCount(0); await expect(workspace).toBeVisible(); await workspace.getByRole("button", { name: "展开报告版本" }).click(); await page.getByRole("dialog", { name: "报告版本" }).getByRole("button", { name: /报告第 2 版/ }).click(); await expect(workspace).toContainText("历史结论:初次演练核验。"); await workspace.getByRole("tab", { name: "数据", exact: true }).click(); await workspace.getByRole("checkbox", { name: "选择第 2 行" }).check(); await workspace.locator("[data-interactive-table]").getByRole("button", { name: "针对这部分提问" }).click(); const quote = await page.evaluate(() => (window as unknown as { quotedReport: { context: { report_revision: number }; reference: { title: string; text: string } } }).quotedReport); expect(quote.context.report_revision).toBe(2); expect(quote.reference.title).toContain("报告第 2 版"); expect(quote.reference.text).toContain("0.28"); expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width); }); }