@a11y-oracle/cypress-plugin 1.1.4 → 1.3.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 CHANGED
@@ -412,6 +412,30 @@ Node-side function (not a Cypress command). Call inside `setupNodeEvents()` to r
412
412
  import { setupOracleReporting } from '@a11y-oracle/cypress-plugin';
413
413
  ```
414
414
 
415
+ ### CDP Adapter
416
+
417
+ #### `createCypressCDPAdapter()`
418
+
419
+ Create a ready-to-use `CDPSessionLike` adapter for custom integrations. This is useful when you need to call `@a11y-oracle/axe-bridge`'s `resolveAllIncomplete()` directly instead of using the built-in Cypress commands.
420
+
421
+ The function encapsulates all the CDP plumbing:
422
+ - Enables `DOM.enable` and `Page.enable` CDP domains
423
+ - Discovers the AUT frame ID from `Page.getFrameTree`
424
+ - Creates an isolated world execution context in the AUT frame
425
+ - Detects iframe position and CSS transform scale
426
+ - Returns a `CDPSessionLike` adapter with scale-aware `Page.captureScreenshot` coordinate translation
427
+
428
+ ```typescript
429
+ import { createCypressCDPAdapter } from '@a11y-oracle/cypress-plugin';
430
+ import { resolveAllIncomplete } from '@a11y-oracle/axe-bridge';
431
+
432
+ // In your custom Cypress command:
433
+ cy.wrap(null).then(async () => {
434
+ const cdp = await createCypressCDPAdapter();
435
+ const resolved = await resolveAllIncomplete(cdp, axeResults, options);
436
+ });
437
+ ```
438
+
415
439
  ### Lifecycle
416
440
 
417
441
  #### `cy.disposeA11yOracle()`
@@ -425,6 +449,7 @@ Types are re-exported from `@a11y-oracle/core-engine` and `@a11y-oracle/audit-fo
425
449
  ```typescript
426
450
  import type {
427
451
  // Core engine types
452
+ CDPSessionLike,
428
453
  SpeechResult,
429
454
  A11yState,
430
455
  A11yFocusedElement,
@@ -459,7 +484,9 @@ Cypress runs the app under test (AUT) inside an iframe within its runner page. T
459
484
 
460
485
  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
486
 
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.
487
+ 4. **Screenshot coordinate translation** — `getBoundingClientRect()` inside the AUT iframe returns iframe-relative coordinates, but `Page.captureScreenshot` clips from the top-level browser viewport. The plugin queries the AUT iframe's position in the viewport and offsets all screenshot clip coordinates accordingly. It also detects the CSS transform scale that Cypress applies to the AUT iframe wrapper (typically ~0.66x in headless Electron), and applies it to coordinates, dimensions, and the clip's `scale` property. This ensures pixel-level analysis (color contrast, focus indicator diffing) captures the correct region even when the AUT is rendered at a scaled size.
488
+
489
+ 5. **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
490
 
464
491
  ### CDP Flow
465
492
 
package/dist/index.d.ts CHANGED
@@ -39,7 +39,8 @@
39
39
  * - Cypress >= 12.0.0
40
40
  */
41
41
  import './lib/commands.js';
42
- export type { SpeechResult, SpeechEngineOptions, A11yState, A11yFocusedElement, A11yFocusIndicator, A11yOrchestratorOptions, ModifierKeys, TabOrderReport, TabOrderEntry, TraversalResult, FocusIndicator, } from '@a11y-oracle/core-engine';
42
+ export type { CDPSessionLike, SpeechResult, SpeechEngineOptions, A11yState, A11yFocusedElement, A11yFocusIndicator, A11yOrchestratorOptions, ModifierKeys, TabOrderReport, TabOrderEntry, TraversalResult, FocusIndicator, } from '@a11y-oracle/core-engine';
43
43
  export type { OracleIssue, OracleNode, OracleCheck, OracleImpact, OracleResultType, AuditContext, } from '@a11y-oracle/audit-formatter';
44
44
  export { setupOracleReporting } from './lib/reporting.js';
45
+ export { createCypressCDPAdapter } from './lib/commands.js';
45
46
  //# sourceMappingURL=index.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAGH,OAAO,mBAAmB,CAAC;AAG3B,YAAY,EACV,cAAc,EACd,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;AAG1D,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -42,3 +42,5 @@
42
42
  import './lib/commands.js';
43
43
  // Node-side reporting setup
44
44
  export { setupOracleReporting } from './lib/reporting.js';
45
+ // Reusable CDP adapter for custom integrations (e.g. axe-bridge resolveAllIncomplete)
46
+ export { createCypressCDPAdapter } from './lib/commands.js';
@@ -29,7 +29,7 @@
29
29
  * cy.disposeA11yOracle();
30
30
  * ```
31
31
  */
