Files
next-tjwater-frontend/tests/browser/investigation-graph-updates.e2e.ts
T
jiang a617aa95e9 fix(workbench): stabilize investigation presentation rendering
Keep investigation graphs stable during updates, defer expensive chart rendering, and prevent stale results when switching tasks. Adopt reference annotations and consolidate frontend documentation.
2026-11-11 19:10:41 +08:00

93 lines
6.5 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { mockAgentApi } from "./support/mock-agent-api";
for (const width of [1440, 375]) {
test(`retains failed investigation paths through updates and new rounds at ${width}px`, async ({ page }, testInfo) => {
await page.setViewportSize({ width, height: 1000 });
await page.emulateMedia({ reducedMotion: "reduce" });
await mockAgentApi(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.setContent('<html lang="zh-CN"><head><link rel="stylesheet" href="/src/styles.css"></head><body><main id="reader" class="workbench-shadcn-surface bg-background text-foreground" style="max-width:900px;margin:auto;padding:12px"></main></body></html>');
await page.evaluate(async () => {
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 { InvestigationTaskReport } = await import("/src/features/workbench/components/investigation-task-report.tsx");
const { investigationDiagramSchema } = await import("/src/features/workbench/api/investigation-report.ts");
const now = "2026-11-10T01:00:00Z";
const initial = investigationDiagramSchema.parse({
title: "SCADA 累计调查脉络", source_mermaid: null,
mermaid: 'flowchart TD\nread["读取全量 SCADA"]\nlocate_1["第一次定位(失败)"]\nread --> locate_1',
node_refs: [
{ node_id: "read", label: "读取全量 SCADA", status: "recorded" },
{ node_id: "locate_1", label: "第一次定位", status: "failed", detail: "计算超时,已保留本次失败记录。" }
], edges: [{ source: "read", target: "locate_1" }]
});
const report = { revision: 1, execution_generation: 1, published_at: now, source_session_id: "session-1", report: {
title: "SCADA 调查", current_conclusion: "首次定位超时,继续核查。", conclusion_boundary: "现有资料",
stage: "final", outcome: "concluded", data_gaps: [], evidence_refs: [], recommendations: [], pending_items: [],
diagram: initial
} };
let task = { task_id: "cumulative-graph", title: "SCADA 数据问题调查", summary: "保留失败路径后继续调查",
execution_generation: 1, status: "running", lifecycle_status: "open", latest_report: report,
latest_plan: null, actions: [], evidence: [], questions: [],
active_run: { run_id: "run-1", status: "running" },
latest_progress: { revision: 1, run_id: "run-1", execution_generation: 1, published_at: now,
progress: { summary: "首次定位失败", diagram: initial } }
};
const root = createRoot(document.getElementById("reader"));
const render = () => root.render(React.createElement(InvestigationTaskReport, {
task, onContinue: () => undefined, renderAction: () => null, renderEvidence: () => null,
onLoadReports: async () => ({ items: [report], next_offset: null })
}));
Object.assign(window, { advanceInvestigationGraph: () => {
const diagram = investigationDiagramSchema.parse({ ...initial,
mermaid: initial.mermaid + '\nlocate_2["调整窗口重试"]\nlocate_1 -->|"重试"| locate_2',
node_refs: [...initial.node_refs, { node_id: "locate_2", label: "调整窗口重试", status: "recorded" }],
edges: [...initial.edges!, { source: "locate_1", target: "locate_2", label: "重试" }]
});
task = { ...task, execution_generation: 2, active_run: { run_id: "run-2", status: "running" },
latest_progress: { ...task.latest_progress, execution_generation: 2, run_id: "run-2", revision: 1,
progress: { summary: "重试成功,原失败记录保留", diagram } }
};
render();
}, startNextGraphRound: () => { task = { ...task, execution_generation: 3 }; render(); },
stopGraphRun: () => {
Object.assign(task, { active_run: null, status: "completed", lifecycle_status: "completed" });
render();
} });
render();
});
const graph = page.getByRole("region", { name: "调查脉络", exact: true });
await expect(graph.locator("svg g.node")).toHaveCount(2);
await expect(graph.locator("svg g.node.tjFailed")).toHaveCount(1);
await graph.getByRole("button", { name: /^第一次定位 ·/ }).click();
await expect(graph.getByLabel("节点详情 第一次定位")).toContainText("计算超时");
await page.evaluate(() => (window as unknown as { advanceInvestigationGraph: () => void }).advanceInvestigationGraph());
await expect(graph.locator("svg g.node")).toHaveCount(3);
await expect(graph.locator("svg g.node.tjFailed")).toHaveCount(1);
await expect(graph.locator("svg g.node.tjRecorded")).toHaveCount(2);
await expect(graph.locator("svg g.node").filter({ hasText: "第一次定位(失败)" })).toHaveCount(1);
await expect(graph.getByLabel("节点详情 第一次定位")).toContainText("计算超时");
await page.evaluate(() => (window as unknown as { startNextGraphRound: () => void }).startNextGraphRound());
await expect(graph.locator("svg g.node")).toHaveCount(3);
await expect(graph.locator("svg g.node.tjFailed > rect")).toHaveCSS("fill", "rgb(254, 242, 242)");
await page.evaluate(() => (window as unknown as { stopGraphRun: () => void }).stopGraphRun());
await expect(graph.locator("svg g.node.tjRecorded")).toHaveCount(2);
await expect(graph.locator("svg g.node.tjFailed")).toHaveCount(1);
await expect(graph).toContainText("不实时同步执行状态");
await graph.screenshot({ path: testInfo.outputPath(`cumulative-failed-path-${width}.png`) });
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width);
await page.getByRole("button", { name: "展开报告版本", exact: true }).click();
await page.getByRole("dialog", { name: "报告版本", exact: true }).getByRole("button", { name: /第 1 轮 · 报告第 1 版/ }).click();
await expect(graph.locator("svg g.node")).toHaveCount(2);
await page.getByRole("button", { name: "返回实时内容", exact: true }).click();
await expect(graph.locator("svg g.node")).toHaveCount(3);
});
}