Unify dock surfaces, headers and dividers, and remove the visible context strip. Limit pointer resizing to 400-640px and reveal the edge grip on hover. Resize and redraw the map in the same layout cycle to prevent blanking and lag. Preserve horizontal centering during conversation control press feedback. Validation: isolated lint, typecheck, 21 focused unit tests, and 10 relevant browser checks passed. Snapshot baselines regenerated from the isolated change. The existing mobile alert summary test also fails on unchanged HEAD because the expected demo alert source is unavailable in this environment.
301 lines
14 KiB
TypeScript
301 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("输入调度问题,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();
|
|
});
|