Files
next-tjwater-frontend/tests/browser/investigation-diagram-line-breaks.e2e.ts
T
jiang ec23d44b02 feat(agent): support Responses steering and project-aware tools
Use the dedicated TJWATER_AGENT_URL and Responses stream protocol. Keep running
turns active while users steer or queue follow-ups and edit pending delivery.
Refresh the model catalog before generation and preserve approval mode selection.

Integrate scoped investigation messaging/presentation, geographic place selection,
current Agent composer styles and supporting workbench contracts.

Validation: 77 focused Vitest checks, 21 browser cases, lint, typecheck and
production build. Preserve direct Server/Agent routing and project authentication.
2026-10-19 18:46:58 +08:00

102 lines
7.2 KiB
TypeScript

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('<!doctype html><html lang="zh-CN"><head><link rel="stylesheet" href="/src/styles.css"></head><body><main id="diagrams" style="max-width:640px;margin:auto;padding:16px"></main></body></html>');
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["上游监测<br>读取SCADA"]\nB --> C["形成边界<br/>结论"]\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") });
});