Files
next-tjwater-frontend/tests/browser/agent-select-menu.e2e.ts
T
jiang ec23d44b02 feat(agent): support Responses steering and project-aware tools
Use the dedicated TJWATER_AGENT_URL and Responses stream protocol. Keep running
turns active while users steer or queue follow-ups and edit pending delivery.
Refresh the model catalog before generation and preserve approval mode selection.

Integrate scoped investigation messaging/presentation, geographic place selection,
current Agent composer styles and supporting workbench contracts.

Validation: 77 focused Vitest checks, 21 browser cases, lint, typecheck and
production build. Preserve direct Server/Agent routing and project authentication.
2026-10-19 18:46:58 +08:00

171 lines
9.4 KiB
TypeScript

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))
));
}