@a11y-oracle/cypress-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 +529 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/lib/commands.d.ts +101 -0
- package/dist/lib/commands.d.ts.map +1 -0
- package/dist/lib/commands.js +363 -0
- package/dist/lib/key-map.d.ts +23 -0
- package/dist/lib/key-map.d.ts.map +1 -0
- package/dist/lib/key-map.js +28 -0
- package/dist/lib/reporting.d.ts +47 -0
- package/dist/lib/reporting.d.ts.map +1 -0
- package/dist/lib/reporting.js +71 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
# @a11y-oracle/cypress-plugin
|
|
2
|
+
|
|
3
|
+
Cypress integration for A11y-Oracle. Provides custom commands that read the browser's Accessibility Tree via Chrome DevTools Protocol, dispatch native keyboard events, and analyze visual focus indicators.
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
describe('Navigation', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
cy.visit('/dropdown-nav.html');
|
|
9
|
+
cy.initA11yOracle();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
cy.disposeA11yOracle();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('Tab to button announces name and role', () => {
|
|
17
|
+
cy.a11yPress('Tab')
|
|
18
|
+
.should('contain', 'Home')
|
|
19
|
+
.and('contain', 'menu item');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install -D @a11y-oracle/cypress-plugin @a11y-oracle/core-engine cypress
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
> **Chrome/Chromium only.** The plugin uses CDP, which is only available in Chrome-family browsers.
|
|
31
|
+
|
|
32
|
+
## Setup
|
|
33
|
+
|
|
34
|
+
### 1. Import Commands
|
|
35
|
+
|
|
36
|
+
Add the plugin to your Cypress support file:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// cypress/support/e2e.ts (or cypress/support/e2e.js)
|
|
40
|
+
import '@a11y-oracle/cypress-plugin';
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
That's it. Importing the package registers all custom commands automatically.
|
|
44
|
+
|
|
45
|
+
### 2. Configure Browser
|
|
46
|
+
|
|
47
|
+
Ensure your Cypress config runs tests in Chrome:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// cypress.config.ts
|
|
51
|
+
import { defineConfig } from 'cypress';
|
|
52
|
+
|
|
53
|
+
export default defineConfig({
|
|
54
|
+
e2e: {
|
|
55
|
+
baseUrl: 'http://localhost:4200',
|
|
56
|
+
specPattern: 'cypress/e2e/**/*.cy.ts',
|
|
57
|
+
supportFile: 'cypress/support/e2e.ts',
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Run with Chrome:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx cypress run --browser chrome
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
### Speech Assertions
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
describe('My Form', () => {
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
cy.visit('/form.html');
|
|
76
|
+
cy.initA11yOracle();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
afterEach(() => {
|
|
80
|
+
cy.disposeA11yOracle();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('Tab navigates to submit button', () => {
|
|
84
|
+
cy.a11yPress('Tab');
|
|
85
|
+
cy.a11yPress('Tab');
|
|
86
|
+
cy.a11yPress('Tab')
|
|
87
|
+
.should('equal', 'Submit, button');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('checkbox announces checked state', () => {
|
|
91
|
+
cy.a11yPress('Tab');
|
|
92
|
+
cy.a11yPress('Space')
|
|
93
|
+
.should('contain', 'checkbox')
|
|
94
|
+
.should('contain', 'checked');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Unified State (Speech + Focus + Indicator)
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
it('Tab returns unified accessibility state', () => {
|
|
103
|
+
cy.a11yPressKey('Tab').then((state) => {
|
|
104
|
+
// Speech
|
|
105
|
+
expect(state.speech).to.contain('Submit');
|
|
106
|
+
expect(state.speechResult?.role).to.equal('button');
|
|
107
|
+
|
|
108
|
+
// Focused element
|
|
109
|
+
expect(state.focusedElement?.tag).to.equal('BUTTON');
|
|
110
|
+
expect(state.focusedElement?.id).to.equal('submit-btn');
|
|
111
|
+
|
|
112
|
+
// Focus indicator (WCAG 2.4.12 AA)
|
|
113
|
+
expect(state.focusIndicator.isVisible).to.be.true;
|
|
114
|
+
expect(state.focusIndicator.meetsWCAG_AA).to.be.true;
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('Shift+Tab navigates backward', () => {
|
|
119
|
+
cy.a11yPressKey('Tab');
|
|
120
|
+
cy.a11yPressKey('Tab');
|
|
121
|
+
cy.a11yPressKey('Tab', { shift: true }).then((state) => {
|
|
122
|
+
expect(state.focusedElement).to.not.be.null;
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Tab Order and Keyboard Trap Detection
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
it('page has correct tab order', () => {
|
|
131
|
+
cy.a11yTraverseTabOrder().then((report) => {
|
|
132
|
+
expect(report.totalCount).to.be.greaterThan(0);
|
|
133
|
+
expect(report.entries[0].tag).to.equal('A');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('modal does not trap keyboard focus', () => {
|
|
138
|
+
cy.a11yTraverseSubTree('#modal-container', 20).then((result) => {
|
|
139
|
+
expect(result.isTrapped).to.be.false;
|
|
140
|
+
expect(result.escapeElement).to.not.be.null;
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Issue Reporting
|
|
146
|
+
|
|
147
|
+
Check focus indicators and keyboard traps with automatic issue reporting. Issues are accumulated via `cy.task('logOracleIssues')` and can be written to a JSON report at the end of the run.
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
describe('Accessibility audit', () => {
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
cy.visit('/my-page.html');
|
|
153
|
+
cy.initA11yOracle();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
afterEach(() => {
|
|
157
|
+
cy.disposeA11yOracle();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('focus indicators pass oracle rules', () => {
|
|
161
|
+
cy.a11yPressKey('Tab');
|
|
162
|
+
cy.a11yCheckFocusAndReport(); // checks + reports issues
|
|
163
|
+
|
|
164
|
+
cy.a11yPressKey('Tab');
|
|
165
|
+
cy.a11yCheckFocusAndReport(); // check each focused element
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('modal is not a keyboard trap', () => {
|
|
169
|
+
cy.get('#open-modal').click();
|
|
170
|
+
cy.a11yCheckTrapAndReport('#modal-dialog', 10);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
#### WCAG Level and Rule Configuration
|
|
176
|
+
|
|
177
|
+
Filter issues by WCAG conformance level or disable specific rules via Cypress env:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// cypress.config.ts
|
|
181
|
+
export default defineConfig({
|
|
182
|
+
e2e: {
|
|
183
|
+
env: {
|
|
184
|
+
wcagLevel: 'wcag21aa', // WCAG 2.1 Level AA (default: 'wcag22aa')
|
|
185
|
+
disabledRules: ['oracle/positive-tabindex'], // Suppress specific rules
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Supported `wcagLevel` values (matching axe-core tag format):
|
|
192
|
+
- `'wcag2a'` / `'wcag2aa'` — WCAG 2.0
|
|
193
|
+
- `'wcag21a'` / `'wcag21aa'` — WCAG 2.1
|
|
194
|
+
- `'wcag22a'` / `'wcag22aa'` — WCAG 2.2 (default)
|
|
195
|
+
|
|
196
|
+
Or override per-command:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
cy.a11yCheckFocusAndReport({ wcagLevel: 'wcag22a' });
|
|
200
|
+
cy.a11yCheckFocusAndReport({ disabledRules: ['oracle/focus-low-contrast'] });
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Set `Cypress.env('failOnErrors')` to `true` to fail the test immediately when issues are found:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
// cypress.config.ts
|
|
207
|
+
export default defineConfig({
|
|
208
|
+
e2e: {
|
|
209
|
+
env: { failOnErrors: true },
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
For detailed remediation guidance on each rule, see the [Remediation Guide](../../docs/REMEDIATION.md).
|
|
215
|
+
|
|
216
|
+
### Node-Side Reporting Setup
|
|
217
|
+
|
|
218
|
+
To accumulate issues across all specs and write a JSON report file, call `setupOracleReporting()` in your Cypress config:
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
// cypress.config.ts
|
|
222
|
+
import { defineConfig } from 'cypress';
|
|
223
|
+
import { setupOracleReporting } from '@a11y-oracle/cypress-plugin';
|
|
224
|
+
|
|
225
|
+
export default defineConfig({
|
|
226
|
+
e2e: {
|
|
227
|
+
setupNodeEvents(on, config) {
|
|
228
|
+
setupOracleReporting(on, config);
|
|
229
|
+
},
|
|
230
|
+
env: { projectName: 'my-app' },
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
This registers the `logOracleIssues` task and writes `oracle-results-{projectName}.json` after the run completes.
|
|
236
|
+
|
|
237
|
+
**Combining with axe-core violations:** If you want oracle issues in the same array as your axe-core violations (for a single upload to Beacon), add the task handler manually:
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
setupNodeEvents(on, config) {
|
|
241
|
+
const allViolations: any[] = [];
|
|
242
|
+
|
|
243
|
+
on('task', {
|
|
244
|
+
logAxeViolations(violations) { allViolations.push(...violations); return null; },
|
|
245
|
+
logOracleIssues(issues) { allViolations.push(...issues); return null; },
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
on('after:run', () => {
|
|
249
|
+
if (allViolations.length > 0) {
|
|
250
|
+
fs.writeFileSync('a11y-results.json', JSON.stringify(allViolations, null, 2));
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
},
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Asserting on Landmarks and Structure
|
|
257
|
+
|
|
258
|
+
Use `getA11yFullTreeSpeech()` to inspect elements that don't have focus:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
it('navigation landmark exists', () => {
|
|
262
|
+
cy.getA11yFullTreeSpeech().then((tree) => {
|
|
263
|
+
const nav = tree.find(r => r.speech.includes('navigation landmark'));
|
|
264
|
+
expect(nav).to.exist;
|
|
265
|
+
expect(nav.speech).to.contain('Main');
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Structured Speech Results
|
|
271
|
+
|
|
272
|
+
Use `getA11ySpeechResult()` to access individual parts of the speech output:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
it('returns structured data', () => {
|
|
276
|
+
cy.a11yPress('Tab');
|
|
277
|
+
cy.getA11ySpeechResult().then((result) => {
|
|
278
|
+
expect(result).to.not.be.null;
|
|
279
|
+
expect(result.name).to.equal('Home');
|
|
280
|
+
expect(result.role).to.contain('menu item');
|
|
281
|
+
expect(result.states).to.be.an('array');
|
|
282
|
+
expect(result.rawNode).to.exist;
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Configuration Options
|
|
288
|
+
|
|
289
|
+
Pass options to `initA11yOracle()` to customize behavior:
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
// Include descriptions from aria-describedby
|
|
293
|
+
cy.initA11yOracle({ includeDescription: true });
|
|
294
|
+
|
|
295
|
+
// Disable "landmark" suffix on landmark roles
|
|
296
|
+
cy.initA11yOracle({ includeLandmarks: false });
|
|
297
|
+
|
|
298
|
+
// Adjust focus settle delay for slow CSS transitions
|
|
299
|
+
cy.initA11yOracle({ focusSettleMs: 100 });
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
| Option | Type | Default | Description |
|
|
303
|
+
|--------|------|---------|-------------|
|
|
304
|
+
| `includeLandmarks` | `boolean` | `true` | Append "landmark" to landmark roles |
|
|
305
|
+
| `includeDescription` | `boolean` | `false` | Include `aria-describedby` text in output |
|
|
306
|
+
| `focusSettleMs` | `number` | `50` | Delay (ms) after key press for focus/CSS to settle |
|
|
307
|
+
|
|
308
|
+
## API Reference
|
|
309
|
+
|
|
310
|
+
### Speech Commands
|
|
311
|
+
|
|
312
|
+
#### `cy.initA11yOracle(options?)`
|
|
313
|
+
|
|
314
|
+
Initialize the plugin. Must be called after `cy.visit()` and before other A11y-Oracle commands. Typically called in `beforeEach()`.
|
|
315
|
+
|
|
316
|
+
Enables CDP domains, discovers the AUT iframe, creates the speech engine and orchestrator.
|
|
317
|
+
|
|
318
|
+
#### `cy.a11yPress(key)`
|
|
319
|
+
|
|
320
|
+
Press a keyboard key via CDP and return the speech for the newly focused element. Yields a string.
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
cy.a11yPress('Tab').should('contain', 'button');
|
|
324
|
+
cy.a11yPress('Enter').should('contain', 'expanded');
|
|
325
|
+
cy.a11yPress('Escape').should('contain', 'collapsed');
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Supported keys: `Tab`, `Enter`, `Space`, `Escape`, `ArrowUp`, `ArrowDown`, `ArrowLeft`, `ArrowRight`, `Home`, `End`, `Backspace`, `Delete`.
|
|
329
|
+
|
|
330
|
+
#### `cy.getA11ySpeech()`
|
|
331
|
+
|
|
332
|
+
Get the speech string for the currently focused element without pressing a key. Yields a string.
|
|
333
|
+
|
|
334
|
+
#### `cy.getA11ySpeechResult()`
|
|
335
|
+
|
|
336
|
+
Get the full structured result for the focused element. Yields `SpeechResult | null`.
|
|
337
|
+
|
|
338
|
+
#### `cy.getA11yFullTreeSpeech()`
|
|
339
|
+
|
|
340
|
+
Get speech for all non-ignored nodes in the accessibility tree. Yields `SpeechResult[]`.
|
|
341
|
+
|
|
342
|
+
### Unified State Commands
|
|
343
|
+
|
|
344
|
+
#### `cy.a11yPressKey(key, modifiers?)`
|
|
345
|
+
|
|
346
|
+
Press a key via native CDP dispatch and return the unified accessibility state. Yields `A11yState`.
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
cy.a11yPressKey('Tab').then((state) => {
|
|
350
|
+
expect(state.speech).to.contain('button');
|
|
351
|
+
expect(state.focusIndicator.meetsWCAG_AA).to.be.true;
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// With modifier keys
|
|
355
|
+
cy.a11yPressKey('Tab', { shift: true }).then((state) => {
|
|
356
|
+
expect(state.focusedElement).to.not.be.null;
|
|
357
|
+
});
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
#### `cy.a11yState()`
|
|
361
|
+
|
|
362
|
+
Get the current unified accessibility state without pressing a key. Yields `A11yState`.
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
cy.get('#my-button').focus();
|
|
366
|
+
cy.a11yState().then((state) => {
|
|
367
|
+
expect(state.speech).to.contain('Submit');
|
|
368
|
+
});
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
#### `cy.a11yTraverseTabOrder()`
|
|
372
|
+
|
|
373
|
+
Extract all tabbable elements in DOM tab order. Yields `TabOrderReport`.
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
cy.a11yTraverseTabOrder().then((report) => {
|
|
377
|
+
expect(report.totalCount).to.be.greaterThan(0);
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
#### `cy.a11yTraverseSubTree(selector, maxTabs?)`
|
|
382
|
+
|
|
383
|
+
Detect whether a container traps keyboard focus (WCAG 2.1.2). Yields `TraversalResult`.
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
cy.a11yTraverseSubTree('#modal', 20).then((result) => {
|
|
387
|
+
expect(result.isTrapped).to.be.false;
|
|
388
|
+
});
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Reporting Commands
|
|
392
|
+
|
|
393
|
+
#### `cy.a11yCheckFocusAndReport(context?)`
|
|
394
|
+
|
|
395
|
+
Check the current focused element and report any issues via `cy.task('logOracleIssues')`. Runs all state-based rules: `oracle/focus-not-visible`, `oracle/focus-low-contrast`, `oracle/focus-missing-name`, `oracle/focus-generic-role`, and `oracle/positive-tabindex`.
|
|
396
|
+
|
|
397
|
+
- `context` — Optional `Partial<AuditContext>`. Defaults to `{ project: Cypress.env('projectName'), specName: Cypress.spec.name, wcagLevel: Cypress.env('wcagLevel'), disabledRules: Cypress.env('disabledRules') }`.
|
|
398
|
+
|
|
399
|
+
#### `cy.a11yCheckTrapAndReport(selector, maxTabs?, context?)`
|
|
400
|
+
|
|
401
|
+
Check a container for keyboard traps and report any issues via `cy.task('logOracleIssues')`.
|
|
402
|
+
|
|
403
|
+
- `selector` — CSS selector for the container to test.
|
|
404
|
+
- `maxTabs` — Maximum Tab presses before declaring a trap. Default `50`.
|
|
405
|
+
- `context` — Optional `Partial<AuditContext>`.
|
|
406
|
+
|
|
407
|
+
#### `setupOracleReporting(on, config)`
|
|
408
|
+
|
|
409
|
+
Node-side function (not a Cypress command). Call inside `setupNodeEvents()` to register the `logOracleIssues` task and write a JSON report in `after:run`. See [Node-Side Reporting Setup](#node-side-reporting-setup).
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
import { setupOracleReporting } from '@a11y-oracle/cypress-plugin';
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Lifecycle
|
|
416
|
+
|
|
417
|
+
#### `cy.disposeA11yOracle()`
|
|
418
|
+
|
|
419
|
+
Dispose the plugin and release CDP resources. Typically called in `afterEach()`.
|
|
420
|
+
|
|
421
|
+
### Types
|
|
422
|
+
|
|
423
|
+
Types are re-exported from `@a11y-oracle/core-engine` and `@a11y-oracle/audit-formatter`:
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
import type {
|
|
427
|
+
// Core engine types
|
|
428
|
+
SpeechResult,
|
|
429
|
+
A11yState,
|
|
430
|
+
A11yFocusedElement,
|
|
431
|
+
A11yFocusIndicator,
|
|
432
|
+
A11yOrchestratorOptions,
|
|
433
|
+
ModifierKeys,
|
|
434
|
+
TabOrderReport,
|
|
435
|
+
TabOrderEntry,
|
|
436
|
+
TraversalResult,
|
|
437
|
+
FocusIndicator,
|
|
438
|
+
// Audit formatter types
|
|
439
|
+
OracleIssue,
|
|
440
|
+
OracleNode,
|
|
441
|
+
OracleCheck,
|
|
442
|
+
OracleImpact,
|
|
443
|
+
OracleResultType,
|
|
444
|
+
AuditContext,
|
|
445
|
+
} from '@a11y-oracle/cypress-plugin';
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
## How It Works
|
|
449
|
+
|
|
450
|
+
The Cypress plugin uses a browser-side CDP approach through `Cypress.automation('remote:debugger:protocol')` — the same pattern used by [cypress-real-events](https://github.com/dmtrKovalenko/cypress-real-events). No Node-side tasks or external libraries are required.
|
|
451
|
+
|
|
452
|
+
### Cypress Iframe Architecture
|
|
453
|
+
|
|
454
|
+
Cypress runs the app under test (AUT) inside an iframe within its runner page. This creates a challenge: CDP commands target the runner page by default, not the AUT. The plugin handles this transparently:
|
|
455
|
+
|
|
456
|
+
1. **Frame discovery** — On `initA11yOracle()`, the plugin calls `Page.getFrameTree()` to enumerate all frames. It identifies the AUT frame by filtering out Cypress internal frames (`/__/`, `__cypress`, `about:blank`).
|
|
457
|
+
|
|
458
|
+
2. **Frame-scoped accessibility queries** — A CDP adapter injects the AUT `frameId` into every `Accessibility.getFullAXTree()` call, ensuring the accessibility tree is scoped to the app, not the runner UI.
|
|
459
|
+
|
|
460
|
+
3. **Isolated execution context** — For `Runtime.evaluate` calls (focus indicator analysis, tab order, trap detection), the plugin creates an isolated world in the AUT frame via `Page.createIsolatedWorld`. This isolated world shares the same DOM (including `document.activeElement`, computed styles, etc.) but has its own JavaScript scope, ensuring evaluations target the AUT content.
|
|
461
|
+
|
|
462
|
+
4. **Focus management** — Before each keyboard event, the plugin uses `DOM.focus()` on the AUT iframe element so that `Input.dispatchKeyEvent` reaches the correct frame.
|
|
463
|
+
|
|
464
|
+
### CDP Flow
|
|
465
|
+
|
|
466
|
+
```
|
|
467
|
+
cy.a11yPressKey('Tab')
|
|
468
|
+
│
|
|
469
|
+
├─ DOM.focus() on AUT iframe
|
|
470
|
+
├─ Input.dispatchKeyEvent (keyDown + keyUp)
|
|
471
|
+
├─ 50ms delay for focus/ARIA state updates
|
|
472
|
+
├─ Accessibility.getFullAXTree({ frameId: autFrameId })
|
|
473
|
+
├─ Runtime.evaluate({ contextId: autContextId }) — focused element
|
|
474
|
+
├─ Runtime.evaluate({ contextId: autContextId }) — focus indicator CSS
|
|
475
|
+
├─ Find focused node in AXTree → speech string
|
|
476
|
+
└─ Assemble A11yState { speech, focusedElement, focusIndicator }
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
## TypeScript
|
|
480
|
+
|
|
481
|
+
The plugin augments the `Cypress.Chainable` interface. TypeScript will pick up the command types automatically when you import the plugin in your support file.
|
|
482
|
+
|
|
483
|
+
If you need types in your test files without importing the support file:
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
/// <reference types="@a11y-oracle/cypress-plugin" />
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
## Troubleshooting
|
|
490
|
+
|
|
491
|
+
### "Could not find the AUT iframe"
|
|
492
|
+
|
|
493
|
+
This error means `initA11yOracle()` was called before `cy.visit()`. The AUT iframe doesn't exist until Cypress loads a page.
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
// Wrong
|
|
497
|
+
cy.initA11yOracle();
|
|
498
|
+
cy.visit('/page.html');
|
|
499
|
+
|
|
500
|
+
// Correct
|
|
501
|
+
cy.visit('/page.html');
|
|
502
|
+
cy.initA11yOracle();
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Speech output contains Cypress UI elements
|
|
506
|
+
|
|
507
|
+
If assertions match Cypress runner elements (like "Stop, button" or "Options, button"), the frame-scoping may have failed. Ensure:
|
|
508
|
+
|
|
509
|
+
- You're running in Chrome (`--browser chrome`)
|
|
510
|
+
- `cy.visit()` completed before `cy.initA11yOracle()`
|
|
511
|
+
- The AUT URL is not `about:blank`
|
|
512
|
+
|
|
513
|
+
### Empty speech string
|
|
514
|
+
|
|
515
|
+
`a11yPress()` returns an empty string when no element has focus after the key press. This can happen if:
|
|
516
|
+
|
|
517
|
+
- The key press moved focus outside the page (e.g., Tab past the last element)
|
|
518
|
+
- The focused element has `role="presentation"` or `role="none"`
|
|
519
|
+
- The focused element is the `RootWebArea` (document body)
|
|
520
|
+
|
|
521
|
+
### Focus indicator shows `contrastRatio: null`
|
|
522
|
+
|
|
523
|
+
This happens when the focus indicator color cannot be reliably parsed. Common causes:
|
|
524
|
+
|
|
525
|
+
- Complex CSS color functions (`color-mix()`, `hsl()`, named colors)
|
|
526
|
+
- Multi-layer gradients as focus indicators
|
|
527
|
+
- `currentColor` as outline color
|
|
528
|
+
|
|
529
|
+
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,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @a11y-oracle/cypress-plugin
|
|
3
|
+
*
|
|
4
|
+
* Cypress plugin for A11y-Oracle accessibility speech testing.
|
|
5
|
+
*
|
|
6
|
+
* Import this module in your Cypress support file to register
|
|
7
|
+
* custom commands that read the browser's accessibility tree
|
|
8
|
+
* and produce standardized speech output.
|
|
9
|
+
*
|
|
10
|
+
* ## Setup
|
|
11
|
+
*
|
|
12
|
+
* ```typescript
|
|
13
|
+
* // cypress/support/e2e.ts
|
|
14
|
+
* import '@a11y-oracle/cypress-plugin';
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* ## Usage
|
|
18
|
+
*
|
|
19
|
+
* ```typescript
|
|
20
|
+
* describe('Navigation', () => {
|
|
21
|
+
* beforeEach(() => {
|
|
22
|
+
* cy.visit('/');
|
|
23
|
+
* cy.initA11yOracle();
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* afterEach(() => {
|
|
27
|
+
* cy.disposeA11yOracle();
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* it('Tab announces first menu item', () => {
|
|
31
|
+
* cy.a11yPress('Tab').should('contain', 'Home');
|
|
32
|
+
* });
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* ## Requirements
|
|
37
|
+
*
|
|
38
|
+
* - Chromium-based browser (Chrome, Edge, Electron)
|
|
39
|
+
* - Cypress >= 12.0.0
|
|
40
|
+
*/
|
|
41
|
+
import './lib/commands.js';
|
|
42
|
+
export type { SpeechResult, SpeechEngineOptions, A11yState, A11yFocusedElement, A11yFocusIndicator, A11yOrchestratorOptions, ModifierKeys, TabOrderReport, TabOrderEntry, TraversalResult, FocusIndicator, } from '@a11y-oracle/core-engine';
|
|
43
|
+
export type { OracleIssue, OracleNode, OracleCheck, OracleImpact, OracleResultType, AuditContext, } from '@a11y-oracle/audit-formatter';
|
|
44
|
+
export { setupOracleReporting } from './lib/reporting.js';
|
|
45
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAGH,OAAO,mBAAmB,CAAC;AAG3B,YAAY,EACV,YAAY,EACZ,mBAAmB,EACnB,SAAS,EACT,kBAAkB,EAClB,kBAAkB,EAClB,uBAAuB,EACvB,YAAY,EACZ,cAAc,EACd,aAAa,EACb,eAAe,EACf,cAAc,GACf,MAAM,0BAA0B,CAAC;AAGlC,YAAY,EACV,WAAW,EACX,UAAU,EACV,WAAW,EACX,YAAY,EACZ,gBAAgB,EAChB,YAAY,GACb,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @a11y-oracle/cypress-plugin
|
|
3
|
+
*
|
|
4
|
+
* Cypress plugin for A11y-Oracle accessibility speech testing.
|
|
5
|
+
*
|
|
6
|
+
* Import this module in your Cypress support file to register
|
|
7
|
+
* custom commands that read the browser's accessibility tree
|
|
8
|
+
* and produce standardized speech output.
|
|
9
|
+
*
|
|
10
|
+
* ## Setup
|
|
11
|
+
*
|
|
12
|
+
* ```typescript
|
|
13
|
+
* // cypress/support/e2e.ts
|
|
14
|
+
* import '@a11y-oracle/cypress-plugin';
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* ## Usage
|
|
18
|
+
*
|
|
19
|
+
* ```typescript
|
|
20
|
+
* describe('Navigation', () => {
|
|
21
|
+
* beforeEach(() => {
|
|
22
|
+
* cy.visit('/');
|
|
23
|
+
* cy.initA11yOracle();
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* afterEach(() => {
|
|
27
|
+
* cy.disposeA11yOracle();
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* it('Tab announces first menu item', () => {
|
|
31
|
+
* cy.a11yPress('Tab').should('contain', 'Home');
|
|
32
|
+
* });
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* ## Requirements
|
|
37
|
+
*
|
|
38
|
+
* - Chromium-based browser (Chrome, Edge, Electron)
|
|
39
|
+
* - Cypress >= 12.0.0
|
|
40
|
+
*/
|
|
41
|
+
// Side-effect import: registers all custom Cypress commands
|
|
42
|
+
import './lib/commands.js';
|
|
43
|
+
// Node-side reporting setup
|
|
44
|
+
export { setupOracleReporting } from './lib/reporting.js';
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module commands
|
|
3
|
+
*
|
|
4
|
+
* Custom Cypress commands for accessibility speech testing and
|
|
5
|
+
* keyboard/focus analysis.
|
|
6
|
+
*
|
|
7
|
+
* Uses `Cypress.automation('remote:debugger:protocol')` to communicate
|
|
8
|
+
* with Chrome DevTools Protocol directly through Cypress's own CDP
|
|
9
|
+
* connection — no external libraries or Node-side tasks required.
|
|
10
|
+
*
|
|
11
|
+
* This is the same proven pattern used by cypress-real-events.
|
|
12
|
+
*
|
|
13
|
+
* ## Cypress iframe architecture
|
|
14
|
+
*
|
|
15
|
+
* Cypress runs the AUT (app under test) inside an iframe within
|
|
16
|
+
* its runner page. This module handles:
|
|
17
|
+
* - Scoping accessibility tree queries to the AUT frame via `frameId`
|
|
18
|
+
* - Executing `Runtime.evaluate` in the AUT frame via `contextId`
|
|
19
|
+
* - Focusing the AUT iframe before dispatching keyboard events
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* // In your test:
|
|
24
|
+
* cy.initA11yOracle();
|
|
25
|
+
* cy.a11yPress('Tab').should('contain', 'Home');
|
|
26
|
+
* cy.a11yPressKey('Tab').then(state => {
|
|
27
|
+
* expect(state.focusIndicator.meetsWCAG_AA).to.be.true;
|
|
28
|
+
* });
|
|
29
|
+
* cy.disposeA11yOracle();
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
import type { SpeechResult, A11yState, A11yOrchestratorOptions, TabOrderReport, TraversalResult, ModifierKeys } from '@a11y-oracle/core-engine';
|
|
33
|
+
import type { AuditContext } from '@a11y-oracle/audit-formatter';
|
|
34
|
+
declare global {
|
|
35
|
+
namespace Cypress {
|
|
36
|
+
interface Chainable {
|
|
37
|
+
/**
|
|
38
|
+
* Initialize A11y-Oracle. Must be called before other a11y commands.
|
|
39
|
+
* Typically called in `beforeEach()`.
|
|
40
|
+
*/
|
|
41
|
+
initA11yOracle(options?: A11yOrchestratorOptions): Chainable<null>;
|
|
42
|
+
/**
|
|
43
|
+
* Press a keyboard key via CDP and return the speech for the
|
|
44
|
+
* newly focused element.
|
|
45
|
+
*
|
|
46
|
+
* @param key - Key name (e.g. `'Tab'`, `'Enter'`, `'ArrowDown'`).
|
|
47
|
+
*/
|
|
48
|
+
a11yPress(key: string): Chainable<string>;
|
|
49
|
+
/** Get the speech string for the currently focused element. */
|
|
50
|
+
getA11ySpeech(): Chainable<string>;
|
|
51
|
+
/** Get the full {@link SpeechResult} for the currently focused element. */
|
|
52
|
+
getA11ySpeechResult(): Chainable<SpeechResult | null>;
|
|
53
|
+
/** Get speech output for every visible node in the accessibility tree. */
|
|
54
|
+
getA11yFullTreeSpeech(): Chainable<SpeechResult[]>;
|
|
55
|
+
/**
|
|
56
|
+
* Press a key via CDP and return the unified accessibility state.
|
|
57
|
+
*
|
|
58
|
+
* @param key - Key name (e.g. `'Tab'`, `'Enter'`, `'ArrowDown'`).
|
|
59
|
+
* @param modifiers - Optional modifier keys.
|
|
60
|
+
*/
|
|
61
|
+
a11yPressKey(key: string, modifiers?: ModifierKeys): Chainable<A11yState>;
|
|
62
|
+
/** Get the current unified accessibility state without pressing a key. */
|
|
63
|
+
a11yState(): Chainable<A11yState>;
|
|
64
|
+
/** Extract all tabbable elements in DOM tab order. */
|
|
65
|
+
a11yTraverseTabOrder(): Chainable<TabOrderReport>;
|
|
66
|
+
/**
|
|
67
|
+
* Detect whether a container traps keyboard focus (WCAG 2.1.2).
|
|
68
|
+
*
|
|
69
|
+
* @param selector - CSS selector for the container to test.
|
|
70
|
+
* @param maxTabs - Maximum Tab presses before declaring a trap. Default 50.
|
|
71
|
+
*/
|
|
72
|
+
a11yTraverseSubTree(selector: string, maxTabs?: number): Chainable<TraversalResult>;
|
|
73
|
+
/**
|
|
74
|
+
* Check the current focused element's focus indicator and report
|
|
75
|
+
* any issues via `cy.task('logOracleIssues')`.
|
|
76
|
+
*
|
|
77
|
+
* Mirrors the pattern of `checkAccessibilityAndReport` for axe-core.
|
|
78
|
+
* Issues are emitted as `OracleIssue[]` and can be accumulated in
|
|
79
|
+
* `setupNodeEvents` for end-of-run reporting.
|
|
80
|
+
*
|
|
81
|
+
* @param context - Optional audit context. Defaults to current spec name and project from env.
|
|
82
|
+
*/
|
|
83
|
+
a11yCheckFocusAndReport(context?: Partial<AuditContext>): Chainable<void>;
|
|
84
|
+
/**
|
|
85
|
+
* Check a container for keyboard traps and report any issues via
|
|
86
|
+
* `cy.task('logOracleIssues')`.
|
|
87
|
+
*
|
|
88
|
+
* @param selector - CSS selector for the container to test.
|
|
89
|
+
* @param maxTabs - Maximum Tab presses before declaring a trap. Default 50.
|
|
90
|
+
* @param context - Optional audit context.
|
|
91
|
+
*/
|
|
92
|
+
a11yCheckTrapAndReport(selector: string, maxTabs?: number, context?: Partial<AuditContext>): Chainable<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Dispose A11y-Oracle and release resources.
|
|
95
|
+
* Typically called in `afterEach()`.
|
|
96
|
+
*/
|
|
97
|
+
disposeA11yOracle(): Chainable<null>;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=commands.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/lib/commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,KAAK,EAEV,YAAY,EACZ,SAAS,EACT,uBAAuB,EACvB,cAAc,EACd,eAAe,EACf,YAAY,EACb,MAAM,0BAA0B,CAAC;AAMlC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAIjE,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,SAAS;YACjB;;;eAGG;YACH,cAAc,CAAC,OAAO,CAAC,EAAE,uBAAuB,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;YAEnE;;;;;eAKG;YACH,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;YAE1C,+DAA+D;YAC/D,aAAa,IAAI,SAAS,CAAC,MAAM,CAAC,CAAC;YAEnC,2EAA2E;YAC3E,mBAAmB,IAAI,SAAS,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;YAEtD,0EAA0E;YAC1E,qBAAqB,IAAI,SAAS,CAAC,YAAY,EAAE,CAAC,CAAC;YAEnD;;;;;eAKG;YACH,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC,SAAS,CAAC,CAAC;YAE1E,0EAA0E;YAC1E,SAAS,IAAI,SAAS,CAAC,SAAS,CAAC,CAAC;YAElC,sDAAsD;YACtD,oBAAoB,IAAI,SAAS,CAAC,cAAc,CAAC,CAAC;YAElD;;;;;eAKG;YACH,mBAAmB,CACjB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,GACf,SAAS,CAAC,eAAe,CAAC,CAAC;YAE9B;;;;;;;;;eASG;YACH,uBAAuB,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;YAE1E;;;;;;;eAOG;YACH,sBAAsB,CACpB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAC9B,SAAS,CAAC,IAAI,CAAC,CAAC;YAEnB;;;eAGG;YACH,iBAAiB,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC;SACtC;KACF;CACF"}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module commands
|
|
3
|
+
*
|
|
4
|
+
* Custom Cypress commands for accessibility speech testing and
|
|
5
|
+
* keyboard/focus analysis.
|
|
6
|
+
*
|
|
7
|
+
* Uses `Cypress.automation('remote:debugger:protocol')` to communicate
|
|
8
|
+
* with Chrome DevTools Protocol directly through Cypress's own CDP
|
|
9
|
+
* connection — no external libraries or Node-side tasks required.
|
|
10
|
+
*
|
|
11
|
+
* This is the same proven pattern used by cypress-real-events.
|
|
12
|
+
*
|
|
13
|
+
* ## Cypress iframe architecture
|
|
14
|
+
*
|
|
15
|
+
* Cypress runs the AUT (app under test) inside an iframe within
|
|
16
|
+
* its runner page. This module handles:
|
|
17
|
+
* - Scoping accessibility tree queries to the AUT frame via `frameId`
|
|
18
|
+
* - Executing `Runtime.evaluate` in the AUT frame via `contextId`
|
|
19
|
+
* - Focusing the AUT iframe before dispatching keyboard events
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* // In your test:
|
|
24
|
+
* cy.initA11yOracle();
|
|
25
|
+
* cy.a11yPress('Tab').should('contain', 'Home');
|
|
26
|
+
* cy.a11yPressKey('Tab').then(state => {
|
|
27
|
+
* expect(state.focusIndicator.meetsWCAG_AA).to.be.true;
|
|
28
|
+
* });
|
|
29
|
+
* cy.disposeA11yOracle();
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
import { SpeechEngine, A11yOrchestrator } from '@a11y-oracle/core-engine';
|
|
33
|
+
import { KEY_DEFINITIONS } from '@a11y-oracle/keyboard-engine';
|
|
34
|
+
import { formatAllIssues, formatTrapIssue, } from '@a11y-oracle/audit-formatter';
|
|
35
|
+
// ── Internals ──────────────────────────────────────────────────────
|
|
36
|
+
let engine = null;
|
|
37
|
+
let orchestrator = null;
|
|
38
|
+
let autFrameId = null;
|
|
39
|
+
let autContextId = null;
|
|
40
|
+
/**
|
|
41
|
+
* Send a raw CDP command through Cypress's automation channel.
|
|
42
|
+
*/
|
|
43
|
+
function sendCDP(command, params = {}) {
|
|
44
|
+
return Cypress.automation('remote:debugger:protocol', {
|
|
45
|
+
command,
|
|
46
|
+
params,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Create a {@link CDPSessionLike} adapter that routes CDP calls through
|
|
51
|
+
* Cypress's built-in `remote:debugger:protocol` automation channel.
|
|
52
|
+
*
|
|
53
|
+
* Automatically injects:
|
|
54
|
+
* - The AUT iframe's `frameId` into `Accessibility.getFullAXTree` calls
|
|
55
|
+
* - The AUT iframe's `contextId` into `Runtime.evaluate` calls
|
|
56
|
+
*
|
|
57
|
+
* This ensures all queries target the AUT content, not the Cypress
|
|
58
|
+
* runner UI.
|
|
59
|
+
*/
|
|
60
|
+
function createFrameAwareCDPAdapter() {
|
|
61
|
+
return {
|
|
62
|
+
send: (method, params) => {
|
|
63
|
+
const p = { ...params };
|
|
64
|
+
// Scope AXTree to AUT frame
|
|
65
|
+
if (method === 'Accessibility.getFullAXTree' && autFrameId) {
|
|
66
|
+
p['frameId'] = autFrameId;
|
|
67
|
+
}
|
|
68
|
+
// Scope Runtime.evaluate to AUT frame's execution context
|
|
69
|
+
if (method === 'Runtime.evaluate' && autContextId !== null) {
|
|
70
|
+
p['contextId'] = autContextId;
|
|
71
|
+
}
|
|
72
|
+
return sendCDP(method, p);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Discover the AUT (app under test) iframe's frame ID
|
|
78
|
+
* from the Cypress runner page's frame tree.
|
|
79
|
+
*
|
|
80
|
+
* The AUT frame is identified by having a URL that doesn't
|
|
81
|
+
* contain `/__/` (runner), `__cypress` (spec iframe), or
|
|
82
|
+
* `about:blank` (snapshot frames).
|
|
83
|
+
*/
|
|
84
|
+
async function findAUTFrameId() {
|
|
85
|
+
const result = await sendCDP('Page.getFrameTree');
|
|
86
|
+
const childFrames = result.frameTree.childFrames || [];
|
|
87
|
+
for (const child of childFrames) {
|
|
88
|
+
const url = child.frame.url || '';
|
|
89
|
+
if (url &&
|
|
90
|
+
!url.includes('/__/') &&
|
|
91
|
+
!url.includes('__cypress') &&
|
|
92
|
+
url !== 'about:blank') {
|
|
93
|
+
return child.frame.id;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Fallback: first child frame with a non-blank URL
|
|
97
|
+
for (const child of childFrames) {
|
|
98
|
+
if (child.frame.url && child.frame.url !== 'about:blank') {
|
|
99
|
+
return child.frame.id;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Discover the execution context ID for the AUT frame.
|
|
106
|
+
*
|
|
107
|
+
* Creates an isolated world in the AUT frame, which shares the
|
|
108
|
+
* same DOM (including `document.activeElement`, computed styles, etc.)
|
|
109
|
+
* but has its own JavaScript scope. This ensures `Runtime.evaluate`
|
|
110
|
+
* calls execute in the AUT, not the Cypress runner.
|
|
111
|
+
*/
|
|
112
|
+
async function findAUTContextId(frameId) {
|
|
113
|
+
try {
|
|
114
|
+
const result = await sendCDP('Page.createIsolatedWorld', {
|
|
115
|
+
frameId,
|
|
116
|
+
worldName: 'a11y-oracle',
|
|
117
|
+
grantUniversalAccess: true,
|
|
118
|
+
});
|
|
119
|
+
return result.executionContextId;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Focus the AUT iframe element so that CDP keyboard events
|
|
127
|
+
* reach the AUT's content instead of the Cypress runner UI.
|
|
128
|
+
*/
|
|
129
|
+
async function focusAUTFrame() {
|
|
130
|
+
try {
|
|
131
|
+
// Get the runner page's document
|
|
132
|
+
const doc = await sendCDP('DOM.getDocument', { depth: 0 });
|
|
133
|
+
if (!doc?.root?.nodeId)
|
|
134
|
+
return;
|
|
135
|
+
// Find all iframes and focus the AUT one
|
|
136
|
+
const iframes = await sendCDP('DOM.querySelectorAll', {
|
|
137
|
+
nodeId: doc.root.nodeId,
|
|
138
|
+
selector: 'iframe',
|
|
139
|
+
});
|
|
140
|
+
if (!iframes?.nodeIds)
|
|
141
|
+
return;
|
|
142
|
+
for (const nodeId of iframes.nodeIds) {
|
|
143
|
+
try {
|
|
144
|
+
const attrs = await sendCDP('DOM.getAttributes', { nodeId });
|
|
145
|
+
const attrList = attrs.attributes;
|
|
146
|
+
// Find the iframe whose src matches the AUT URL
|
|
147
|
+
const srcIndex = attrList.indexOf('src');
|
|
148
|
+
if (srcIndex >= 0) {
|
|
149
|
+
const src = attrList[srcIndex + 1];
|
|
150
|
+
if (src &&
|
|
151
|
+
!src.includes('/__/') &&
|
|
152
|
+
!src.includes('__cypress') &&
|
|
153
|
+
src !== 'about:blank') {
|
|
154
|
+
await sendCDP('DOM.focus', { nodeId });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Also check by name attribute (Cypress names AUT frames)
|
|
159
|
+
const nameIndex = attrList.indexOf('name');
|
|
160
|
+
if (nameIndex >= 0) {
|
|
161
|
+
const name = attrList[nameIndex + 1];
|
|
162
|
+
if (name && name.startsWith('Your project:')) {
|
|
163
|
+
await sendCDP('DOM.focus', { nodeId });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// Skip iframes we can't inspect
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// DOM query failed — frame focus is best-effort
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Dispatch a real keyboard event (keyDown + keyUp) via CDP.
|
|
179
|
+
*/
|
|
180
|
+
async function dispatchKey(key) {
|
|
181
|
+
const keyDef = KEY_DEFINITIONS[key];
|
|
182
|
+
if (!keyDef) {
|
|
183
|
+
const supported = Object.keys(KEY_DEFINITIONS).join(', ');
|
|
184
|
+
throw new Error(`Unknown key: "${key}". Supported keys: ${supported}`);
|
|
185
|
+
}
|
|
186
|
+
// Ensure the AUT iframe has focus before each key dispatch
|
|
187
|
+
await focusAUTFrame();
|
|
188
|
+
await sendCDP('Input.dispatchKeyEvent', {
|
|
189
|
+
type: 'keyDown',
|
|
190
|
+
key: keyDef.key,
|
|
191
|
+
code: keyDef.code,
|
|
192
|
+
windowsVirtualKeyCode: keyDef.keyCode,
|
|
193
|
+
nativeVirtualKeyCode: keyDef.keyCode,
|
|
194
|
+
});
|
|
195
|
+
await sendCDP('Input.dispatchKeyEvent', {
|
|
196
|
+
type: 'keyUp',
|
|
197
|
+
key: keyDef.key,
|
|
198
|
+
code: keyDef.code,
|
|
199
|
+
windowsVirtualKeyCode: keyDef.keyCode,
|
|
200
|
+
nativeVirtualKeyCode: keyDef.keyCode,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
// ── Commands ───────────────────────────────────────────────────────
|
|
204
|
+
Cypress.Commands.add('initA11yOracle', (options) => {
|
|
205
|
+
cy.wrap(null, { log: false }).then(async () => {
|
|
206
|
+
// Enable required CDP domains
|
|
207
|
+
await sendCDP('DOM.enable');
|
|
208
|
+
await sendCDP('Page.enable');
|
|
209
|
+
// Discover the AUT frame
|
|
210
|
+
autFrameId = await findAUTFrameId();
|
|
211
|
+
if (!autFrameId) {
|
|
212
|
+
throw new Error('A11y-Oracle: Could not find the AUT iframe. ' +
|
|
213
|
+
'Ensure cy.visit() was called before cy.initA11yOracle().');
|
|
214
|
+
}
|
|
215
|
+
// Get the AUT frame's execution context for Runtime.evaluate
|
|
216
|
+
autContextId = await findAUTContextId(autFrameId);
|
|
217
|
+
// Focus the AUT iframe so key events reach it
|
|
218
|
+
await focusAUTFrame();
|
|
219
|
+
// Create the CDP adapter that routes to the AUT frame
|
|
220
|
+
const adapter = createFrameAwareCDPAdapter();
|
|
221
|
+
// Create the speech engine scoped to the AUT frame
|
|
222
|
+
engine = new SpeechEngine(adapter, options);
|
|
223
|
+
await engine.enable();
|
|
224
|
+
// Create the orchestrator for unified state
|
|
225
|
+
orchestrator = new A11yOrchestrator(adapter, options);
|
|
226
|
+
await orchestrator.enable();
|
|
227
|
+
return null;
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
Cypress.Commands.add('a11yPress', (key) => {
|
|
231
|
+
cy.wrap(null, { log: false }).then(async () => {
|
|
232
|
+
if (!engine) {
|
|
233
|
+
throw new Error('A11y-Oracle not initialized. Call cy.initA11yOracle() first.');
|
|
234
|
+
}
|
|
235
|
+
await dispatchKey(key);
|
|
236
|
+
// Allow browser to update focus and ARIA states
|
|
237
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
238
|
+
const result = await engine.getSpeech();
|
|
239
|
+
return result?.speech ?? '';
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
Cypress.Commands.add('getA11ySpeech', () => {
|
|
243
|
+
cy.wrap(null, { log: false }).then(async () => {
|
|
244
|
+
if (!engine) {
|
|
245
|
+
throw new Error('A11y-Oracle not initialized. Call cy.initA11yOracle() first.');
|
|
246
|
+
}
|
|
247
|
+
const result = await engine.getSpeech();
|
|
248
|
+
return result?.speech ?? '';
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
Cypress.Commands.add('getA11ySpeechResult', () => {
|
|
252
|
+
cy.wrap(null, { log: false }).then(async () => {
|
|
253
|
+
if (!engine) {
|
|
254
|
+
throw new Error('A11y-Oracle not initialized. Call cy.initA11yOracle() first.');
|
|
255
|
+
}
|
|
256
|
+
return (await engine.getSpeech()) ?? null;
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
Cypress.Commands.add('getA11yFullTreeSpeech', () => {
|
|
260
|
+
cy.wrap(null, { log: false }).then(async () => {
|
|
261
|
+
if (!engine) {
|
|
262
|
+
throw new Error('A11y-Oracle not initialized. Call cy.initA11yOracle() first.');
|
|
263
|
+
}
|
|
264
|
+
return engine.getFullTreeSpeech();
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
Cypress.Commands.add('a11yPressKey', (key, modifiers) => {
|
|
268
|
+
cy.wrap(null, { log: false }).then(async () => {
|
|
269
|
+
if (!orchestrator) {
|
|
270
|
+
throw new Error('A11y-Oracle not initialized. Call cy.initA11yOracle() first.');
|
|
271
|
+
}
|
|
272
|
+
// Ensure the AUT iframe has focus before key dispatch
|
|
273
|
+
await focusAUTFrame();
|
|
274
|
+
return orchestrator.pressKey(key, modifiers);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
Cypress.Commands.add('a11yState', () => {
|
|
278
|
+
cy.wrap(null, { log: false }).then(async () => {
|
|
279
|
+
if (!orchestrator) {
|
|
280
|
+
throw new Error('A11y-Oracle not initialized. Call cy.initA11yOracle() first.');
|
|
281
|
+
}
|
|
282
|
+
return orchestrator.getState();
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
Cypress.Commands.add('a11yTraverseTabOrder', () => {
|
|
286
|
+
cy.wrap(null, { log: false }).then(async () => {
|
|
287
|
+
if (!orchestrator) {
|
|
288
|
+
throw new Error('A11y-Oracle not initialized. Call cy.initA11yOracle() first.');
|
|
289
|
+
}
|
|
290
|
+
return orchestrator.traverseTabOrder();
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
Cypress.Commands.add('a11yTraverseSubTree', (selector, maxTabs) => {
|
|
294
|
+
cy.wrap(null, { log: false }).then(async () => {
|
|
295
|
+
if (!orchestrator) {
|
|
296
|
+
throw new Error('A11y-Oracle not initialized. Call cy.initA11yOracle() first.');
|
|
297
|
+
}
|
|
298
|
+
return orchestrator.traverseSubTree(selector, maxTabs);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
/**
|
|
302
|
+
* Build an AuditContext from optional overrides, falling back to
|
|
303
|
+
* Cypress.spec.name and Cypress.env('projectName').
|
|
304
|
+
*/
|
|
305
|
+
function resolveAuditContext(overrides) {
|
|
306
|
+
return {
|
|
307
|
+
project: overrides?.project ?? Cypress.env('projectName') ?? '',
|
|
308
|
+
specName: overrides?.specName ?? Cypress.spec.name,
|
|
309
|
+
wcagLevel: overrides?.wcagLevel ?? Cypress.env('wcagLevel') ?? undefined,
|
|
310
|
+
disabledRules: overrides?.disabledRules ?? Cypress.env('disabledRules') ?? undefined,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
Cypress.Commands.add('a11yCheckFocusAndReport', (context) => {
|
|
314
|
+
cy.wrap(null, { log: false }).then(async () => {
|
|
315
|
+
if (!orchestrator) {
|
|
316
|
+
throw new Error('A11y-Oracle not initialized. Call cy.initA11yOracle() first.');
|
|
317
|
+
}
|
|
318
|
+
const ctx = resolveAuditContext(context);
|
|
319
|
+
const state = await orchestrator.getState();
|
|
320
|
+
const issues = formatAllIssues(state, ctx);
|
|
321
|
+
if (issues.length > 0) {
|
|
322
|
+
cy.task('log', `[A11y-Oracle] ${issues.length} focus issue(s) detected on ${ctx.specName}`);
|
|
323
|
+
cy.task('logOracleIssues', issues).then(() => {
|
|
324
|
+
if (Cypress.env('failOnErrors')) {
|
|
325
|
+
throw new Error(`[A11y-Oracle] ${issues.length} focus indicator issue(s) detected: ${issues.map((issue) => issue.ruleId).join(', ')}`);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
Cypress.Commands.add('a11yCheckTrapAndReport', (selector, maxTabs, context) => {
|
|
332
|
+
cy.wrap(null, { log: false }).then(async () => {
|
|
333
|
+
if (!orchestrator) {
|
|
334
|
+
throw new Error('A11y-Oracle not initialized. Call cy.initA11yOracle() first.');
|
|
335
|
+
}
|
|
336
|
+
const ctx = resolveAuditContext(context);
|
|
337
|
+
const result = await orchestrator.traverseSubTree(selector, maxTabs ?? 50);
|
|
338
|
+
const issues = formatTrapIssue(result, selector, ctx);
|
|
339
|
+
if (issues.length > 0) {
|
|
340
|
+
cy.task('log', `[A11y-Oracle] Keyboard trap detected in ${selector} on ${ctx.specName}`);
|
|
341
|
+
cy.task('logOracleIssues', issues).then(() => {
|
|
342
|
+
if (Cypress.env('failOnErrors')) {
|
|
343
|
+
throw new Error(`[A11y-Oracle] Keyboard trap detected in ${selector}`);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
Cypress.Commands.add('disposeA11yOracle', () => {
|
|
350
|
+
cy.wrap(null, { log: false }).then(async () => {
|
|
351
|
+
if (orchestrator) {
|
|
352
|
+
await orchestrator.disable();
|
|
353
|
+
orchestrator = null;
|
|
354
|
+
}
|
|
355
|
+
if (engine) {
|
|
356
|
+
// Engine was already disabled via orchestrator (same CDP session)
|
|
357
|
+
engine = null;
|
|
358
|
+
}
|
|
359
|
+
autFrameId = null;
|
|
360
|
+
autContextId = null;
|
|
361
|
+
return null;
|
|
362
|
+
});
|
|
363
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module key-map
|
|
3
|
+
*
|
|
4
|
+
* Maps human-readable key names to CDP `Input.dispatchKeyEvent` parameters.
|
|
5
|
+
* Used by the {@link commands} module to dispatch real keyboard events
|
|
6
|
+
* through the Chrome DevTools Protocol.
|
|
7
|
+
*/
|
|
8
|
+
export interface KeyDefinition {
|
|
9
|
+
/** The `key` property for CDP (e.g. `'Tab'`, `'Enter'`). */
|
|
10
|
+
key: string;
|
|
11
|
+
/** The `code` property for CDP (e.g. `'Tab'`, `'Enter'`). */
|
|
12
|
+
code: string;
|
|
13
|
+
/** The Windows virtual key code (e.g. `9` for Tab). */
|
|
14
|
+
keyCode: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Map of key names to their CDP Input.dispatchKeyEvent parameters.
|
|
18
|
+
*
|
|
19
|
+
* Supports all common navigation and interaction keys used in
|
|
20
|
+
* WCAG keyboard accessibility patterns.
|
|
21
|
+
*/
|
|
22
|
+
export declare const KEY_DEFINITIONS: Record<string, KeyDefinition>;
|
|
23
|
+
//# sourceMappingURL=key-map.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key-map.d.ts","sourceRoot":"","sources":["../../src/lib/key-map.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,aAAa;IAC5B,4DAA4D;IAC5D,GAAG,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAczD,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module key-map
|
|
3
|
+
*
|
|
4
|
+
* Maps human-readable key names to CDP `Input.dispatchKeyEvent` parameters.
|
|
5
|
+
* Used by the {@link commands} module to dispatch real keyboard events
|
|
6
|
+
* through the Chrome DevTools Protocol.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Map of key names to their CDP Input.dispatchKeyEvent parameters.
|
|
10
|
+
*
|
|
11
|
+
* Supports all common navigation and interaction keys used in
|
|
12
|
+
* WCAG keyboard accessibility patterns.
|
|
13
|
+
*/
|
|
14
|
+
export const KEY_DEFINITIONS = {
|
|
15
|
+
Tab: { key: 'Tab', code: 'Tab', keyCode: 9 },
|
|
16
|
+
Enter: { key: 'Enter', code: 'Enter', keyCode: 13 },
|
|
17
|
+
' ': { key: ' ', code: 'Space', keyCode: 32 },
|
|
18
|
+
Space: { key: ' ', code: 'Space', keyCode: 32 },
|
|
19
|
+
Escape: { key: 'Escape', code: 'Escape', keyCode: 27 },
|
|
20
|
+
ArrowUp: { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 },
|
|
21
|
+
ArrowDown: { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 },
|
|
22
|
+
ArrowLeft: { key: 'ArrowLeft', code: 'ArrowLeft', keyCode: 37 },
|
|
23
|
+
ArrowRight: { key: 'ArrowRight', code: 'ArrowRight', keyCode: 39 },
|
|
24
|
+
Home: { key: 'Home', code: 'Home', keyCode: 36 },
|
|
25
|
+
End: { key: 'End', code: 'End', keyCode: 35 },
|
|
26
|
+
Backspace: { key: 'Backspace', code: 'Backspace', keyCode: 8 },
|
|
27
|
+
Delete: { key: 'Delete', code: 'Delete', keyCode: 46 },
|
|
28
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module reporting
|
|
3
|
+
*
|
|
4
|
+
* Node-side setup for Oracle issue reporting in Cypress.
|
|
5
|
+
*
|
|
6
|
+
* Call `setupOracleReporting()` inside `setupNodeEvents()` in your
|
|
7
|
+
* `cypress.config.ts` to register the `logOracleIssues` task and
|
|
8
|
+
* automatically write a JSON report file at the end of each run.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* // cypress.config.ts
|
|
13
|
+
* import { setupOracleReporting } from '@a11y-oracle/cypress-plugin';
|
|
14
|
+
*
|
|
15
|
+
* export default defineConfig({
|
|
16
|
+
* e2e: {
|
|
17
|
+
* setupNodeEvents(on, config) {
|
|
18
|
+
* setupOracleReporting(on, config);
|
|
19
|
+
* },
|
|
20
|
+
* env: { projectName: 'my-app' },
|
|
21
|
+
* },
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Or, to combine oracle issues with your existing axe-core violations
|
|
26
|
+
* array, just add the `logOracleIssues` task yourself:
|
|
27
|
+
*
|
|
28
|
+
* ```typescript
|
|
29
|
+
* on('task', {
|
|
30
|
+
* logAxeViolations(violations) { allViolations.push(...violations); return null; },
|
|
31
|
+
* logOracleIssues(issues) { allViolations.push(...issues); return null; },
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* Register Oracle reporting tasks and after:run hook.
|
|
37
|
+
*
|
|
38
|
+
* Registers:
|
|
39
|
+
* - `logOracleIssues` task — accumulates OracleIssue objects in memory
|
|
40
|
+
* - `after:run` hook — writes accumulated issues to
|
|
41
|
+
* `oracle-results-${projectName}.json`
|
|
42
|
+
*
|
|
43
|
+
* @param on - Cypress `on` function from `setupNodeEvents`
|
|
44
|
+
* @param config - Cypress plugin config object
|
|
45
|
+
*/
|
|
46
|
+
export declare function setupOracleReporting(on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions): void;
|
|
47
|
+
//# sourceMappingURL=reporting.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reporting.d.ts","sourceRoot":"","sources":["../../src/lib/reporting.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAKH;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,OAAO,CAAC,YAAY,EACxB,MAAM,EAAE,OAAO,CAAC,mBAAmB,GAClC,IAAI,CA4BN"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module reporting
|
|
3
|
+
*
|
|
4
|
+
* Node-side setup for Oracle issue reporting in Cypress.
|
|
5
|
+
*
|
|
6
|
+
* Call `setupOracleReporting()` inside `setupNodeEvents()` in your
|
|
7
|
+
* `cypress.config.ts` to register the `logOracleIssues` task and
|
|
8
|
+
* automatically write a JSON report file at the end of each run.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* // cypress.config.ts
|
|
13
|
+
* import { setupOracleReporting } from '@a11y-oracle/cypress-plugin';
|
|
14
|
+
*
|
|
15
|
+
* export default defineConfig({
|
|
16
|
+
* e2e: {
|
|
17
|
+
* setupNodeEvents(on, config) {
|
|
18
|
+
* setupOracleReporting(on, config);
|
|
19
|
+
* },
|
|
20
|
+
* env: { projectName: 'my-app' },
|
|
21
|
+
* },
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Or, to combine oracle issues with your existing axe-core violations
|
|
26
|
+
* array, just add the `logOracleIssues` task yourself:
|
|
27
|
+
*
|
|
28
|
+
* ```typescript
|
|
29
|
+
* on('task', {
|
|
30
|
+
* logAxeViolations(violations) { allViolations.push(...violations); return null; },
|
|
31
|
+
* logOracleIssues(issues) { allViolations.push(...issues); return null; },
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
/// <reference types="node" />
|
|
36
|
+
import * as fs from 'fs';
|
|
37
|
+
/**
|
|
38
|
+
* Register Oracle reporting tasks and after:run hook.
|
|
39
|
+
*
|
|
40
|
+
* Registers:
|
|
41
|
+
* - `logOracleIssues` task — accumulates OracleIssue objects in memory
|
|
42
|
+
* - `after:run` hook — writes accumulated issues to
|
|
43
|
+
* `oracle-results-${projectName}.json`
|
|
44
|
+
*
|
|
45
|
+
* @param on - Cypress `on` function from `setupNodeEvents`
|
|
46
|
+
* @param config - Cypress plugin config object
|
|
47
|
+
*/
|
|
48
|
+
export function setupOracleReporting(on, config) {
|
|
49
|
+
const allIssues = [];
|
|
50
|
+
const projectName = config.env?.['projectName'] ?? 'default-project';
|
|
51
|
+
on('task', {
|
|
52
|
+
logOracleIssues(issues) {
|
|
53
|
+
if (Array.isArray(issues)) {
|
|
54
|
+
const enriched = issues.map((v) => ({
|
|
55
|
+
...v,
|
|
56
|
+
project: v.project || projectName,
|
|
57
|
+
}));
|
|
58
|
+
allIssues.push(...enriched);
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
on('after:run', () => {
|
|
64
|
+
if (allIssues.length > 0) {
|
|
65
|
+
const reportPath = `oracle-results-${projectName}.json`;
|
|
66
|
+
fs.writeFileSync(reportPath, JSON.stringify(allIssues, null, 2));
|
|
67
|
+
console.log(`\n[A11y-Oracle] Report saved to ${reportPath}`);
|
|
68
|
+
console.log(`[A11y-Oracle] Total issues found: ${allIssues.length}`);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@a11y-oracle/cypress-plugin",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Cypress custom commands for accessibility speech assertions with iframe-aware CDP routing",
|
|
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/cypress-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/cypress-plugin",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"accessibility",
|
|
18
|
+
"a11y",
|
|
19
|
+
"cypress",
|
|
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
|
+
"cypress": ">=12.0.0"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@a11y-oracle/core-engine": "1.0.0",
|
|
49
|
+
"@a11y-oracle/keyboard-engine": "1.0.0",
|
|
50
|
+
"@a11y-oracle/audit-formatter": "1.0.0",
|
|
51
|
+
"tslib": "^2.3.0"
|
|
52
|
+
}
|
|
53
|
+
}
|