import { expect, test } from "@playwright/test"; import { mockAgentApi } from "./support/mock-agent-api"; test("调查 SVG 兼容 HTML 换行并保留节点交互与错误兜底", async ({ page }, testInfo) => { await page.route("http://127.0.0.1:8787/api/v1/**", (route) => route.fulfill({ json: { items: [] } })); await mockAgentApi(page); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.setContent('
'); await page.evaluate(async () => { // Reuse Vite's exact module identities, including its per-port cache and // version query; importing a second React breaks the component's hooks. 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 module paths unavailable"); const reactModule = await import(reactUrl); const React = reactModule.default ?? reactModule; const clientModule = await import(clientUrl); const { createRoot } = clientModule.default ?? clientModule; const { InvestigationDiagramView } = await import("/src/features/workbench/components/investigation-diagram-view.tsx"); const { investigationDiagramPlugins } = await import("/src/features/workbench/utils/investigation-diagram-style.ts"); const getMermaid = investigationDiagramPlugins.mermaid.getMermaid.bind(investigationDiagramPlugins.mermaid); Object.assign(window, { diagramRenderCount: 0 }); investigationDiagramPlugins.mermaid.getMermaid = (config: unknown) => { const renderer = getMermaid(config); return { ...renderer, render(id: string, source: string) { if (source.includes("读取任务上下文")) (window as unknown as { diagramRenderCount: number }).diagramRenderCount++; return renderer.render(id, source); } }; }; let diagram = { title: "换行回归检查", mermaid: 'flowchart TD\nsubgraph Evidence["证据核对"]\nA["读取任务上下文"] --> B["上游监测
读取SCADA"]\nB --> C["形成边界
结论"]\nend', node_refs: [{ node_id: "B", label: "上游监测", evidence_refs: ["e1"], action_refs: [], status: "running" }] }; const root = createRoot(document.getElementById("diagrams")); const render = () => root.render(React.createElement(React.Fragment, null, React.createElement(InvestigationDiagramView, { diagram, running: true, renderDetails: () => React.createElement("p", null, "已保存的 SCADA 引用依据") }), React.createElement(InvestigationDiagramView, { diagram: { title: "错误兜底检查", mermaid: "flowchart TD\nA[未闭合", node_refs: [] }, renderDetails: () => null }) )); Object.assign(window, { updateDiagram: () => { diagram = { ...diagram, mermaid: `${diagram.mermaid}\nC --> D[补齐现场资料]` }; render(); } }); render(); }); const graph = page.getByRole("region", { name: "调查脉络", exact: true }).first(); await expect(graph.locator("svg .node")).toHaveCount(3); await expect(graph.locator("svg foreignObject br")).toHaveCount(2); await expect(graph.locator("svg .cluster-label")).toContainText("证据核对"); await expect(graph.locator("svg .tjRunning")).toHaveCount(1); await expect(graph).not.toContainText("This page contains the following errors"); await graph.locator(".node").filter({ hasText: "读取SCADA" }).click(); await expect(graph.getByLabel("节点详情 上游监测")).toContainText("已保存的 SCADA 引用依据"); await graph.getByRole("button", { name: "Zoom in", exact: true }).click(); await graph.getByRole("button", { name: "Reset zoom and pan", exact: true }).click(); await graph.locator("svg.flowchart").evaluate((element) => { Object.assign(window, { originalDiagram: element }); }); await graph.getByRole("button", { name: "放大查看", exact: true }).click(); const detail = page.getByRole("dialog", { name: "换行回归检查", exact: true }); await expect(detail.locator("svg .node")).toHaveCount(3); expect(await page.evaluate(() => (window as unknown as { diagramRenderCount: number }).diagramRenderCount)).toBe(1); expect(await detail.locator("svg.flowchart").evaluate((element) => element === (window as unknown as { originalDiagram: Element }).originalDiagram)).toBe(true); await expect(detail.getByLabel("节点详情 上游监测")).toContainText("已保存的 SCADA 引用依据"); await detail.getByRole("button", { name: "Zoom in", exact: true }).click(); const box = (await detail.boundingBox())!; expect(box.width).toBeLessThan(1440 - 32); expect(box.height).toBeLessThan(900); expect(box.x).toBeGreaterThan(16); expect(await page.evaluate(() => document.fullscreenElement === null)).toBe(true); const close = page.getByRole("button", { name: "关闭详情", exact: true }); await expect(close).toBeVisible(); await page.keyboard.press("Escape"); await expect(close).toHaveCount(0); await expect(graph.getByRole("button", { name: "放大查看", exact: true })).toBeFocused(); await expect(graph.locator('[role="application"]')).toHaveCSS("transform", "matrix(1.1, 0, 0, 1.1, 0, 0)"); // Streamdown renders diagrams lazily when their block enters the viewport. await page.getByRole("region", { name: "调查脉络", exact: true }).nth(1).scrollIntoViewIfNeeded(); await expect(page.getByText("图示暂不可用,仍可查看结论、证据和待审核动作。", { exact: true })).toBeVisible(); await graph.screenshot({ path: testInfo.outputPath("line-breaks-desktop.png") }); await page.setViewportSize({ width: 375, height: 800 }); await expect(graph.locator("svg .node")).toHaveCount(3); await graph.getByRole("button", { name: "放大查看", exact: true }).click(); await expect(detail.locator("svg .node")).toHaveCount(3); expect(await page.evaluate(() => (window as unknown as { diagramRenderCount: number }).diagramRenderCount)).toBe(1); const narrowBox = (await detail.boundingBox())!; expect(narrowBox.width).toBeLessThanOrEqual(343); expect(narrowBox.height).toBeLessThanOrEqual(688); expect(narrowBox.x).toBeGreaterThanOrEqual(15); await detail.screenshot({ path: testInfo.outputPath("diagram-expanded-narrow.png") }); await page.evaluate(() => (window as unknown as { updateDiagram: () => void }).updateDiagram()); await expect(detail.locator("svg .node")).toHaveCount(4); expect(await page.evaluate(() => (window as unknown as { diagramRenderCount: number }).diagramRenderCount)).toBe(2); await close.click(); await expect(detail).toHaveCount(0); await expect(graph.locator("svg .node")).toHaveCount(4); await graph.getByRole("button", { name: "放大查看", exact: true }).click(); await expect(detail.locator("svg .node")).toHaveCount(4); expect(await page.evaluate(() => (window as unknown as { diagramRenderCount: number }).diagramRenderCount)).toBe(2); await page.keyboard.press("Escape"); await expect(detail).toHaveCount(0); await graph.screenshot({ path: testInfo.outputPath("line-breaks-mobile.png") }); });