650 lines
26 KiB
TypeScript
650 lines
26 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.965 0.012 248)");
|
|
await expect(agentPanel).toHaveCSS("backdrop-filter", "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("直接说出你想调查的问题,我会整理监测与空间证据,并将分析结果带回地图。")
|
|
).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);
|
|
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 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", {
|
|
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 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("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(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
|
|
};
|
|
}
|