Files
next-tjwater-frontend/tests/browser/base-layer-focus.e2e.ts
T
jiang 617ba4d87c feat(ui): refine gem-blue acrylic palette and focus borders
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.
2026-10-09 10:57:06 +08:00

84 lines
4.5 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
import { mockAgentApi } from "./support/mock-agent-api";
import { mockWorkbenchContext } from "./support/mock-workbench-context";
import { expectThemeColor } from "./support/theme-colors";
test.beforeEach(async ({ page }) => {
await page.emulateMedia({ reducedMotion: "reduce" });
await mockWorkbenchContext(page);
await mockAgentApi(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
});
for (const width of [320, 375, 1440]) {
test(`layer panel basemap selection has one border and no persistent outer ring at ${width}px`, async ({ page }) => {
await page.setViewportSize({ width, height: 1000 });
await page.getByRole("button", { name: /图层:管理地图图层/ }).click();
const panel = page.getByRole("region", { name: "图层", exact: true });
const card = panel.getByRole("button", { name: "无底图", exact: true });
await expect(card).toHaveAttribute("aria-pressed", "true");
await expectThemeColor(card, "border-color", "--action-blue");
await expect(card).toHaveCSS("border-width", "1px");
await expect(card).toHaveCSS("box-shadow", "none");
await card.click();
await expect(card).toHaveCSS("outline-style", "none");
const restingBox = await card.boundingBox();
await page.keyboard.press("Tab");
await card.focus();
await expect(card).toHaveCSS("outline-width", "1px");
await expect(card).toHaveCSS("outline-offset", "-1px");
await expectThemeColor(card, "outline-color", "--action-focus-ring");
expect(await card.boundingBox()).toEqual(restingBox);
await page.keyboard.press("Enter");
await expect(card).toHaveAttribute("aria-pressed", "true");
await panel.screenshot({ path: `test-results/form-focus/layer-panel-${width}.png` });
});
}
test("standalone basemap controls share the same pointer and keyboard boundary", async ({ page }) => {
await renderControls(page);
for (const label of ["面板底图", "浮动底图"]) {
const control = page.getByRole("region", { name: label, exact: true });
if (label === "浮动底图") await control.getByRole("button", { name: "浮动底图:当前 浅色" }).hover();
const selected = control.getByRole("button", { name: "浅色", exact: true });
await expect(selected).toHaveCSS("border-width", "1px");
await expectThemeColor(selected, "border-color", "--action-blue");
const restingShadow = await selected.evaluate((element) => getComputedStyle(element).boxShadow);
expect(restingShadow).not.toMatch(/0px 0px 0px [12]px/);
await selected.click();
await expect(selected).toHaveCSS("outline-style", "none");
await page.keyboard.press("Tab");
await selected.focus();
await expect(selected).toHaveCSS("outline-width", "1px");
await expect(selected).toHaveCSS("outline-offset", "-1px");
await expect(selected).toHaveCSS("box-shadow", restingShadow);
const next = control.getByRole("button", { name: "影像", exact: true });
await next.focus();
await page.keyboard.press("Enter");
await expect(next).toHaveAttribute("aria-pressed", "true");
await expect(selected).toHaveAttribute("aria-pressed", "false");
await control.screenshot({ path: `test-results/form-focus/${label}.png` });
}
});
async function renderControls(page: Page) {
await page.evaluate(async () => {
const paths = ["/node_modules/.vite/deps/react.js", "/node_modules/.vite/deps/react-dom_client.js", "/src/features/map/core/components/base-layers-control.tsx"];
const [react, dom, controls] = await Promise.all(paths.map((path) => import(path)));
const h = react.default.createElement;
function Examples() {
const [panelId, setPanelId] = react.default.useState("light");
const [floatingId, setFloatingId] = react.default.useState("light");
const options = [{ id: "light", label: "浅色" }, { id: "satellite", label: "影像" }];
return h(react.default.Fragment, null,
h(controls.BaseLayersControl, { title: "面板底图", variant: "panel", options, activeLayerId: panelId, onSelectLayer: setPanelId }),
h(controls.BaseLayersControl, { title: "浮动底图", options, activeLayerId: floatingId, onSelectLayer: setFloatingId })
);
}
const container = document.createElement("div");
container.style.cssText = "position:fixed;z-index:40;top:80px;right:40px;padding:24px;background:var(--surface-reading);display:grid;gap:32px";
document.body.append(container);
dom.default.createRoot(container).render(h(Examples));
});
}