@a11y-oracle/cypress-plugin 1.3.0 → 1.3.2

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
@@ -29,6 +29,8 @@ npm install -D @a11y-oracle/cypress-plugin @a11y-oracle/core-engine cypress
29
29
 
30
30
  > **Chrome/Chromium only.** The plugin uses CDP, which is only available in Chrome-family browsers.
31
31
 
32
+ > **⚠️ Stability Notice — Playwright recommended.** The Cypress plugin is functional but has known stability constraints with large test suites. We recommend using [`@a11y-oracle/playwright-plugin`](../playwright-plugin/README.md) for the most reliable experience. See [Known Limitations](#known-limitations) below for details.
33
+
32
34
  ## Setup
33
35
 
34
36
  ### 1. Import Commands
@@ -513,6 +515,22 @@ If you need types in your test files without importing the support file:
513
515
  /// <reference types="@a11y-oracle/cypress-plugin" />
514
516
  ```
515
517
 
518
+ ## Known Limitations
519
+
520
+ ### CDP Resource Accumulation in Long Test Suites
521
+
522
+ Cypress runs the application under test (AUT) inside an iframe within its runner page. To interact with the AUT's accessibility tree, the plugin must create an **isolated execution world** in the AUT frame via `Page.createIsolatedWorld` on every `initA11yOracle()` call. Unlike Playwright — which provides native, first-class CDP sessions — Cypress's iframe architecture means these isolated worlds accumulate browser-side resources that Chrome does not fully reclaim, even after `disposeA11yOracle()` cleans up its own references.
523
+
524
+ **What this means in practice:**
525
+
526
+ - Test suites with many spec files or many tests per file may experience increasing memory pressure over the course of a run.
527
+ - In v1.3.0 and earlier, this caused a deterministic hang after approximately 16 `init`/`dispose` cycles, because `Accessibility.getFullAXTree` would stall when traversing nodes across all accumulated contexts ([#14](https://github.com/a11y-oracle/a11y-oracle/issues/14)).
528
+ - v1.3.1 mitigated the hang by properly destroying isolated worlds on dispose, but the underlying architectural constraint — that Cypress proxies all CDP calls through its runner and manages frame contexts differently than Playwright — remains.
529
+
530
+ **Recommendation:** If you are starting a new project or have the flexibility to choose your E2E framework, use [`@a11y-oracle/playwright-plugin`](../playwright-plugin/README.md). Playwright provides direct CDP session access without iframe indirection, making it inherently more stable and performant for A11y-Oracle's CDP-heavy workflow.
531
+
532
+ If you need to stay on Cypress, the plugin is fully functional — just be aware of these constraints for very large suites.
533
+
516
534
  ## Troubleshooting
517
535
 
518
536
  ### "Could not find the AUT iframe"
@@ -1 +1 @@
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"}
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
@@ -131,6 +165,29 @@ async function findAUTFrameId() {
131
165
  * calls execute in the AUT, not the Cypress runner.
132
166
  */
133
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
+ }
134
191
  try {
135
192
  const result = await sendCDP('Page.createIsolatedWorld', {
136
193
  frameId,
@@ -476,6 +533,13 @@ Cypress.Commands.add('disposeA11yOracle', () => {
476
533
  // Engine was already disabled via orchestrator (same CDP session)
477
534
  engine = null;
478
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;
479
543
  autFrameId = null;
480
544
  autContextId = null;
481
545
  autIframeBounds = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a11y-oracle/cypress-plugin",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
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.3.0",
49
- "@a11y-oracle/keyboard-engine": "1.3.0",
50
- "@a11y-oracle/audit-formatter": "1.3.0",
48
+ "@a11y-oracle/core-engine": "1.3.2",
49
+ "@a11y-oracle/keyboard-engine": "1.3.2",
50
+ "@a11y-oracle/audit-formatter": "1.3.2",
51
51
  "tslib": "^2.3.0"
52
52
  }
53
53
  }