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.
324 lines
14 KiB
TypeScript
324 lines
14 KiB
TypeScript
import { expect, test, type Page } from "@playwright/test";
|
|
import { mockAgentApi } from "./support/mock-agent-api";
|
|
import { mockWorkbenchContext } from "./support/mock-workbench-context";
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await mockAgentApi(page);
|
|
});
|
|
|
|
async function openAgentPanel(page: Page) {
|
|
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
|
|
await expect(page.getByRole("complementary", { name: "Agent 命令面板" })).toBeVisible();
|
|
}
|
|
|
|
test("the docked Agent collapses and restores the main workspace", async ({ page }) => {
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await openAgentPanel(page);
|
|
|
|
const agentPanel = page.getByRole("complementary", { name: "Agent 命令面板" });
|
|
const workspace = page.locator("#main-workspace");
|
|
await expect
|
|
.poll(async () => Math.round((await workspace.boundingBox())?.x ?? 0))
|
|
.toBeGreaterThan(400);
|
|
const expandedOffset = Math.round((await workspace.boundingBox())?.x ?? 0);
|
|
await expect
|
|
.poll(async () => Math.round((await agentPanel.boundingBox())?.width ?? 0))
|
|
.toBe(expandedOffset);
|
|
|
|
await page.getByRole("button", { name: "折叠 Agent 面板" }).click();
|
|
await expect(page.getByLabel("Agent 折叠栏")).toBeVisible();
|
|
await expect.poll(async () => Math.round((await workspace.boundingBox())?.x ?? 0)).toBe(0);
|
|
|
|
await page.getByLabel(/展开 Agent 助手面板/).click();
|
|
await expect(agentPanel).toBeVisible();
|
|
await expect
|
|
.poll(async () => Math.round((await workspace.boundingBox())?.x ?? 0))
|
|
.toBe(expandedOffset);
|
|
});
|
|
|
|
test("dragging the Agent edge keeps the workspace attached and preserves the draft", async ({ page }) => {
|
|
await mockWorkbenchContext(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await openAgentPanel(page);
|
|
const panel = page.getByRole("complementary", { name: "Agent 命令面板" });
|
|
const workspace = page.locator("#main-workspace");
|
|
const handle = page.getByRole("separator", { name: "调整 Agent 面板宽度" });
|
|
const prompt = panel.getByPlaceholder("询问供水问题,或描述需要处理的任务");
|
|
await expect(panel.getByText("当前上下文", { exact: true })).toHaveCount(0);
|
|
await prompt.fill("保留尚未发送的诊断问题");
|
|
await expect(panel).toHaveCSS("width", "500px");
|
|
await page.waitForFunction(() =>
|
|
Boolean(window.__waterNetworkMap?.getLayer("scada-pressure-hit")) &&
|
|
!window.__waterNetworkMap?.isMoving()
|
|
);
|
|
const cameraBefore = await readCamera(page);
|
|
expect(await handle.evaluate((element) => (element as HTMLElement).tabIndex)).toBe(-1);
|
|
const grip = handle.locator(".agent-resize-grip");
|
|
await expect(grip).toHaveCSS("opacity", "0");
|
|
const box = (await handle.boundingBox())!;
|
|
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
|
await expect(grip).toHaveCSS("opacity", "1");
|
|
await page.screenshot({ path: "/tmp/nexttjwater-agent-resize-hover.png" });
|
|
await page.mouse.down();
|
|
await expect(handle).toHaveAttribute("data-resizing", "true");
|
|
await expect(panel).toHaveCSS("width", "500px");
|
|
for (const nextWidth of [540, 580, 620]) {
|
|
await page.mouse.move(nextWidth, box.y + box.height / 2);
|
|
await expect(panel).toHaveCSS("width", `${nextWidth}px`);
|
|
const panelBox = (await panel.boundingBox())!;
|
|
const workspaceBox = (await workspace.boundingBox())!;
|
|
expect(workspaceBox.x).toBe(panelBox.x + panelBox.width);
|
|
}
|
|
await page.screenshot({ path: "test-results/agent-history-ui/desktop-resizing.png" });
|
|
await page.mouse.up();
|
|
await expect(handle).toHaveAttribute("data-resizing", "false");
|
|
await expect(prompt).toHaveValue("保留尚未发送的诊断问题");
|
|
await expect.poll(() => readCamera(page)).toEqual(cameraBefore);
|
|
await page.mouse.move(1000, 450);
|
|
await expect(grip).toHaveCSS("opacity", "0");
|
|
await expect(panel).toHaveCSS("width", "620px");
|
|
await page.getByRole("button", { name: "折叠 Agent 面板" }).click();
|
|
await expect(page.getByLabel("Agent 折叠栏")).toBeVisible();
|
|
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
|
|
await expect(panel).toHaveCSS("width", "620px");
|
|
await expect(prompt).toHaveValue("保留尚未发送的诊断问题");
|
|
});
|
|
|
|
async function readCamera(page: Page) {
|
|
return page.evaluate(() => {
|
|
const map = window.__waterNetworkMap!;
|
|
return {
|
|
center: map.getCenter().toArray(),
|
|
zoom: map.getZoom(),
|
|
bearing: map.getBearing(),
|
|
pitch: map.getPitch()
|
|
};
|
|
});
|
|
}
|
|
|
|
test("Agent dragging stays within usable widths and adapts to the viewport", async ({ page }) => {
|
|
await mockWorkbenchContext(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await openAgentPanel(page);
|
|
const panel = page.getByRole("complementary", { name: "Agent 命令面板" });
|
|
const handle = page.getByRole("separator", { name: "调整 Agent 面板宽度" });
|
|
const workspace = page.locator("#main-workspace");
|
|
for (const [targetX, expectedWidth] of [[250, 400], [1000, 640], [620, 620]]) {
|
|
const box = (await handle.boundingBox())!;
|
|
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
|
await page.mouse.down();
|
|
await page.mouse.move(targetX, box.y + box.height / 2);
|
|
await page.mouse.up();
|
|
await expect(panel).toHaveCSS("width", `${expectedWidth}px`);
|
|
await expect.poll(async () => (await workspace.boundingBox())?.x).toBe(expectedWidth);
|
|
if (expectedWidth === 400) {
|
|
await page.screenshot({ path: "test-results/agent-history-ui/desktop-resize-minimum.png" });
|
|
}
|
|
}
|
|
await page.setViewportSize({ width: 1024, height: 768 });
|
|
await expect(panel).toHaveCSS("width", "500px");
|
|
await expect.poll(async () => (await workspace.boundingBox())?.x).toBe(500);
|
|
await page.setViewportSize({ width: 1920, height: 1080 });
|
|
await expect(panel).toHaveCSS("width", "620px");
|
|
await expect.poll(async () => (await workspace.boundingBox())?.x).toBe(620);
|
|
});
|
|
|
|
test("the map stays painted while the Agent and viewport change size", async ({ page }) => {
|
|
await mockWorkbenchContext(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await openAgentPanel(page);
|
|
await page.waitForFunction(() =>
|
|
Boolean(window.__waterNetworkMap?.getLayer("scada-pressure-hit")) &&
|
|
!window.__waterNetworkMap?.isMoving()
|
|
);
|
|
const initialAlpha = await page.evaluate(() => {
|
|
const map = window.__waterNetworkMap!;
|
|
const canvas = map.getCanvas();
|
|
const gl = canvas.getContext("webgl2") ?? canvas.getContext("webgl");
|
|
if (!gl) throw new Error("The map has no WebGL context");
|
|
const pixel = new Uint8Array(4);
|
|
const readAlpha = () => {
|
|
gl.readPixels(
|
|
Math.floor(gl.drawingBufferWidth / 2), Math.floor(gl.drawingBufferHeight / 2),
|
|
1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel
|
|
);
|
|
return pixel[3];
|
|
};
|
|
const state = window as typeof window & {
|
|
__mapResizeProbe?: { samples: number; blankFrames: number; sameMap: boolean };
|
|
};
|
|
const probe = { samples: 0, blankFrames: 0, sameMap: true };
|
|
state.__mapResizeProbe = probe;
|
|
map.on("resize", () => queueMicrotask(() => {
|
|
probe.samples += 1;
|
|
if (readAlpha() === 0) probe.blankFrames += 1;
|
|
probe.sameMap &&= window.__waterNetworkMap === map;
|
|
}));
|
|
map.redraw();
|
|
return readAlpha();
|
|
});
|
|
expect(initialAlpha).toBe(255);
|
|
const cameraBefore = await readCamera(page);
|
|
const handle = page.getByRole("separator", { name: "调整 Agent 面板宽度" });
|
|
const box = (await handle.boundingBox())!;
|
|
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
|
await page.mouse.down();
|
|
await page.mouse.move(620, box.y + box.height / 2, { steps: 12 });
|
|
await page.mouse.up();
|
|
for (const viewport of [
|
|
{ width: 1280, height: 800 }, { width: 1920, height: 1080 }
|
|
]) {
|
|
await page.setViewportSize(viewport);
|
|
await expect.poll(async () => page.evaluate(() => {
|
|
const canvas = window.__waterNetworkMap!.getCanvas();
|
|
return canvas.clientWidth === document.querySelector("#workspace-map")!.clientWidth;
|
|
})).toBe(true);
|
|
}
|
|
const probe = await page.evaluate(() => (window as typeof window & {
|
|
__mapResizeProbe?: { samples: number; blankFrames: number; sameMap: boolean };
|
|
}).__mapResizeProbe!);
|
|
expect(probe.samples).toBeGreaterThan(0);
|
|
expect(probe.sameMap).toBe(true);
|
|
expect(probe.blankFrames).toBe(0);
|
|
await expect.poll(() => readCamera(page)).toEqual(cameraBefore);
|
|
});
|
|
|
|
test("the map canvas follows workspace resizing before the next paint", async ({ page }) => {
|
|
await mockWorkbenchContext(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await openAgentPanel(page);
|
|
await page.waitForFunction(() =>
|
|
Boolean(window.__waterNetworkMap?.getLayer("scada-pressure-hit")) &&
|
|
!window.__waterNetworkMap?.isMoving()
|
|
);
|
|
await page.evaluate(() => {
|
|
const map = window.__waterNetworkMap!;
|
|
const container = map.getContainer();
|
|
const probe = { samples: 0, mismatches: 0, maxWidthDifference: 0 };
|
|
(window as typeof window & {
|
|
__mapSizeProbe?: typeof probe;
|
|
}).__mapSizeProbe = probe;
|
|
new ResizeObserver(() => queueMicrotask(() => {
|
|
const difference = Math.abs(container.clientWidth - map.getCanvas().clientWidth);
|
|
probe.samples += 1;
|
|
if (difference > 0) probe.mismatches += 1;
|
|
probe.maxWidthDifference = Math.max(probe.maxWidthDifference, difference);
|
|
})).observe(container);
|
|
});
|
|
const handle = page.getByRole("separator", { name: "调整 Agent 面板宽度" });
|
|
const box = (await handle.boundingBox())!;
|
|
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
|
await page.mouse.down();
|
|
for (const width of [620, 420, 600]) {
|
|
await page.mouse.move(width, box.y + box.height / 2, { steps: 12 });
|
|
}
|
|
await page.mouse.up();
|
|
const probe = await page.evaluate(() => (window as typeof window & {
|
|
__mapSizeProbe?: { samples: number; mismatches: number; maxWidthDifference: number };
|
|
}).__mapSizeProbe!);
|
|
expect(probe.samples).toBeGreaterThan(1);
|
|
expect(probe.maxWidthDifference).toBe(0);
|
|
});
|
|
|
|
test("the floating condition feed coordinates with the docked Agent", async ({ page }) => {
|
|
await page.setViewportSize({ width: 1280, height: 900 });
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await openAgentPanel(page);
|
|
|
|
await expect(page.locator('aside[aria-label="Agent 命令面板"]')).toBeVisible();
|
|
const workspace = page.locator("#main-workspace");
|
|
await expect
|
|
.poll(async () => Math.round((await workspace.boundingBox())?.x ?? 0))
|
|
.toBeGreaterThan(400);
|
|
const expandedOffset = Math.round((await workspace.boundingBox())?.x ?? 0);
|
|
const prompt = page.getByPlaceholder("询问供水问题,或描述需要处理的任务");
|
|
await prompt.fill("保留尚未发送的诊断问题");
|
|
const conditionToggle = page
|
|
.locator("header.acrylic-navigation")
|
|
.getByRole("button", { name: "工况任务", exact: true });
|
|
await conditionToggle.click();
|
|
const conditionPanel = page.getByRole("region", { name: "工况任务", exact: true });
|
|
await expect(conditionPanel).toBeVisible();
|
|
await expect(page.locator('aside[aria-label="Agent 命令面板"]')).toBeVisible();
|
|
await expect(prompt).toHaveValue("保留尚未发送的诊断问题");
|
|
await conditionPanel.getByRole("button", { name: "展开工况任务" }).click();
|
|
await expect(page.getByLabel("Agent 折叠栏")).toBeVisible();
|
|
await expect(conditionPanel.getByRole("button", { name: "收起工况任务" })).toBeVisible();
|
|
await conditionPanel.getByRole("button", { name: "收起工况任务" }).click();
|
|
await expect(page.locator('aside[aria-label="Agent 命令面板"]')).toBeVisible();
|
|
await expect
|
|
.poll(async () => Math.round((await workspace.boundingBox())?.x ?? 0))
|
|
.toBe(expandedOffset);
|
|
await expect(prompt).toHaveValue("保留尚未发送的诊断问题");
|
|
await conditionToggle.click();
|
|
await expect(conditionPanel).toBeHidden();
|
|
});
|
|
|
|
test("Agent resize handle stays hidden in the mobile layout", async ({ page }) => {
|
|
await page.setViewportSize({ width: 375, height: 812 });
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
|
|
await expect(page.getByRole("separator", { name: "调整 Agent 面板宽度" })).toBeHidden();
|
|
});
|
|
|
|
test.describe("mobile touch layout", () => {
|
|
test.use({ hasTouch: true, isMobile: true, viewport: { width: 375, height: 812 } });
|
|
|
|
test("mobile Agent toggle opens the conversation panel by touch", async ({ page }) => {
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
|
|
const openButton = page.getByRole("button", { name: "打开 Agent 面板" });
|
|
const buttonBox = await openButton.boundingBox();
|
|
expect(buttonBox).not.toBeNull();
|
|
|
|
await page.touchscreen.tap(
|
|
buttonBox!.x + buttonBox!.width / 2,
|
|
buttonBox!.y + buttonBox!.height / 2
|
|
);
|
|
|
|
await expect(
|
|
page.locator('aside[aria-label="Agent 命令面板"]').filter({ visible: true })
|
|
).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "关闭 Agent 面板" })).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test("mobile alert summary opens the Agent conversation panel", async ({ page }) => {
|
|
await page.clock.setFixedTime(new Date("2026-07-21T10:40:00+08:00"));
|
|
await page.setViewportSize({ width: 375, height: 812 });
|
|
await mockWorkbenchContext(page, {
|
|
availability: "ready",
|
|
source: "none",
|
|
scenario_availability: "not_configured",
|
|
artifact_availability: "not_configured",
|
|
schedule_availability: "not_configured",
|
|
work_order_availability: "not_configured",
|
|
alert_availability: "ready",
|
|
generated_at: "2026-07-21T02:40:00Z",
|
|
conditions: [],
|
|
alerts: [{
|
|
id: "test-alert",
|
|
condition_id: null,
|
|
source: { task_type: "other_task", task_id: "test-task", run_id: "test-run" },
|
|
title: "供水压力告警",
|
|
description: "待核对压力异常。",
|
|
occurred_at: "2026-07-21T02:30:00Z",
|
|
severity: "warning"
|
|
}],
|
|
scenarios: [],
|
|
latest_analysis: null,
|
|
work_orders: []
|
|
});
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
|
|
await page.getByRole("button", { name: "查看待办与告警,当前 1 条待处理事项" }).click();
|
|
await expect(page.getByText("业务告警来源:后端运行态", { exact: true })).toBeVisible();
|
|
await page.getByRole("button", { name: "工况汇总" }).click();
|
|
|
|
await expect(
|
|
page.locator('aside[aria-label="Agent 命令面板"]').filter({ visible: true })
|
|
).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "关闭 Agent 面板" })).toBeVisible();
|
|
await expect(page.getByText("已发起工况汇总", { exact: true })).toBeVisible();
|
|
});
|