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.
This commit is contained in:
@@ -35,7 +35,7 @@ test("调查 SVG 兼容 HTML 换行并保留节点交互与错误兜底", async
|
||||
};
|
||||
const root = createRoot(document.getElementById("diagrams"));
|
||||
const render = () => root.render(React.createElement(React.Fragment, null,
|
||||
React.createElement(InvestigationDiagramView, { diagram, running: true,
|
||||
React.createElement(InvestigationDiagramView, { diagram,
|
||||
renderDetails: () => React.createElement("p", null, "已保存的 SCADA 引用依据") }),
|
||||
React.createElement(InvestigationDiagramView, { diagram: { title: "错误兜底检查", mermaid: "flowchart TD\nA[未闭合", node_refs: [] },
|
||||
renderDetails: () => null })
|
||||
@@ -47,7 +47,7 @@ test("调查 SVG 兼容 HTML 换行并保留节点交互与错误兜底", async
|
||||
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.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 引用依据");
|
||||
@@ -81,10 +81,17 @@ test("调查 SVG 兼容 HTML 换行并保留节点交互与错误兜底", async
|
||||
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())!;
|
||||
expect(narrowBox.width).toBeLessThanOrEqual(343);
|
||||
expect(narrowBox.height).toBeLessThanOrEqual(688);
|
||||
expect(narrowBox.x).toBeGreaterThanOrEqual(15);
|
||||
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);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect, test } from "@playwright/test";
|
||||
import { mockAgentApi } from "./support/mock-agent-api";
|
||||
|
||||
for (const width of [1440, 375, 320]) {
|
||||
test(`investigation diagram displays recorded node states at ${width}px`, async ({ page }, testInfo) => {
|
||||
test(`investigation diagram displays saved reference annotations at ${width}px`, async ({ page }, testInfo) => {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await mockAgentApi(page);
|
||||
@@ -18,84 +18,57 @@ for (const width of [1440, 375, 320]) {
|
||||
const clientModule = await import(clientUrl);
|
||||
const { createRoot } = clientModule.default ?? clientModule;
|
||||
const { InvestigationDiagramView } = await import("/src/features/workbench/components/investigation-diagram-view.tsx");
|
||||
const diagram = {
|
||||
title: "调查节点状态",
|
||||
mermaid: 'flowchart TD\nP[待启动核查] --> R[核对监测]:::tjAction\nR --> C[核查完成]:::tjHuman\nR --> W[等待系统结果]\nR --> H[等待人工复核]\nC --> F:::tjHuman\nC --> S[未选用分支]\nW --> X[取消分支]\nH --> U:::tjAction\nstyle R fill:#FFF6E8,stroke:#C4A168;',
|
||||
const { investigationDiagramSchema } = await import("/src/features/workbench/api/investigation-report.ts");
|
||||
const diagram = investigationDiagramSchema.parse({
|
||||
title: "调查流程参考",
|
||||
mermaid: "flowchart TD\nREF[核查方向] --> RESULT[监测结果]\nREF --> NEED[资料缺口]\nRESULT --> FAIL[首次尝试失败]\nRESULT --> OTHER[未采用分支]\nNEED --> OLD[旧执行中标记]:::tjRunning\nNEED --> UNSET[无标记步骤]",
|
||||
node_refs: [
|
||||
{ node_id: "P", label: "待启动核查", status: "pending", evidence_refs: [], action_refs: [] },
|
||||
{ node_id: "R", label: "核对监测", status: "running", evidence_refs: ["e1"], action_refs: ["50000000-0000-4000-8000-000000000001"] },
|
||||
{ node_id: "C", label: "核查完成", status: "completed", evidence_refs: [], action_refs: [] },
|
||||
{ node_id: "W", label: "等待系统结果", status: "waiting", evidence_refs: [], action_refs: [] },
|
||||
{ node_id: "H", label: "等待人工复核", status: "waiting_human", evidence_refs: [], action_refs: [] },
|
||||
{ node_id: "F", label: "查询失败", status: "failed", evidence_refs: [], action_refs: [] },
|
||||
{ node_id: "S", label: "未选用分支", status: "skipped", evidence_refs: [], action_refs: [] },
|
||||
{ node_id: "X", label: "取消分支", status: "cancelled", evidence_refs: [], action_refs: [] },
|
||||
{ node_id: "U", label: "状态未记录", evidence_refs: ["e2"], action_refs: [] }
|
||||
{ node_id: "REF", label: "核查方向", status: "reference" },
|
||||
{ node_id: "RESULT", label: "监测结果", status: "recorded" },
|
||||
{ node_id: "NEED", label: "资料缺口", status: "needs_input" },
|
||||
{ node_id: "FAIL", label: "首次尝试失败", status: "failed", detail: "此前计算超时,保留原尝试。" },
|
||||
{ node_id: "OTHER", label: "未采用分支", status: "not_taken" },
|
||||
{ node_id: "OLD", label: "旧执行中标记", status: "running" },
|
||||
{ node_id: "UNSET", label: "无标记步骤" }
|
||||
]
|
||||
};
|
||||
});
|
||||
const root = createRoot(document.getElementById("state-reader"));
|
||||
const render = (running: boolean) => root.render(React.createElement(InvestigationDiagramView, {
|
||||
diagram: { ...diagram, node_refs: [...diagram.node_refs] }, running,
|
||||
renderDetails: () => React.createElement("p", null, "状态来自节点快照,关联证据不代表步骤已经完成。")
|
||||
const render = () => root.render(React.createElement(InvestigationDiagramView, {
|
||||
diagram: { ...diagram, node_refs: [...diagram.node_refs] },
|
||||
renderDetails: () => React.createElement("p", null, "图中标记来自最近整理,实际任务状态在任务记录中查看。")
|
||||
}));
|
||||
Object.assign(window, { setDiagramState: (running: boolean, status = "running") => {
|
||||
diagram.node_refs[1].status = status;
|
||||
render(running);
|
||||
} });
|
||||
render(true);
|
||||
Object.assign(window, { updateReferenceAnnotation: () => { diagram.node_refs[0].status = "recorded"; render(); } });
|
||||
render();
|
||||
});
|
||||
const diagram = page.getByRole("region", { name: "调查脉络", exact: true });
|
||||
const node = (className: string) => diagram.locator(`svg g.node.${className}`);
|
||||
const shape = (className: string) => node(className).locator("rect,polygon,circle,ellipse,path").first();
|
||||
await expect(diagram.locator("svg g.node")).toHaveCount(9);
|
||||
for (const [className, fill, stroke] of [
|
||||
["tjPending", "rgb(246, 248, 252)", "rgb(165, 181, 204)"],
|
||||
["tjRunning", "rgb(232, 240, 255)", "rgb(66, 99, 199)"],
|
||||
["tjCompleted", "rgb(237, 247, 244)", "rgb(107, 158, 139)"],
|
||||
["tjWaiting", "rgb(255, 251, 234)", "rgb(200, 172, 96)"],
|
||||
["tjWaitingHuman", "rgb(255, 246, 232)", "rgb(196, 161, 104)"],
|
||||
["tjFailed", "rgb(254, 242, 242)", "rgb(217, 108, 117)"],
|
||||
["tjSkipped", "rgb(246, 248, 252)", "rgb(165, 181, 204)"],
|
||||
["tjCancelled", "rgb(241, 245, 249)", "rgb(148, 163, 184)"],
|
||||
["tjUnknown", "rgb(255, 255, 255)", "rgb(204, 214, 229)"]
|
||||
]) {
|
||||
await expect(node(className)).toHaveCount(1);
|
||||
await expect(shape(className)).toHaveCSS("fill", fill);
|
||||
await expect(shape(className)).toHaveCSS("stroke", stroke);
|
||||
await expect(diagram.locator("svg g.node")).toHaveCount(7);
|
||||
await expect(diagram.locator("svg g.node.tjReference")).toHaveCount(3);
|
||||
for (const [className, fill] of [["tjRecorded", "rgb(237, 247, 244)"], ["tjNeedsInput", "rgb(255, 251, 234)"],
|
||||
["tjFailed", "rgb(254, 242, 242)"], ["tjNotTaken", "rgb(246, 248, 252)"]]) {
|
||||
await expect(diagram.locator(`svg g.node.${className}`)).toHaveCount(1);
|
||||
await expect(diagram.locator(`svg g.node.${className}`).locator("rect,polygon").first()).toHaveCSS("fill", fill);
|
||||
}
|
||||
const legend = diagram.getByLabel("图示状态说明");
|
||||
await expect(legend).toContainText("等待依赖 / 外部结果");
|
||||
await expect(legend).toContainText("等待人工处理");
|
||||
await expect(legend).toContainText("未标注状态");
|
||||
expect(await shape("tjSkipped").evaluate((element) => getComputedStyle(element).strokeDasharray)).not.toBe("none");
|
||||
await diagram.getByRole("button", { name: /^核对监测 · 正在执行/ }).click();
|
||||
await expect(diagram.getByLabel("节点详情 核对监测")).toContainText("正在执行");
|
||||
await expect(diagram.locator("svg g.node.tjRunning,svg g.node.tjRecordedRunning")).toHaveCount(0);
|
||||
await expect(diagram).toContainText("不实时同步执行状态");
|
||||
await expect(diagram.getByLabel("图示状态说明")).toContainText("已有结果");
|
||||
await diagram.getByRole("button", { name: /^首次尝试失败 ·/ }).click();
|
||||
await expect(diagram.getByLabel("节点详情 首次尝试失败")).toContainText("此前计算超时");
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width);
|
||||
await diagram.screenshot({ path: testInfo.outputPath(`node-states-${width}.png`) });
|
||||
|
||||
await diagram.screenshot({ path: testInfo.outputPath(`reference-states-${width}.png`) });
|
||||
await diagram.getByRole("button", { name: "放大查看", exact: true }).click();
|
||||
const reader = page.getByRole("dialog", { name: "调查节点状态", exact: true });
|
||||
await expect(reader.locator("svg g.node.tjRunning > rect")).toHaveCSS("fill", "rgb(232, 240, 255)");
|
||||
const bounds = (await reader.boundingBox())!;
|
||||
expect(bounds.width).toBeLessThan(width);
|
||||
expect(bounds.height).toBeLessThan(900);
|
||||
const reader = page.getByRole("dialog", { name: "调查流程参考", exact: true });
|
||||
await expect(reader.locator("svg g.node")).toHaveCount(7);
|
||||
await reader.getByRole("button", { name: "Zoom in", exact: true }).click();
|
||||
const application = reader.getByRole("application");
|
||||
const transform = await application.evaluate((element) => {
|
||||
element.setAttribute("data-state-test-instance", "retained");
|
||||
element.setAttribute("data-reference-instance", "retained");
|
||||
return (element as HTMLElement).style.transform;
|
||||
});
|
||||
await page.evaluate(() => (window as unknown as { setDiagramState: (running: boolean) => void }).setDiagramState(false));
|
||||
const archived = reader.locator("svg g.node.tjRecordedRunning > rect");
|
||||
await expect(archived).toHaveCSS("fill", "rgb(246, 248, 252)");
|
||||
await expect(reader.getByLabel("图示状态说明")).toContainText("记录时执行中");
|
||||
await expect(reader.locator("svg g.node.tjCompleted > rect")).toHaveCSS("fill", "rgb(237, 247, 244)");
|
||||
await expect(reader.locator("svg g.node")).toHaveCount(9);
|
||||
await expect(application).toHaveAttribute("data-state-test-instance", "retained");
|
||||
await page.evaluate(() => (window as unknown as { updateReferenceAnnotation: () => void }).updateReferenceAnnotation());
|
||||
await expect(reader.locator("svg g.node.tjRecorded")).toHaveCount(2);
|
||||
await expect(reader.locator("svg g.node.tjFailed")).toHaveCount(1);
|
||||
await expect(application).toHaveAttribute("data-reference-instance", "retained");
|
||||
expect(await application.evaluate((element) => (element as HTMLElement).style.transform)).toBe(transform);
|
||||
await page.evaluate(() => (window as unknown as { setDiagramState: (running: boolean, status: string) => void }).setDiagramState(true, "completed"));
|
||||
await expect(reader.locator("svg g.node.tjCompleted")).toHaveCount(2);
|
||||
await expect(reader.locator("svg g.node.tjRunning")).toHaveCount(0);
|
||||
await expect(reader.getByLabel("节点详情 核对监测")).toContainText("已完成");
|
||||
await expect(reader).not.toContainText("正在执行");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -20,13 +20,19 @@ test("调查图加载与尺寸变化保持阅读位置,刷新后仍可缩放
|
||||
const renderer = investigationDiagramPlugins.mermaid.getMermaid.bind(investigationDiagramPlugins.mermaid);
|
||||
let release: (() => void) | undefined;
|
||||
let renderPending = false;
|
||||
let holdRender = true;
|
||||
let completedRenders = 0;
|
||||
investigationDiagramPlugins.mermaid.getMermaid = (config: unknown) => {
|
||||
const instance = renderer(config);
|
||||
return { ...instance, async render(id: string, source: string) {
|
||||
renderPending = true;
|
||||
await new Promise<void>((resolve) => { release = resolve; });
|
||||
renderPending = false;
|
||||
return instance.render(id, source);
|
||||
if (holdRender) {
|
||||
renderPending = true;
|
||||
await new Promise<void>((resolve) => { release = resolve; });
|
||||
renderPending = false;
|
||||
}
|
||||
const result = await instance.render(id, source);
|
||||
completedRenders++;
|
||||
return result;
|
||||
} };
|
||||
};
|
||||
const root = createRoot(document.getElementById("reader"));
|
||||
@@ -45,6 +51,8 @@ test("调查图加载与尺寸变化保持阅读位置,刷新后仍可缩放
|
||||
));
|
||||
Object.assign(window, { investigationFixture: {
|
||||
release: () => release?.(), pending: () => renderPending,
|
||||
hold: (value: boolean) => { holdRender = value; },
|
||||
completed: () => completedRenders,
|
||||
update: (next: string) => { source = next; render(); }
|
||||
}, documentHeights });
|
||||
render();
|
||||
@@ -82,6 +90,22 @@ test("调查图加载与尺寸变化保持阅读位置,刷新后仍可缩放
|
||||
await diagram.getByRole("button", { name: "Zoom in", exact: true }).click();
|
||||
await expect(diagram.locator('[role="application"]')).toHaveCSS("transform", "matrix(1.1, 0, 0, 1.1, 0, 0)");
|
||||
await diagram.getByRole("button", { name: "Reset zoom and pan", exact: true }).click();
|
||||
// A pending layout must not overwrite a newer source restored from cache.
|
||||
await page.evaluate(() => {
|
||||
const api = (window as unknown as { investigationFixture: { update: (source: string) => void } }).investigationFixture;
|
||||
api.update("flowchart LR\nA[读取工单] --> B[核对监测] --> C[迟到的排版]");
|
||||
});
|
||||
await expect.poll(() => fixture("pending")).toBe(true);
|
||||
await page.evaluate(() => {
|
||||
const api = (window as unknown as { investigationFixture: { hold: (value: boolean) => void; update: (source: string) => void } }).investigationFixture;
|
||||
api.hold(false);
|
||||
api.update("flowchart LR\nA[读取工单] --> B[核对监测]");
|
||||
});
|
||||
await expect.poll(() => page.evaluate(() => (window as unknown as { investigationFixture: { completed: () => number } }).investigationFixture.completed())).toBe(3);
|
||||
await fixture("release");
|
||||
await expect.poll(() => page.evaluate(() => (window as unknown as { investigationFixture: { completed: () => number } }).investigationFixture.completed())).toBe(4);
|
||||
await page.evaluate(() => new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))));
|
||||
await expect(diagram.locator("svg .node")).toHaveCount(2);
|
||||
const checkCollapse = async (keyboard: boolean) => {
|
||||
const before = await page.locator("#reading-anchor").evaluate((element) => element.getBoundingClientRect().top);
|
||||
await diagram.getByRole("button", { name: "放大查看", exact: true }).click();
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { investigationFixture, mockInvestigationPresentation, selectInvestigation, type SavedPresentationEvent } from "./support/mock-investigation-presentation";
|
||||
|
||||
const first = investigationFixture(71, "切换性能调查甲");
|
||||
const second = investigationFixture(72, "切换性能调查乙");
|
||||
const events: SavedPresentationEvent[] = [first, second].flatMap((task, taskIndex) =>
|
||||
Array.from({ length: 4 }, (_, index) => ({
|
||||
task_id: task.task_id, run_id: task.active_run!.run_id, execution_generation: 1,
|
||||
event_seq: 11 + taskIndex * 4 + index, occurred_at: "2026-11-10T00:00:00Z",
|
||||
command: { action: "open_chart", chart_id: `${taskIndex}-${index}`, evidence_refs: ["synthetic:reading"],
|
||||
chart: { title: `${taskIndex ? "乙" : "甲"}任务图表${index}`, chart_type: "line",
|
||||
x_data: ["07:30", "07:45", "08:00"], series: [{ name: taskIndex ? "乙任务序列" : "甲任务序列", data: [30, 29, 25] }] } },
|
||||
})));
|
||||
|
||||
test("shows the selected report and skeleton while history waits, without rebuilding the previous task's charts", async ({ page }, testInfo) => {
|
||||
await mockInvestigationPresentation(page, [first, second], [...events]);
|
||||
await selectInvestigation(page, first.title);
|
||||
const panel = page.locator('[data-presentation-scope="investigation"]');
|
||||
await expect(panel.getByRole("img", { name: "甲任务图表0", exact: true })).toBeVisible();
|
||||
await page.evaluate(async () => {
|
||||
const source = await (await fetch("/src/features/agent/answer-ui/responsive-chart.tsx")).text();
|
||||
const url = source.match(/from "([^"\n]*echarts-for-react[^"\n]*)"/)?.[1];
|
||||
if (!url) throw new Error("Vite chart module unavailable");
|
||||
const { default: Chart } = await import(url);
|
||||
const calls: string[] = [];
|
||||
Object.assign(window, { taskSwitchChartInitializations: calls });
|
||||
let prototype = Chart.prototype;
|
||||
while (prototype && !Object.hasOwn(prototype, "initEchartsInstance")) prototype = Object.getPrototypeOf(prototype);
|
||||
const original = prototype.initEchartsInstance;
|
||||
prototype.initEchartsInstance = function (...args: unknown[]) {
|
||||
calls.push(this.props.option.series[0].name);
|
||||
return original.apply(this, args);
|
||||
};
|
||||
});
|
||||
let release!: () => void;
|
||||
const waiting = new Promise<void>((resolve) => { release = resolve; });
|
||||
await page.route(`**/operational-tasks/${second.task_id}/presentation-events**`, async (route) => {
|
||||
await waiting;
|
||||
await route.fallback();
|
||||
});
|
||||
await page.getByRole("region", { name: "工况任务", exact: true }).getByText(second.title, { exact: true }).first().click();
|
||||
await expect(page.locator('article[aria-label="调查任务报告"] h2')).toHaveText(second.title);
|
||||
await expect(panel.getByRole("status", { name: "正在加载调查展示", exact: true })).toBeVisible();
|
||||
await expect(panel.getByRole("img", { name: "甲任务图表0", exact: true })).toHaveCount(0);
|
||||
expect(await page.evaluate(() => (window as unknown as { taskSwitchChartInitializations: string[] })
|
||||
.taskSwitchChartInitializations.filter((name) => name === "甲任务序列"))).toEqual([]);
|
||||
await panel.screenshot({ path: testInfo.outputPath("task-switch-loading-desktop.png") });
|
||||
release();
|
||||
await expect(panel.getByRole("img", { name: "乙任务图表0", exact: true })).toBeVisible();
|
||||
await expect(panel.getByRole("status", { name: "正在加载调查展示", exact: true })).toHaveCount(0);
|
||||
await expect(page.getByRole("dialog", { name: "图表管理", exact: true })).toHaveCount(0);
|
||||
expect(await page.evaluate(() => (window as unknown as { taskSwitchChartInitializations: string[] })
|
||||
.taskSwitchChartInitializations.filter((name) => name === "甲任务序列"))).toEqual([]);
|
||||
});
|
||||
|
||||
test("keeps loading feedback readable on a narrow viewport and discards a late task response", async ({ page }, testInfo) => {
|
||||
await page.setViewportSize({ width: 375, height: 800 });
|
||||
await mockInvestigationPresentation(page, [first, second], [...events]);
|
||||
await selectInvestigation(page, first.title);
|
||||
let release!: () => void;
|
||||
const waiting = new Promise<void>((resolve) => { release = resolve; });
|
||||
await page.route(`**/operational-tasks/${second.task_id}/presentation-events**`, async (route) => {
|
||||
await waiting;
|
||||
await route.fallback();
|
||||
});
|
||||
// The map task selector remains available independently of the report reader.
|
||||
const taskPanel = page.getByRole("region", { name: "工况任务", exact: true }).filter({ visible: true });
|
||||
await taskPanel.getByRole("button", { name: "收起工况任务", exact: true }).click();
|
||||
await taskPanel.getByText(second.title, { exact: true }).first().click();
|
||||
await expect(page.locator('article[aria-label="调查任务报告"] h2').filter({ visible: true })).toHaveText(second.title);
|
||||
const loading = page.getByRole("status", { name: "正在加载调查展示", exact: true });
|
||||
await expect(loading).toBeVisible();
|
||||
await taskPanel.getByRole("button", { name: "关闭工况任务", exact: true }).click();
|
||||
await expect(loading).toBeVisible();
|
||||
await page.screenshot({ path: testInfo.outputPath("task-switch-loading-mobile.png") });
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(375);
|
||||
await selectInvestigation(page, first.title);
|
||||
release();
|
||||
await expect(page.locator('article[aria-label="调查任务报告"] h2').filter({ visible: true })).toHaveText(first.title);
|
||||
await expect(page.getByRole("img", { name: "乙任务图表0", exact: true })).toHaveCount(0);
|
||||
});
|
||||
Reference in New Issue
Block a user