Derive the gem-blue palette with OKLCH tonal and complementary-color rules while preserving acrylic opacity, blur and surface hierarchy. Darker focus tokens exposed stacked field rings, fallback outlines and persistent basemap selection rings. Keep fallback focus styles in the base layer and use one themed field boundary or inset card stroke. Preserve keyboard focus, invalid states and native input semantics. Add browser regression coverage for pointer and keyboard focus, all basemap presentations, narrow layouts and rendered contrast. Validation in an isolated checkout: pnpm lint, pnpm typecheck, pnpm test (393 tests), focused browser suites and pnpm build.
141 lines
8.0 KiB
TypeScript
141 lines
8.0 KiB
TypeScript
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");
|
|
const body = panel.getByText("直接说出你想调查的问题,我会整理监测与空间证据,并将分析结果带回地图。");
|
|
expect(await readRenderedContrast(body)).toBeGreaterThanOrEqual(4.5);
|
|
expect(await readRenderedContrast(panel.getByText("对话主题"))).toBeGreaterThanOrEqual(4.5);
|
|
|
|
await page.getByPlaceholder("输入调度问题,Agent 将通过后端会话流式响应").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" });
|
|
});
|