32
- import type { SpeechResult, A11yState, A11yOrchestratorOptions, TabOrderReport, TraversalResult, ModifierKeys } from '@a11y-oracle/core-engine';
32
+ import type { CDPSessionLike, SpeechResult, A11yState, A11yOrchestratorOptions, TabOrderReport, TraversalResult, ModifierKeys } from '@a11y-oracle/core-engine';
33
33
  import type { AuditContext } from '@a11y-oracle/audit-formatter';
34
34
  declare global {
35
35
  namespace Cypress {
@@ -98,4 +98,28 @@ declare global {
98
98
  }
99
99
  }
100
100
  }
101
+ /**
102
+ * Create a ready-to-use CDP adapter for the Cypress AUT iframe.
103
+ *
104
+ * Encapsulates all the CDP plumbing needed to use
105
+ * `@a11y-oracle/axe-bridge`'s `resolveAllIncomplete()` in a custom
106
+ * Cypress command:
107
+ *
108
+ * 1. Enables required CDP domains (`DOM.enable`, `Page.enable`)
109
+ * 2. Discovers the AUT frame ID from `Page.getFrameTree`
110
+ * 3. Creates an isolated world execution context in the AUT frame
111
+ * 4. Detects iframe position and CSS transform scale
112
+ * 5. Returns a `CDPSessionLike` adapter with scale-aware coordinate
113
+ * translation for `Page.captureScreenshot`
114
+ *
115
+ * @example
116
+ * ```typescript
117
+ * import { createCypressCDPAdapter } from '@a11y-oracle/cypress-plugin';
118
+ * import { resolveAllIncomplete } from '@a11y-oracle/axe-bridge';
119
+ *
120
+ * const cdp = await createCypressCDPAdapter();
121
+ * const resolved = await resolveAllIncomplete(cdp, axeResults, options);
122
+ * ```
123
+ */
124
+ export declare function createCypressCDPAdapter(): Promise<CDPSessionLike>;
101
125
  //# sourceMappingURL=commands.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/lib/commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,KAAK,EACV,cAAc,EACd,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;AAsQD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,cAAc,CAAC,CAwCvE"}
@@ -74,15 +74,20 @@ function createFrameAwareCDPAdapter() {
74
74
  // Runtime.evaluate runs inside the AUT iframe (via contextId), so
75
75
  // getBoundingClientRect() returns iframe-relative coords. But
76
76
  // Page.captureScreenshot clips relative to the top-level viewport.
77
+ // Cypress scales the AUT iframe via a CSS transform on a wrapper
78
+ // element, so we must also apply the display scale factor.
77
79
  if (method === 'Page.captureScreenshot' &&
78
80
  p['clip'] &&
79
81
  autIframeBounds &&
80
- (autIframeBounds.x !== 0 || autIframeBounds.y !== 0)) {
82
+ (autIframeBounds.x !== 0 || autIframeBounds.y !== 0 || autIframeBounds.scale !== 1)) {
81
83
  const clip = p['clip'];
84
+ const s = autIframeBounds.scale;
82
85
  p['clip'] = {
83
- ...clip,
84
- x: clip.x + autIframeBounds.x,
85
- y: clip.y + autIframeBounds.y,
86
+ x: autIframeBounds.x + clip.x * s,
87
+ y: autIframeBounds.y + clip.y * s,
88
+ width: clip.width * s,
89
+ height: clip.height * s,
90
+ scale: (clip.scale || 1) / s,
86
91
  };
87
92
  }
88
93
  return sendCDP(method, p);
@@ -139,12 +144,18 @@ async function findAUTContextId(frameId) {
139
144
  }
140
145
  }
