microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/1873-devcontainer

Branches

Tags

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

Clone

HTTPS

Download ZIP

docs/docusaurus/e2e/focus-management.spec.ts

124lines · modecode

1import { test, expect } from '@playwright/test';
2import { testFocusTrapEscape, validateRovingTabindex } from './_helpers/focus';
3
4// Behavioral keyboard/focus conformance against real Docusaurus hooks. These
5// assertions exercise runtime keyboard behavior (WCAG 2.1.1, 2.1.2, 2.4.3) that
6// the static axe-based specs cannot reach. Contrast and structural ARIA checks
7// stay in the existing axe specs to avoid redundant coverage.
8
9test.describe('Focus management', () => {
10 // WCAG 2.4.3 Focus Order + 2.4.1 Bypass Blocks: the skip link must be the
11 // first element to receive focus from the top of the page.
12 test('skip link is first in the focus order', async ({ page }) => {
13 await page.goto('/hve-core/');
14
15 await page.keyboard.press('Tab');
16
17 const skipLink = page.getByRole('link', { name: /skip to main content/i });
18 await expect(skipLink).toBeFocused();
19 });
20
21 // WCAG 2.1.1 Keyboard: the color-mode toggle must be operable by keyboard and
22 // flip the document theme on Enter.
23 test('color-mode toggle is keyboard operable', async ({ page }) => {
24 await page.goto('/hve-core/docs/getting-started/');
25
26 const toggle = page.getByRole('button', {
27 name: /switch between dark and light mode/i,
28 });
29 await expect(toggle).toBeVisible();
30
31 const initialTheme = await page.locator('html').getAttribute('data-theme');
32
33 // Click to focus the control, then activate via the keyboard. This theme's
34 // toggle flips state on Enter, which is the accessibility-relevant path.
35 await toggle.click();
36 await page.keyboard.press('Enter');
37
38 await expect
39 .poll(async () => page.locator('html').getAttribute('data-theme'))
40 .not.toBe(initialTheme);
41 });
42
43 // WCAG 2.1.1 Keyboard / 2.4.3 Focus Order: opening the navbar dropdown (when
44 // present) must expose keyboard-reachable menu items. The Docusaurus default
45 // theme implements its navbar dropdown as a disclosure-navigation menu where
46 // every item is a naturally tabbable <a> (no roving tabindex). Roving tabindex
47 // is only asserted when the menu is actually built as a roving-tabindex widget,
48 // guarding against false failures as the plan requires.
49 test('navbar dropdown exposes keyboard-reachable items when present', async ({ page }) => {
50 await page.goto('/hve-core/docs/getting-started/');
51
52 const dropdownToggle = page.locator('.navbar__item.dropdown .navbar__link').first();
53
54 // Dropdown navbar items are optional in the site config; skip cleanly when
55 // none exist rather than producing a false failure.
56 if ((await dropdownToggle.count()) === 0) {
57 test.skip(true, 'No dropdown navbar item is configured.');
58 return;
59 }
60
61 await dropdownToggle.click();
62
63 const dropdownMenu = page.locator('.navbar__item.dropdown .dropdown__menu').first();
64 await expect(dropdownMenu).toBeVisible();
65
66 // Branch on the widget pattern. A roving-tabindex menu carries explicit
67 // tabindex attributes; a disclosure-navigation menu carries none.
68 const rovingItemCount = await dropdownMenu.locator('[tabindex]').count();
69
70 if (rovingItemCount > 0) {
71 // Composite roving-tabindex widget: exactly one tabindex="0", rest -1.
72 expect(await validateRovingTabindex(page, '.navbar__item.dropdown .dropdown__menu')).toBe(true);
73 return;
74 }
75
76 // Disclosure-navigation menu: every link must be individually keyboard
77 // focusable so the menu is fully operable without a pointer (WCAG 2.1.1).
78 const menuLinks = dropdownMenu.locator('a');
79 const linkCount = await menuLinks.count();
80 expect(linkCount).toBeGreaterThan(0);
81
82 for (let index = 0; index < linkCount; index += 1) {
83 const link = menuLinks.nth(index);
84 await link.focus();
85 await expect(link).toBeFocused();
86 }
87 });
88});
89
90// WCAG 2.1.2 No Keyboard Trap: the mobile sidebar must let keyboard focus leave
91// the container. Some themes release focus on Escape; the Docusaurus default
92// theme instead provides a keyboard-operable "Close navigation bar" control.
93// Either mechanism satisfies No Keyboard Trap, so the spec accepts both.
94test.describe('Mobile sidebar focus trap', () => {
95 test.use({ viewport: { width: 390, height: 844 } });
96
97 test('keyboard focus can leave the open mobile sidebar', async ({ page }) => {
98 await page.goto('/hve-core/docs/getting-started/');
99
100 const toggle = page.locator('.navbar__toggle');
101 await expect(toggle).toBeVisible();
102
103 await toggle.click();
104
105 const mobileSidebar = page.locator('.navbar-sidebar');
106 await expect(mobileSidebar).toBeVisible();
107
108 // Preferred path: focus leaves the container on Escape when the theme wires
109 // it. The Docusaurus default theme does not, so fall through when it fails.
110 if (await testFocusTrapEscape(page, '.navbar-sidebar')) {
111 return;
112 }
113
114 // Docusaurus default theme path: the dedicated close control must be
115 // keyboard operable and must collapse the sidebar, releasing focus.
116 const closeButton = page.getByRole('button', { name: /close navigation bar/i });
117 await closeButton.focus();
118 await expect(closeButton).toBeFocused();
119
120 await page.keyboard.press('Enter');
121
122 await expect(mobileSidebar).toBeHidden();
123 });
124});
125