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:
@@ -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("正在执行");
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user