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") });
});