141
146
  /**
142
- * Get the AUT iframe's position in the top-level viewport.
147
+ * Get the AUT iframe's position and display scale in the top-level viewport.
143
148
  *
144
149
  * Runs `Runtime.evaluate` in the top-level context (without `contextId`)
145
150
  * to find the AUT iframe and return its bounding rect origin. Adds
146
151
  * `clientLeft`/`clientTop` to account for any iframe border.
147
152
  *
153
+ * Also computes the display scale by comparing the iframe's rendered width
154
+ * (`getBoundingClientRect().width`) to its CSS content width (`clientWidth`).
155
+ * Cypress scales the AUT iframe via a CSS transform on an ancestor wrapper
156
+ * element (not the iframe itself), so the iframe's own computed transform
157
+ * is `none`. The empirical width comparison correctly detects the scale.
158
+ *
148
159
  * Used to translate iframe-relative coordinates from
149
160
  * `getBoundingClientRect()` to viewport-absolute coordinates for
150
161
  * `Page.captureScreenshot` clips.
@@ -157,10 +168,16 @@ async function getAUTIframeBounds() {
157
168
  const src = f.getAttribute('src') || f.src || '';
158
169
  if (src && !src.includes('/__/') && !src.includes('__cypress') && src !== 'about:blank') {
159
170
  const rect = f.getBoundingClientRect();
160
- return { x: rect.x + f.clientLeft, y: rect.y + f.clientTop };
171
+ const contentWidth = f.clientWidth + 2 * f.clientLeft;
172
+ const scale = contentWidth > 0 ? rect.width / contentWidth : 1;
173
+ return {
174
+ x: rect.x + f.clientLeft * scale,
175
+ y: rect.y + f.clientTop * scale,
176
+ scale: scale,
177
+ };
161
178
  }
162
179
  }
163
- return { x: 0, y: 0 };
180
+ return { x: 0, y: 0, scale: 1 };
164
181
  })()`,
165
182
  returnByValue: true,
166
183
  }));
@@ -244,6 +261,63 @@ async function dispatchKey(key) {
244
261
  nativeVirtualKeyCode: keyDef.keyCode,
245
262
  });
246
263
  }
264
+ // ── Public API ─────────────────────────────────────────────────────
265
+ /**
266
+ * Create a ready-to-use CDP adapter for the Cypress AUT iframe.
267
+ *
268
+ * Encapsulates all the CDP plumbing needed to use
269
+ * `@a11y-oracle/axe-bridge`'s `resolveAllIncomplete()` in a custom
270
+ * Cypress command:
271
+ *
272
+ * 1. Enables required CDP domains (`DOM.enable`, `Page.enable`)
273
+ * 2. Discovers the AUT frame ID from `Page.getFrameTree`
274
+ * 3. Creates an isolated world execution context in the AUT frame
275
+ * 4. Detects iframe position and CSS transform scale
276
+ * 5. Returns a `CDPSessionLike` adapter with scale-aware coordinate
277
+ * translation for `Page.captureScreenshot`
278
+ *
279
+ * @example
280
+ * ```typescript
281
+ * import { createCypressCDPAdapter } from '@a11y-oracle/cypress-plugin';
282
+ * import { resolveAllIncomplete } from '@a11y-oracle/axe-bridge';
283
+ *
284
+ * const cdp = await createCypressCDPAdapter();
285
+ * const resolved = await resolveAllIncomplete(cdp, axeResults, options);
286
+ * ```
287
+ */
288
+ export async function createCypressCDPAdapter() {
289
+ await sendCDP('DOM.enable');
290
+ await sendCDP('Page.enable');
291
+ const frameId = await findAUTFrameId();
292
+ const contextId = frameId ? await findAUTContextId(frameId) : null;
293
+ const iframeBounds = await getAUTIframeBounds();
294
+ return {
295
+ send: (method, params) => {
296
+ const p = { ...params };
297
+ if (method === 'Accessibility.getFullAXTree' && frameId) {
298
+ p['frameId'] = frameId;
299
+ }
300
+ if (method === 'Runtime.evaluate' && contextId !== null) {
301
+ p['contextId'] = contextId;
302
+ }
303
+ if (method === 'Page.captureScreenshot' &&
304
+ p['clip'] &&
305
+ iframeBounds &&
306
+ (iframeBounds.x !== 0 || iframeBounds.y !== 0 || iframeBounds.scale !== 1)) {
307
+ const clip = p['clip'];
308
+ const s = iframeBounds.scale;
309
+ p['clip'] = {
310
+ x: iframeBounds.x + clip.x * s,
311
+ y: iframeBounds.y + clip.y * s,
312
+ width: clip.width * s,
313
+ height: clip.height * s,
314
+ scale: (clip.scale || 1) / s,
315
+ };
316
+ }
317
+ return sendCDP(method, p);
318
+ },
319
+ };
320
+ }
247
321
  // ── Commands ───────────────────────────────────────────────────────
248
322
  Cypress.Commands.add('initA11yOracle', (options) => {
249
323
  cy.wrap(null, { log: false }).then(async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a11y-oracle/cypress-plugin",
3
- "version": "1.1.4",
3
+ "version": "1.3.0",
4
4
  "description": "Cypress custom commands for accessibility speech assertions with iframe-aware CDP routing",
5
5
  "license": "MIT",
6
6
  "author": "a11y-oracle",
@@ -45,9 +45,9 @@
45
45
  "cypress": ">=12.0.0"
46
46
  },
47
47
  "dependencies": {
48
- "@a11y-oracle/core-engine": "1.1.4",
49
- "@a11y-oracle/keyboard-engine": "1.1.4",
50
- "@a11y-oracle/audit-formatter": "1.1.4",
48
+ "@a11y-oracle/core-engine": "1.3.0",
49
+ "@a11y-oracle/keyboard-engine": "1.3.0",
50
+ "@a11y-oracle/audit-formatter": "1.3.0",
51
51
  "tslib": "^2.3.0"
52
52
  }
53
53
  }