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) => { await page.setViewportSize({ width, height: 900 }); await page.emulateMedia({ reducedMotion: "reduce" }); await mockAgentApi(page); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.setContent('
'); 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 { 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;', 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: [] } ] }; 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, "状态来自节点快照,关联证据不代表步骤已经完成。") })); Object.assign(window, { setDiagramState: (running: boolean, status = "running") => { diagram.node_refs[1].status = status; render(running); } }); render(true); }); 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); } 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("正在执行"); expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width); await diagram.screenshot({ path: testInfo.outputPath(`node-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); 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"); 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"); 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("已完成"); }); }