@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 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).
@@ -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
+ }