feat(workbench): integrate runtime data and map tooling

This commit is contained in:
2026-09-22 10:45:46 +08:00
parent bf44cc0f25
commit bebcf2a06c
163 changed files with 15642 additions and 1312 deletions
+46 -27
View File
@@ -1,6 +1,7 @@
import { createServer, type Server } from "node:http";
import type { AddressInfo } from "node:net";
import { expect, test } from "@playwright/test";
import { mockScadaApi } from "./support/mock-scada-api";
const STREAM_CHUNK_SIZE = 18;
const STREAM_CHUNK_DELAY_MS = 20;
@@ -178,6 +179,22 @@ test.afterAll(async () => {
});
test("stream completion and the plan card remain positionally stable", async ({ page }) => {
await mockScadaApi(page);
await page.route("**/runtime-config.js", (route) =>
route.fulfill({
contentType: "application/javascript",
body: `globalThis.__TJWATER_CONFIG__ = ${JSON.stringify({
TJWATER_AUTH_MODE: "disabled",
TJWATER_SERVER_API_BASE_URL: "https://tjwater-api.invalid",
TJWATER_AGENT_API_BASE_URL: "http://127.0.0.1:8787",
TJWATER_MAP_URL: "https://stream-map.invalid/geoserver",
TJWATER_GEOSERVER_WORKSPACE: "tjwater_next",
TJWATER_MAPBOX_ACCESS_TOKEN: "",
TJWATER_ENABLE_MSW: "false"
})};`
})
);
await page.route("https://stream-map.invalid/**", (route) => route.fulfill({ status: 204 }));
await page.route("**/api/v1/agent/chat/**", async (route) => {
const path = new URL(route.request().url()).pathname;
@@ -206,6 +223,7 @@ test("stream completion and the plan card remain positionally stable", async ({
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
await expect(
page.getByRole("button", { name: "选择 Agent 模型" }).filter({ visible: true }).first()
@@ -290,33 +308,26 @@ test("stream completion and the plan card remain positionally stable", async ({
await expect
.poll(() => assistantMessage.evaluate((element) => getComputedStyle(element).boxShadow))
.toBe(await userMessage.evaluate((element) => getComputedStyle(element).boxShadow));
const operationalBrief = page.locator(".agent-panel-operational-float");
const operationalBrief = page.locator(".agent-panel-control").filter({
has: page.getByRole("button", { name: "预览影响", exact: true })
});
await expect(operationalBrief).toBeVisible();
await expect(operationalBrief).toHaveCSS("position", "absolute");
await expect(operationalBrief).not.toHaveClass(/agent-panel-floating-acrylic/);
await expect(operationalBrief).toHaveCSS("background-color", "rgba(255, 255, 255, 0.86)");
await expect(operationalBrief).toContainText("Agent 分析中");
await expect(operationalBrief.getByRole("button", { name: "预览影响" })).toBeDisabled();
await expect(operationalBrief.getByRole("button", { name: "检查证据" })).toBeDisabled();
await expect(operationalBrief).toHaveCSS("position", "static");
await expect(operationalBrief).toHaveCSS("border-radius", "16px");
await expect(operationalBrief).toHaveCSS("background-image", "none");
await expect(operationalBrief).toHaveCSS("border-top-width", "0px");
await expect(operationalBrief).toHaveCSS(
"box-shadow",
"rgba(15, 33, 55, 0.14) 0px 12px 32px 0px, rgba(15, 33, 55, 0.08) 0px 3px 10px 0px"
);
// Desktop uses an inline brief above the conversation, not the legacy floating overlay.
await expect(page.locator(".agent-panel-shell")).toHaveClass(/surface-dock/);
await expect(page.locator(".agent-panel-operational-float")).toHaveCount(0);
await expect
.poll(() => operationalBrief.evaluate((element) => getComputedStyle(element).boxShadow))
.not.toContain("inset");
await expect
.poll(() => operationalBrief.evaluate((element) => getComputedStyle(element).backdropFilter))
.toContain("blur(");
const operationalBriefBox = await operationalBrief.boundingBox();
const conversationBox = await page.locator(".agent-panel-conversation-canvas").boundingBox();
expect(operationalBriefBox).not.toBeNull();
expect(conversationBox).not.toBeNull();
expect(operationalBriefBox!.y).toBeLessThan(conversationBox!.y + conversationBox!.height);
await expect(page.locator(".agent-panel-conversation-content-has-brief")).toHaveCSS(
"padding-top",
"208px"
);
.poll(() =>
operationalBrief.evaluate((element) => {
const conversation = document.querySelector(".agent-panel-conversation")!;
return conversation.getBoundingClientRect().top - element.getBoundingClientRect().bottom;
})
)
.toBeGreaterThanOrEqual(0);
const liveProgress = page.getByRole("list", { name: "最近执行进度" });
await expect(liveProgress.locator("li")).toHaveCount(3);
@@ -596,9 +607,17 @@ test("stream completion and the plan card remain positionally stable", async ({
boxShadow: style.boxShadow
};
};
expect(await scrollToLatestButton.evaluate(readAcrylicMaterial)).toEqual(
await page.locator(".agent-panel-composer").evaluate(readAcrylicMaterial)
);
await expect(prompt).toBeVisible();
await expect(page.locator(".agent-panel-composer")).toHaveCount(0);
await expect
.poll(() =>
prompt.evaluate((element) => {
const form = element.closest("form")!;
const scroll = document.querySelector(".agent-conversation-scroll")!;
return form.getBoundingClientRect().top - scroll.getBoundingClientRect().bottom;
})
)
.toBeGreaterThanOrEqual(0);
const selectableAssistantMessage = page.getByRole("heading", { name: "诊断结果" }).first();
await selectableAssistantMessage.evaluate((element) => {