Use the dedicated TJWATER_AGENT_URL and Responses stream protocol. Keep running turns active while users steer or queue follow-ups and edit pending delivery. Refresh the model catalog before generation and preserve approval mode selection. Integrate scoped investigation messaging/presentation, geographic place selection, current Agent composer styles and supporting workbench contracts. Validation: 77 focused Vitest checks, 21 browser cases, lint, typecheck and production build. Preserve direct Server/Agent routing and project authentication.
126 lines
11 KiB
TypeScript
126 lines
11 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { writeFile } from "node:fs/promises";
|
|
import { mockAgentApi } from "./support/mock-agent-api";
|
|
|
|
test("调查图样式对照保留中文标题、分工和真实 SVG 关系", 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.setViewportSize({ width: 1536, height: 1250 });
|
|
await page.setContent(`<html lang="zh-CN"><head><link rel="stylesheet" href="/src/styles.css"></head><body>
|
|
<main id="style-comparison" style="padding:32px;background:#EFF2F7;min-height:100vh;color:#26334D;font-family:'Noto Sans SC Variable',sans-serif">
|
|
<h1 style="font-size:22px;font-weight:600;margin-bottom:8px">调查脉络 · 三种视觉表达</h1>
|
|
<p style="font-size:13px;color:#68778E;margin-bottom:24px">使用截图中的节点与关系制作的样式设计示例</p>
|
|
<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px">
|
|
<section><h2 style="font-size:16px;font-weight:600">A · 工程流程</h2><p style="font-size:12px;color:#68778E;margin:8px 0 16px">白底、分工分组、明确的节点和连线层次</p><div id="engineering"></div></section>
|
|
<section><h2 style="font-size:16px;font-weight:600">B · 墨色报告</h2><p style="font-size:12px;color:#68778E;margin:8px 0 16px">黑白、直角、虚实线区分分工,适合打印</p><div id="ink"></div></section>
|
|
<section><h2 style="font-size:16px;font-weight:600">C · 手绘白板</h2><p style="font-size:12px;color:#68778E;margin:8px 0 16px">纸色、手绘线条,适合讲解设计思路</p><div id="sketch"></div></section>
|
|
</div>
|
|
</main><style>
|
|
#style-comparison [data-streamdown="mermaid"], #style-comparison [data-streamdown="mermaid"] > div {max-height:none!important;background:transparent;}
|
|
#style-comparison [data-streamdown="mermaid"] [role="img"] {width:100%;}
|
|
#style-comparison .font-mono {display:none;}
|
|
</style></body></html>`);
|
|
const renderCandidate = async (candidate: string) => page.evaluate(async (selected) => {
|
|
const reactModule = await import("/node_modules/.vite/deps/react.js");
|
|
const React = reactModule.default ?? reactModule;
|
|
const clientModule = await import("/node_modules/.vite/deps/react-dom_client.js");
|
|
const { createRoot } = clientModule.default ?? clientModule;
|
|
const { MessageResponse } = await import("/src/shared/ai-elements/message.tsx");
|
|
const { investigationDiagramConfig, investigationDiagramPlugins, INVESTIGATION_DIAGRAM_FONT, styleInvestigationDiagram } = await import("/src/features/workbench/utils/investigation-diagram-style.ts");
|
|
const { investigationDiagram } = await import("/src/features/workbench/utils/investigation-presentation.ts");
|
|
const titles = ["读取后端任务上下文与授权范围", "查询授权范围内的现有数据", "核实事件位置与生产资产映射", "现场核验爆管情况与影响范围", "停水隔离", "抢修作业", "复水与回访"];
|
|
const edges = ["已读上下文后查询数据", "依据查询结果核实资产映射", "映射缺失需现场核验", "确认爆管后停水", "停水后抢修", "抢修后复水"];
|
|
const diagram = investigationDiagram({ execution_generation: 1, latest_report: null, latest_plan: { execution_generation: 1, plan: {
|
|
nodes: titles.map((title, index) => ({ id: `node${index}`, title, owner: index < 3 ? "agent" : "human", kind: "activity", evidence_refs: [], action_refs: [] })),
|
|
edges: edges.map((condition, index) => ({ from: `node${index}`, to: `node${index + 1}`, condition }))
|
|
} } }, null);
|
|
await document.fonts.load(`500 15px ${INVESTIGATION_DIAGRAM_FONT}`, titles.join(""));
|
|
let source = styleInvestigationDiagram(diagram);
|
|
let config = investigationDiagramConfig;
|
|
if (selected === "ink") {
|
|
source += "\nclassDef tjAgent fill:#FFFFFF,stroke:#4B5262,color:#29303E,stroke-width:1.3px;\nclassDef tjHuman fill:#FFFFFF,stroke:#4B5262,color:#29303E,stroke-width:1.3px,stroke-dasharray:5 3;";
|
|
config = { ...config, themeVariables: { ...config.themeVariables, clusterBkg: "#FFFFFF", clusterBorder: "#C5CBD5", lineColor: "#606A7B" },
|
|
themeCSS: config.themeCSS + " .node rect,.cluster rect {rx:0;ry:0;} .nodeLabel{font-weight:400;} .cluster-label .nodeLabel{background:#FFFFFF;}" };
|
|
}
|
|
if (selected === "sketch") {
|
|
source += "\nclassDef tjAgent fill:#EDF4E9,stroke:#788A6D,color:#3B5135,stroke-width:1.3px;\nclassDef tjHuman fill:#FFF4DC,stroke:#B39964,color:#6B572B,stroke-width:1.3px;";
|
|
config = { ...config, look: "handDrawn", flowchart: { ...config.flowchart, curve: "basis", rankSpacing: 44 },
|
|
themeVariables: { ...config.themeVariables, fontSize: "16px", clusterBkg: "#FFFCF4", clusterBorder: "#CAC3AF", lineColor: "#8E8979" },
|
|
themeCSS: config.themeCSS + " .cluster-label .nodeLabel{background:#FFFCF4;}" };
|
|
}
|
|
createRoot(document.getElementById(selected)).render(React.createElement(MessageResponse, {
|
|
plugins: investigationDiagramPlugins,
|
|
mermaid: { config }, controls: { mermaid: { panZoom: false, fullscreen: false, download: false, copy: false } },
|
|
children: "```mermaid\n" + source + "\n```"
|
|
}));
|
|
}, candidate);
|
|
for (const candidate of ["engineering", "ink", "sketch"]) {
|
|
await renderCandidate(candidate);
|
|
const surface = page.locator(`#${candidate}`);
|
|
await surface.scrollIntoViewIfNeeded();
|
|
await expect(surface.locator("svg[id]")).toHaveCount(1);
|
|
await expect(surface).toContainText("读取后端任务上下文与授权范围");
|
|
await expect(surface).toContainText("人工 · 现场协作");
|
|
await writeFile(testInfo.outputPath(`${candidate}.svg`), await surface.locator("svg[id]").evaluate((svg) => svg.outerHTML));
|
|
await surface.screenshot({ path: testInfo.outputPath(`${candidate}.png`) });
|
|
}
|
|
await page.locator("#style-comparison").screenshot({ path: testInfo.outputPath("investigation-style-comparison.png") });
|
|
await page.setViewportSize({ width: 1740, height: 1450 });
|
|
await page.evaluate(async () => {
|
|
const reactModule = await import("/node_modules/.vite/deps/react.js");
|
|
const React = reactModule.default ?? reactModule;
|
|
const clientModule = await import("/node_modules/.vite/deps/react-dom_client.js");
|
|
const { createRoot } = clientModule.default ?? clientModule;
|
|
const { MessageResponse } = await import("/src/shared/ai-elements/message.tsx");
|
|
const { investigationDiagramConfig, investigationDiagramPlugins, INVESTIGATION_DIAGRAM_FONT, styleInvestigationDiagram } = await import("/src/features/workbench/utils/investigation-diagram-style.ts");
|
|
const { investigationDiagram } = await import("/src/features/workbench/utils/investigation-presentation.ts");
|
|
const labels = ["工单接入与信息登记", "上游监测点数据质量核查", "上游演示指标阈值比对", "相邻片区同类投诉排查", "投诉分布范围确认", "阶段性判断", "二次供水水箱清洗记录核验", "现场采样", "水箱清洗处置", "复测", "居民回访"];
|
|
const links = [[0,1,"并行核查"],[0,2,"并行核查"],[0,3,"并行核查"],[0,4,"并行核查"],
|
|
[1,5,"数据可用性结论"],[2,5,"上游水质结论"],[3,5,"范围排除依据"],[4,5,"投诉分布依据"],
|
|
[5,7,"条件分支:需现场核验"],[5,6,"条件分支:疑似二次供水问题 → 核验清洗记录"],
|
|
[6,8,"条件分支:清洗记录缺失或异常 → 清洗处置"],[7,9,"条件分支:采样支持处置后复测"],
|
|
[8,9,"条件分支:处置后复测"],[9,10,"条件分支:复测结果反馈回访"]];
|
|
const diagram = investigationDiagram({ execution_generation: 1, latest_report: null, latest_plan: { execution_generation: 1, plan: {
|
|
nodes: labels.map((title, index) => ({ id: `parallel${index}`, title, owner: index < 6 ? "agent" : "human", kind: "activity",
|
|
evidence_refs: index < 7 ? [`design-example-${index}`] : [], action_refs: [] })),
|
|
edges: links.map(([from,to,condition]) => ({ from: `parallel${from}`, to: `parallel${to}`, condition }))
|
|
} } }, null);
|
|
await document.fonts.load(`500 15px ${INVESTIGATION_DIAGRAM_FONT}`, labels.join(""));
|
|
document.getElementById("style-comparison").remove();
|
|
const container = document.createElement("main"); container.id = "parallel-comparison";
|
|
container.style.cssText = "padding:32px;background:#FFFFFF;font-family:'Noto Sans SC Variable',sans-serif";
|
|
document.body.append(container);
|
|
createRoot(container).render(React.createElement(MessageResponse, {
|
|
plugins: investigationDiagramPlugins,
|
|
mermaid: { config: investigationDiagramConfig }, controls: { mermaid: { panZoom: false, fullscreen: false, download: false, copy: false } },
|
|
children: "```mermaid\n" + styleInvestigationDiagram(diagram) + "\n```"
|
|
}));
|
|
const style = document.createElement("style");
|
|
style.textContent = '#parallel-comparison [data-streamdown="mermaid"],#parallel-comparison [data-streamdown="mermaid"] > div {max-height:none!important;background:#FFFFFF} #parallel-comparison [data-streamdown="mermaid"] [role="img"]{width:100%} #parallel-comparison .font-mono{display:none}';
|
|
document.head.append(style);
|
|
});
|
|
const parallel = page.locator("#parallel-comparison");
|
|
await parallel.scrollIntoViewIfNeeded();
|
|
await expect(parallel.locator("svg[id]")).toHaveCount(1);
|
|
const titleGap = await parallel.evaluate((element) => {
|
|
const title = [...element.querySelectorAll(".cluster-label")].find((item) => item.textContent?.includes("Agent"));
|
|
const firstNode = [...element.querySelectorAll(".node")].find((item) => item.textContent?.includes("工单接入"));
|
|
return firstNode!.getBoundingClientRect().top - title!.getBoundingClientRect().bottom;
|
|
});
|
|
const captionOverlaps = await parallel.evaluate((element) => {
|
|
const labels = [...element.querySelectorAll(".edgeLabel")].map((label) => label.getBoundingClientRect());
|
|
return [...element.querySelectorAll(".cluster-label")].flatMap((caption) => {
|
|
const box = caption.getBoundingClientRect();
|
|
return labels.some((label) => box.left < label.right && box.right > label.left && box.top < label.bottom && box.bottom > label.top)
|
|
? [caption.textContent]
|
|
: [];
|
|
});
|
|
});
|
|
await writeFile(testInfo.outputPath("parallel-diagram.svg"), await parallel.locator("svg[id]").evaluate((svg) => svg.outerHTML));
|
|
await parallel.screenshot({ path: testInfo.outputPath("parallel-diagram.png") });
|
|
await writeFile(testInfo.outputPath("parallel-diagram-spacing.json"), JSON.stringify({ viewport: page.viewportSize(), titleGap }, null, 2));
|
|
expect(titleGap).toBeGreaterThanOrEqual(18);
|
|
expect(captionOverlaps).toEqual([]);
|
|
});
|