import { expect, test } from "@playwright/test"; import { mockAgentApi } from "./support/mock-agent-api"; import { mockWorkbenchContext } from "./support/mock-workbench-context"; import { expectThemeColor, readColorAlpha, readRenderedContrast, readThemeColor } from "./support/theme-colors"; test.beforeEach(async ({ page }) => { await page.emulateMedia({ reducedMotion: "reduce" }); await mockWorkbenchContext(page); await mockAgentApi(page); await page.goto("/", { waitUntil: "domcontentloaded" }); }); test("gem-blue actions and semantic labels remain readable on the new surfaces", async ({ page }) => { await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); await expectThemeColor(panel, "background-color", "--agent-dock-surface"); const tint = await panel.evaluate((element) => { const canvas = document.createElement("canvas"); const context = canvas.getContext("2d")!; context.fillStyle = getComputedStyle(element).backgroundColor; context.fillRect(0, 0, 1, 1); return Array.from(context.getImageData(0, 0, 1, 1).data); }); // Low-chroma material interpolation must stay in the cool silver/blue family. expect(tint[2]).toBeGreaterThan(tint[1]); expect(tint[2]).toBeGreaterThan(tint[0]); await expectThemeColor(panel.locator("form.agent-panel-control"), "background-color", "--surface-reading"); expect(await readRenderedContrast(panel.getByText("对话主题"))).toBeGreaterThanOrEqual(4.5); await page.getByPlaceholder("询问供水问题,或描述需要处理的任务").fill("检查压力变化"); const send = panel.getByRole("button", { name: "发送 Agent 指令", exact: true }); await expect(send).toBeEnabled(); await expectThemeColor(send, "background-color", "--action-blue"); expect(await readRenderedContrast(send)).toBeGreaterThanOrEqual(4.5); await send.hover(); await expectThemeColor(send, "background-color", "--action-blue-hover"); expect(await readRenderedContrast(send)).toBeGreaterThanOrEqual(4.5); // Exercise every shared semantic tone after CSS variables and color-mix are resolved. await panel.evaluate((element) => { const samples = document.createElement("div"); samples.dataset.paletteSamples = "true"; for (const tone of ["neutral", "info", "success", "warning", "danger"]) { const label = document.createElement("span"); label.className = `status-badge status-tone-${tone}`; label.dataset.paletteTone = tone; label.textContent = tone; samples.append(label); } element.append(samples); }); for (const tone of ["neutral", "info", "success", "warning", "danger"]) { expect(await readRenderedContrast(panel.locator(`[data-palette-tone="${tone}"]`)), tone) .toBeGreaterThanOrEqual(4.5); } }); test("selected controls preserve readable hover, pressed and keyboard focus states", async ({ page }) => { const control = page.getByRole("button", { name: /任务浮条/ }); await expect(control).toHaveAttribute("aria-pressed", "true"); expect(await readRenderedContrast(control)).toBeGreaterThanOrEqual(4.5); await control.hover(); await expectThemeColor(control, "background-color", "--action-selection-soft-hover"); expect(await readRenderedContrast(control)).toBeGreaterThanOrEqual(4.5); const box = (await control.boundingBox())!; await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); await page.mouse.down(); await expectThemeColor(control, "background-color", "--action-selection-soft-pressed"); expect(await readRenderedContrast(control)).toBeGreaterThanOrEqual(4.5); await page.mouse.move(720, 460); await page.mouse.up(); await page.keyboard.press("Tab"); await control.focus(); expect(await control.evaluate((element) => element.matches(":focus-visible"))).toBe(true); expect(await control.evaluate((element) => getComputedStyle(element).boxShadow)).not.toBe("none"); }); test("floating panel text stays readable over light and dark map backgrounds", async ({ page }) => { // A solid underlay gives the transparent material a known actual backdrop. await page.addStyleTag({ content: "#workspace-map .maplibregl-canvas { visibility: hidden; } .acrylic-panel { background-image: none; }" }); await page.getByRole("button", { name: /图层:管理地图图层/ }).click(); const layers = page.getByRole("region", { name: "图层", exact: true }); await expect(layers).toBeVisible(); const title = layers.getByText("图层", { exact: true }).first(); for (const tone of ["light", "satellite"]) { await page.locator("main").evaluate((element, value) => { element.setAttribute("data-basemap-tone", value); (element as HTMLElement).style.backgroundColor = value === "light" ? "#f5f7fa" : "#172b3a"; }, tone); await expectThemeColor(layers, "background-color", "--acrylic-panel"); expect(await readRenderedContrast(title), tone).toBeGreaterThanOrEqual(4.5); const color = await layers.evaluate((element) => getComputedStyle(element).backgroundColor); expect(readColorAlpha(color)).toBeCloseTo(tone === "light" ? 0.56 : 0.72, 2); await expect(layers).toHaveCSS("backdrop-filter", "blur(24px) saturate(" + (tone === "light" ? "1.08" : "1.02") + ")"); } }); for (const width of [320, 375, 390]) { test(`new palette keeps Agent input and controls usable at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 844 }); await page.getByRole("button", { name: "打开 Agent 面板" }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); const composer = panel.locator("form.agent-panel-control"); await expect(composer).toBeVisible(); const material = panel.locator(".agent-panel-composer"); await expectThemeColor(material, "background-color", "--agent-white-acrylic-surface"); expect(readColorAlpha(await material.evaluate((element) => getComputedStyle(element).backgroundColor))).toBeCloseTo(0.86, 2); await expect(material).toHaveCSS("backdrop-filter", "blur(18px) saturate(1.08)"); expect(await readRenderedContrast(panel.getByText("对话主题"))).toBeGreaterThanOrEqual(4.5); for (const element of [panel, composer]) { const box = (await element.boundingBox())!; expect(box.x).toBeGreaterThanOrEqual(0); expect(box.x + box.width).toBeLessThanOrEqual(width); } expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width); await page.screenshot({ path: `test-results/palette/agent-${width}.png` }); }); } test("desktop palette captures dock, history, conditions and map tools", async ({ page }) => { await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); await page.screenshot({ path: "test-results/palette/desktop-1440.png" }); await page.setViewportSize({ width: 1920, height: 1080 }); await page.screenshot({ path: "test-results/palette/desktop-1920.png" }); await panel.getByRole("button", { name: "打开 Agent 历史记录" }).click(); const history = panel.getByRole("region", { name: "Agent 历史记录" }); await expect(history).toBeVisible(); expect(await readThemeColor(history, "--agent-dock-overlay-surface")) .toBe(await history.evaluate((element) => getComputedStyle(element).backgroundColor)); await page.screenshot({ path: "test-results/palette/desktop-history.png" }); await page.keyboard.press("Escape"); await page.getByRole("button", { name: "工况任务", exact: true }).click(); const conditions = page.getByRole("region", { name: "工况任务", exact: true }); await expect(conditions).toBeVisible(); await conditions.getByRole("button", { name: "展开工况任务" }).click(); await page.screenshot({ path: "test-results/palette/desktop-report.png" }); });