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("输入调度问题,Agent 将通过后端会话流式响应"); 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("输入调度问题,Agent 将通过后端会话流式响应"); 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 page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /查看异常处置面板/ }).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(); });