Keep investigation graphs stable during updates, defer expensive chart rendering, and prevent stale results when switching tasks. Adopt reference annotations and consolidate frontend documentation.
109 lines
7.6 KiB
TypeScript
109 lines
7.6 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,
|
|
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 .tjReference")).toHaveCount(3);
|
|
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);
|
|
await detail.evaluate(async (element) => {
|
|
await Promise.allSettled(element.getAnimations()
|
|
.filter((animation) => Number.isFinite(Number(animation.effect?.getComputedTiming().endTime)))
|
|
.map((animation) => animation.finished));
|
|
});
|
|
const narrowBox = (await detail.boundingBox())!;
|
|
const viewport = page.viewportSize()!;
|
|
expect(narrowBox.x).toBeGreaterThanOrEqual(0);
|
|
expect(narrowBox.y).toBeGreaterThanOrEqual(0);
|
|
expect(narrowBox.x + narrowBox.width).toBeLessThanOrEqual(viewport.width);
|
|
expect(narrowBox.y + narrowBox.height).toBeLessThanOrEqual(viewport.height);
|
|
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") });
|
|
});
|