import { expect, test, type Page } from "playwright/test"; test("frontend actions replace SCADA analysis results and remain legible at 375px", async ({ page }) => { const receipts: Array> = []; const wfsRequests: string[] = []; await mockAgentActions(page, receipts, wfsRequests); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.waitForFunction(() => Boolean(window.__waterNetworkMap?.getSource("agent-scada-analysis")) ); await expect( page.getByRole("button", { name: "选择 Agent 模型" }).filter({ visible: true }).first() ).toContainText("测试模型"); await page.evaluate(() => { const map = window.__waterNetworkMap; [ "agent-scada-analysis-label", "agent-scada-analysis-indicator", "agent-scada-analysis-level", "agent-scada-analysis-casing" ].forEach((id) => { if (map?.getLayer(id)) map.removeLayer(id); }); }); const prompt = page .getByPlaceholder(/输入调度问题/) .filter({ visible: true }) .first(); await prompt.fill("将 MP01 标为高、MP02 标为低并显示编号"); await page .getByRole("button", { name: "发送 Agent 指令" }) .filter({ visible: true }) .first() .click(); await page.waitForFunction(() => { const source = window.__waterNetworkMap?.getSource("agent-scada-analysis") as unknown as { _data?: { features?: Array<{ properties?: Record }> }; }; const features = source?._data?.features ?? []; return features.length === 1 && features[0].properties?.sensor_id === "MP02"; }); await page.waitForFunction(() => { const map = window.__waterNetworkMap; return Boolean( map?.isSourceLoaded("agent-scada-analysis") && map.queryRenderedFeatures(undefined, { layers: ["agent-scada-analysis-level"] }).length > 0 && map.queryRenderedFeatures(undefined, { layers: ["agent-scada-analysis-label"] }).length > 0 ); }); const rendered = await page.evaluate(() => { const map = window.__waterNetworkMap; const source = map?.getSource("agent-scada-analysis") as unknown as { _data?: { features?: Array<{ properties?: Record }> }; }; const feature = source?._data?.features?.[0]; const labelLayer = map?.getLayer("agent-scada-analysis-label"); const levelLayer = map?.getLayer("agent-scada-analysis-level"); const indicatorLayer = map?.getLayer("agent-scada-analysis-indicator"); return { label: feature?.properties?.analysis_label, labelLayer: labelLayer?.type, labelSize: map?.getLayoutProperty("agent-scada-analysis-label", "text-size"), labelFont: map?.getLayoutProperty("agent-scada-analysis-label", "text-font"), level: feature?.properties?.analysis_level, levelLayer: levelLayer?.type, levelColors: map?.getPaintProperty("agent-scada-analysis-level", "circle-stroke-color"), indicatorLayer: indicatorLayer?.type }; }); expect(rendered).toEqual({ label: "MP02 · 低", labelLayer: "symbol", labelSize: 18, labelFont: ["Noto Sans Regular"], level: "low", levelLayer: "circle", levelColors: [ "match", ["get", "analysis_level"], "high", "#F43F5E", "medium", "#F59E0B", "low", "#10B981", "#8B5CF6" ], indicatorLayer: "circle" }); await expect.poll(() => receipts.length).toBe(2); expect(receipts).toEqual( expect.arrayContaining([ expect.objectContaining({ status: "failed", error: expect.objectContaining({ code: "ACTION_SUPERSEDED" }) }), expect.objectContaining({ status: "succeeded", output: expect.objectContaining({ rendered_ids: ["MP02"], fitted: true }) }) ]) ); expect(wfsRequests).toEqual(expect.arrayContaining(["MP01", "MP02"])); await page.setViewportSize({ width: 375, height: 812 }); await page.evaluate(() => window.__waterNetworkMap?.resize()); expect( await page.evaluate(() => { const source = window.__waterNetworkMap?.getSource("agent-scada-analysis") as unknown as { _data?: { features?: Array<{ properties?: Record }> }; }; return { label: source?._data?.features?.[0]?.properties?.analysis_label, viewportWidth: document.documentElement.clientWidth }; }) ).toEqual({ label: "MP02 · 低", viewportWidth: 375 }); await page.setViewportSize({ width: 1440, height: 900 }); await page.getByRole("button", { name: "新建 Agent 对话" }).filter({ visible: true }).click(); await page.waitForFunction(() => { const source = window.__waterNetworkMap?.getSource("agent-scada-analysis") as unknown as { _data?: { features?: unknown[] }; }; return source?._data?.features?.length === 0; }); const nextPrompt = page .getByPlaceholder(/输入调度问题/) .filter({ visible: true }) .first(); await nextPrompt.fill("新一组分析:将 MP01 标为高、MP02 标为低并显示编号"); await page .getByRole("button", { name: "发送 Agent 指令" }) .filter({ visible: true }) .first() .click(); await page.waitForFunction(() => { const source = window.__waterNetworkMap?.getSource("agent-scada-analysis") as unknown as { _data?: { features?: Array<{ properties?: Record }> }; }; const features = source?._data?.features ?? []; return features.length === 1 && features[0].properties?.analysis_label === "MP02 · 低"; }); await page.waitForTimeout(250); expect( await page.evaluate(() => { const source = window.__waterNetworkMap?.getSource("agent-scada-analysis") as unknown as { _data?: { features?: Array<{ properties?: Record }> }; }; return source?._data?.features?.map((feature) => feature.properties?.analysis_label); }) ).toEqual(["MP02 · 低"]); await expect.poll(() => receipts.length).toBe(4); }); async function mockAgentActions( page: Page, receipts: Array>, wfsRequests: string[] ) { let streamRequestCount = 0; await page.route(/\/wenzhou\/ows\?/, async (route) => { const url = new URL(route.request().url()); expect(route.request().method()).toBe("GET"); expect(url.searchParams.get("typeNames")).toBe("wenzhou:geo_scadas_mat"); const sensorId = url.searchParams.get("cql_filter")?.match(/'([^']+)'/)?.[1] ?? ""; wfsRequests.push(sensorId); if (sensorId === "MP01") await new Promise((resolve) => setTimeout(resolve, 150)); await route.fulfill({ json: { type: "FeatureCollection", features: [ { type: "Feature", geometry: { type: "Point", coordinates: sensorId === "MP01" ? [120.7, 28] : [120.72, 28.02] }, properties: { sensor_id: sensorId } } ] } }).catch(() => undefined); }); await page.route("**/api/v1/agent/chat/**", async (route) => { const url = new URL(route.request().url()); const path = url.pathname; if (/\/frontend-actions\/[^/]+\/result$/.test(path)) { receipts.push(route.request().postDataJSON() as Record); await route.fulfill({ json: { ok: true } }); return; } if (path.endsWith("/frontend-action-registry")) { await route.fulfill({ json: { schema_version: "frontend-action-registry@1", actions: [ { id: "render_scada_analysis", version: "frontend-action@1" }, { id: "clear_scada_analysis", version: "frontend-action@1" } ] } }); return; } if (path.endsWith("/models")) { await route.fulfill({ json: { default_model: "test/model", models: [ { id: "test/model", label: "测试模型", description: "SCADA 动作测试", icon: "bolt" } ] } }); return; } if (path.endsWith("/sessions")) { await route.fulfill({ json: { sessions: [] } }); return; } if (path.endsWith("/ui-registry")) { await route.fulfill({ json: { schema_version: "agent-ui-registry@1", components: [], actions: [] } }); return; } if (path.endsWith("/stream") && route.request().method() === "POST") { streamRequestCount += 1; const now = Date.now(); const sessionId = `scada-session-${streamRequestCount}`; const chunks = [ { type: "start", messageId: `assistant-scada-action-${streamRequestCount}` }, frontendActionChunk( `action-${streamRequestCount}-first`, `call-${streamRequestCount}-first`, sessionId, "MP01", "high", now ), frontendActionChunk( `action-${streamRequestCount}-second`, `call-${streamRequestCount}-second`, sessionId, "MP02", "low", now ), { type: "text-start", id: "answer" }, { type: "text-delta", id: "answer", delta: "已更新地图结果。" }, { type: "text-end", id: "answer" }, { type: "finish", finishReason: "stop" } ]; await route.fulfill({ headers: { "Cache-Control": "no-cache", "Content-Type": "text/event-stream", "X-Vercel-AI-UI-Message-Stream": "v1" }, body: chunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join("") }); return; } await route.fulfill({ json: {} }); }); } function frontendActionChunk( actionId: string, toolCallId: string, sessionId: string, sensorId: string, level: "high" | "low", now: number ) { return { type: "data-frontend_action", data: { version: "frontend-action@1", actionId, toolCallId, sessionId, session_id: sessionId, name: "render_scada_analysis", params: { items: [{ sensor_id: sensorId, level }] }, issuedAt: now, expiresAt: now + 15_000 }, transient: true }; }