@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 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
@@ -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;AA8UD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,cAAc,CAAC,CAwCvE"}
@@ -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
- return Cypress.automation('remote:debugger:protocol', {
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
- ...clip,
84
- x: clip.x + autIframeBounds.x,
85
- y: clip.y + autIframeBounds.y,
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
- return { x: rect.x + f.clientLeft, y: rect.y + f.clientTop };
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.2.0",
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.2.0",
49
- "@a11y-oracle/keyboard-engine": "1.2.0",
50
- "@a11y-oracle/audit-formatter": "1.2.0",
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
  }