import { expect, test, type Page } from "@playwright/test"; import { axNode } from "../testing/ax.ts"; const trigger = (page: Page) => page.locator("#trigger"); const item = (page: Page, id: string) => page.locator(`#${id}`); const isOpen = (page: Page, id = "menu") => page.locator(`#${id}`).evaluate((el) => el.matches(":popover-open")); test("the button, menu, items, and radio group expose roles, names, and state", async ({ page }) => { await page.goto("/menu.html"); await expect(trigger(page)).toHaveAttribute("aria-haspopup", "menu"); await expect(trigger(page)).toHaveAttribute("aria-expanded", "false"); await trigger(page).click(); await expect(trigger(page)).toHaveAttribute("aria-expanded", "true"); await expect.poll(() => axNode(page, "menu", "Actions")).toBeTruthy(); await expect.poll(() => axNode(page, "menuitem", "New file")).toBeTruthy(); await expect.poll(() => axNode(page, "menuitem", "Disabled")).toMatchObject({ disabled: true }); await expect.poll(() => axNode(page, "group", "Accounts")).toBeTruthy(); await expect.poll(() => axNode(page, "menuitemradio", "alice.test")).toMatchObject({ checked: "true" }); await expect.poll(() => axNode(page, "menuitemradio", "bob.test")).toMatchObject({ checked: "false" }); await expect.poll(async () => (await axNode(page, "separator", ""))?.role).toBe("separator"); }); test("Enter opens at the first item; arrows wrap past disabled items; Home, End, and a letter jump", async ({ page }) => { await page.goto("/menu.html"); await trigger(page).focus(); await page.keyboard.press("Enter"); await expect(item(page, "new")).toBeFocused(); await page.keyboard.press("ArrowDown"); await expect(item(page, "open")).toBeFocused(); await page.keyboard.press("ArrowDown"); await expect(item(page, "alice")).toBeFocused(); await page.keyboard.press("End"); await expect(item(page, "save")).toBeFocused(); await page.keyboard.press("ArrowDown"); await expect(item(page, "new")).toBeFocused(); await page.keyboard.press("ArrowUp"); await expect(item(page, "save")).toBeFocused(); await page.keyboard.press("Home"); await expect(item(page, "new")).toBeFocused(); await page.keyboard.press("b"); await expect(item(page, "bob")).toBeFocused(); }); test("ArrowDown and ArrowUp on the button open at the first and last item; Escape returns focus", async ({ page }) => { await page.goto("/menu.html"); await trigger(page).focus(); await page.keyboard.press("ArrowDown"); await expect(item(page, "new")).toBeFocused(); await page.keyboard.press("Escape"); expect(await isOpen(page)).toBe(false); await expect(trigger(page)).toBeFocused(); await expect(trigger(page)).toHaveAttribute("aria-expanded", "false"); await page.keyboard.press("ArrowUp"); await expect(item(page, "save")).toBeFocused(); await page.keyboard.press("Escape"); await expect(trigger(page)).toBeFocused(); }); test("choosing an item closes the menu, returns focus, and fires menuselect", async ({ page }) => { await page.goto("/menu.html"); await trigger(page).focus(); await page.keyboard.press("Enter"); await expect(item(page, "new")).toBeFocused(); await page.keyboard.press("ArrowDown"); await page.keyboard.press("Enter"); await expect(page.locator("#log")).toHaveText("selected open"); expect(await isOpen(page)).toBe(false); await expect(trigger(page)).toBeFocused(); await page.keyboard.press(" "); await expect(item(page, "new")).toBeFocused(); await page.keyboard.press("End"); await page.keyboard.press(" "); await expect(page.locator("#log")).toHaveText("selected save"); await expect(trigger(page)).toBeFocused(); }); test("Tab closes the menu and moves on from the button", async ({ page }) => { await page.goto("/menu.html"); await trigger(page).focus(); await page.keyboard.press("Enter"); await expect(item(page, "new")).toBeFocused(); await page.keyboard.press("Tab"); expect(await isOpen(page)).toBe(false); await expect(page.locator("#after")).toBeFocused(); }); test("a pointer open leaves focus on the button; a click outside closes and returns focus there", async ({ page }) => { await page.goto("/menu.html"); await trigger(page).click(); expect(await isOpen(page)).toBe(true); await expect(item(page, "new")).not.toBeFocused(); await page.keyboard.press("ArrowDown"); await expect(item(page, "new")).toBeFocused(); await page.mouse.click(5, 300); expect(await isOpen(page)).toBe(false); await expect(trigger(page)).toBeFocused(); await trigger(page).click(); await trigger(page).click(); expect(await isOpen(page)).toBe(false); await trigger(page).click(); await item(page, "disabled").click(); expect(await isOpen(page)).toBe(true); await expect(page.locator("#log")).toHaveText(""); await item(page, "bob").click(); await expect(page.locator("#log")).toHaveText("selected bob"); expect(await isOpen(page)).toBe(false); }); test("the menu opens below its button and stays inside the viewport at the edge", async ({ page }) => { await page.setViewportSize({ width: 390, height: 600 }); await page.goto("/menu.html"); await trigger(page).click(); const button = (await trigger(page).boundingBox())!; const menu = (await page.locator("#menu").boundingBox())!; expect(menu.y).toBeGreaterThanOrEqual(button.y + button.height); expect(menu.x).toBeCloseTo(button.x, 0); await page.keyboard.press("Escape"); await page.locator("#edge").click(); const edge = (await page.locator("#edge").boundingBox())!; const flipped = (await page.locator("#edge-menu").boundingBox())!; expect(flipped.x + flipped.width).toBeLessThanOrEqual(390); expect(flipped.x + flipped.width).toBeCloseTo(edge.x + edge.width, 0); expect(flipped.y).toBeGreaterThanOrEqual(edge.y + edge.height); });