import { expect, test, type Locator } from "@playwright/test"; import { mockAgentApi } from "./support/mock-agent-api"; import { mockWorkbenchContext } from "./support/mock-workbench-context"; import { expectThemeColor, readColorAlpha, readThemeColor } from "./support/theme-colors"; test.beforeEach(async ({ page }) => { 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" } ] }); }); type BoundingBox = { x: number; y: number; width: number; height: number; }; async function waitForAnimations(locator: Locator) { await locator.evaluate(async (element) => { const animations = element .getAnimations({ subtree: true }) .filter((animation) => animation.effect?.getTiming().iterations !== Infinity); await Promise.all(animations.map((animation) => animation.finished.catch(() => undefined))); }); } test("desktop Agent docks beside the workspace with a solid reading surface", async ({ page }) => { await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const topBar = page.locator("header.acrylic-navigation"); const workspace = page.locator("#main-workspace"); const agentPanel = page.locator('aside[aria-label="Agent 命令面板"]'); const agentHeader = agentPanel.locator(".agent-panel-header"); const agentConversationScroll = agentPanel.locator(".agent-conversation-scroll"); const agentControls = agentPanel.locator(".agent-panel-band").last(); await expect(topBar).toBeVisible(); await expect(workspace).toBeVisible(); await expect(agentPanel).toBeVisible(); await expect(agentHeader).toBeVisible(); await expect(agentControls).toBeVisible(); await waitForAnimations(agentPanel); await expectAcrylicSurface(topBar); await expectThemeColor(agentPanel, "background-color", "--agent-dock-surface"); await expect(agentPanel).toHaveCSS("backdrop-filter", "none"); await expect(agentPanel).toHaveCSS("border-radius", "0px"); await expect(agentPanel).toHaveCSS("border-top-width", "0px"); await expect(agentPanel).toHaveCSS("border-bottom-width", "0px"); await expect(agentPanel).toHaveCSS("border-left-width", "0px"); await expect(agentPanel).toHaveCSS("border-right-width", "1px"); await expectThemeColor(agentPanel, "border-right-color", "--surface-divider"); await expect(agentPanel).toHaveCSS("box-shadow", "none"); await expect(agentPanel).toHaveAttribute("data-agent-starting", "true"); await expect(agentControls).toHaveCSS("border-top-width", "0px"); await expect(agentControls).toHaveCSS("background-color", "rgba(0, 0, 0, 0)"); await expectAcrylicSurface(agentPanel.locator("form.agent-panel-control")); const inputBody = agentPanel.locator(".agent-panel-input-body"); const inputTools = agentPanel.locator(".agent-panel-input-tools"); await expectThemeColor(inputBody, "background-color", "--agent-dock-reading"); await expectThemeColor(inputBody, "border-color", "--surface-border"); await expect(inputBody).toHaveCSS("display", "block"); await expect(inputTools).toHaveCSS("background-color", "rgba(0, 0, 0, 0)"); const startComposer = agentPanel.locator(".agent-panel-start-composer"); const suggestions = agentPanel.getByRole("group", { name: "推荐问题" }); await expect(suggestions.getByRole("button")).toHaveCount(3); await expect(agentPanel.getByRole("heading", { name: "我能帮你处理什么?" })).toBeVisible(); await expect(agentPanel.locator(".agent-panel-main-content")).toBeHidden(); const panelBox = await agentPanel.boundingBox(); expect(panelBox).not.toBeNull(); expect(panelBox!.x).toBe(0); const topBarBox = await topBar.boundingBox(); expect(panelBox!.y).toBe(topBarBox!.y + topBarBox!.height); expect(panelBox!.y + panelBox!.height).toBe(page.viewportSize()!.height); expect(panelBox!.width).toBe(500); await expect.poll(async () => (await workspace.boundingBox())?.x).toBe(500); const headerBox = await agentHeader.boundingBox(); const workspaceBox = await workspace.boundingBox(); expect(headerBox!.height).toBe(topBarBox!.height); expect(headerBox!.y).toBe(topBarBox!.y + topBarBox!.height); expect(workspaceBox!.y).toBe(topBarBox!.y); await expect(topBar).toHaveCSS("box-shadow", "none"); await expectAcrylicSurface(agentHeader); await expect(agentHeader.locator(":scope > div").first()).toHaveCSS("background-color", "rgba(0, 0, 0, 0)"); expect(await topBar.evaluate((element) => getComputedStyle(element).borderBottomColor)).toBe( await agentPanel.evaluate((element) => getComputedStyle(element).borderRightColor) ); await expect(agentConversationScroll).toHaveCSS("scrollbar-gutter", "stable both-edges"); const beforeFocus = await getSurfacePresentation(agentPanel); await page.getByPlaceholder("询问供水问题,或描述需要处理的任务").focus(); await expectThemeColor(inputBody, "border-color", "--field-focus-border"); await expect(inputBody).toHaveCSS("box-shadow", "none"); await expect(agentPanel.locator("form.agent-panel-control > [data-slot=input-group]")).toHaveCSS("border-width", "0px"); await expect.poll(() => getSurfacePresentation(agentPanel)).toEqual(beforeFocus); await agentPanel.getByRole("button", { name: "选择对话归属" }).click(); const projectMenu = page.getByRole("menu"); await expect(projectMenu.getByRole("menuitem", { name: "个人会话" })).toBeVisible(); await expect(projectMenu.getByRole("menuitem", { name: "当前项目" })).toBeVisible(); await expect(projectMenu.getByRole("menuitem", { name: "新建项目" })).toBeVisible(); await expect(projectMenu.getByRole("menuitem", { name: "切换项目" })).toHaveCount(0); await page.screenshot({ path: "test-results/agent-history-ui/project-menu.png" }); await page.keyboard.press("Escape"); await page.screenshot({ path: "test-results/agent-history-ui/desktop-dock.png" }); for (const viewport of [ { width: 1024, height: 768, panelWidth: 500 }, { width: 1920, height: 1080, panelWidth: 540 } ]) { await page.setViewportSize({ width: viewport.width, height: viewport.height }); await expect(agentPanel).toHaveCSS("width", `${viewport.panelWidth}px`); await expect.poll(async () => (await workspace.boundingBox())?.x).toBe(viewport.panelWidth); const resizedPanelBox = await agentPanel.boundingBox(); expect(resizedPanelBox!.x).toBe(0); expect(resizedPanelBox!.y).toBe(topBarBox!.y + topBarBox!.height); expect(resizedPanelBox!.y + resizedPanelBox!.height).toBe(viewport.height); await page.screenshot({ path: `test-results/agent-history-ui/desktop-dock-${viewport.width}.png` }); } await page.setViewportSize({ width: 1440, height: 640 }); const handle = page.getByRole("separator", { name: "调整 Agent 面板宽度" }); for (const width of [400, 640]) { 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(width, box.y + box.height / 2); await page.mouse.up(); await page.mouse.move(1000, 450); await expect(agentPanel).toHaveCSS("width", `${width}px`); expect(await startComposer.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true); expect(await inputTools.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true); const textarea = agentPanel.locator("textarea"); await textarea.fill("核对近期供水压力变化,说明监测证据和需要现场核验的事项。".repeat(20)); const composerBox = (await startComposer.boundingBox())!; expect(composerBox.y).toBeGreaterThanOrEqual(topBarBox!.height * 2); expect(composerBox.y + composerBox.height).toBeLessThanOrEqual(640); await textarea.fill(""); await agentPanel.screenshot({ path: `test-results/agent-history-ui/desktop-material-${width}.png` }); } }); test("desktop Agent portal menus keep local material and respect reduced transparency", async ({ page }) => { await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); await waitForAnimations(panel); for (const name of ["选择 Agent 模型", "权限批准模式"]) { const trigger = panel.getByRole("button", { name }); await trigger.click(); const menu = page.getByRole("menu"); await expect(menu).toBeVisible(); await waitForAnimations(menu); await expect(menu).toHaveAttribute("data-agent-presentation", "desktop-dock"); expect(await menu.evaluate((element) => element.closest("aside"))).toBeNull(); await expectThemeColor(menu, "background-color", "--agent-dock-overlay-surface"); await expectAcrylicSurface(menu); await page.keyboard.press("Escape"); await expect(trigger).toBeFocused(); } const cdp = await page.context().newCDPSession(page); await cdp.send("Emulation.setEmulatedMedia", { features: [{ name: "prefers-reduced-transparency", value: "reduce" }] }); expect(await page.evaluate(() => matchMedia("(prefers-reduced-transparency: reduce)").matches)).toBe(true); await panel.getByRole("button", { name: "选择 Agent 模型" }).click(); const menu = page.getByRole("menu"); await expectThemeColor(menu, "background-color", "--palette-paper"); await expect(menu).toHaveCSS("backdrop-filter", "none"); await page.keyboard.press("Escape"); await panel.getByRole("button", { name: "打开 Agent 历史记录" }).click(); const history = panel.getByRole("region", { name: "Agent 历史记录" }); await expectThemeColor(panel.locator(".agent-panel-header"), "background-color", "--palette-paper"); await expect(panel.locator(".agent-panel-header")).toHaveCSS("backdrop-filter", "none"); await expect(history).toHaveCSS("backdrop-filter", "none"); }); test("desktop Agent history remains usable inside the docked navigation pane", async ({ page }) => { await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const agentPanel = page.locator('aside[aria-label="Agent 命令面板"]'); const historyButton = agentPanel.getByRole("button", { name: "打开 Agent 历史记录" }); await historyButton.click(); const history = agentPanel.getByRole("region", { name: "Agent 历史记录" }); await expect(history).toBeVisible(); await waitForAnimations(history); await expect(historyButton).toHaveAttribute("aria-expanded", "true"); await expectUnifiedHistoryStructure(history); await expectThemeColor(agentPanel.locator(".agent-panel-header"), "background-color", "--agent-dock-overlay-surface"); await expectAcrylicSurface(agentPanel.locator(".agent-panel-header")); await expect(history).toHaveCSS("background-color", "rgba(0, 0, 0, 0)"); await expect(history).toHaveCSS("backdrop-filter", "none"); await history.getByRole("button", { name: "重命名对话" }).first().click(); const renameInput = history.locator("input").first(); const editingItem = renameInput.locator( "xpath=ancestor::*[contains(@class, 'agent-history-item')][1]" ); await expect(renameInput).toBeFocused(); await expect(renameInput).toHaveCSS("box-shadow", "none"); await expectThemeColor(renameInput, "border-color", "--action-blue-hover"); await expect(editingItem).toHaveCSS("border-color", "rgba(0, 0, 0, 0)"); await page.screenshot({ path: "test-results/agent-history-ui/desktop-rename.png" }); await history.getByRole("button", { name: "取消重命名" }).click(); await page.keyboard.press("Escape"); await expect(history).toBeHidden(); await expect(historyButton).toBeFocused(); await expect(historyButton).toHaveAttribute("aria-expanded", "false"); }); test("desktop Agent press feedback keeps pointer clicks and keyboard activation stable", async ({ page }) => { await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); await waitForAnimations(panel); const trigger = panel.getByRole("button", { name: "打开 Agent 历史记录" }); const history = panel.getByRole("region", { name: "Agent 历史记录" }); const before = await trigger.boundingBox(); await page.mouse.move(before!.x + before!.width / 2, before!.y + before!.height / 2); await page.mouse.down(); const pressed = await trigger.boundingBox(); expect(pressed!.y).toBe(before!.y + 1); expect(pressed!.width).toBe(before!.width); await page.mouse.up(); await expect(trigger).toHaveAttribute("aria-expanded", "true"); await expect(history).toBeVisible(); await waitForAnimations(panel); await page.keyboard.press("Tab"); await trigger.focus(); expect(await trigger.evaluate((element) => element.matches(":focus-visible"))).toBe(true); const focused = await trigger.boundingBox(); await page.keyboard.down("Space"); expect(await trigger.boundingBox()).toEqual(focused); await page.keyboard.up("Space"); await expect(history).toBeHidden(); await expect(trigger).toBeFocused(); }); test("mobile Agent mirrors the desktop floating history surface", async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: "打开 Agent 面板" }).click(); const agentPanel = page.locator('aside[aria-label="Agent 命令面板"]').filter({ visible: true }); await expect(agentPanel).toBeVisible(); await waitForAnimations(agentPanel); const mobileSheet = page.getByRole("region", { name: "Agent 工作台抽屉" }); const sheetHandle = mobileSheet.locator(".mobile-workbench-sheet-handle"); const agentHeader = agentPanel.locator(".agent-panel-header"); const agentConversation = agentPanel.getByRole("log"); const historyButton = agentPanel.getByRole("button", { name: "打开 Agent 历史记录" }); const headerBefore = await agentHeader.boundingBox(); const conversationBefore = await agentConversation.boundingBox(); const historyButtonPresentationBefore = await getSurfacePresentation(historyButton); const mobileSheetFrameBefore = await expectAcrylicOutline(mobileSheet); await historyButton.click(); const history = agentPanel.getByRole("region", { name: "Agent 历史记录" }); await expect(history).toBeVisible(); await waitForAnimations(history); await page.mouse.move(10, 100); await expectAttachedHistoryExtension(history); await expectTransparentExpandedHeader(agentHeader, mobileSheet); await expectUnifiedHistoryStructure(history); await expect.poll(() => getAcrylicFrame(mobileSheet)).toEqual(mobileSheetFrameBefore); await expect(agentPanel).toHaveCSS("box-shadow", "none"); await expect .poll(() => getSurfacePresentation(historyButton)) .toEqual(historyButtonPresentationBefore); await expect(sheetHandle).toHaveCSS("background-color", "rgba(0, 0, 0, 0)"); const panelBox = await agentPanel.boundingBox(); const headerAfter = await agentHeader.boundingBox(); const historyBox = await history.boundingBox(); const conversationAfter = await agentConversation.boundingBox(); expect(panelBox).not.toBeNull(); expect(headerBefore).not.toBeNull(); expect(headerAfter).not.toBeNull(); expect(historyBox).not.toBeNull(); expect(conversationBefore).not.toBeNull(); expect(conversationAfter).not.toBeNull(); expectRoundedValue(headerAfter!.y, headerBefore!.y); expectRoundedValue(headerAfter!.x, panelBox!.x); expectRoundedValue(headerAfter!.y, panelBox!.y - 28); expectRoundedValue(headerAfter!.width, panelBox!.width); expect(headerAfter!.height).toBeGreaterThan(headerBefore!.height); expectRoundedValue(historyBox!.y, headerBefore!.y + headerBefore!.height); expectRoundedValue(historyBox!.width, headerAfter!.width); expect(headerAfter!.height).toBeLessThan(panelBox!.height); expectRoundedBox(conversationAfter!, conversationBefore!); await expect(agentConversation).toHaveClass(/agent-panel-conversation-canvas/); await expect(agentPanel.locator(".agent-panel-composer")).toBeVisible(); await page.screenshot({ path: "test-results/agent-history-ui/mobile-history.png" }); await page.keyboard.press("Escape"); await expect(history).toBeHidden(); await expect(historyButton).toBeFocused(); }); test("docked Agent respects forced color mode", async ({ page }) => { await page.emulateMedia({ forcedColors: "active" }); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const navigation = page.getByRole("complementary", { name: "Agent 命令面板" }); const workspace = page.locator("#main-workspace"); await expect(navigation).toBeVisible(); await expect(navigation).toHaveCSS("backdrop-filter", "none"); await expect(navigation).toHaveCSS("box-shadow", "none"); await expect(navigation).toHaveCSS("border-right-width", "1px"); await expect(navigation).toHaveCSS("border-right-color", "rgb(0, 0, 0)"); await expect(workspace).toBeVisible(); await expect(page.getByRole("toolbar", { name: "工作区窗口任务栏" })).toHaveCount(0); await page.screenshot({ path: "test-results/agent-history-ui/forced-colors.png" }); }); 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\/agent\/chat\/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 = panel.getByRole("region", { name: "Agent 历史记录" }); await expect(history.locator(".agent-history-session-trigger")).toContainText(originalTitle); await history.getByRole("button", { name: "重命名对话", exact: true }).click(); const input = history.getByRole("textbox"); await expect(input).toBeFocused(); await expectThemeColor(input, "border-color", "--action-blue-hover"); 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: "重命名对话", 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: "删除对话", 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: "删除对话", 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(); } }); async function expectAcrylicSurface(shell: Locator, allowedFilteredDescendant?: string) { const composition = await shell.evaluate((element, allowedSelector) => { const style = getComputedStyle(element); const filteredDescendants: string[] = []; for (const descendant of element.querySelectorAll("*")) { if ( getComputedStyle(descendant).backdropFilter !== "none" && !(allowedSelector && descendant.matches(allowedSelector)) ) { filteredDescendants.push(descendant.className); } } return { backdropFilter: style.backdropFilter, backgroundColor: style.backgroundColor, filteredDescendants, transform: style.transform, willChange: style.willChange }; }, allowedFilteredDescendant); expect(composition.backdropFilter).toContain("blur("); expect(getAlpha(composition.backgroundColor)).toBeLessThan(1); expect(composition.filteredDescendants).toEqual([]); expect(composition.transform).toBe("none"); expect(composition.willChange).toBe("auto"); } async function expectContextualAcrylicSurface( context: Locator, { backdropFilter = "none", maximumAlpha = 0.7, minimumAlpha = 0.35 }: { backdropFilter?: string; maximumAlpha?: number; minimumAlpha?: number; } = {} ) { const composition = await context.evaluate((element) => { const style = getComputedStyle(element); return { backdropFilter: style.backdropFilter, backgroundColor: style.backgroundColor, backgroundImage: style.backgroundImage, boxShadow: style.boxShadow }; }); if (backdropFilter === "none") { expect(composition.backdropFilter).toBe("none"); } else { expect(composition.backdropFilter).toContain(backdropFilter); } expect(getAlpha(composition.backgroundColor)).toBeGreaterThan(minimumAlpha); expect(getAlpha(composition.backgroundColor)).toBeLessThan(maximumAlpha); expect(composition.backgroundImage).not.toBe("none"); expect(composition.boxShadow).not.toBe("none"); } async function expectAttachedHistoryExtension(history: Locator) { const presentation = await history.evaluate((element) => { const style = getComputedStyle(element); return { backdropFilter: style.backdropFilter, backgroundColor: style.backgroundColor, backgroundImage: style.backgroundImage, borderBottomLeftRadius: style.borderBottomLeftRadius, borderBottomRightRadius: style.borderBottomRightRadius, borderBottomWidth: style.borderBottomWidth, borderLeftWidth: style.borderLeftWidth, borderRightWidth: style.borderRightWidth, borderTopWidth: style.borderTopWidth, borderTopLeftRadius: style.borderTopLeftRadius, borderTopRightRadius: style.borderTopRightRadius, boxShadow: style.boxShadow }; }); expect(presentation.backdropFilter).toContain("blur("); expect(getAlpha(presentation.backgroundColor)).toBeGreaterThanOrEqual(0.78); expect(getAlpha(presentation.backgroundColor)).toBeLessThanOrEqual(0.9); expect(presentation.backgroundImage).not.toBe("none"); expect(presentation.borderBottomWidth).toBe("0px"); expect(presentation.borderLeftWidth).toBe("0px"); expect(presentation.borderRightWidth).toBe("0px"); expect(presentation.borderTopWidth).toBe("0px"); expect(presentation.borderTopLeftRadius).toBe("0px"); expect(presentation.borderTopRightRadius).toBe("0px"); expect(presentation.borderBottomLeftRadius).toBe("16px"); expect(presentation.borderBottomRightRadius).toBe("16px"); expect(presentation.boxShadow).toBe("none"); } async function getAcrylicFrame(surface: Locator) { return surface.evaluate((element) => { const style = getComputedStyle(element); return { borderBottomColor: style.borderBottomColor, borderLeftColor: style.borderLeftColor, borderRightColor: style.borderRightColor, borderTopColor: style.borderTopColor, boxShadow: style.boxShadow }; }); } async function expectAcrylicOutline(surface: Locator) { const frame = await getAcrylicFrame(surface); expect(frame.borderTopColor).not.toBe("rgba(0, 0, 0, 0)"); expect(frame.borderRightColor).not.toBe("rgba(0, 0, 0, 0)"); expect(frame.borderBottomColor).not.toBe("rgba(0, 0, 0, 0)"); expect(frame.borderLeftColor).not.toBe("rgba(0, 0, 0, 0)"); expect(frame.boxShadow).toContain("0px 1px 0px 0px inset"); expect(frame.boxShadow).toContain("0px 0px 0px 1px"); return frame; } async function expectUnifiedHistoryStructure(history: Locator) { const structure = await history.evaluate((element) => { const paragraphs = Array.from(element.querySelectorAll("p")); const heading = paragraphs.find((paragraph) => paragraph.textContent?.trim() === "历史记录"); const count = paragraphs.find((paragraph) => /^(?:\d+ 个会话|暂无历史会话)$/.test(paragraph.textContent?.trim() ?? "") ); return { countTagName: count?.tagName ?? null, headingTagName: heading?.tagName ?? null, siblings: Boolean(heading && count && heading.parentElement === count.parentElement) }; }); expect(structure).toEqual({ countTagName: "P", headingTagName: "P", siblings: true }); await expect(history.getByRole("button", { name: "重命名对话" }).first()).toBeVisible(); await expect(history.getByRole("button", { name: "删除对话" }).first()).toBeVisible(); } async function expectTransparentExpandedHeader(header: Locator, agentSurface: Locator) { const [presentation, agentOutlineColor] = await Promise.all([ header.evaluate((element) => { const style = getComputedStyle(element); return { backgroundColor: style.backgroundColor, borderBottomColor: style.borderBottomColor, borderBottomLeftRadius: style.borderBottomLeftRadius, borderBottomRightRadius: style.borderBottomRightRadius, borderBottomWidth: style.borderBottomWidth, borderLeftWidth: style.borderLeftWidth, borderRightWidth: style.borderRightWidth, borderTopWidth: style.borderTopWidth, boxShadow: style.boxShadow }; }), readThemeColor(agentSurface, "--acrylic-outline") ]); expect(getAlpha(presentation.backgroundColor)).toBe(0); expect(presentation.borderBottomColor).toBe(agentOutlineColor); expect(presentation.borderBottomLeftRadius).toBe("16px"); expect(presentation.borderBottomRightRadius).toBe("16px"); expect(presentation.borderBottomWidth).toBe("1px"); expect(presentation.borderLeftWidth).toBe("0px"); expect(presentation.borderRightWidth).toBe("0px"); expect(presentation.borderTopWidth).toBe("0px"); expect(presentation.boxShadow).toContain("0px 12px 12px -12px"); } async function getSurfacePresentation(surface: Locator) { return surface.evaluate((element) => { const style = getComputedStyle(element); return { backdropFilter: style.backdropFilter, backgroundColor: style.backgroundColor, backgroundImage: style.backgroundImage, borderColor: style.borderColor, borderRadius: style.borderRadius, boxShadow: style.boxShadow, color: style.color }; }); } function getAlpha(color: string) { return readColorAlpha(color); } async function expectLightweightMaterialContext(context: Locator) { const style = await context.evaluate((element) => { const computedStyle = getComputedStyle(element); return { backdropFilter: computedStyle.backdropFilter, backgroundColor: computedStyle.backgroundColor, webkitBackdropFilter: computedStyle.getPropertyValue("-webkit-backdrop-filter") }; }); expect(style.backdropFilter).toBe("none"); expect(getAlpha(style.backgroundColor)).toBeGreaterThan(0); expect(getAlpha(style.backgroundColor)).toBeLessThanOrEqual(0.3); expect(["", "none"]).toContain(style.webkitBackdropFilter); } async function expectUnfilteredInternalSurfaces(surfaces: Locator) { const surfaceStyles = await surfaces.evaluateAll((elements) => elements.map((element) => { const style = getComputedStyle(element); return { backdropFilter: style.backdropFilter, className: element.className, webkitBackdropFilter: style.getPropertyValue("-webkit-backdrop-filter") }; }) ); expect(surfaceStyles.length).toBeGreaterThan(0); for (const style of surfaceStyles) { expect(style.backdropFilter).toBe("none"); expect(["", "none"]).toContain(style.webkitBackdropFilter); expect(String(style.className)).not.toContain("acrylic-"); } } function expectRoundedBox(actual: BoundingBox, expected: BoundingBox) { expect(Math.round(actual.x)).toBe(Math.round(expected.x)); expect(Math.round(actual.y)).toBe(Math.round(expected.y)); expect(Math.round(actual.width)).toBe(Math.round(expected.width)); expect(Math.round(actual.height)).toBe(Math.round(expected.height)); } function expectRoundedValue(actual: number, expected: number) { expect(Math.abs(Math.round(actual) - Math.round(expected))).toBeLessThanOrEqual(1); } async function getFloatingSurfaceStyles(header: Locator, composer: Locator) { const control = composer.locator(".agent-panel-control"); const [headerStyles, composerStyles, controlStyles] = await Promise.all([ header.evaluate((element) => { const style = getComputedStyle(element); return { shadow: style.boxShadow, transform: style.transform }; }), composer.evaluate((element) => { const style = getComputedStyle(element); return { shadow: style.boxShadow, transform: style.transform }; }), control.evaluate((element) => { const style = getComputedStyle(element); return { borderColor: style.borderColor, boxShadow: style.boxShadow }; }) ]); return { composerShadow: composerStyles.shadow, composerTransform: composerStyles.transform, controlBorderColor: controlStyles.borderColor, controlBoxShadow: controlStyles.boxShadow, headerShadow: headerStyles.shadow, headerTransform: headerStyles.transform }; }