microsoft/hve-core
Publicmirrored from https://github.com/microsoft/hve-coreAvailable
docs/docusaurus/e2e/search.spec.ts
83lines · modecode
| 1 | import { test, expect } from '@playwright/test'; |
| 2 | import AxeBuilder from '@axe-core/playwright'; |
| 3 | |
| 4 | const WCAG_TAGS = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']; |
| 5 | |
| 6 | // Local search (@easyops-cn/docusaurus-search-local) is swizzled/ejected under |
| 7 | // src/theme/SearchBar so the input can be wired to the WAI-ARIA APG Combobox |
| 8 | // pattern. The widget must be operable, surface results, expose a conformant |
| 9 | // combobox/listbox structure, and respond to keyboard navigation. |
| 10 | test.describe('Search', () => { |
| 11 | async function openResults(page: import('@playwright/test').Page) { |
| 12 | await page.goto('/hve-core/docs/getting-started/'); |
| 13 | |
| 14 | const searchInput = page.locator('.navbar__search-input').first(); |
| 15 | await expect(searchInput).toBeVisible(); |
| 16 | |
| 17 | await searchInput.click(); |
| 18 | await searchInput.fill('getting started'); |
| 19 | |
| 20 | // The local search renders a suggestions dropdown anchored to the input; |
| 21 | // @easyops-cn/docusaurus-search-local emits hashed `suggestion_*` classes. |
| 22 | const results = page.locator('[class*="suggestion_"]').first(); |
| 23 | await expect(results).toBeVisible({ timeout: 15000 }); |
| 24 | |
| 25 | return searchInput; |
| 26 | } |
| 27 | |
| 28 | test('typing a query surfaces operable results that pass an axe scan', async ({ page }) => { |
| 29 | await openResults(page); |
| 30 | |
| 31 | // Scoped scan of the open combobox + listbox. The swizzled SearchBar now |
| 32 | // marks the input as role="combobox" and the library emits the |
| 33 | // role="listbox"/role="option" tree, so the dropdown must be violation-free. |
| 34 | const results = await new AxeBuilder({ page }) |
| 35 | .withTags(WCAG_TAGS) |
| 36 | .include('.navbar__search') |
| 37 | .analyze(); |
| 38 | expect(results.violations).toEqual([]); |
| 39 | }); |
| 40 | |
| 41 | test('the combobox exposes the required APG structure', async ({ page }) => { |
| 42 | const searchInput = await openResults(page); |
| 43 | |
| 44 | await expect(searchInput).toHaveAttribute('role', 'combobox'); |
| 45 | await expect(searchInput).toHaveAttribute('aria-autocomplete', 'list'); |
| 46 | await expect(searchInput).toHaveAttribute('aria-expanded', 'true'); |
| 47 | |
| 48 | // aria-controls must reference the live listbox. |
| 49 | const listbox = page.locator('[role="listbox"]').first(); |
| 50 | const listboxId = await listbox.getAttribute('id'); |
| 51 | expect(listboxId).toBeTruthy(); |
| 52 | await expect(searchInput).toHaveAttribute('aria-controls', listboxId as string); |
| 53 | |
| 54 | // Options carry role="option" with stable ids for aria-activedescendant. |
| 55 | await expect(page.locator('[role="option"]').first()).toBeVisible(); |
| 56 | }); |
| 57 | |
| 58 | test('the results dropdown is keyboard operable', async ({ page }) => { |
| 59 | const searchInput = await openResults(page); |
| 60 | |
| 61 | // SC 2.1.1: ArrowDown moves the active option and syncs aria-activedescendant. |
| 62 | await searchInput.press('ArrowDown'); |
| 63 | |
| 64 | const activeId = await searchInput.getAttribute('aria-activedescendant'); |
| 65 | expect(activeId).toBeTruthy(); |
| 66 | const activeOption = page.locator(`#${activeId}`); |
| 67 | await expect(activeOption).toHaveAttribute('aria-selected', 'true'); |
| 68 | |
| 69 | // Enter activates the cursored option and navigates to its document. |
| 70 | await searchInput.press('Enter'); |
| 71 | await expect(page).not.toHaveURL(/\/docs\/getting-started\/?$/, { timeout: 15000 }); |
| 72 | }); |
| 73 | |
| 74 | test('Escape dismisses the open dropdown', async ({ page }) => { |
| 75 | const searchInput = await openResults(page); |
| 76 | |
| 77 | // APG: Escape dismisses the popup and collapses the combobox. |
| 78 | await searchInput.press('Escape'); |
| 79 | |
| 80 | await expect(page.locator('[role="option"]').first()).toBeHidden({ timeout: 5000 }); |
| 81 | await expect(searchInput).toHaveAttribute('aria-expanded', 'false'); |
| 82 | }); |
| 83 | }); |
| 84 | |