@a11y-oracle/cypress-plugin 1.2.0 → 1.3.1
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 +26 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/lib/commands.d.ts +25 -1
- package/dist/lib/commands.d.ts.map +1 -1
- package/dist/lib/commands.js +146 -8
- package/package.json +4 -4
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,7 @@ 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. **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. This ensures pixel-level analysis (color contrast, focus indicator diffing) captures the correct region.
|
|
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.
|
|
463
488
|
|
|
464
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.
|
|
465
490
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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';
|
package/dist/lib/commands.d.ts
CHANGED
|
@@ -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,
|
|
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;AA8UD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,cAAc,CAAC,CAwCvE"}
|
package/dist/lib/commands.js
CHANGED
|
@@ -38,14 +38,48 @@ let orchestrator = null;
|
|
|
38
38
|
let autFrameId = null;
|
|
39
39
|
let autContextId = null;
|
|
40
40
|
let autIframeBounds = null;
|
|
41
|
+
/**
|
|
42
|
+
* Cached isolated world state for reuse across init/dispose cycles.
|
|
43
|
+
*
|
|
44
|
+
* `Page.createIsolatedWorld` accumulates execution contexts that Chrome
|
|
45
|
+
* never garbage-collects during same-origin navigations. After ~16
|
|
46
|
+
* iterations the browser hangs on subsequent CDP calls. By caching the
|
|
47
|
+
* context ID and verifying it with a cheap `Runtime.evaluate` probe we
|
|
48
|
+
* avoid creating a new world when the previous one is still alive.
|
|
49
|
+
*/
|
|
50
|
+
let _cachedWorldFrameId = null;
|
|
51
|
+
let _cachedWorldContextId = null;
|
|
52
|
+
/** Timeout (ms) applied to every CDP call to surface hangs as errors. */
|
|
53
|
+
const CDP_TIMEOUT_MS = 30_000;
|
|
54
|
+
/**
|
|
55
|
+
* Wrap a promise with a timeout to prevent indefinite hangs.
|
|
56
|
+
*/
|
|
57
|
+
function withTimeout(promise, ms, label) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const timer = setTimeout(() => {
|
|
60
|
+
reject(new Error(`A11y-Oracle: CDP call "${label}" timed out after ${ms}ms`));
|
|
61
|
+
}, ms);
|
|
62
|
+
promise.then((value) => {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
resolve(value);
|
|
65
|
+
}, (error) => {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
reject(error);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
41
71
|
/**
|
|
42
72
|
* Send a raw CDP command through Cypress's automation channel.
|
|
73
|
+
*
|
|
74
|
+
* Every call is guarded by a timeout so that accumulated isolated worlds
|
|
75
|
+
* or other browser-side issues surface as errors instead of silent hangs.
|
|
43
76
|
*/
|
|
44
77
|
function sendCDP(command, params = {}) {
|
|
45
|
-
|
|
78
|
+
const raw = Cypress.automation('remote:debugger:protocol', {
|
|
46
79
|
command,
|
|
47
80
|
params,
|
|
48
81
|
});
|
|
82
|
+
return withTimeout(raw, CDP_TIMEOUT_MS, command);
|
|
49
83
|
}
|
|
50
84
|
/**
|
|
51
85
|
* Create a {@link CDPSessionLike} adapter that routes CDP calls through
|
|
@@ -74,15 +108,20 @@ function createFrameAwareCDPAdapter() {
|
|
|
74
108
|
// Runtime.evaluate runs inside the AUT iframe (via contextId), so
|
|
75
109
|
// getBoundingClientRect() returns iframe-relative coords. But
|
|
76
110
|
// Page.captureScreenshot clips relative to the top-level viewport.
|
|
111
|
+
// Cypress scales the AUT iframe via a CSS transform on a wrapper
|
|
112
|
+
// element, so we must also apply the display scale factor.
|
|
77
113
|
if (method === 'Page.captureScreenshot' &&
|
|
78
114
|
p['clip'] &&
|
|
79
115
|
autIframeBounds &&
|
|
80
|
-
(autIframeBounds.x !== 0 || autIframeBounds.y !== 0)) {
|
|
116
|
+
(autIframeBounds.x !== 0 || autIframeBounds.y !== 0 || autIframeBounds.scale !== 1)) {
|
|
81
117
|
const clip = p['clip'];
|
|
118
|
+
const s = autIframeBounds.scale;
|
|
82
119
|
p['clip'] = {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
120
|
+
x: autIframeBounds.x + clip.x * s,
|
|
121
|
+
y: autIframeBounds.y + clip.y * s,
|
|
122
|
+
width: clip.width * s,
|
|
123
|
+
height: clip.height * s,
|
|
124
|
+
scale: (clip.scale || 1) / s,
|
|
86
125
|
};
|
|
87
126
|
}
|
|
88
127
|
return sendCDP(method, p);
|
|
@@ -126,6 +165,29 @@ async function findAUTFrameId() {
|
|
|
126
165
|
* calls execute in the AUT, not the Cypress runner.
|
|
127
166
|
*/
|
|
128
167
|
async function findAUTContextId(frameId) {
|
|
168
|
+
// Try to reuse the context that was handed off by the previous
|
|
169
|
+
// disposeA11yOracle() call. A lightweight Runtime.evaluate probe
|
|
170
|
+
// confirms the context is still alive (Chrome destroys isolated worlds
|
|
171
|
+
// on cross-origin navigation but may preserve them for same-origin).
|
|
172
|
+
// Always consume (clear) the cache regardless of outcome.
|
|
173
|
+
const cachedCtx = _cachedWorldContextId;
|
|
174
|
+
const cachedFrame = _cachedWorldFrameId;
|
|
175
|
+
_cachedWorldContextId = null;
|
|
176
|
+
_cachedWorldFrameId = null;
|
|
177
|
+
if (cachedCtx !== null && cachedFrame === frameId) {
|
|
178
|
+
try {
|
|
179
|
+
await sendCDP('Runtime.evaluate', {
|
|
180
|
+
expression: '1',
|
|
181
|
+
contextId: cachedCtx,
|
|
182
|
+
returnByValue: true,
|
|
183
|
+
});
|
|
184
|
+
// Context is still valid — reuse it.
|
|
185
|
+
return cachedCtx;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// Context was destroyed (frame navigated), fall through to create.
|
|
189
|
+
}
|
|
190
|
+
}
|
|
129
191
|
try {
|
|
130
192
|
const result = await sendCDP('Page.createIsolatedWorld', {
|
|
131
193
|
frameId,
|
|
@@ -139,12 +201,18 @@ async function findAUTContextId(frameId) {
|
|
|
139
201
|
}
|
|
140
202
|
}
|
|
141
203
|
/**
|
|
142
|
-
* Get the AUT iframe's position in the top-level viewport.
|
|
204
|
+
* Get the AUT iframe's position and display scale in the top-level viewport.
|
|
143
205
|
*
|
|
144
206
|
* Runs `Runtime.evaluate` in the top-level context (without `contextId`)
|
|
145
207
|
* to find the AUT iframe and return its bounding rect origin. Adds
|
|
146
208
|
* `clientLeft`/`clientTop` to account for any iframe border.
|
|
147
209
|
*
|
|
210
|
+
* Also computes the display scale by comparing the iframe's rendered width
|
|
211
|
+
* (`getBoundingClientRect().width`) to its CSS content width (`clientWidth`).
|
|
212
|
+
* Cypress scales the AUT iframe via a CSS transform on an ancestor wrapper
|
|
213
|
+
* element (not the iframe itself), so the iframe's own computed transform
|
|
214
|
+
* is `none`. The empirical width comparison correctly detects the scale.
|
|
215
|
+
*
|
|
148
216
|
* Used to translate iframe-relative coordinates from
|
|
149
217
|
* `getBoundingClientRect()` to viewport-absolute coordinates for
|
|
150
218
|
* `Page.captureScreenshot` clips.
|
|
@@ -157,10 +225,16 @@ async function getAUTIframeBounds() {
|
|
|
157
225
|
const src = f.getAttribute('src') || f.src || '';
|
|
158
226
|
if (src && !src.includes('/__/') && !src.includes('__cypress') && src !== 'about:blank') {
|
|
159
227
|
const rect = f.getBoundingClientRect();
|
|
160
|
-
|
|
228
|
+
const contentWidth = f.clientWidth + 2 * f.clientLeft;
|
|
229
|
+
const scale = contentWidth > 0 ? rect.width / contentWidth : 1;
|
|
230
|
+
return {
|
|
231
|
+
x: rect.x + f.clientLeft * scale,
|
|
232
|
+
y: rect.y + f.clientTop * scale,
|
|
233
|
+
scale: scale,
|
|
234
|
+
};
|
|
161
235
|
}
|
|
162
236
|
}
|
|
163
|
-
return { x: 0, y: 0 };
|
|
237
|
+
return { x: 0, y: 0, scale: 1 };
|
|
164
238
|
})()`,
|
|
165
239
|
returnByValue: true,
|
|
166
240
|
}));
|
|
@@ -244,6 +318,63 @@ async function dispatchKey(key) {
|
|
|
244
318
|
nativeVirtualKeyCode: keyDef.keyCode,
|
|
245
319
|
});
|
|
246
320
|
}
|
|
321
|
+
// ── Public API ─────────────────────────────────────────────────────
|
|
322
|
+
/**
|
|
323
|
+
* Create a ready-to-use CDP adapter for the Cypress AUT iframe.
|
|
324
|
+
*
|
|
325
|
+
* Encapsulates all the CDP plumbing needed to use
|
|
326
|
+
* `@a11y-oracle/axe-bridge`'s `resolveAllIncomplete()` in a custom
|
|
327
|
+
* Cypress command:
|
|
328
|
+
*
|
|
329
|
+
* 1. Enables required CDP domains (`DOM.enable`, `Page.enable`)
|
|
330
|
+
* 2. Discovers the AUT frame ID from `Page.getFrameTree`
|
|
331
|
+
* 3. Creates an isolated world execution context in the AUT frame
|
|
332
|
+
* 4. Detects iframe position and CSS transform scale
|
|
333
|
+
* 5. Returns a `CDPSessionLike` adapter with scale-aware coordinate
|
|
334
|
+
* translation for `Page.captureScreenshot`
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* ```typescript
|
|
338
|
+
* import { createCypressCDPAdapter } from '@a11y-oracle/cypress-plugin';
|
|
339
|
+
* import { resolveAllIncomplete } from '@a11y-oracle/axe-bridge';
|
|
340
|
+
*
|
|
341
|
+
* const cdp = await createCypressCDPAdapter();
|
|
342
|
+
* const resolved = await resolveAllIncomplete(cdp, axeResults, options);
|
|
343
|
+
* ```
|
|
344
|
+
*/
|
|
345
|
+
export async function createCypressCDPAdapter() {
|
|
346
|
+
await sendCDP('DOM.enable');
|
|
347
|
+
await sendCDP('Page.enable');
|
|
348
|
+
const frameId = await findAUTFrameId();
|
|
349
|
+
const contextId = frameId ? await findAUTContextId(frameId) : null;
|
|
350
|
+
const iframeBounds = await getAUTIframeBounds();
|
|
351
|
+
return {
|
|
352
|
+
send: (method, params) => {
|
|
353
|
+
const p = { ...params };
|
|
354
|
+
if (method === 'Accessibility.getFullAXTree' && frameId) {
|
|
355
|
+
p['frameId'] = frameId;
|
|
356
|
+
}
|
|
357
|
+
if (method === 'Runtime.evaluate' && contextId !== null) {
|
|
358
|
+
p['contextId'] = contextId;
|
|
359
|
+
}
|
|
360
|
+
if (method === 'Page.captureScreenshot' &&
|
|
361
|
+
p['clip'] &&
|
|
362
|
+
iframeBounds &&
|
|
363
|
+
(iframeBounds.x !== 0 || iframeBounds.y !== 0 || iframeBounds.scale !== 1)) {
|
|
364
|
+
const clip = p['clip'];
|
|
365
|
+
const s = iframeBounds.scale;
|
|
366
|
+
p['clip'] = {
|
|
367
|
+
x: iframeBounds.x + clip.x * s,
|
|
368
|
+
y: iframeBounds.y + clip.y * s,
|
|
369
|
+
width: clip.width * s,
|
|
370
|
+
height: clip.height * s,
|
|
371
|
+
scale: (clip.scale || 1) / s,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
return sendCDP(method, p);
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
}
|
|
247
378
|
// ── Commands ───────────────────────────────────────────────────────
|
|
248
379
|
Cypress.Commands.add('initA11yOracle', (options) => {
|
|
249
380
|
cy.wrap(null, { log: false }).then(async () => {
|
|
@@ -402,6 +533,13 @@ Cypress.Commands.add('disposeA11yOracle', () => {
|
|
|
402
533
|
// Engine was already disabled via orchestrator (same CDP session)
|
|
403
534
|
engine = null;
|
|
404
535
|
}
|
|
536
|
+
// Hand off the current isolated world to the cache so the next
|
|
537
|
+
// initA11yOracle() can reuse it instead of leaking a new one.
|
|
538
|
+
// CDP provides no API to destroy an isolated world; reuse is
|
|
539
|
+
// the only way to prevent accumulation. The cache is consumed
|
|
540
|
+
// (cleared) at the start of the next findAUTContextId() call.
|
|
541
|
+
_cachedWorldContextId = autContextId;
|
|
542
|
+
_cachedWorldFrameId = autFrameId;
|
|
405
543
|
autFrameId = null;
|
|
406
544
|
autContextId = null;
|
|
407
545
|
autIframeBounds = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a11y-oracle/cypress-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
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.
|
|
49
|
-
"@a11y-oracle/keyboard-engine": "1.
|
|
50
|
-
"@a11y-oracle/audit-formatter": "1.
|
|
48
|
+
"@a11y-oracle/core-engine": "1.3.1",
|
|
49
|
+
"@a11y-oracle/keyboard-engine": "1.3.1",
|
|
50
|
+
"@a11y-oracle/audit-formatter": "1.3.1",
|
|
51
51
|
"tslib": "^2.3.0"
|
|
52
52
|
}
|
|
53
53
|
}
|