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/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(); }); } test("sends the selected approval mode and can return to manual approval", async ({ page }) => { await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const trigger = page.getByRole("button", { name: "权限批准模式" }); for (const [label, mode] of [ ["自动批准本轮工具权限请求", "always"], ["需要审批的请求逐次确认", "request"], ] as const) { await expect(trigger).toBeEnabled(); await trigger.click(); await page.getByRole("menuitem", { name: new RegExp(label) }).click(); await page.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" }).fill(`验证权限模式 ${mode}`); const sent = page.waitForRequest((request) => request.method() === "POST" && request.url().endsWith("/api/v1/responses")); await page.getByRole("button", { name: "发送 Agent 指令" }).click(); expect((await sent).postDataJSON().tjwater.approval_mode).toBe(mode); } await expect(trigger).toBeEnabled(); }); test("replaces a removed model before sending and shows the model actually submitted", async ({ page }) => { await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const trigger = page.getByRole("button", { name: "选择 Agent 模型" }); await expect(trigger).toHaveAccessibleDescription("当前模型:快速"); await trigger.click(); await page.getByRole("menuitem", { name: /深入分析和复杂任务处理/ }).click(); await expect(trigger).toHaveAccessibleDescription("当前模型:专家"); // Simulate an Agent restart with a new catalog while this page stays open. await page.route("**/api/v1/models", (route) => route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: { default_model: "waternetwork/gpt-6-luna", models: [{ id: "waternetwork/gpt-6-luna", label: "快速", description: "当前可用模型", icon: "bolt" }] } })); await page.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" }).fill("检查更新后的快速模型"); const sent = page.waitForRequest((request) => request.method() === "POST" && request.url().endsWith("/api/v1/responses")); await page.getByRole("button", { name: "发送 Agent 指令" }).click(); expect((await sent).postDataJSON().model).toBe("waternetwork/gpt-6-luna"); await expect(trigger).toHaveAccessibleDescription("当前模型:快速"); }); test("keeps an explicitly selected model when it remains available", async ({ page }) => { await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const trigger = page.getByRole("button", { name: "选择 Agent 模型" }); await expect(trigger).toHaveAccessibleDescription("当前模型:快速"); await trigger.click(); await page.getByRole("menuitem", { name: /深入分析和复杂任务处理/ }).click(); await page.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" }).fill("保留专家模型选择"); const sent = page.waitForRequest((request) => request.method() === "POST" && request.url().endsWith("/api/v1/responses")); await page.getByRole("button", { name: "发送 Agent 指令" }).click(); expect((await sent).postDataJSON().model).toBe("test/expert"); await expect(trigger).toHaveAccessibleDescription("当前模型:专家"); }); test("does not send a generation request when the refreshed catalog is empty", async ({ page }) => { await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); await expect(page.getByRole("button", { name: "选择 Agent 模型" })).toHaveAccessibleDescription("当前模型:快速"); await page.route("**/api/v1/models", (route) => route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: { default_model: "test/fast", models: [] } })); const requests: string[] = []; page.on("request", (request) => { if (request.method() === "POST" && request.url().endsWith("/api/v1/responses")) requests.push(request.url()); }); await page.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" }).fill("检查空模型目录"); await page.getByRole("button", { name: "发送 Agent 指令" }).click(); await expect(page.getByText("Agent 后端当前没有可用模型,请检查模型配置。").first()).toBeVisible(); expect(requests).toEqual([]); }); 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)) )); }