@a11y-oracle/playwright-plugin 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,376 @@
1
+ # @a11y-oracle/playwright-plugin
2
+
3
+ Playwright integration for A11y-Oracle. Provides a test fixture and wrapper class that reads the browser's Accessibility Tree via Chrome DevTools Protocol, dispatches native keyboard events, and analyzes visual focus indicators.
4
+
5
+ ```typescript
6
+ import { test, expect } from '@a11y-oracle/playwright-plugin';
7
+
8
+ test('dropdown button announces correctly', async ({ page, a11y }) => {
9
+ await page.goto('/dropdown-nav.html');
10
+
11
+ const speech = await a11y.press('Tab');
12
+ expect(speech).toContain('Home');
13
+ expect(speech).toContain('menu item');
14
+ });
15
+ ```
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install -D @a11y-oracle/playwright-plugin @a11y-oracle/core-engine @playwright/test
21
+ ```
22
+
23
+ > **Chromium only.** CDP sessions are not available for Firefox or WebKit in Playwright.
24
+
25
+ ## Usage
26
+
27
+ ### Test Fixture (Recommended)
28
+
29
+ The plugin exports an extended `test` function that injects an `a11y` fixture. The CDP session is created before each test and cleaned up automatically after.
30
+
31
+ ```typescript
32
+ import { test, expect } from '@a11y-oracle/playwright-plugin';
33
+
34
+ test.describe('Navigation', () => {
35
+ test.beforeEach(async ({ page }) => {
36
+ await page.goto('/my-page.html');
37
+ });
38
+
39
+ test('Tab to button announces name and role', async ({ a11y }) => {
40
+ const speech = await a11y.press('Tab');
41
+ expect(speech).toBe('Submit, button');
42
+ });
43
+
44
+ test('checkbox announces checked state', async ({ a11y }) => {
45
+ await a11y.press('Tab');
46
+ await a11y.press('Tab');
47
+ const speech = await a11y.press('Space');
48
+ expect(speech).toContain('checkbox');
49
+ expect(speech).toContain('checked');
50
+ });
51
+
52
+ test('navigation landmark exists', async ({ a11y }) => {
53
+ const tree = await a11y.getFullTreeSpeech();
54
+ const nav = tree.find(r => r.speech.includes('navigation landmark'));
55
+ expect(nav).toBeDefined();
56
+ });
57
+ });
58
+ ```
59
+
60
+ ### Unified State API
61
+
62
+ The `pressKey()` method returns a complete `A11yState` snapshot combining speech output, focused element info, and focus indicator analysis:
63
+
64
+ ```typescript
65
+ test('focus indicator meets WCAG 2.4.12 AA', async ({ page, a11y }) => {
66
+ await page.goto('/my-page.html');
67
+
68
+ const state = await a11y.pressKey('Tab');
69
+
70
+ // Speech
71
+ expect(state.speech).toContain('Submit');
72
+ expect(state.speechResult?.role).toBe('button');
73
+
74
+ // Focused element
75
+ expect(state.focusedElement?.tag).toBe('BUTTON');
76
+ expect(state.focusedElement?.id).toBe('submit-btn');
77
+ expect(state.focusedElement?.tabIndex).toBe(0);
78
+
79
+ // Focus indicator CSS analysis
80
+ expect(state.focusIndicator.isVisible).toBe(true);
81
+ expect(state.focusIndicator.contrastRatio).toBeGreaterThanOrEqual(3.0);
82
+ expect(state.focusIndicator.meetsWCAG_AA).toBe(true);
83
+ });
84
+
85
+ test('Shift+Tab navigates backward', async ({ page, a11y }) => {
86
+ await page.goto('/my-page.html');
87
+
88
+ await a11y.pressKey('Tab');
89
+ const state1 = await a11y.pressKey('Tab');
90
+ const state2 = await a11y.pressKey('Tab', { shift: true });
91
+
92
+ expect(state2.focusedElement?.id).toBe(state1.focusedElement?.id);
93
+ });
94
+ ```
95
+
96
+ ### Tab Order and Keyboard Trap Detection
97
+
98
+ ```typescript
99
+ test('page has correct tab order', async ({ page, a11y }) => {
100
+ await page.goto('/my-page.html');
101
+
102
+ const report = await a11y.traverseTabOrder();
103
+ expect(report.totalCount).toBeGreaterThan(0);
104
+ expect(report.entries[0].tag).toBe('A');
105
+ });
106
+
107
+ test('modal does not trap keyboard focus', async ({ page, a11y }) => {
108
+ await page.goto('/modal.html');
109
+
110
+ const result = await a11y.traverseSubTree('#modal-container', 20);
111
+ expect(result.isTrapped).toBe(false);
112
+ expect(result.escapeElement).not.toBeNull();
113
+ });
114
+ ```
115
+
116
+ ### Audit and Issue Reporting
117
+
118
+ Use `OracleAuditor` from `@a11y-oracle/audit-formatter` to automatically check WCAG rules on every interaction and accumulate any issues:
119
+
120
+ ```bash
121
+ npm install -D @a11y-oracle/audit-formatter
122
+ ```
123
+
124
+ ```typescript
125
+ import { test, expect } from '@a11y-oracle/playwright-plugin';
126
+ import { OracleAuditor } from '@a11y-oracle/audit-formatter';
127
+
128
+ test('all focus indicators pass oracle rules', async ({ page, a11y }) => {
129
+ await page.goto('/my-page.html');
130
+
131
+ const auditor = new OracleAuditor(a11y, {
132
+ project: 'my-app',
133
+ specName: 'navigation.spec.ts',
134
+ });
135
+
136
+ // Each pressKey() automatically checks all 5 state-based rules
137
+ await auditor.pressKey('Tab');
138
+ await auditor.pressKey('Tab');
139
+ await auditor.pressKey('Tab');
140
+
141
+ // checkTrap() automatically checks keyboard-trap
142
+ await auditor.checkTrap('#modal-container');
143
+
144
+ // Assert no issues found across all interactions
145
+ expect(auditor.getIssues()).toHaveLength(0);
146
+ });
147
+ ```
148
+
149
+ To write issues to a JSON report file at the end of the suite:
150
+
151
+ ```typescript
152
+ import { test } from '@a11y-oracle/playwright-plugin';
153
+ import { OracleAuditor, type OracleIssue } from '@a11y-oracle/audit-formatter';
154
+ import * as fs from 'fs';
155
+
156
+ const allIssues: OracleIssue[] = [];
157
+
158
+ test('check page focus indicators', async ({ page, a11y }) => {
159
+ await page.goto('/my-page.html');
160
+ const auditor = new OracleAuditor(a11y, {
161
+ project: 'my-app',
162
+ specName: 'nav.spec.ts',
163
+ });
164
+
165
+ await auditor.pressKey('Tab');
166
+ await auditor.pressKey('Tab');
167
+ allIssues.push(...auditor.getIssues());
168
+ });
169
+
170
+ test.afterAll(() => {
171
+ if (allIssues.length > 0) {
172
+ fs.writeFileSync('oracle-results.json', JSON.stringify(allIssues, null, 2));
173
+ }
174
+ });
175
+ ```
176
+
177
+ For detailed remediation guidance on each rule, see the [Remediation Guide](../../docs/REMEDIATION.md).
178
+
179
+ ### Customizing Options
180
+
181
+ Override speech engine options per test group using `test.use()`:
182
+
183
+ ```typescript
184
+ test.describe('without landmark suffix', () => {
185
+ test.use({ a11yOptions: { includeLandmarks: false } });
186
+
187
+ test('nav role without landmark', async ({ page, a11y }) => {
188
+ await page.goto('/my-page.html');
189
+ const tree = await a11y.getFullTreeSpeech();
190
+ const nav = tree.find(r => r.role === 'navigation');
191
+ expect(nav?.speech).toBe('Main, navigation');
192
+ });
193
+ });
194
+ ```
195
+
196
+ Available options:
197
+
198
+ | Option | Type | Default | Description |
199
+ |--------|------|---------|-------------|
200
+ | `includeLandmarks` | `boolean` | `true` | Append "landmark" to landmark roles |
201
+ | `includeDescription` | `boolean` | `false` | Include `aria-describedby` text in output |
202
+ | `focusSettleMs` | `number` | `50` | Delay (ms) after key press for focus/CSS to settle |
203
+
204
+ ### Manual Usage
205
+
206
+ If you need more control over the lifecycle (e.g., attaching to a specific page mid-test), use the `A11yOracle` class directly:
207
+
208
+ ```typescript
209
+ import { A11yOracle } from '@a11y-oracle/playwright-plugin';
210
+ import { test, expect } from '@playwright/test';
211
+
212
+ test('manual setup', async ({ page }) => {
213
+ await page.goto('/my-page.html');
214
+
215
+ const a11y = new A11yOracle(page, { includeDescription: true });
216
+ await a11y.init();
217
+
218
+ const speech = await a11y.press('Tab');
219
+ expect(speech).toContain('button');
220
+
221
+ await a11y.dispose();
222
+ });
223
+ ```
224
+
225
+ ## API Reference
226
+
227
+ ### `A11yOracle`
228
+
229
+ Manages a CDP session and provides accessibility testing for the current page.
230
+
231
+ #### `constructor(page: Page, options?: A11yOrchestratorOptions)`
232
+
233
+ Create a new instance.
234
+
235
+ - `page` — Playwright `Page` to attach to.
236
+ - `options.includeLandmarks` — Append "landmark" to landmark roles. Default `true`.
237
+ - `options.includeDescription` — Include description text. Default `false`.
238
+ - `options.focusSettleMs` — Delay after key press for focus/CSS to settle. Default `50`.
239
+
240
+ #### `init(): Promise<void>`
241
+
242
+ Open a CDP session and enable the Accessibility domain. Must be called before any other method. The test fixture calls this automatically.
243
+
244
+ #### Speech-Only API
245
+
246
+ ##### `press(key: string): Promise<string>`
247
+
248
+ Press a keyboard key (via Playwright's `page.keyboard.press()`) and return the speech for the newly focused element. Returns an empty string if no element has focus.
249
+
250
+ ```typescript
251
+ const speech = await a11y.press('Tab');
252
+ // "Products, button, collapsed"
253
+ ```
254
+
255
+ ##### `getSpeech(): Promise<string>`
256
+
257
+ Get the speech string for the currently focused element without pressing a key.
258
+
259
+ ##### `getSpeechResult(): Promise<SpeechResult | null>`
260
+
261
+ Get the full structured result for the focused element.
262
+
263
+ ##### `getFullTreeSpeech(): Promise<SpeechResult[]>`
264
+
265
+ Get speech for all non-ignored nodes in the accessibility tree.
266
+
267
+ #### Unified State API
268
+
269
+ ##### `pressKey(key: string, modifiers?: ModifierKeys): Promise<A11yState>`
270
+
271
+ Dispatch a key via native CDP `Input.dispatchKeyEvent` and return the unified accessibility state. Unlike `press()`, this uses hardware-level key dispatch and returns the full state.
272
+
273
+ ```typescript
274
+ const state = await a11y.pressKey('Tab');
275
+ // state.speech → "Products, button, collapsed"
276
+ // state.focusedElement → { tag: 'BUTTON', id: '...', ... }
277
+ // state.focusIndicator → { isVisible: true, meetsWCAG_AA: true, ... }
278
+ ```
279
+
280
+ ##### `getA11yState(): Promise<A11yState>`
281
+
282
+ Get the current unified state without pressing a key.
283
+
284
+ ```typescript
285
+ await page.focus('#my-button');
286
+ const state = await a11y.getA11yState();
287
+ ```
288
+
289
+ ##### `traverseTabOrder(): Promise<TabOrderReport>`
290
+
291
+ Extract all tabbable elements in DOM tab order.
292
+
293
+ ##### `traverseSubTree(selector: string, maxTabs?: number): Promise<TraversalResult>`
294
+
295
+ Detect whether a container traps keyboard focus (WCAG 2.1.2).
296
+
297
+ #### Lifecycle
298
+
299
+ ##### `dispose(): Promise<void>`
300
+
301
+ Detach the CDP session and free resources. The test fixture calls this automatically.
302
+
303
+ ### Test Fixture
304
+
305
+ The `test` export extends Playwright's `test` with two fixtures:
306
+
307
+ | Fixture | Type | Description |
308
+ |---------|------|-------------|
309
+ | `a11y` | `A11yOracle` | Initialized instance, auto-disposed after each test |
310
+ | `a11yOptions` | `A11yOrchestratorOptions` | Override via `test.use()` |
311
+
312
+ ### Exports
313
+
314
+ ```typescript
315
+ // Test fixture (recommended)
316
+ export { test, expect } from '@a11y-oracle/playwright-plugin';
317
+
318
+ // Manual usage
319
+ export { A11yOracle } from '@a11y-oracle/playwright-plugin';
320
+
321
+ // Fixture types
322
+ export type { A11yOracleFixtures } from '@a11y-oracle/playwright-plugin';
323
+
324
+ // Re-exported types from core-engine
325
+ export type {
326
+ A11yState,
327
+ A11yFocusedElement,
328
+ A11yFocusIndicator,
329
+ A11yOrchestratorOptions,
330
+ SpeechResult,
331
+ SpeechEngineOptions,
332
+ ModifierKeys,
333
+ TabOrderReport,
334
+ TabOrderEntry,
335
+ TraversalResult,
336
+ FocusIndicator,
337
+ } from '@a11y-oracle/playwright-plugin';
338
+ ```
339
+
340
+ ## Playwright Config
341
+
342
+ The plugin requires Chromium. A typical `playwright.config.ts`:
343
+
344
+ ```typescript
345
+ import { defineConfig } from '@playwright/test';
346
+
347
+ export default defineConfig({
348
+ use: {
349
+ baseURL: 'http://localhost:4200',
350
+ },
351
+ projects: [
352
+ {
353
+ name: 'chromium',
354
+ use: { browserName: 'chromium' },
355
+ },
356
+ ],
357
+ webServer: {
358
+ command: 'npm run serve',
359
+ url: 'http://localhost:4200',
360
+ reuseExistingServer: !process.env.CI,
361
+ },
362
+ });
363
+ ```
364
+
365
+ ## How It Works
366
+
367
+ 1. The fixture opens a CDP session via `page.context().newCDPSession(page)`
368
+ 2. It creates both a `SpeechEngine` and an `A11yOrchestrator` on that session
369
+ 3. **`press(key)`** — Uses Playwright's keyboard API, waits 50ms, then reads the AXTree for speech
370
+ 4. **`pressKey(key)`** — Uses native CDP `Input.dispatchKeyEvent` for hardware-level dispatch, waits `focusSettleMs`, then collects speech + focused element + focus indicator in parallel
371
+ 5. Focus indicator analysis runs `Runtime.evaluate` to read computed CSS styles and calculate contrast ratios
372
+ 6. On dispose, the CDP session is detached
373
+
374
+ The speech format follows: `[Computed Name], [Role], [State/Properties]`
375
+
376
+ For the full list of role and state mappings, see the [@a11y-oracle/core-engine README](../core-engine/README.md).
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @module @a11y-oracle/playwright-plugin
3
+ *
4
+ * Playwright integration for A11y-Oracle. Provides a test fixture and
5
+ * wrapper class for asserting accessibility speech output in Playwright
6
+ * tests.
7
+ *
8
+ * ## Quick Start
9
+ *
10
+ * ```typescript
11
+ * import { test, expect } from '@a11y-oracle/playwright-plugin';
12
+ *
13
+ * test('button announces correctly', async ({ page, a11y }) => {
14
+ * await page.goto('/my-page.html');
15
+ * const speech = await a11y.press('Tab');
16
+ * expect(speech).toBe('Submit, button');
17
+ * });
18
+ * ```
19
+ *
20
+ * ## Manual Usage
21
+ *
22
+ * ```typescript
23
+ * import { A11yOracle } from '@a11y-oracle/playwright-plugin';
24
+ * import { test, expect } from '@playwright/test';
25
+ *
26
+ * test('manual setup', async ({ page }) => {
27
+ * const a11y = new A11yOracle(page);
28
+ * await a11y.init();
29
+ * // ... assertions ...
30
+ * await a11y.dispose();
31
+ * });
32
+ * ```
33
+ *
34
+ * @packageDocumentation
35
+ */
36
+ export { A11yOracle } from './lib/a11y-oracle.js';
37
+ export { test, expect } from './lib/fixture.js';
38
+ export type { A11yOracleFixtures } from './lib/fixture.js';
39
+ export type { A11yState, A11yFocusedElement, A11yFocusIndicator, A11yOrchestratorOptions, SpeechResult, SpeechEngineOptions, ModifierKeys, TabOrderReport, TabOrderEntry, TraversalResult, FocusIndicator, } from '@a11y-oracle/core-engine';
40
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAG3D,YAAY,EACV,SAAS,EACT,kBAAkB,EAClB,kBAAkB,EAClB,uBAAuB,EACvB,YAAY,EACZ,mBAAmB,EACnB,YAAY,EACZ,cAAc,EACd,aAAa,EACb,eAAe,EACf,cAAc,GACf,MAAM,0BAA0B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @module @a11y-oracle/playwright-plugin
3
+ *
4
+ * Playwright integration for A11y-Oracle. Provides a test fixture and
5
+ * wrapper class for asserting accessibility speech output in Playwright
6
+ * tests.
7
+ *
8
+ * ## Quick Start
9
+ *
10
+ * ```typescript
11
+ * import { test, expect } from '@a11y-oracle/playwright-plugin';
12
+ *
13
+ * test('button announces correctly', async ({ page, a11y }) => {
14
+ * await page.goto('/my-page.html');
15
+ * const speech = await a11y.press('Tab');
16
+ * expect(speech).toBe('Submit, button');
17
+ * });
18
+ * ```
19
+ *
20
+ * ## Manual Usage
21
+ *
22
+ * ```typescript
23
+ * import { A11yOracle } from '@a11y-oracle/playwright-plugin';
24
+ * import { test, expect } from '@playwright/test';
25
+ *
26
+ * test('manual setup', async ({ page }) => {
27
+ * const a11y = new A11yOracle(page);
28
+ * await a11y.init();
29
+ * // ... assertions ...
30
+ * await a11y.dispose();
31
+ * });
32
+ * ```
33
+ *
34
+ * @packageDocumentation
35
+ */
36
+ export { A11yOracle } from './lib/a11y-oracle.js';
37
+ export { test, expect } from './lib/fixture.js';
@@ -0,0 +1,161 @@
1
+ /**
2
+ * @module a11y-oracle
3
+ *
4
+ * Playwright wrapper around the core {@link SpeechEngine} and
5
+ * {@link A11yOrchestrator}. Manages the CDP session lifecycle and
6
+ * provides a clean API for accessibility speech and keyboard/focus
7
+ * assertions in Playwright tests.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { A11yOracle } from '@a11y-oracle/playwright-plugin';
12
+ *
13
+ * const a11y = new A11yOracle(page);
14
+ * await a11y.init();
15
+ *
16
+ * // Speech-only API (backward compatible)
17
+ * const speech = await a11y.press('Tab');
18
+ * expect(speech).toBe('Products, button, collapsed');
19
+ *
20
+ * // Unified state API (new)
21
+ * const state = await a11y.pressKey('Tab');
22
+ * expect(state.speech).toContain('Products');
23
+ * expect(state.focusIndicator.meetsWCAG_AA).toBe(true);
24
+ *
25
+ * await a11y.dispose();
26
+ * ```
27
+ */
28
+ import type { Page } from '@playwright/test';
29
+ import type { SpeechResult, A11yState, A11yOrchestratorOptions, TabOrderReport, TraversalResult, ModifierKeys } from '@a11y-oracle/core-engine';
30
+ /**
31
+ * Playwright wrapper that manages a CDP session and provides
32
+ * accessibility speech output and keyboard/focus analysis for the
33
+ * currently focused element.
34
+ *
35
+ * For most use cases, prefer the {@link test} fixture from the
36
+ * package root, which handles init/dispose automatically.
37
+ *
38
+ * ## CDP Requirement
39
+ *
40
+ * This class requires a Chromium-based browser. CDP sessions are
41
+ * not available for Firefox or WebKit in Playwright.
42
+ */
43
+ export declare class A11yOracle {
44
+ private page;
45
+ private cdpSession;
46
+ private engine;
47
+ private orchestrator;
48
+ private options;
49
+ /**
50
+ * Create a new A11yOracle instance.
51
+ *
52
+ * @param page - The Playwright Page to attach to.
53
+ * @param options - Optional speech engine and orchestrator configuration.
54
+ */
55
+ constructor(page: Page, options?: A11yOrchestratorOptions);
56
+ /**
57
+ * Initialize the CDP session and enable the Accessibility domain.
58
+ *
59
+ * Must be called before any other method. The {@link test} fixture
60
+ * calls this automatically.
61
+ *
62
+ * @throws Error if the browser does not support CDP (e.g., Firefox).
63
+ */
64
+ init(): Promise<void>;
65
+ /**
66
+ * Press a keyboard key and return the speech for the newly focused element.
67
+ *
68
+ * Internally calls `page.keyboard.press(key)` followed by a short delay
69
+ * to allow the browser to update focus and ARIA states before reading
70
+ * the accessibility tree.
71
+ *
72
+ * @param key - The key to press (e.g., `'Tab'`, `'Enter'`, `'Escape'`).
73
+ * Uses Playwright's key name format.
74
+ * @returns The speech string for the focused element after the key press,
75
+ * or an empty string if no element has focus.
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * const speech = await a11y.press('Tab');
80
+ * expect(speech).toBe('Products, button, collapsed');
81
+ * ```
82
+ */
83
+ press(key: string): Promise<string>;
84
+ /**
85
+ * Get the speech string for the currently focused element.
86
+ *
87
+ * @returns The speech string (e.g., `"Products, button, collapsed"`),
88
+ * or an empty string if no element has focus.
89
+ *
90
+ * @throws Error if {@link init} has not been called.
91
+ */
92
+ getSpeech(): Promise<string>;
93
+ /**
94
+ * Get the full {@link SpeechResult} for the currently focused element.
95
+ *
96
+ * @returns The full speech result, or `null` if no element has focus.
97
+ * @throws Error if {@link init} has not been called.
98
+ */
99
+ getSpeechResult(): Promise<SpeechResult | null>;
100
+ /**
101
+ * Get speech output for ALL non-ignored nodes in the accessibility tree.
102
+ *
103
+ * @returns Array of {@link SpeechResult} objects for every visible node.
104
+ * @throws Error if {@link init} has not been called.
105
+ */
106
+ getFullTreeSpeech(): Promise<SpeechResult[]>;
107
+ /**
108
+ * Dispatch a key via CDP and return the unified accessibility state.
109
+ *
110
+ * Unlike {@link press}, this uses native CDP key dispatch (not
111
+ * Playwright's keyboard API) and returns the full {@link A11yState}
112
+ * including speech, focused element info, and focus indicator analysis.
113
+ *
114
+ * @param key - Key name (e.g. `'Tab'`, `'Enter'`, `'ArrowDown'`).
115
+ * @param modifiers - Optional modifier keys (shift, ctrl, alt, meta).
116
+ * @returns Unified accessibility state snapshot.
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * const state = await a11y.pressKey('Tab');
121
+ * expect(state.speech).toContain('Products');
122
+ * expect(state.focusIndicator.meetsWCAG_AA).toBe(true);
123
+ * ```
124
+ */
125
+ pressKey(key: string, modifiers?: ModifierKeys): Promise<A11yState>;
126
+ /**
127
+ * Get the current unified accessibility state without pressing a key.
128
+ *
129
+ * @returns Unified accessibility state snapshot.
130
+ */
131
+ getA11yState(): Promise<A11yState>;
132
+ /**
133
+ * Alias for {@link getA11yState} that satisfies the
134
+ * {@link OrchestratorLike} interface from `@a11y-oracle/audit-formatter`.
135
+ *
136
+ * @returns Unified accessibility state snapshot.
137
+ */
138
+ getState(): Promise<A11yState>;
139
+ /**
140
+ * Extract all tabbable elements in DOM tab order.
141
+ *
142
+ * @returns Report with sorted tab order entries and total count.
143
+ */
144
+ traverseTabOrder(): Promise<TabOrderReport>;
145
+ /**
146
+ * Detect whether a container traps keyboard focus (WCAG 2.1.2).
147
+ *
148
+ * @param selector - CSS selector for the container to test.
149
+ * @param maxTabs - Maximum Tab presses before declaring a trap. Default 50.
150
+ * @returns Traversal result indicating whether focus is trapped.
151
+ */
152
+ traverseSubTree(selector: string, maxTabs?: number): Promise<TraversalResult>;
153
+ /**
154
+ * Detach the CDP session and clean up resources.
155
+ *
156
+ * The {@link test} fixture calls this automatically after each test.
157
+ */
158
+ dispose(): Promise<void>;
159
+ private assertOrchestrator;
160
+ }
161
+ //# sourceMappingURL=a11y-oracle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a11y-oracle.d.ts","sourceRoot":"","sources":["../../src/lib/a11y-oracle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAc,MAAM,kBAAkB,CAAC;AAGzD,OAAO,KAAK,EACV,YAAY,EACZ,SAAS,EACT,uBAAuB,EACvB,cAAc,EACd,eAAe,EACf,YAAY,EACb,MAAM,0BAA0B,CAAC;AAElC;;;;;;;;;;;;GAYG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,IAAI,CAAO;IACnB,OAAO,CAAC,UAAU,CAA2B;IAC7C,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,YAAY,CAAiC;IACrD,OAAO,CAAC,OAAO,CAA0B;IAEzC;;;;;OAKG;gBACS,IAAI,EAAE,IAAI,EAAE,OAAO,GAAE,uBAA4B;IAK7D;;;;;;;OAOG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAS3B;;;;;;;;;;;;;;;;;OAiBG;IACG,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAOzC;;;;;;;OAOG;IACG,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC;IAKlC;;;;;OAKG;IACG,eAAe,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAOrD;;;;;OAKG;IACG,iBAAiB,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IASlD;;;;;;;;;;;;;;;;;OAiBG;IACG,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,SAAS,CAAC;IAKzE;;;;OAIG;IACG,YAAY,IAAI,OAAO,CAAC,SAAS,CAAC;IAKxC;;;;;OAKG;IACG,QAAQ,IAAI,OAAO,CAAC,SAAS,CAAC;IAIpC;;;;OAIG;IACG,gBAAgB,IAAI,OAAO,CAAC,cAAc,CAAC;IAKjD;;;;;;OAMG;IACG,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,eAAe,CAAC;IAO3B;;;;OAIG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAY9B,OAAO,CAAC,kBAAkB;CAK3B"}
@@ -0,0 +1,217 @@
1
+ /**
2
+ * @module a11y-oracle
3
+ *
4
+ * Playwright wrapper around the core {@link SpeechEngine} and
5
+ * {@link A11yOrchestrator}. Manages the CDP session lifecycle and
6
+ * provides a clean API for accessibility speech and keyboard/focus
7
+ * assertions in Playwright tests.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { A11yOracle } from '@a11y-oracle/playwright-plugin';
12
+ *
13
+ * const a11y = new A11yOracle(page);
14
+ * await a11y.init();
15
+ *
16
+ * // Speech-only API (backward compatible)
17
+ * const speech = await a11y.press('Tab');
18
+ * expect(speech).toBe('Products, button, collapsed');
19
+ *
20
+ * // Unified state API (new)
21
+ * const state = await a11y.pressKey('Tab');
22
+ * expect(state.speech).toContain('Products');
23
+ * expect(state.focusIndicator.meetsWCAG_AA).toBe(true);
24
+ *
25
+ * await a11y.dispose();
26
+ * ```
27
+ */
28
+ import { SpeechEngine } from '@a11y-oracle/core-engine';
29
+ import { A11yOrchestrator } from '@a11y-oracle/core-engine';
30
+ /**
31
+ * Playwright wrapper that manages a CDP session and provides
32
+ * accessibility speech output and keyboard/focus analysis for the
33
+ * currently focused element.
34
+ *
35
+ * For most use cases, prefer the {@link test} fixture from the
36
+ * package root, which handles init/dispose automatically.
37
+ *
38
+ * ## CDP Requirement
39
+ *
40
+ * This class requires a Chromium-based browser. CDP sessions are
41
+ * not available for Firefox or WebKit in Playwright.
42
+ */
43
+ export class A11yOracle {
44
+ page;
45
+ cdpSession = null;
46
+ engine = null;
47
+ orchestrator = null;
48
+ options;
49
+ /**
50
+ * Create a new A11yOracle instance.
51
+ *
52
+ * @param page - The Playwright Page to attach to.
53
+ * @param options - Optional speech engine and orchestrator configuration.
54
+ */
55
+ constructor(page, options = {}) {
56
+ this.page = page;
57
+ this.options = options;
58
+ }
59
+ /**
60
+ * Initialize the CDP session and enable the Accessibility domain.
61
+ *
62
+ * Must be called before any other method. The {@link test} fixture
63
+ * calls this automatically.
64
+ *
65
+ * @throws Error if the browser does not support CDP (e.g., Firefox).
66
+ */
67
+ async init() {
68
+ this.cdpSession = await this.page.context().newCDPSession(this.page);
69
+ this.engine = new SpeechEngine(this.cdpSession, this.options);
70
+ this.orchestrator = new A11yOrchestrator(this.cdpSession, this.options);
71
+ await this.orchestrator.enable();
72
+ }
73
+ // ── Speech-only API (backward compatible) ──────────────────────
74
+ /**
75
+ * Press a keyboard key and return the speech for the newly focused element.
76
+ *
77
+ * Internally calls `page.keyboard.press(key)` followed by a short delay
78
+ * to allow the browser to update focus and ARIA states before reading
79
+ * the accessibility tree.
80
+ *
81
+ * @param key - The key to press (e.g., `'Tab'`, `'Enter'`, `'Escape'`).
82
+ * Uses Playwright's key name format.
83
+ * @returns The speech string for the focused element after the key press,
84
+ * or an empty string if no element has focus.
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * const speech = await a11y.press('Tab');
89
+ * expect(speech).toBe('Products, button, collapsed');
90
+ * ```
91
+ */
92
+ async press(key) {
93
+ await this.page.keyboard.press(key);
94
+ // Allow browser to update focus and ARIA states
95
+ await this.page.waitForTimeout(50);
96
+ return this.getSpeech();
97
+ }
98
+ /**
99
+ * Get the speech string for the currently focused element.
100
+ *
101
+ * @returns The speech string (e.g., `"Products, button, collapsed"`),
102
+ * or an empty string if no element has focus.
103
+ *
104
+ * @throws Error if {@link init} has not been called.
105
+ */
106
+ async getSpeech() {
107
+ const result = await this.getSpeechResult();
108
+ return result?.speech ?? '';
109
+ }
110
+ /**
111
+ * Get the full {@link SpeechResult} for the currently focused element.
112
+ *
113
+ * @returns The full speech result, or `null` if no element has focus.
114
+ * @throws Error if {@link init} has not been called.
115
+ */
116
+ async getSpeechResult() {
117
+ if (!this.engine) {
118
+ throw new Error('A11yOracle not initialized. Call init() first.');
119
+ }
120
+ return this.engine.getSpeech();
121
+ }
122
+ /**
123
+ * Get speech output for ALL non-ignored nodes in the accessibility tree.
124
+ *
125
+ * @returns Array of {@link SpeechResult} objects for every visible node.
126
+ * @throws Error if {@link init} has not been called.
127
+ */
128
+ async getFullTreeSpeech() {
129
+ if (!this.engine) {
130
+ throw new Error('A11yOracle not initialized. Call init() first.');
131
+ }
132
+ return this.engine.getFullTreeSpeech();
133
+ }
134
+ // ── Unified state API (new) ────────────────────────────────────
135
+ /**
136
+ * Dispatch a key via CDP and return the unified accessibility state.
137
+ *
138
+ * Unlike {@link press}, this uses native CDP key dispatch (not
139
+ * Playwright's keyboard API) and returns the full {@link A11yState}
140
+ * including speech, focused element info, and focus indicator analysis.
141
+ *
142
+ * @param key - Key name (e.g. `'Tab'`, `'Enter'`, `'ArrowDown'`).
143
+ * @param modifiers - Optional modifier keys (shift, ctrl, alt, meta).
144
+ * @returns Unified accessibility state snapshot.
145
+ *
146
+ * @example
147
+ * ```typescript
148
+ * const state = await a11y.pressKey('Tab');
149
+ * expect(state.speech).toContain('Products');
150
+ * expect(state.focusIndicator.meetsWCAG_AA).toBe(true);
151
+ * ```
152
+ */
153
+ async pressKey(key, modifiers) {
154
+ this.assertOrchestrator();
155
+ return this.orchestrator.pressKey(key, modifiers);
156
+ }
157
+ /**
158
+ * Get the current unified accessibility state without pressing a key.
159
+ *
160
+ * @returns Unified accessibility state snapshot.
161
+ */
162
+ async getA11yState() {
163
+ this.assertOrchestrator();
164
+ return this.orchestrator.getState();
165
+ }
166
+ /**
167
+ * Alias for {@link getA11yState} that satisfies the
168
+ * {@link OrchestratorLike} interface from `@a11y-oracle/audit-formatter`.
169
+ *
170
+ * @returns Unified accessibility state snapshot.
171
+ */
172
+ async getState() {
173
+ return this.getA11yState();
174
+ }
175
+ /**
176
+ * Extract all tabbable elements in DOM tab order.
177
+ *
178
+ * @returns Report with sorted tab order entries and total count.
179
+ */
180
+ async traverseTabOrder() {
181
+ this.assertOrchestrator();
182
+ return this.orchestrator.traverseTabOrder();
183
+ }
184
+ /**
185
+ * Detect whether a container traps keyboard focus (WCAG 2.1.2).
186
+ *
187
+ * @param selector - CSS selector for the container to test.
188
+ * @param maxTabs - Maximum Tab presses before declaring a trap. Default 50.
189
+ * @returns Traversal result indicating whether focus is trapped.
190
+ */
191
+ async traverseSubTree(selector, maxTabs) {
192
+ this.assertOrchestrator();
193
+ return this.orchestrator.traverseSubTree(selector, maxTabs);
194
+ }
195
+ // ── Lifecycle ──────────────────────────────────────────────────
196
+ /**
197
+ * Detach the CDP session and clean up resources.
198
+ *
199
+ * The {@link test} fixture calls this automatically after each test.
200
+ */
201
+ async dispose() {
202
+ if (this.orchestrator) {
203
+ await this.orchestrator.disable();
204
+ this.orchestrator = null;
205
+ }
206
+ if (this.cdpSession) {
207
+ await this.cdpSession.detach();
208
+ this.cdpSession = null;
209
+ this.engine = null;
210
+ }
211
+ }
212
+ assertOrchestrator() {
213
+ if (!this.orchestrator) {
214
+ throw new Error('A11yOracle not initialized. Call init() first.');
215
+ }
216
+ }
217
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @module fixture
3
+ *
4
+ * Playwright test fixture that provides an automatically managed
5
+ * {@link A11yOracle} instance as the `a11y` fixture parameter.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { test, expect } from '@a11y-oracle/playwright-plugin';
10
+ *
11
+ * test('navigation announces correctly', async ({ page, a11y }) => {
12
+ * await page.goto('/dropdown-nav.html');
13
+ *
14
+ * const speech = await a11y.press('Tab');
15
+ * expect(speech).toBe('Products, button, collapsed');
16
+ * });
17
+ * ```
18
+ */
19
+ import { A11yOracle } from './a11y-oracle.js';
20
+ import type { A11yOrchestratorOptions } from '@a11y-oracle/core-engine';
21
+ /**
22
+ * Type definition for the A11y-Oracle Playwright fixtures.
23
+ *
24
+ * - `a11y`: An initialized {@link A11yOracle} instance, ready to use.
25
+ * - `a11yOptions`: Configuration options for the orchestrator (speech engine
26
+ * and focus analysis). Override in `test.use()` to customize behavior.
27
+ */
28
+ export type A11yOracleFixtures = {
29
+ /** An initialized A11yOracle instance for the current page. */
30
+ a11y: A11yOracle;
31
+ /** Orchestrator options. Override via `test.use({ a11yOptions: { ... } })`. */
32
+ a11yOptions: A11yOrchestratorOptions;
33
+ };
34
+ /**
35
+ * Extended Playwright test with the `a11y` fixture.
36
+ *
37
+ * The fixture automatically:
38
+ * 1. Creates a new {@link A11yOracle} instance for each test
39
+ * 2. Initializes the CDP session
40
+ * 3. Disposes the session after the test completes
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * import { test, expect } from '@a11y-oracle/playwright-plugin';
45
+ *
46
+ * test('button announces name and role', async ({ page, a11y }) => {
47
+ * await page.goto('/my-page.html');
48
+ * await a11y.press('Tab');
49
+ * expect(await a11y.getSpeech()).toBe('Submit, button');
50
+ * });
51
+ *
52
+ * // Customize options for a test group:
53
+ * test.describe('without landmarks', () => {
54
+ * test.use({ a11yOptions: { includeLandmarks: false } });
55
+ *
56
+ * test('nav without landmark suffix', async ({ page, a11y }) => {
57
+ * await page.goto('/my-page.html');
58
+ * const all = await a11y.getFullTreeSpeech();
59
+ * const nav = all.find(r => r.role === 'navigation');
60
+ * expect(nav?.speech).toBe('Main, navigation');
61
+ * });
62
+ * });
63
+ * ```
64
+ */
65
+ export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & A11yOracleFixtures, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
66
+ /** Re-export Playwright's expect for convenience. */
67
+ export { expect } from '@playwright/test';
68
+ //# sourceMappingURL=fixture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixture.d.ts","sourceRoot":"","sources":["../../src/lib/fixture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAExE;;;;;;GAMG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,+DAA+D;IAC/D,IAAI,EAAE,UAAU,CAAC;IACjB,+EAA+E;IAC/E,WAAW,EAAE,uBAAuB,CAAC;CACtC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,IAAI,kQAUf,CAAC;AAEH,qDAAqD;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @module fixture
3
+ *
4
+ * Playwright test fixture that provides an automatically managed
5
+ * {@link A11yOracle} instance as the `a11y` fixture parameter.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { test, expect } from '@a11y-oracle/playwright-plugin';
10
+ *
11
+ * test('navigation announces correctly', async ({ page, a11y }) => {
12
+ * await page.goto('/dropdown-nav.html');
13
+ *
14
+ * const speech = await a11y.press('Tab');
15
+ * expect(speech).toBe('Products, button, collapsed');
16
+ * });
17
+ * ```
18
+ */
19
+ import { test as base } from '@playwright/test';
20
+ import { A11yOracle } from './a11y-oracle.js';
21
+ /**
22
+ * Extended Playwright test with the `a11y` fixture.
23
+ *
24
+ * The fixture automatically:
25
+ * 1. Creates a new {@link A11yOracle} instance for each test
26
+ * 2. Initializes the CDP session
27
+ * 3. Disposes the session after the test completes
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * import { test, expect } from '@a11y-oracle/playwright-plugin';
32
+ *
33
+ * test('button announces name and role', async ({ page, a11y }) => {
34
+ * await page.goto('/my-page.html');
35
+ * await a11y.press('Tab');
36
+ * expect(await a11y.getSpeech()).toBe('Submit, button');
37
+ * });
38
+ *
39
+ * // Customize options for a test group:
40
+ * test.describe('without landmarks', () => {
41
+ * test.use({ a11yOptions: { includeLandmarks: false } });
42
+ *
43
+ * test('nav without landmark suffix', async ({ page, a11y }) => {
44
+ * await page.goto('/my-page.html');
45
+ * const all = await a11y.getFullTreeSpeech();
46
+ * const nav = all.find(r => r.role === 'navigation');
47
+ * expect(nav?.speech).toBe('Main, navigation');
48
+ * });
49
+ * });
50
+ * ```
51
+ */
52
+ export const test = base.extend({
53
+ // Default options (empty = use engine defaults)
54
+ a11yOptions: [{}, { option: true }],
55
+ a11y: async ({ page, a11yOptions }, use) => {
56
+ const a11y = new A11yOracle(page, a11yOptions);
57
+ await a11y.init();
58
+ await use(a11y);
59
+ await a11y.dispose();
60
+ },
61
+ });
62
+ /** Re-export Playwright's expect for convenience. */
63
+ export { expect } from '@playwright/test';
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@a11y-oracle/playwright-plugin",
3
+ "version": "1.0.0",
4
+ "description": "Playwright test fixture for accessibility speech assertions, keyboard navigation, and focus indicator validation",
5
+ "license": "MIT",
6
+ "author": "a11y-oracle",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/a11y-oracle/a11y-oracle.git",
10
+ "directory": "libs/playwright-plugin"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/a11y-oracle/a11y-oracle/issues"
14
+ },
15
+ "homepage": "https://github.com/a11y-oracle/a11y-oracle/tree/main/libs/playwright-plugin",
16
+ "keywords": [
17
+ "accessibility",
18
+ "a11y",
19
+ "playwright",
20
+ "screen-reader",
21
+ "wcag",
22
+ "testing",
23
+ "e2e"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "type": "module",
29
+ "main": "./dist/index.js",
30
+ "module": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "exports": {
33
+ "./package.json": "./package.json",
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js",
37
+ "default": "./dist/index.js"
38
+ }
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "!**/*.tsbuildinfo"
43
+ ],
44
+ "peerDependencies": {
45
+ "@playwright/test": ">=1.40.0"
46
+ },
47
+ "dependencies": {
48
+ "@a11y-oracle/core-engine": "1.0.0",
49
+ "tslib": "^2.3.0"
50
+ }
51
+ }