@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 +376 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/lib/a11y-oracle.d.ts +161 -0
- package/dist/lib/a11y-oracle.d.ts.map +1 -0
- package/dist/lib/a11y-oracle.js +217 -0
- package/dist/lib/fixture.d.ts +68 -0
- package/dist/lib/fixture.d.ts.map +1 -0
- package/dist/lib/fixture.js +63 -0
- package/package.json +51 -0
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).
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|