import { expect, test, type Page } from "@playwright/test"; import { mockAgentApi } from "./support/mock-agent-api"; import { createAnalysisRuntimeSnapshot, MOCK_NON_SPATIAL_ANALYSIS, MOCK_SPATIAL_ANALYSIS } from "./support/mock-analysis-runtime"; import { mockScadaApi } from "./support/mock-scada-api"; test("the workspace starts with Agent collapsed and docks it only when opened", async ({ page }) => { await page.setViewportSize({ width: 1600, height: 900 }); await prepareMainFrame(page); await page.goto("/", { waitUntil: "domcontentloaded" }); const workspace = page.locator("#main-workspace"); const map = page.locator("#workspace-map"); await expect(workspace).toHaveAttribute("data-layout-mode", "default"); await expect(map).toBeVisible(); await expect(page.getByLabel("Agent 折叠栏")).toBeVisible(); await expect(page.getByLabel("Agent 命令面板")).toHaveCount(0); await expect.poll(async () => (await workspace.boundingBox())?.x).toBe(0); await expect(map.getByRole("navigation", { name: "地图工具" })).toBeVisible(); await expect(page.getByRole("region", { name: "工况任务", exact: true })).toHaveCount(0); await expect(page.getByRole("button", { name: "任务视图" })).toHaveCount(0); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); await expect(panel).toBeVisible(); await expectDockedWorkspace(page); await expect(map).toBeVisible(); await page.getByRole("button", { name: "折叠 Agent 面板" }).click(); await expect(page.getByLabel("Agent 折叠栏")).toBeVisible(); await expect.poll(async () => (await workspace.boundingBox())?.x).toBe(0); await expect.poll(async () => (await workspace.boundingBox())?.width).toBe(1600); }); test("a persisted Artifact opens and can be reduced to a preview", async ({ page }) => { await page.setViewportSize({ width: 1600, height: 900 }); await prepareMainFrame(page); await page.goto("/", { waitUntil: "domcontentloaded" }); await openFirstArtifact(page); await expect(page.locator("#main-workspace")).toHaveAttribute("data-layout-mode", "map_split"); await expect(page.getByRole("heading", { name: "北区爆管定位" })).toBeVisible(); await page.getByRole("button", { name: "将成果收起为预览" }).click(); const peek = page.getByRole("complementary", { name: "后端分析成果预览" }); await expect(peek).toBeVisible(); await expect(peek).toContainText("北区爆管定位"); await expect(page.locator("#main-workspace")).toHaveAttribute("data-layout-mode", "default"); await expect(page.getByRole("heading", { name: "北区爆管定位" })).toHaveCount(0); await peek.getByRole("button", { name: "打开成果" }).click(); await expect(page.locator("#main-workspace")).toHaveAttribute("data-layout-mode", "map_split"); await expect(page.locator("#workspace-map")).toBeVisible(); await expect(page.locator("#artifact-workspace")).toBeVisible(); await expect(page.getByRole("heading", { name: "北区爆管定位" })).toBeVisible(); await expect(page.getByLabel("Agent 折叠栏")).toBeVisible(); }); test("Agent docks beside an active Artifact and restores split view when collapsed", async ({ page }) => { await page.setViewportSize({ width: 1600, height: 900 }); await prepareMainFrame(page); await page.goto("/", { waitUntil: "domcontentloaded" }); await openFirstArtifact(page); const workspace = page.locator("#main-workspace"); const before = await workspace.boundingBox(); expect(before).not.toBeNull(); await expect(workspace).toHaveAttribute("data-layout-mode", "map_split"); await page.getByLabel(/展开 Agent 助手面板/).click(); await expect(page.getByLabel("Agent 命令面板")).toBeVisible(); await expectDockedWorkspace(page); const panelBox = await page.getByLabel("Agent 命令面板").boundingBox(); expect(panelBox).not.toBeNull(); await expect .poll(async () => (await workspace.boundingBox())?.width) .toBe(before!.width - panelBox!.width); await expect(workspace).toHaveAttribute("data-layout-mode", "focus"); await expect(page.locator("#workspace-map")).toBeHidden(); const title = page.getByRole("heading", { name: "北区爆管定位" }); await expect(title).toBeVisible(); const titleBox = await title.boundingBox(); expect(titleBox!.x).toBeGreaterThanOrEqual(panelBox!.x + panelBox!.width); await page.getByRole("button", { name: "折叠 Agent 面板" }).click(); await expect(page.getByLabel("Agent 折叠栏")).toBeVisible(); await expect.poll(async () => (await workspace.boundingBox())?.x).toBe(before!.x); await expect.poll(async () => (await workspace.boundingBox())?.width).toBe(before!.width); await expect(workspace).toHaveAttribute("data-layout-mode", "map_split"); await expect(page.locator("#workspace-map")).toBeVisible(); await expect(title).toBeVisible(); await page.evaluate(() => document.fonts.ready); await page.screenshot({ path: "test-results/workspace-regression/restored-split.png" }); }); test("spatial Artifacts switch between split, focus and peek", async ({ page }) => { await page.setViewportSize({ width: 1600, height: 900 }); await prepareMainFrame(page); await page.goto("/", { waitUntil: "domcontentloaded" }); await openFirstArtifact(page); await page.getByRole("button", { name: "专注分析" }).click(); await expect(page.locator("#main-workspace")).toHaveAttribute("data-layout-mode", "focus"); await expect(page.locator("#workspace-map")).toBeHidden(); await page.getByRole("button", { name: "半屏显示" }).click(); await expect(page.locator("#main-workspace")).toHaveAttribute("data-layout-mode", "map_split"); await expect(page.locator("#workspace-map")).toBeVisible(); await page.getByRole("button", { name: "将成果收起为预览" }).click(); await expect(page.locator("#main-workspace")).toHaveAttribute("data-layout-mode", "default"); await expect(page.getByRole("complementary", { name: "后端分析成果预览" })).toBeVisible(); await expect(page.getByLabel("Agent 折叠栏")).toBeVisible(); await expect(page.getByLabel("Agent 命令面板")).toHaveCount(0); await page.getByRole("button", { name: "销毁预览成果" }).click(); await expect(page.getByRole("complementary", { name: "后端分析成果预览" })).toHaveCount(0); await expect(page.locator("#workspace-map")).toBeVisible(); }); test("non-spatial Artifacts open directly in focus mode", async ({ page }) => { await page.setViewportSize({ width: 1600, height: 900 }); await prepareMainFrame(page, MOCK_NON_SPATIAL_ANALYSIS); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: "打开最新后端分析成果" }).click(); await expect(page.locator("#main-workspace")).toHaveAttribute("data-layout-mode", "focus"); await expect(page.getByRole("heading", { name: "供水服务分区平衡分析" })).toBeVisible(); await expect(page.locator("#workspace-map")).toBeHidden(); await expect(page.getByRole("button", { name: "显示地图" })).toHaveCount(0); }); test("narrow workspaces switch between Artifact and map as pages", async ({ page }) => { await page.setViewportSize({ width: 1180, height: 820 }); await prepareMainFrame(page); await page.goto("/", { waitUntil: "domcontentloaded" }); await openFirstArtifact(page); await expect(page.locator("#main-workspace")).toHaveAttribute("data-layout-mode", "focus"); await page.getByRole("button", { name: "显示地图" }).click(); await expect(page.locator("#main-workspace")).toHaveAttribute("data-layout-mode", "map_only"); await expect(page.locator("#workspace-map")).toBeVisible(); await expect(page.locator("#artifact-workspace")).toBeHidden(); await page.getByRole("button", { name: "返回分析成果" }).click(); await expect(page.locator("#main-workspace")).toHaveAttribute("data-layout-mode", "focus"); await expect(page.locator("#artifact-workspace")).toBeVisible(); }); test("the condition feed remains floating and starts closed", async ({ page }) => { await page.setViewportSize({ width: 1600, height: 900 }); await prepareMainFrame(page); await page.goto("/", { waitUntil: "domcontentloaded" }); await expect(page.getByRole("region", { name: "工况任务", exact: true })).toHaveCount(0); await page.getByRole("button", { name: "工况任务", exact: true }).click(); await expect(page.getByRole("region", { name: "工况任务", exact: true })).toBeVisible(); }); async function openFirstArtifact(page: Page) { await page.getByRole("button", { name: "打开最新后端分析成果" }).click(); } test("header actions remain separate across compact and wide breakpoints", async ({ page }) => { await prepareMainFrame(page); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.evaluate(() => document.fonts.ready); for (const width of [1600, 1536, 1440, 1280, 1024, 375, 320, 1800, 1920]) { await page.setViewportSize({ width, height: 900 }); const titleSize = await page.locator("header h1").evaluate((element) => ({ available: element.clientWidth, content: element.scrollWidth })); expect( titleSize.content, `${width}px: header title ${JSON.stringify(titleSize)}` ).toBeLessThanOrEqual(titleSize.available); const buttons = await page .locator("header button") .filter({ visible: true }) .evaluateAll((elements) => elements .map((element) => { const box = element.getBoundingClientRect(); return { name: element.getAttribute("aria-label") ?? element.textContent, left: box.left, right: box.right }; }) .sort((a, b) => a.left - b.left) ); for (let index = 0; index < buttons.length; index += 1) { const button = buttons[index]; expect(button.left, `${width}px: ${button.name} left edge`).toBeGreaterThanOrEqual(0); expect(button.right, `${width}px: ${button.name} right edge`).toBeLessThanOrEqual(width); if (index) expect( button.left, `${width}px: ${buttons[index - 1].name} overlaps ${button.name}` ).toBeGreaterThanOrEqual(buttons[index - 1].right); } await page .locator("header") .screenshot({ path: `test-results/workspace-regression/${width}-header.png` }); } }); async function expectDockedWorkspace(page: Page) { await expect .poll(() => page.evaluate(() => { const panel = document .querySelector('aside[aria-label="Agent 命令面板"]')! .getBoundingClientRect(); const workspace = document.querySelector("#main-workspace")!.getBoundingClientRect(); return { panelLeft: panel.left, gap: workspace.left - panel.right, combinedWidth: panel.width + workspace.width }; }) ) .toEqual({ panelLeft: 0, gap: 0, combinedWidth: page.viewportSize()!.width }); } async function prepareMainFrame(page: Page, latestAnalysis: unknown = MOCK_SPATIAL_ANALYSIS) { await mockAgentApi(page); await page.route("**/runtime-config.js", async (route) => { await route.fulfill({ contentType: "application/javascript", body: `globalThis.__TJWATER_CONFIG__ = { TJWATER_AUTH_MODE: "disabled", TJWATER_MAPBOX_ACCESS_TOKEN: "", TJWATER_MAP_URL: "https://workspace-map.invalid/geoserver", TJWATER_GEOSERVER_WORKSPACE: "tjwater_next", TJWATER_SERVER_API_BASE_URL: "https://tjwater-api.invalid", TJWATER_AGENT_API_BASE_URL: "http://127.0.0.1:8787", TJWATER_ENABLE_DEV_PANEL: "true", TJWATER_ENABLE_MSW: "false" };` }); }); await page.route("https://workspace-map.invalid/**", async (route) => { await route.fulfill({ status: 204 }); }); await mockScadaApi(page, [], createAnalysisRuntimeSnapshot(latestAnalysis)); }