import { expect, test, type Locator } from "@playwright/test"; import { mockAgentApi } from "./support/mock-agent-api"; import { mockWorkbenchContext } from "./support/mock-workbench-context"; import { expectThemeColor } from "./support/theme-colors"; // Retain the historical filename; the Agent now has an independent solid theme. test.beforeEach(async ({ page }) => { await page.emulateMedia({ reducedMotion: "reduce" }); await mockWorkbenchContext(page); await mockAgentApi(page, { sessions: [ { session_id: "browser-history-session", title: "城南供水压力复核", created_at: "2026-07-21T09:30:00+08:00", updated_at: "2026-07-21T10:35:00+08:00", run_status: "completed" } ] }); }); async function waitForAnimations(locator: Locator) { await locator.evaluate(async (element) => { await Promise.all( element .getAnimations({ subtree: true }) .filter((animation) => animation.effect?.getTiming().iterations !== Infinity) .map((animation) => animation.finished.catch(() => undefined)) ); }); } test("Agent separates conversation navigation, context and composer from the map theme", async ({ page }) => { await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); const header = panel.locator(".agent-panel-header"); const context = panel.locator(".agent-chat-context-bar"); const form = panel.locator("form"); await waitForAnimations(panel); await expect(page.locator("header.acrylic-navigation")).toHaveCSS("backdrop-filter", /blur/); await expect(header).toHaveCSS("background-color", "rgb(255, 255, 255)"); await expect(header).toHaveCSS("backdrop-filter", "none"); await expect(header.getByLabel("Agent 状态")).toHaveCSS("width", "64px"); await expect(header.getByRole("group", { name: "对话操作" }).getByRole("button")).toHaveCount(3); await expect(panel.getByRole("heading", { name: "有什么可以帮你?" })).toBeVisible(); await expect(context).toHaveCount(0); await expect(context.getByRole("button")).toHaveCount(0); await expect(form).toHaveCount(1); await expect(form).toHaveCSS("background-color", "rgb(255, 255, 255)"); const panelBox = (await panel.boundingBox())!; const headerBox = (await header.boundingBox())!; const divider = panel.locator("[data-agent-header-divider]"); const contextBox = (await divider.boundingBox())!; expect(panelBox.x).toBe(0); expect(contextBox.y).toBeGreaterThanOrEqual(headerBox.y + headerBox.height); await form.locator("textarea").focus(); await expectThemeColor(form, "border-color", "--action-blue"); await header.getByRole("button", { name: "选择主题" }).click(); await expect(page.getByRole("menuitem", { name: "新建主题", exact: true })).toBeVisible(); await expect(page.getByRole("menuitem", { name: "新建项目", exact: true })).toHaveCount(0); await page.keyboard.press("Escape"); await panel.screenshot({ path: "test-results/agent-history-ui/agent-new-start.png" }); const handle = page.getByRole("separator", { name: "调整 Agent 面板宽度" }); for (const [key, width] of [["Home", 400], ["End", 640]] as const) { await handle.focus(); await page.keyboard.press(key); await expect.poll(async () => Math.round((await panel.boundingBox())!.width)).toBe(width); const bounds = (await header.boundingBox())!; const contextBounds = (await divider.boundingBox())!; const actions = (await header.getByRole("group", { name: "对话操作" }).boundingBox())!; const identity = (await header.getByText("供水助手", { exact: true }).boundingBox())!; expect(actions.x).toBeGreaterThan(identity.x + identity.width); expect(actions.x + actions.width).toBeLessThanOrEqual(bounds.x + bounds.width - 12); await page.screenshot({ path: `/tmp/nexttjwater-orb-64/header-${width}.png`, clip: { ...bounds, height: contextBounds.y + contextBounds.height - bounds.y } }); } }); test("Agent portal menus use solid surfaces and return focus with reduced transparency", async ({ page }) => { await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); const cdp = await page.context().newCDPSession(page); await cdp.send("Emulation.setEmulatedMedia", { features: [{ name: "prefers-reduced-transparency", value: "reduce" }] }); for (const name of ["选择 Agent 模型", "权限批准模式"]) { const trigger = panel.getByRole("button", { name }); await trigger.click(); const menu = page.getByRole("menu"); await expect(menu).toBeVisible(); await expect(menu).toHaveCSS("background-color", "rgb(255, 255, 255)"); await expect(menu).toHaveCSS("backdrop-filter", "none"); expect(await menu.evaluate((element) => element.closest("aside"))).toBeNull(); await page.keyboard.press("Escape"); await expect(trigger).toBeFocused(); } }); test("Agent history keeps rename, cancel and focus restoration usable", async ({ page }) => { await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); const trigger = panel.getByRole("button", { name: "打开 Agent 历史记录" }); await trigger.click(); const history = page.getByRole("region", { name: "Agent 历史记录" }); await expect(history).toBeVisible(); await history.getByRole("button", { name: /会话操作/ }).first().click(); await page.getByRole("menuitem", { name: "重命名对话", exact: true }).click(); await expect(history.getByRole("textbox")).toBeFocused(); await history.getByRole("textbox").fill("新的供水核验主题"); await history.getByRole("button", { name: "取消重命名" }).click(); await expect(history.getByText("城南供水压力复核", { exact: true })).toBeVisible(); await page.keyboard.press("Escape"); await expect(history).toBeHidden(); await expect(trigger).toBeFocused(); }); test("Agent navigation responds to both pointer and keyboard activation", async ({ page }) => { await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); const trigger = panel.getByRole("button", { name: "打开 Agent 历史记录" }); await trigger.click(); await expect(panel.getByRole("button", { name: "打开 Agent 历史记录", includeHidden: true })).toHaveAttribute("aria-expanded", "true"); await page.keyboard.press("Escape"); await expect(trigger).toHaveAttribute("aria-expanded", "false"); await expect(trigger).toBeFocused(); await page.keyboard.press("Space"); await expect(page.getByRole("dialog", { name: "历史对话" }).getByRole("searchbox")).toBeFocused(); await page.keyboard.press("Escape"); await expect(trigger).toHaveAttribute("aria-expanded", "false"); await expect(trigger).toBeFocused(); }); test("mobile Agent opens full height with an unobstructed message region and layered Escape", async ({ page }) => { await page.setViewportSize({ width: 320, height: 844 }); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: "打开 Agent 面板" }).click(); const sheet = page.getByRole("region", { name: "Agent 工作台抽屉" }); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); await expect(sheet).toHaveAttribute("data-snap", "full"); await expect(panel).toBeVisible(); const header = panel.locator(".agent-panel-header"); await expect(header.getByLabel("Agent 状态")).toHaveCSS("width", "64px"); const controls = await panel .locator(".agent-panel-header > div") .first() .getByRole("button") .evaluateAll((elements) => elements.map((element) => { const { left, right } = element.getBoundingClientRect(); return { left, right }; }) ); for (let index = 0; index < controls.length; index += 1) { expect(controls[index].left).toBeGreaterThanOrEqual(0); expect(controls[index].right).toBeLessThanOrEqual(320); if (index > 0) expect(controls[index].left).toBeGreaterThanOrEqual(controls[index - 1].right); } const headerBounds = (await header.boundingBox())!; const contextBounds = (await panel.locator("[data-agent-header-divider]").boundingBox())!; await page.screenshot({ path: "/tmp/nexttjwater-orb-64/header-320.png", clip: { x: 0, y: headerBounds.y, width: 320, height: contextBounds.y + contextBounds.height - headerBounds.y } }); const model = panel.getByRole("button", { name: "选择 Agent 模型" }); await model.click(); await expect(page.getByRole("menu")).toBeVisible(); await page.keyboard.press("Escape"); await expect(page.getByRole("menu")).toBeHidden(); await expect(panel).toBeVisible(); await expect(model).toBeFocused(); const historyButton = panel.getByRole("button", { name: "打开 Agent 历史记录" }); await historyButton.click(); await expect(page.getByRole("region", { name: "Agent 历史记录" })).toBeVisible(); await page.keyboard.press("Escape"); await expect(page.getByRole("region", { name: "Agent 历史记录" })).toBeHidden(); await expect(panel).toBeVisible(); await expect(historyButton).toBeFocused(); }); test("Agent keeps system borders and visible controls in forced color mode", async ({ page }) => { await page.emulateMedia({ forcedColors: "active" }); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); await expect(panel).toHaveCSS("backdrop-filter", "none"); await expect(panel).toHaveCSS("border-right-width", "1px"); await expect(panel).toHaveCSS("border-right-color", "rgb(0, 0, 0)"); await expect(panel.getByRole("button", { name: "发送 Agent 指令" })).toBeVisible(); await panel.getByRole("button", { name: "打开 Agent 历史记录" }).click(); const history = page.getByRole("dialog", { name: "历史对话", exact: true }); await expect(history).toHaveCSS("background-color", "rgb(255, 255, 255)"); await expect(history).toHaveCSS("border-top-color", "rgb(0, 0, 0)"); }); test("long history titles keep rename and explicit delete usable across viewport sizes", async ({ page }) => { test.setTimeout(60_000); const sessionId = "browser-history-long-session"; const originalTitle = "城南供水压力复核:夜间最小流量与压力变送器连续性联合调查,待现场反馈后人工审核"; let title = originalTitle; let deleted = false; const writes: Array<{ method: string; path: string; body: unknown }> = []; await page.route(/\/api\/v1\/sessions(?:\?.*)?$/, (route) => route.request().method() === "OPTIONS" ? route.fallback() : route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: { sessions: deleted ? [] : [ { session_id: sessionId, title, created_at: 100, updated_at: 200, run_status: "completed" } ] } }) ); await page.route("**/api/v1/session/**", async (route) => { const request = route.request(); const method = request.method(); if (method === "OPTIONS") return route.fallback(); const path = new URL(request.url()).pathname; if (method === "PATCH" && path === `/api/v1/session/${sessionId}/title`) { const body = request.postDataJSON(); title = body.title; writes.push({ method, path, body }); return route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: {} }); } if (method === "DELETE" && path === `/api/v1/session/${sessionId}`) { deleted = true; writes.push({ method, path, body: null }); return route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: {} }); } return route.fallback(); }); for (const width of [320, 375, 1440]) { title = originalTitle; deleted = false; writes.length = 0; await page.setViewportSize({ width, height: 844 }); await page.goto("/", { waitUntil: "domcontentloaded" }); await page .getByRole("button", { name: width < 1024 ? "打开 Agent 面板" : /展开 Agent 助手面板/ }) .click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); const modelButton = panel.getByRole("button", { name: "选择 Agent 模型" }); await expect(modelButton).toBeVisible({ timeout: 10_000 }); await expect(modelButton).toHaveAccessibleDescription("当前模型:测试模型"); await expect(modelButton).toHaveAttribute("title", "测试模型"); await waitForAnimations(panel); const approvalBox = await panel.getByRole("button", { name: "权限批准模式" }).boundingBox(); const modelBox = await modelButton.boundingBox(); expect(approvalBox!.x + approvalBox!.width).toBeLessThanOrEqual(modelBox!.x); const controls = await panel .locator('[data-slot="input-group-addon"][data-align="block-end"]') .getByRole("button") .evaluateAll((elements) => elements.map((element) => { const box = element.getBoundingClientRect(); return { left: box.left, right: box.right }; }) ); expect(controls.length).toBeGreaterThanOrEqual(3); for (let index = 0; index < controls.length; index += 1) { expect(controls[index].left).toBeGreaterThanOrEqual(0); expect(controls[index].right).toBeLessThanOrEqual(width); if (index > 0) expect(controls[index - 1].right).toBeLessThanOrEqual(controls[index].left); } await modelButton.click(); await expect(page.getByRole("menuitem", { name: /测试模型/ })).toBeVisible(); if (width < 1024) { await page.screenshot({ path: `test-results/agent-history-ui/${width}-model-menu.png` }); } await page.keyboard.press("Escape"); await expect(modelButton).toBeFocused(); const approvalButton = panel.getByRole("button", { name: "权限批准模式" }); await approvalButton.click(); await expect(page.getByRole("menuitem", { name: /请求批准/ })).toBeVisible(); await page.keyboard.press("Escape"); await expect(approvalButton).toBeFocused(); const historyButton = panel.getByRole("button", { name: "打开 Agent 历史记录" }); await historyButton.click(); const history = page.getByRole("region", { name: "Agent 历史记录" }); await expect(history.locator(".agent-history-session-trigger")).toContainText(originalTitle); await history.getByRole("button", { name: /会话操作/ }).click(); await page.getByRole("menuitem", { name: "重命名对话", exact: true }).click(); const input = history.getByRole("textbox"); await expect(input).toBeFocused(); await expectThemeColor(input, "border-color", "--field-focus-border"); await input.fill(`${originalTitle}(已复核)`); await expect(history.getByRole("button", { name: "保存对话标题" })).toBeVisible(); await waitForAnimations(history); const box = await history.boundingBox(); expect(box!.x).toBeGreaterThanOrEqual(0); expect(box!.x + box!.width).toBeLessThanOrEqual(width); expect(await history.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe( true ); const inputBox = await input.boundingBox(); const saveBox = await history.getByRole("button", { name: "保存对话标题" }).boundingBox(); expect(inputBox!.width).toBeGreaterThan(40); expect(inputBox!.x + inputBox!.width).toBeLessThanOrEqual(saveBox!.x); await page.screenshot({ path: `test-results/agent-history-ui/${width}-long-rename.png` }); await history.getByRole("button", { name: "取消重命名" }).click(); await expect(history.locator(".agent-history-session-trigger")).toContainText(originalTitle); expect(writes).toEqual([]); await history.getByRole("button", { name: /会话操作/ }).click(); await page.getByRole("menuitem", { name: "重命名对话", exact: true }).click(); await input.fill("城南压力复核(已复核)"); await history.getByRole("button", { name: "保存对话标题" }).click(); await expect(history.locator(".agent-history-session-trigger")).toContainText( "城南压力复核(已复核)" ); expect(writes).toEqual([ { method: "PATCH", path: `/api/v1/session/${sessionId}/title`, body: { title: "城南压力复核(已复核)", is_title_manually_edited: true } } ]); await history.getByRole("button", { name: /会话操作/ }).click(); await page.getByRole("menuitem", { name: "删除对话", exact: true }).click(); await expect(history.getByText("删除后无法从历史记录中恢复")).toBeVisible(); await expect(history.getByRole("button", { name: "确认", exact: true })).toBeVisible(); await history.getByRole("button", { name: "取消删除" }).click(); expect(writes).toHaveLength(1); await history.getByRole("button", { name: /会话操作/ }).click(); await page.getByRole("menuitem", { name: "删除对话", exact: true }).click(); await history.getByRole("button", { name: "确认", exact: true }).click(); await expect(history.getByText("暂无历史记录", { exact: true })).toBeVisible(); expect(writes).toHaveLength(2); expect(writes[1]).toEqual({ method: "DELETE", path: `/api/v1/session/${sessionId}`, body: null }); await page.keyboard.press("Escape"); await expect(history).toBeHidden(); await expect(historyButton).toBeFocused(); } });