Radix focus transfers can preserve :focus-visible during mouse hover and when returning focus to the alert trigger. Track actual input modality so pointer interactions keep the thin hover border and keyboard navigation retains its focus rings. Cover desktop and narrow layouts, pointer selection, keyboard return focus, and switching input modes with browser regressions.
88 lines
4.5 KiB
TypeScript
88 lines
4.5 KiB
TypeScript
import { expect, test, type Locator, type Page } from "@playwright/test";
|
|
import { investigationFixture, mockInvestigationPresentation } from "./support/mock-investigation-presentation";
|
|
import type { OperationalTaskNotification } from "../../src/features/workbench/api/operational-task-client";
|
|
|
|
async function openAlertQueue(page: Page) {
|
|
const task = investigationFixture(1, "通知焦点测试调查");
|
|
const notification: OperationalTaskNotification = {
|
|
notification_id: "96000000-0000-4000-8000-000000000021", task_id: task.task_id,
|
|
kind: "human_input_required", title: "待核验漏点与阀门", description: "请核对现场情况后继续调查。",
|
|
required_permission: null, lifecycle_status: "active", subject_type: "question", subject_id: null,
|
|
resolved_at: null, resolution_reason: null, status: "unread", version: 1,
|
|
created_at: task.created_at, read_at: null, read_by: null
|
|
};
|
|
await mockInvestigationPresentation(page, [task], [], [notification]);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
const trigger = page.getByRole("button", { name: /查看待办与告警/ });
|
|
await expect(trigger).toHaveAccessibleName("查看待办与告警,当前 1 条待处理事项", { timeout: 15_000 });
|
|
await trigger.click();
|
|
await expect(page.getByRole("menu")).toBeVisible();
|
|
return {
|
|
trigger,
|
|
item: page.getByRole("menuitem", { name: `打开通知:${notification.title}` }),
|
|
row: page.locator(`[data-alert-id="operational-task-notification:${notification.notification_id}"]`)
|
|
};
|
|
}
|
|
|
|
async function ringShadow(element: Locator) {
|
|
return element.evaluate((node) => getComputedStyle(node).getPropertyValue("--tw-ring-shadow").trim());
|
|
}
|
|
|
|
for (const width of [1920, 375]) {
|
|
test(`鼠标悬停通知保持细边框且键盘导航仍有焦点环 ${width}`, async ({ page }, testInfo) => {
|
|
await page.setViewportSize({ width, height: 900 });
|
|
const { trigger, item, row } = await openAlertQueue(page);
|
|
await item.hover();
|
|
await expect(item).toBeFocused();
|
|
await expect(row).toHaveCSS("background-color", "rgb(218, 229, 253)");
|
|
await expect(row).toHaveCSS("border-width", "1px");
|
|
await expect(item).toHaveCSS("box-shadow", "none");
|
|
await expect(item).toHaveCSS("outline-style", "none");
|
|
await testInfo.attach("pointer-alert-focus", {
|
|
body: await page.getByRole("menu").screenshot({ path: testInfo.outputPath("pointer-alert-focus.png") }),
|
|
contentType: "image/png"
|
|
});
|
|
|
|
// Switching to keyboard on the same focused item must immediately show its indicator.
|
|
await page.keyboard.press("Home");
|
|
await expect(item).toBeFocused();
|
|
await expect.poll(() => ringShadow(item)).toContain("inset 0 0 0 calc(2px");
|
|
await testInfo.attach("keyboard-alert-focus", {
|
|
body: await page.getByRole("menu").screenshot({ path: testInfo.outputPath("keyboard-alert-focus.png") }),
|
|
contentType: "image/png"
|
|
});
|
|
await page.keyboard.press("Escape");
|
|
await expect(page.getByRole("menu")).toHaveCount(0);
|
|
await expect(trigger).toBeFocused();
|
|
await expect.poll(() => ringShadow(trigger)).toContain("calc(2px + 2px)");
|
|
});
|
|
|
|
test(`鼠标选择通知后顶部按钮无外环且再次切换键盘可见焦点 ${width}`, async ({ page }, testInfo) => {
|
|
await page.setViewportSize({ width, height: 900 });
|
|
const { trigger, item } = await openAlertQueue(page);
|
|
await item.click();
|
|
await expect(page.getByRole("menu")).toHaveCount(0);
|
|
await expect(trigger).toBeFocused();
|
|
await expect.poll(() => ringShadow(trigger)).toContain("calc(0px + 0px)");
|
|
await expect(trigger).toHaveCSS("outline-style", "none");
|
|
await testInfo.attach("pointer-alert-trigger", {
|
|
body: await trigger.screenshot({ path: testInfo.outputPath("pointer-alert-trigger.png"), animations: "disabled" }),
|
|
contentType: "image/png"
|
|
});
|
|
|
|
// Tab originates on another control, outside the menu and trigger handlers.
|
|
await page.keyboard.press("Tab");
|
|
await page.keyboard.press("Shift+Tab");
|
|
await expect(trigger).toBeFocused();
|
|
await expect.poll(() => ringShadow(trigger)).toContain("calc(2px + 2px)");
|
|
await testInfo.attach("keyboard-alert-trigger", {
|
|
body: await trigger.screenshot({ path: testInfo.outputPath("keyboard-alert-trigger.png"), animations: "disabled" }),
|
|
contentType: "image/png"
|
|
});
|
|
|
|
// Pointer movement must also clear an existing keyboard indicator.
|
|
await trigger.hover();
|
|
await expect.poll(() => ringShadow(trigger)).toContain("calc(0px + 0px)");
|
|
});
|
|
}
|