feat(workbench): integrate runtime data and map tooling
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { expect, test, type Locator } from "@playwright/test";
|
||||
import { mockAgentApi } from "./support/mock-agent-api";
|
||||
import { mockWorkbenchContext } from "./support/mock-workbench-context";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await mockWorkbenchContext(page);
|
||||
await mockAgentApi(page, {
|
||||
sessions: [
|
||||
{
|
||||
@@ -31,11 +33,12 @@ async function waitForAnimations(locator: Locator) {
|
||||
});
|
||||
}
|
||||
|
||||
test("desktop Agent lives in the solid Main Frame navigation pane", async ({ page }) => {
|
||||
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 navigation = page.getByRole("complementary", { name: "工作台导航" });
|
||||
const workspace = page.locator("#main-workspace");
|
||||
const agentPanel = page.locator('aside[aria-label="Agent 命令面板"]');
|
||||
const agentHeader = agentPanel.locator(".agent-panel-header");
|
||||
const agentConversation = agentPanel.getByRole("log");
|
||||
@@ -43,19 +46,18 @@ test("desktop Agent lives in the solid Main Frame navigation pane", async ({ pag
|
||||
const agentControls = agentPanel.locator(".agent-panel-band").last();
|
||||
|
||||
await expect(topBar).toBeVisible();
|
||||
await expect(navigation).toBeVisible();
|
||||
await expect(workspace).toBeVisible();
|
||||
await expect(agentPanel).toBeVisible();
|
||||
await expect(agentHeader).toBeVisible();
|
||||
await expect(agentControls).toBeVisible();
|
||||
await waitForAnimations(agentPanel);
|
||||
|
||||
await expectAcrylicSurface(topBar);
|
||||
await expect(navigation).toHaveCSS("background-color", "rgb(237, 241, 245)");
|
||||
await expect(navigation).toHaveCSS("backdrop-filter", "none");
|
||||
await expect(agentPanel).toHaveCSS("background-color", "rgb(247, 249, 251)");
|
||||
await expect(agentPanel).toHaveCSS("background-color", "oklch(0.965 0.012 248)");
|
||||
await expect(agentPanel).toHaveCSS("backdrop-filter", "none");
|
||||
await expect(agentPanel).toHaveCSS("border-radius", "0px");
|
||||
await expect(agentPanel).toHaveCSS("box-shadow", "none");
|
||||
await expect(agentPanel).toHaveCSS("border-top-left-radius", "0px");
|
||||
await expect(agentPanel).toHaveCSS("border-top-right-radius", "16px");
|
||||
await expect(agentPanel).not.toHaveCSS("box-shadow", "none");
|
||||
await expect(agentControls).toHaveCSS("border-top-width", "1px");
|
||||
await expect(
|
||||
agentPanel.getByText("直接说出你想调查的问题,我会整理监测与空间证据,并将分析结果带回地图。")
|
||||
@@ -69,22 +71,21 @@ test("desktop Agent lives in the solid Main Frame navigation pane", async ({ pag
|
||||
expect(panelBox).not.toBeNull();
|
||||
expect(conversationBox).not.toBeNull();
|
||||
expect(scrollBox).not.toBeNull();
|
||||
expect(panelBox!.x).toBe(48);
|
||||
expect(panelBox!.width).toBe(319);
|
||||
expect(
|
||||
Math.abs(conversationBox!.x - panelBox!.x)
|
||||
).toBeLessThanOrEqual(1);
|
||||
expect(panelBox!.x).toBe(0);
|
||||
expect(panelBox!.width).toBe(500);
|
||||
await expect.poll(async () => (await workspace.boundingBox())?.x).toBe(500);
|
||||
expect(Math.abs(conversationBox!.x - panelBox!.x)).toBeLessThanOrEqual(1);
|
||||
await expect(agentConversationScroll).toHaveCSS("scrollbar-gutter", "stable both-edges");
|
||||
|
||||
const beforeFocus = await getSurfacePresentation(agentPanel);
|
||||
await page.getByPlaceholder("输入调度问题,Agent 将通过后端会话流式响应").focus();
|
||||
await expect
|
||||
.poll(() => getSurfacePresentation(agentPanel))
|
||||
.toEqual(beforeFocus);
|
||||
await expect.poll(() => getSurfacePresentation(agentPanel)).toEqual(beforeFocus);
|
||||
await page.screenshot({ path: "test-results/agent-history-ui/desktop-dock.png" });
|
||||
});
|
||||
|
||||
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", {
|
||||
@@ -108,6 +109,7 @@ test("desktop Agent history remains usable inside the docked navigation pane", a
|
||||
await expect(renameInput).toHaveCSS("box-shadow", "none");
|
||||
await expect(renameInput).toHaveCSS("border-color", "rgb(203, 213, 225)");
|
||||
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");
|
||||
@@ -173,23 +175,173 @@ test("mobile Agent mirrors the desktop floating history surface", async ({ page
|
||||
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("Main Frame navigation respects forced color mode", async ({ page }) => {
|
||||
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: "工作台导航" });
|
||||
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(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
|
||||
}) => {
|
||||
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.fulfill({
|
||||
json: {
|
||||
sessions: deleted
|
||||
? []
|
||||
: [
|
||||
{
|
||||
session_id: sessionId,
|
||||
title,
|
||||
created_at: 100,
|
||||
updated_at: 200,
|
||||
run_status: "completed"
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
);
|
||||
await page.route("**/api/v1/agent/chat/session/**", async (route) => {
|
||||
const request = route.request();
|
||||
const method = request.method();
|
||||
const path = new URL(request.url()).pathname;
|
||||
if (method === "PATCH" && path === `/api/v1/agent/chat/session/${sessionId}/title`) {
|
||||
const body = request.postDataJSON();
|
||||
title = body.title;
|
||||
writes.push({ method, path, body });
|
||||
return route.fulfill({ json: {} });
|
||||
}
|
||||
if (method === "DELETE" && path === `/api/v1/agent/chat/session/${sessionId}`) {
|
||||
deleted = true;
|
||||
writes.push({ method, path, body: null });
|
||||
return route.fulfill({ 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();
|
||||
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 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/agent/chat/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/agent/chat/session/${sessionId}`,
|
||||
body: null
|
||||
});
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(history).toBeHidden();
|
||||
await expect(historyButton).toBeFocused();
|
||||
}
|
||||
});
|
||||
|
||||
async function expectAcrylicSurface(shell: Locator, allowedFilteredDescendant?: string) {
|
||||
|
||||
Reference in New Issue
Block a user