microsoft/hve-core

Public

mirrored from https://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
2015-accessibility-planner-playbook

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

docs/docusaurus/e2e/search.spec.ts

83lines · modecode

1import { test, expect } from '@playwright/test';
2import AxeBuilder from '@axe-core/playwright';
3
4const 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.
10test.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