Unify dock surfaces, headers and dividers, and remove the visible context strip. Limit pointer resizing to 400-640px and reveal the edge grip on hover. Resize and redraw the map in the same layout cycle to prevent blanking and lag. Preserve horizontal centering during conversation control press feedback. Validation: isolated lint, typecheck, 21 focused unit tests, and 10 relevant browser checks passed. Snapshot baselines regenerated from the isolated change. The existing mobile alert summary test also fails on unchanged HEAD because the expected demo alert source is unavailable in this environment.
760 lines
32 KiB
TypeScript
760 lines
32 KiB
TypeScript
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: [
|
|
{
|
|
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"
|
|
}
|
|
]
|
|
});
|
|
});
|
|
|
|
type BoundingBox = {
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
};
|
|
|
|
async function waitForAnimations(locator: Locator) {
|
|
await locator.evaluate(async (element) => {
|
|
const animations = element
|
|
.getAnimations({ subtree: true })
|
|
.filter((animation) => animation.effect?.getTiming().iterations !== Infinity);
|
|
await Promise.all(animations.map((animation) => animation.finished.catch(() => undefined)));
|
|
});
|
|
}
|
|
|
|
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 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");
|
|
const agentConversationScroll = agentPanel.locator(".agent-conversation-scroll");
|
|
const agentControls = agentPanel.locator(".agent-panel-band").last();
|
|
|
|
await expect(topBar).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(agentPanel).toHaveCSS("background-color", "oklch(0.972 0.005 250)");
|
|
await expect(agentPanel).toHaveCSS("backdrop-filter", "none");
|
|
await expect(agentPanel).toHaveCSS("border-radius", "0px");
|
|
await expect(agentPanel).toHaveCSS("border-top-width", "0px");
|
|
await expect(agentPanel).toHaveCSS("border-bottom-width", "0px");
|
|
await expect(agentPanel).toHaveCSS("border-left-width", "0px");
|
|
await expect(agentPanel).toHaveCSS("border-right-width", "1px");
|
|
await expect(agentPanel).toHaveCSS("border-right-color", "oklch(0.905 0.009 250)");
|
|
await expect(agentPanel).toHaveCSS("box-shadow", "none");
|
|
await expect(agentControls).toHaveCSS("border-top-width", "1px");
|
|
await expect(agentControls).toHaveCSS("background-color", "oklch(0.985 0.003 250)");
|
|
await expect(agentPanel.locator("form.agent-panel-control")).toHaveCSS("background-color", "rgb(255, 255, 255)");
|
|
const capabilities = agentPanel.getByRole("list", { name: "Agent 分析能力" });
|
|
await expect(capabilities.getByRole("listitem")).toHaveCount(4);
|
|
await expect(capabilities).toHaveCSS("box-shadow", "none");
|
|
for (const item of await capabilities.getByRole("listitem").all()) {
|
|
await expect(item).toHaveCSS("border-right-width", "0px");
|
|
await expect(item).toHaveCSS("border-bottom-width", "0px");
|
|
}
|
|
await expect(
|
|
agentPanel.getByText("直接说出你想调查的问题,我会整理监测与空间证据,并将分析结果带回地图。")
|
|
).toHaveCSS("color", "rgb(51, 65, 85)");
|
|
await expect(agentPanel.getByText("水力瓶颈诊断")).toHaveCSS("color", "rgb(30, 41, 59)");
|
|
await expect(agentPanel.getByText("水力瓶颈诊断")).toHaveCSS("font-size", "14px");
|
|
|
|
const panelBox = await agentPanel.boundingBox();
|
|
const conversationBox = await agentConversation.boundingBox();
|
|
const scrollBox = await agentConversationScroll.boundingBox();
|
|
expect(panelBox).not.toBeNull();
|
|
expect(conversationBox).not.toBeNull();
|
|
expect(scrollBox).not.toBeNull();
|
|
expect(panelBox!.x).toBe(0);
|
|
const topBarBox = await topBar.boundingBox();
|
|
expect(panelBox!.y).toBe(topBarBox!.y + topBarBox!.height);
|
|
expect(panelBox!.y + panelBox!.height).toBe(page.viewportSize()!.height);
|
|
expect(panelBox!.width).toBe(500);
|
|
await expect.poll(async () => (await workspace.boundingBox())?.x).toBe(500);
|
|
const headerBox = await agentHeader.boundingBox();
|
|
const workspaceBox = await workspace.boundingBox();
|
|
expect(headerBox!.height).toBe(topBarBox!.height);
|
|
expect(headerBox!.y).toBe(workspaceBox!.y);
|
|
await expect(topBar).toHaveCSS("box-shadow", "none");
|
|
await expect(agentHeader).toHaveCSS("box-shadow", "none");
|
|
await expect(agentHeader.locator(":scope > div").first()).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
|
expect(await topBar.evaluate((element) => getComputedStyle(element).borderBottomColor)).toBe(
|
|
await agentPanel.evaluate((element) => getComputedStyle(element).borderRightColor)
|
|
);
|
|
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 page.screenshot({ path: "test-results/agent-history-ui/desktop-dock.png" });
|
|
|
|
for (const viewport of [
|
|
{ width: 1024, height: 768, panelWidth: 500 },
|
|
{ width: 1920, height: 1080, panelWidth: 540 }
|
|
]) {
|
|
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
|
await expect(agentPanel).toHaveCSS("width", `${viewport.panelWidth}px`);
|
|
await expect.poll(async () => (await workspace.boundingBox())?.x).toBe(viewport.panelWidth);
|
|
const resizedPanelBox = await agentPanel.boundingBox();
|
|
expect(resizedPanelBox!.x).toBe(0);
|
|
expect(resizedPanelBox!.y).toBe(topBarBox!.y + topBarBox!.height);
|
|
expect(resizedPanelBox!.y + resizedPanelBox!.height).toBe(viewport.height);
|
|
await page.screenshot({
|
|
path: `test-results/agent-history-ui/desktop-dock-${viewport.width}.png`
|
|
});
|
|
}
|
|
});
|
|
|
|
test("desktop Agent portal menus keep local material and respect reduced transparency", async ({ page }) => {
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
|
|
const panel = page.getByRole("complementary", { name: "Agent 命令面板" });
|
|
await waitForAnimations(panel);
|
|
|
|
for (const name of ["选择 Agent 模型", "权限批准模式"]) {
|
|
const trigger = panel.getByRole("button", { name });
|
|
await trigger.click();
|
|
const menu = page.getByRole("menu");
|
|
await expect(menu).toHaveAttribute("data-agent-presentation", "desktop-dock");
|
|
expect(await menu.evaluate((element) => element.closest("aside"))).toBeNull();
|
|
await expect(menu).toHaveCSS("background-color", "rgba(255, 255, 255, 0.94)");
|
|
await expect(menu).toHaveCSS("backdrop-filter", "blur(12px) saturate(1.03)");
|
|
await expect(menu).toHaveCSS("background-image", "none");
|
|
await page.keyboard.press("Escape");
|
|
await expect(trigger).toBeFocused();
|
|
}
|
|
|
|
const cdp = await page.context().newCDPSession(page);
|
|
await cdp.send("Emulation.setEmulatedMedia", {
|
|
features: [{ name: "prefers-reduced-transparency", value: "reduce" }]
|
|
});
|
|
expect(await page.evaluate(() => matchMedia("(prefers-reduced-transparency: reduce)").matches)).toBe(true);
|
|
await panel.getByRole("button", { name: "选择 Agent 模型" }).click();
|
|
const menu = page.getByRole("menu");
|
|
await expect(menu).toHaveCSS("background-color", "rgb(255, 255, 255)");
|
|
await expect(menu).toHaveCSS("backdrop-filter", "none");
|
|
await page.keyboard.press("Escape");
|
|
await panel.getByRole("button", { name: "打开 Agent 历史记录" }).click();
|
|
const history = panel.getByRole("region", { name: "Agent 历史记录" });
|
|
await expect(history).toHaveCSS("background-color", "rgb(255, 255, 255)");
|
|
await expect(history).toHaveCSS("backdrop-filter", "none");
|
|
});
|
|
|
|
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", {
|
|
name: "打开 Agent 历史记录"
|
|
});
|
|
|
|
await historyButton.click();
|
|
|
|
const history = agentPanel.getByRole("region", { name: "Agent 历史记录" });
|
|
await expect(history).toBeVisible();
|
|
await waitForAnimations(history);
|
|
await expect(historyButton).toHaveAttribute("aria-expanded", "true");
|
|
await expectUnifiedHistoryStructure(history);
|
|
await expect(history).toHaveCSS("background-color", "rgba(255, 255, 255, 0.94)");
|
|
await expect(history).toHaveCSS("backdrop-filter", "blur(12px) saturate(1.03)");
|
|
|
|
await history.getByRole("button", { name: "重命名对话" }).first().click();
|
|
const renameInput = history.locator("input").first();
|
|
const editingItem = renameInput.locator(
|
|
"xpath=ancestor::*[contains(@class, 'agent-history-item')][1]"
|
|
);
|
|
await expect(renameInput).toBeFocused();
|
|
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");
|
|
await expect(history).toBeHidden();
|
|
await expect(historyButton).toBeFocused();
|
|
await expect(historyButton).toHaveAttribute("aria-expanded", "false");
|
|
});
|
|
|
|
test("desktop Agent press feedback keeps pointer clicks and keyboard activation stable", async ({ page }) => {
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
|
|
const panel = page.getByRole("complementary", { name: "Agent 命令面板" });
|
|
await waitForAnimations(panel);
|
|
const trigger = panel.getByRole("button", { name: "打开 Agent 历史记录" });
|
|
const history = panel.getByRole("region", { name: "Agent 历史记录" });
|
|
const before = await trigger.boundingBox();
|
|
await page.mouse.move(before!.x + before!.width / 2, before!.y + before!.height / 2);
|
|
await page.mouse.down();
|
|
const pressed = await trigger.boundingBox();
|
|
expect(pressed!.y).toBe(before!.y + 1);
|
|
expect(pressed!.width).toBe(before!.width);
|
|
await page.mouse.up();
|
|
await expect(trigger).toHaveAttribute("aria-expanded", "true");
|
|
await expect(history).toBeVisible();
|
|
await waitForAnimations(panel);
|
|
|
|
await page.keyboard.press("Tab");
|
|
await trigger.focus();
|
|
expect(await trigger.evaluate((element) => element.matches(":focus-visible"))).toBe(true);
|
|
const focused = await trigger.boundingBox();
|
|
await page.keyboard.down("Space");
|
|
expect(await trigger.boundingBox()).toEqual(focused);
|
|
await page.keyboard.up("Space");
|
|
await expect(history).toBeHidden();
|
|
await expect(trigger).toBeFocused();
|
|
});
|
|
|
|
test("mobile Agent mirrors the desktop floating history surface", async ({ page }) => {
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByRole("button", { name: "打开 Agent 面板" }).click();
|
|
|
|
const agentPanel = page.locator('aside[aria-label="Agent 命令面板"]').filter({ visible: true });
|
|
await expect(agentPanel).toBeVisible();
|
|
await waitForAnimations(agentPanel);
|
|
const mobileSheet = page.getByRole("region", { name: "Agent 工作台抽屉" });
|
|
const sheetHandle = mobileSheet.locator(".mobile-workbench-sheet-handle");
|
|
const agentHeader = agentPanel.locator(".agent-panel-header");
|
|
const agentConversation = agentPanel.getByRole("log");
|
|
const historyButton = agentPanel.getByRole("button", {
|
|
name: "打开 Agent 历史记录"
|
|
});
|
|
const headerBefore = await agentHeader.boundingBox();
|
|
const conversationBefore = await agentConversation.boundingBox();
|
|
const historyButtonPresentationBefore = await getSurfacePresentation(historyButton);
|
|
const mobileSheetFrameBefore = await expectAcrylicOutline(mobileSheet);
|
|
|
|
await historyButton.click();
|
|
|
|
const history = agentPanel.getByRole("region", { name: "Agent 历史记录" });
|
|
await expect(history).toBeVisible();
|
|
await waitForAnimations(history);
|
|
await page.mouse.move(10, 100);
|
|
await expectAttachedHistoryExtension(history);
|
|
await expectTransparentExpandedHeader(agentHeader, mobileSheet);
|
|
await expectUnifiedHistoryStructure(history);
|
|
await expect.poll(() => getAcrylicFrame(mobileSheet)).toEqual(mobileSheetFrameBefore);
|
|
await expect(agentPanel).toHaveCSS("box-shadow", "none");
|
|
await expect
|
|
.poll(() => getSurfacePresentation(historyButton))
|
|
.toEqual(historyButtonPresentationBefore);
|
|
await expect(sheetHandle).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
|
|
|
const panelBox = await agentPanel.boundingBox();
|
|
const headerAfter = await agentHeader.boundingBox();
|
|
const historyBox = await history.boundingBox();
|
|
const conversationAfter = await agentConversation.boundingBox();
|
|
expect(panelBox).not.toBeNull();
|
|
expect(headerBefore).not.toBeNull();
|
|
expect(headerAfter).not.toBeNull();
|
|
expect(historyBox).not.toBeNull();
|
|
expect(conversationBefore).not.toBeNull();
|
|
expect(conversationAfter).not.toBeNull();
|
|
expectRoundedValue(headerAfter!.y, headerBefore!.y);
|
|
expectRoundedValue(headerAfter!.x, panelBox!.x);
|
|
expectRoundedValue(headerAfter!.y, panelBox!.y - 28);
|
|
expectRoundedValue(headerAfter!.width, panelBox!.width);
|
|
expect(headerAfter!.height).toBeGreaterThan(headerBefore!.height);
|
|
expectRoundedValue(historyBox!.y, headerBefore!.y + headerBefore!.height);
|
|
expectRoundedValue(historyBox!.width, headerAfter!.width);
|
|
expect(headerAfter!.height).toBeLessThan(panelBox!.height);
|
|
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("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: "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(navigation).toHaveCSS("border-right-width", "1px");
|
|
await expect(navigation).toHaveCSS("border-right-color", "rgb(0, 0, 0)");
|
|
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
|
|
}) => {
|
|
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\/agent\/chat\/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/agent/chat/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/agent/chat/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/agent/chat/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 = 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) {
|
|
const composition = await shell.evaluate((element, allowedSelector) => {
|
|
const style = getComputedStyle(element);
|
|
const filteredDescendants: string[] = [];
|
|
|
|
for (const descendant of element.querySelectorAll("*")) {
|
|
if (
|
|
getComputedStyle(descendant).backdropFilter !== "none" &&
|
|
!(allowedSelector && descendant.matches(allowedSelector))
|
|
) {
|
|
filteredDescendants.push(descendant.className);
|
|
}
|
|
}
|
|
|
|
return {
|
|
backdropFilter: style.backdropFilter,
|
|
backgroundColor: style.backgroundColor,
|
|
filteredDescendants,
|
|
transform: style.transform,
|
|
willChange: style.willChange
|
|
};
|
|
}, allowedFilteredDescendant);
|
|
|
|
expect(composition.backdropFilter).toContain("blur(");
|
|
expect(getAlpha(composition.backgroundColor)).toBeLessThan(1);
|
|
expect(composition.filteredDescendants).toEqual([]);
|
|
expect(composition.transform).toBe("none");
|
|
expect(composition.willChange).toBe("auto");
|
|
}
|
|
|
|
async function expectContextualAcrylicSurface(
|
|
context: Locator,
|
|
{
|
|
backdropFilter = "none",
|
|
maximumAlpha = 0.7,
|
|
minimumAlpha = 0.35
|
|
}: {
|
|
backdropFilter?: string;
|
|
maximumAlpha?: number;
|
|
minimumAlpha?: number;
|
|
} = {}
|
|
) {
|
|
const composition = await context.evaluate((element) => {
|
|
const style = getComputedStyle(element);
|
|
|
|
return {
|
|
backdropFilter: style.backdropFilter,
|
|
backgroundColor: style.backgroundColor,
|
|
backgroundImage: style.backgroundImage,
|
|
boxShadow: style.boxShadow
|
|
};
|
|
});
|
|
|
|
if (backdropFilter === "none") {
|
|
expect(composition.backdropFilter).toBe("none");
|
|
} else {
|
|
expect(composition.backdropFilter).toContain(backdropFilter);
|
|
}
|
|
expect(getAlpha(composition.backgroundColor)).toBeGreaterThan(minimumAlpha);
|
|
expect(getAlpha(composition.backgroundColor)).toBeLessThan(maximumAlpha);
|
|
expect(composition.backgroundImage).not.toBe("none");
|
|
expect(composition.boxShadow).not.toBe("none");
|
|
}
|
|
|
|
async function expectAttachedHistoryExtension(history: Locator) {
|
|
const presentation = await history.evaluate((element) => {
|
|
const style = getComputedStyle(element);
|
|
|
|
return {
|
|
backdropFilter: style.backdropFilter,
|
|
backgroundColor: style.backgroundColor,
|
|
backgroundImage: style.backgroundImage,
|
|
borderBottomLeftRadius: style.borderBottomLeftRadius,
|
|
borderBottomRightRadius: style.borderBottomRightRadius,
|
|
borderBottomWidth: style.borderBottomWidth,
|
|
borderLeftWidth: style.borderLeftWidth,
|
|
borderRightWidth: style.borderRightWidth,
|
|
borderTopWidth: style.borderTopWidth,
|
|
borderTopLeftRadius: style.borderTopLeftRadius,
|
|
borderTopRightRadius: style.borderTopRightRadius,
|
|
boxShadow: style.boxShadow
|
|
};
|
|
});
|
|
|
|
expect(presentation.backdropFilter).toContain("blur(");
|
|
expect(getAlpha(presentation.backgroundColor)).toBeGreaterThanOrEqual(0.78);
|
|
expect(getAlpha(presentation.backgroundColor)).toBeLessThanOrEqual(0.9);
|
|
expect(presentation.backgroundImage).not.toBe("none");
|
|
expect(presentation.borderBottomWidth).toBe("0px");
|
|
expect(presentation.borderLeftWidth).toBe("0px");
|
|
expect(presentation.borderRightWidth).toBe("0px");
|
|
expect(presentation.borderTopWidth).toBe("0px");
|
|
expect(presentation.borderTopLeftRadius).toBe("0px");
|
|
expect(presentation.borderTopRightRadius).toBe("0px");
|
|
expect(presentation.borderBottomLeftRadius).toBe("16px");
|
|
expect(presentation.borderBottomRightRadius).toBe("16px");
|
|
expect(presentation.boxShadow).toBe("none");
|
|
}
|
|
|
|
async function getAcrylicFrame(surface: Locator) {
|
|
return surface.evaluate((element) => {
|
|
const style = getComputedStyle(element);
|
|
|
|
return {
|
|
borderBottomColor: style.borderBottomColor,
|
|
borderLeftColor: style.borderLeftColor,
|
|
borderRightColor: style.borderRightColor,
|
|
borderTopColor: style.borderTopColor,
|
|
boxShadow: style.boxShadow
|
|
};
|
|
});
|
|
}
|
|
|
|
async function expectAcrylicOutline(surface: Locator) {
|
|
const frame = await getAcrylicFrame(surface);
|
|
|
|
expect(frame.borderTopColor).not.toBe("rgba(0, 0, 0, 0)");
|
|
expect(frame.borderRightColor).not.toBe("rgba(0, 0, 0, 0)");
|
|
expect(frame.borderBottomColor).not.toBe("rgba(0, 0, 0, 0)");
|
|
expect(frame.borderLeftColor).not.toBe("rgba(0, 0, 0, 0)");
|
|
expect(frame.boxShadow).toContain("0px 1px 0px 0px inset");
|
|
expect(frame.boxShadow).toContain("0px 0px 0px 1px");
|
|
|
|
return frame;
|
|
}
|
|
|
|
async function expectUnifiedHistoryStructure(history: Locator) {
|
|
const structure = await history.evaluate((element) => {
|
|
const paragraphs = Array.from(element.querySelectorAll("p"));
|
|
const heading = paragraphs.find((paragraph) => paragraph.textContent?.trim() === "历史记录");
|
|
const count = paragraphs.find((paragraph) =>
|
|
/^(?:\d+ 个会话|暂无历史会话)$/.test(paragraph.textContent?.trim() ?? "")
|
|
);
|
|
|
|
return {
|
|
countTagName: count?.tagName ?? null,
|
|
headingTagName: heading?.tagName ?? null,
|
|
siblings: Boolean(heading && count && heading.parentElement === count.parentElement)
|
|
};
|
|
});
|
|
|
|
expect(structure).toEqual({
|
|
countTagName: "P",
|
|
headingTagName: "P",
|
|
siblings: true
|
|
});
|
|
await expect(history.getByRole("button", { name: "重命名对话" }).first()).toBeVisible();
|
|
await expect(history.getByRole("button", { name: "删除对话" }).first()).toBeVisible();
|
|
}
|
|
|
|
async function expectTransparentExpandedHeader(header: Locator, agentSurface: Locator) {
|
|
const [presentation, agentOutlineColor] = await Promise.all([
|
|
header.evaluate((element) => {
|
|
const style = getComputedStyle(element);
|
|
|
|
return {
|
|
backgroundColor: style.backgroundColor,
|
|
borderBottomColor: style.borderBottomColor,
|
|
borderBottomLeftRadius: style.borderBottomLeftRadius,
|
|
borderBottomRightRadius: style.borderBottomRightRadius,
|
|
borderBottomWidth: style.borderBottomWidth,
|
|
borderLeftWidth: style.borderLeftWidth,
|
|
borderRightWidth: style.borderRightWidth,
|
|
borderTopWidth: style.borderTopWidth,
|
|
boxShadow: style.boxShadow
|
|
};
|
|
}),
|
|
agentSurface.evaluate((element) =>
|
|
getComputedStyle(element).getPropertyValue("--acrylic-outline").trim()
|
|
)
|
|
]);
|
|
|
|
expect(getAlpha(presentation.backgroundColor)).toBe(0);
|
|
expect(presentation.borderBottomColor).toBe(agentOutlineColor);
|
|
expect(presentation.borderBottomLeftRadius).toBe("16px");
|
|
expect(presentation.borderBottomRightRadius).toBe("16px");
|
|
expect(presentation.borderBottomWidth).toBe("1px");
|
|
expect(presentation.borderLeftWidth).toBe("0px");
|
|
expect(presentation.borderRightWidth).toBe("0px");
|
|
expect(presentation.borderTopWidth).toBe("0px");
|
|
expect(presentation.boxShadow).toContain("0px 12px 12px -12px");
|
|
}
|
|
|
|
async function getSurfacePresentation(surface: Locator) {
|
|
return surface.evaluate((element) => {
|
|
const style = getComputedStyle(element);
|
|
|
|
return {
|
|
backdropFilter: style.backdropFilter,
|
|
backgroundColor: style.backgroundColor,
|
|
backgroundImage: style.backgroundImage,
|
|
borderColor: style.borderColor,
|
|
borderRadius: style.borderRadius,
|
|
boxShadow: style.boxShadow,
|
|
color: style.color
|
|
};
|
|
});
|
|
}
|
|
|
|
function getAlpha(color: string) {
|
|
const match = color.match(/^rgba\([^,]+,[^,]+,[^,]+,\s*([\d.]+)\)$/);
|
|
|
|
return match ? Number(match[1]) : 1;
|
|
}
|
|
|
|
async function expectLightweightMaterialContext(context: Locator) {
|
|
const style = await context.evaluate((element) => {
|
|
const computedStyle = getComputedStyle(element);
|
|
|
|
return {
|
|
backdropFilter: computedStyle.backdropFilter,
|
|
backgroundColor: computedStyle.backgroundColor,
|
|
webkitBackdropFilter: computedStyle.getPropertyValue("-webkit-backdrop-filter")
|
|
};
|
|
});
|
|
|
|
expect(style.backdropFilter).toBe("none");
|
|
expect(getAlpha(style.backgroundColor)).toBeGreaterThan(0);
|
|
expect(getAlpha(style.backgroundColor)).toBeLessThanOrEqual(0.3);
|
|
expect(["", "none"]).toContain(style.webkitBackdropFilter);
|
|
}
|
|
|
|
async function expectUnfilteredInternalSurfaces(surfaces: Locator) {
|
|
const surfaceStyles = await surfaces.evaluateAll((elements) =>
|
|
elements.map((element) => {
|
|
const style = getComputedStyle(element);
|
|
|
|
return {
|
|
backdropFilter: style.backdropFilter,
|
|
className: element.className,
|
|
webkitBackdropFilter: style.getPropertyValue("-webkit-backdrop-filter")
|
|
};
|
|
})
|
|
);
|
|
|
|
expect(surfaceStyles.length).toBeGreaterThan(0);
|
|
for (const style of surfaceStyles) {
|
|
expect(style.backdropFilter).toBe("none");
|
|
expect(["", "none"]).toContain(style.webkitBackdropFilter);
|
|
expect(String(style.className)).not.toContain("acrylic-");
|
|
}
|
|
}
|
|
|
|
function expectRoundedBox(actual: BoundingBox, expected: BoundingBox) {
|
|
expect(Math.round(actual.x)).toBe(Math.round(expected.x));
|
|
expect(Math.round(actual.y)).toBe(Math.round(expected.y));
|
|
expect(Math.round(actual.width)).toBe(Math.round(expected.width));
|
|
expect(Math.round(actual.height)).toBe(Math.round(expected.height));
|
|
}
|
|
|
|
function expectRoundedValue(actual: number, expected: number) {
|
|
expect(Math.abs(Math.round(actual) - Math.round(expected))).toBeLessThanOrEqual(1);
|
|
}
|
|
|
|
async function getFloatingSurfaceStyles(header: Locator, composer: Locator) {
|
|
const control = composer.locator(".agent-panel-control");
|
|
const [headerStyles, composerStyles, controlStyles] = await Promise.all([
|
|
header.evaluate((element) => {
|
|
const style = getComputedStyle(element);
|
|
|
|
return {
|
|
shadow: style.boxShadow,
|
|
transform: style.transform
|
|
};
|
|
}),
|
|
composer.evaluate((element) => {
|
|
const style = getComputedStyle(element);
|
|
|
|
return {
|
|
shadow: style.boxShadow,
|
|
transform: style.transform
|
|
};
|
|
}),
|
|
control.evaluate((element) => {
|
|
const style = getComputedStyle(element);
|
|
|
|
return {
|
|
borderColor: style.borderColor,
|
|
boxShadow: style.boxShadow
|
|
};
|
|
})
|
|
]);
|
|
|
|
return {
|
|
composerShadow: composerStyles.shadow,
|
|
composerTransform: composerStyles.transform,
|
|
controlBorderColor: controlStyles.borderColor,
|
|
controlBoxShadow: controlStyles.boxShadow,
|
|
headerShadow: headerStyles.shadow,
|
|
headerTransform: headerStyles.transform
|
|
};
|
|
}
|