370 lines
18 KiB
TypeScript
370 lines
18 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";
|
|
|
|
// 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();
|
|
}
|
|
});
|