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"; test.beforeEach(async ({ page }) => { await page.emulateMedia({ reducedMotion: "reduce" }); await mockWorkbenchContext(page); await mockAgentApi(page); await page.route("**/api/v1/agent/chat/models", (route) => route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: { default_model: "test/fast", models: [ { id: "test/fast", label: "快速", description: "快速回答和日常任务处理", icon: "bolt" }, { id: "test/expert", label: "专家", description: "深入分析和复杂任务处理", icon: "sparkle" } ] } })); }); for (const width of [1440, 375]) { test(`model options show hover and keyboard highlight without changing selection at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 900 }); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: width < 1024 ? "打开 Agent 面板" : /展开 Agent 助手面板/ }).click(); // Radix hides the trigger from the accessibility tree while its modal menu is open. const trigger = page.locator('button[aria-label="选择 Agent 模型"]'); await expect(trigger).toHaveAccessibleDescription("当前模型:快速"); await trigger.click(); await waitForMenuAnimations(page.getByRole("menu")); const fast = page.getByRole("menuitem", { name: /快速回答和日常任务处理/ }); const expert = page.getByRole("menuitem", { name: /深入分析和复杂任务处理/ }); const box = await expert.boundingBox(); for (const item of [fast, expert]) { await item.hover(); await expect(item).toHaveAttribute("data-highlighted", ""); await expectThemeColor(item, "background-color", "--action-selection-soft-hover"); await expectThemeColor(item, "border-color", "--action-border-hover"); await expect(trigger).toHaveAttribute("aria-description", "当前模型:快速"); } expect(await expert.boundingBox()).toEqual(box); await page.getByRole("menu").screenshot({ path: `test-results/agent-select-menu/model-${width}.png` }); await page.keyboard.press("ArrowUp"); await expect(fast).toBeFocused(); await expectThemeColor(fast, "background-color", "--action-selection-soft-hover"); await page.keyboard.press("ArrowDown"); await expect(expert).toBeFocused(); await page.keyboard.press("Enter"); await expect(page.getByRole("menu")).toHaveCount(0); await expect(trigger).toHaveAccessibleDescription("当前模型:专家"); await expect(trigger).toBeFocused(); }); test(`approval options highlight both selected and unselected modes at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 900 }); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: width < 1024 ? "打开 Agent 面板" : /展开 Agent 助手面板/ }).click(); const trigger = page.locator('button[aria-label="权限批准模式"]'); await trigger.click(); await waitForMenuAnimations(page.getByRole("menu")); const request = page.getByRole("menuitem", { name: /工具权限逐次确认/ }); const always = page.getByRole("menuitem", { name: /自动允许本轮权限请求/ }); for (const item of [request, always]) { await item.hover(); await expectThemeColor(item, "background-color", "--action-selection-soft-hover"); await expectThemeColor(item, "border-color", "--action-border-hover"); await expect(trigger).toContainText("请求批准"); } await page.getByRole("menu").screenshot({ path: `test-results/agent-select-menu/approval-${width}.png` }); await always.click(); await expect(page.getByRole("menu")).toHaveCount(0); await expect(trigger).toBeFocused(); await expect(trigger).toContainText("始终允许"); await trigger.click(); await waitForMenuAnimations(page.getByRole("menu")); await always.hover(); await expectThemeColor(always, "background-color", "--action-selection-soft-hover"); await page.keyboard.press("ArrowUp"); await expect(request).toBeFocused(); await expectThemeColor(request, "background-color", "--action-selection-soft-hover"); await page.keyboard.press("Enter"); await expect(trigger).toContainText("请求批准"); await expect(trigger).toBeFocused(); }); } async function waitForMenuAnimations(menu: Locator) { await expect(menu).toBeVisible(); await menu.evaluate((element) => Promise.all( element.getAnimations({ subtree: true }) .filter((animation) => animation.playState === "running") .map((animation) => animation.finished.catch(() => undefined)) )); }