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)"); }); }