@@ -4,7 +4,27 @@ const browserStackLog = (message) => {
44 if ( ! Cypress . env ( 'BROWSERSTACK_LOGS' ) ) return ;
55 cy . task ( 'browserstack_log' , message ) ;
66}
7-
7+
8+ // Circuit breaker for a dead/unresponsive accessibility scanner.
9+ // Each hung scan/save costs up to ACCESSIBILITY_SCAN_TIMEOUT (default 25s). Without a
10+ // breaker, a scanner that never responds stalls EVERY test's afterEach (and every
11+ // wrapped command) by that much. After N consecutive timeouts we stop attempting
12+ // accessibility work for the remainder of this spec file (module state resets per spec).
13+ let consecutiveA11yTimeouts = 0 ;
14+ let a11yCircuitOpen = false ;
15+ let a11yCircuitLogged = false ;
16+ const getA11yCircuitLimit = ( ) => parseInt ( Cypress . env ( 'ACCESSIBILITY_SCAN_CIRCUIT_LIMIT' ) ) || 3 ;
17+ const noteA11yTimeout = ( ) => {
18+ consecutiveA11yTimeouts += 1 ;
19+ if ( ! a11yCircuitOpen && consecutiveA11yTimeouts >= getA11yCircuitLimit ( ) ) {
20+ a11yCircuitOpen = true ;
21+ // eslint-disable-next-line no-console
22+ console . warn ( 'BrowserStack Accessibility: scanner did not respond ' + consecutiveA11yTimeouts + ' consecutive times; skipping accessibility scans for the remaining tests in this spec.' ) ;
23+ }
24+ } ;
25+ const noteA11ySuccess = ( ) => { consecutiveA11yTimeouts = 0 ; } ;
26+
27+
828const commandsToWrap = [ 'visit' , 'click' , 'type' , 'request' , 'dblclick' , 'rightclick' , 'clear' , 'check' , 'uncheck' , 'select' , 'trigger' , 'selectFile' , 'scrollIntoView' , 'scroll' , 'scrollTo' , 'blur' , 'focus' , 'go' , 'reload' , 'submit' , 'viewport' , 'origin' ] ;
929// scroll is not a default function in cypress.
1030const commandToOverwrite = [ 'visit' , 'click' , 'type' , 'request' , 'dblclick' , 'rightclick' , 'clear' , 'check' , 'uncheck' , 'select' , 'trigger' , 'selectFile' , 'scrollIntoView' , 'scrollTo' , 'blur' , 'focus' , 'go' , 'reload' , 'submit' , 'viewport' , 'origin' ] ;
@@ -44,56 +64,74 @@ const performModifiedScan = (originalFn, Subject, stateType, ...args) => {
4464}
4565
4666const performScan = ( win , payloadToSend ) =>
47- new Promise ( async ( resolve , reject ) => {
48- const isHttpOrHttps = / ^ ( h t t p | h t t p s ) : $ / . test ( win . location . protocol ) ;
49- if ( ! isHttpOrHttps ) {
50- return resolve ( ) ;
67+ new Promise ( ( resolve ) => {
68+ // This promise MUST always settle (never hang, never reject). It runs inside the
69+ // global afterEach; if it hangs, cy.wrap()'s 30s timeout fails the hook and Cypress skips
70+ // the rest of the spec. Failure modes guarded here:
71+ // - the injected scanner never dispatches A11Y_SCAN_FINISHED (page mid-navigation / slow scan)
72+ // - win is cross-origin (e.g. an SSO redirect) so win.location / win.document throw synchronously
73+ if ( a11yCircuitOpen ) {
74+ // Scanner has repeatedly not responded in this spec — don't stall this test too.
75+ return resolve ( "Accessibility scan skipped: scanner unresponsive (circuit open)" ) ;
5176 }
77+ let settled = false ;
78+ const finish = ( val ) => { if ( ! settled ) { settled = true ; clearTimeout ( overallTimer ) ; resolve ( val ) ; } } ;
79+ const overallTimeout = parseInt ( Cypress . env ( 'ACCESSIBILITY_SCAN_TIMEOUT' ) ) || 25000 ;
80+ const overallTimer = setTimeout ( ( ) => { noteA11yTimeout ( ) ; finish ( "Accessibility scan timed out" ) ; } , overallTimeout ) ;
5281
53- function findAccessibilityAutomationElement ( ) {
54- return win . document . querySelector ( "#accessibility-automation-element" ) ;
55- }
82+ try {
83+ const isHttpOrHttps = / ^ ( h t t p | h t t p s ) : $ / . test ( win . location . protocol ) ;
84+ if ( ! isHttpOrHttps ) {
85+ return finish ( ) ;
86+ }
5687
57- function waitForScannerReadiness ( retryCount = 100 , retryInterval = 100 ) {
58- return new Promise ( async ( resolve , reject ) => {
59- let count = 0 ;
60- const intervalID = setInterval ( async ( ) => {
61- if ( count > retryCount ) {
62- clearInterval ( intervalID ) ;
63- return reject (
64- new Error (
65- "Accessibility Automation Scanner is not ready on the page."
66- )
67- ) ;
68- } else if ( findAccessibilityAutomationElement ( ) ) {
69- clearInterval ( intervalID ) ;
70- return resolve ( "Scanner set" ) ;
71- } else {
72- count += 1 ;
73- }
74- } , retryInterval ) ;
75- } ) ;
76- }
88+ function findAccessibilityAutomationElement ( ) {
89+ return win . document . querySelector ( "#accessibility-automation-element" ) ;
90+ }
7791
78- function startScan ( ) {
79- function onScanComplete ( ) {
80- win . removeEventListener ( "A11Y_SCAN_FINISHED" , onScanComplete ) ;
81- return resolve ( ) ;
92+ function waitForScannerReadiness ( retryCount = 100 , retryInterval = 100 ) {
93+ return new Promise ( ( resolve , reject ) => {
94+ let count = 0 ;
95+ const intervalID = setInterval ( ( ) => {
96+ if ( count > retryCount ) {
97+ clearInterval ( intervalID ) ;
98+ return reject (
99+ new Error (
100+ "Accessibility Automation Scanner is not ready on the page."
101+ )
102+ ) ;
103+ } else if ( findAccessibilityAutomationElement ( ) ) {
104+ clearInterval ( intervalID ) ;
105+ return resolve ( "Scanner set" ) ;
106+ } else {
107+ count += 1 ;
108+ }
109+ } , retryInterval ) ;
110+ } ) ;
82111 }
83112
84- win . addEventListener ( "A11Y_SCAN_FINISHED" , onScanComplete ) ;
85- const e = new CustomEvent ( "A11Y_SCAN" , { detail : payloadToSend } ) ;
86- win . dispatchEvent ( e ) ;
87- }
113+ function startScan ( ) {
114+ function onScanComplete ( ) {
115+ win . removeEventListener ( "A11Y_SCAN_FINISHED" , onScanComplete ) ;
116+ if ( ! settled ) noteA11ySuccess ( ) ;
117+ return finish ( ) ;
118+ }
88119
89- if ( findAccessibilityAutomationElement ( ) ) {
90- startScan ( ) ;
91- } else {
92- waitForScannerReadiness ( )
93- . then ( startScan )
94- . catch ( async ( err ) => {
95- return resolve ( "Scanner is not ready on the page after multiple retries. performscan" ) ;
96- } ) ;
120+ win . addEventListener ( "A11Y_SCAN_FINISHED" , onScanComplete ) ;
121+ const e = new CustomEvent ( "A11Y_SCAN" , { detail : payloadToSend } ) ;
122+ win . dispatchEvent ( e ) ;
123+ }
124+
125+ if ( findAccessibilityAutomationElement ( ) ) {
126+ startScan ( ) ;
127+ } else {
128+ waitForScannerReadiness ( )
129+ . then ( startScan )
130+ . catch ( ( ) => finish ( "Scanner is not ready on the page after multiple retries. performscan" ) ) ;
131+ }
132+ } catch ( err ) {
133+ // cross-origin window access or any unexpected error must not fail the hook
134+ finish ( ) ;
97135 }
98136} )
99137
@@ -206,11 +244,20 @@ new Promise((resolve) => {
206244} ) ;
207245
208246const saveTestResults = ( win , payloadToSend ) =>
209- new Promise ( ( resolve , reject ) => {
247+ new Promise ( ( resolve ) => {
248+ // Must always settle (see performScan note) so a slow/absent A11Y_RESULTS_SAVED
249+ // event or a cross-origin window cannot fail the afterEach hook.
250+ if ( a11yCircuitOpen ) {
251+ return resolve ( "Accessibility results save skipped: scanner unresponsive (circuit open)" ) ;
252+ }
253+ let settled = false ;
254+ const finish = ( val ) => { if ( ! settled ) { settled = true ; clearTimeout ( overallTimer ) ; resolve ( val ) ; } } ;
255+ const overallTimeout = parseInt ( Cypress . env ( 'ACCESSIBILITY_SCAN_TIMEOUT' ) ) || 25000 ;
256+ const overallTimer = setTimeout ( ( ) => { noteA11yTimeout ( ) ; finish ( "Accessibility results save timed out" ) ; } , overallTimeout ) ;
210257 try {
211258 const isHttpOrHttps = / ^ ( h t t p | h t t p s ) : $ / . test ( win . location . protocol ) ;
212259 if ( ! isHttpOrHttps ) {
213- resolve ( "Unable to save accessibility results, Invalid URL." ) ;
260+ finish ( "Unable to save accessibility results, Invalid URL." ) ;
214261 return ;
215262 }
216263
@@ -241,7 +288,9 @@ new Promise( (resolve, reject) => {
241288
242289 function saveResults ( ) {
243290 function onResultsSaved ( event ) {
244- return resolve ( ) ;
291+ win . removeEventListener ( "A11Y_RESULTS_SAVED" , onResultsSaved ) ;
292+ if ( ! settled ) noteA11ySuccess ( ) ;
293+ return finish ( ) ;
245294 }
246295 win . addEventListener ( "A11Y_RESULTS_SAVED" , onResultsSaved ) ;
247296 const e = new CustomEvent ( "A11Y_SAVE_RESULTS" , {
@@ -255,13 +304,11 @@ new Promise( (resolve, reject) => {
255304 } else {
256305 waitForScannerReadiness ( )
257306 . then ( saveResults )
258- . catch ( async ( err ) => {
259- return resolve ( "Scanner is not ready on the page after multiple retries. after run" ) ;
260- } ) ;
307+ . catch ( ( ) => finish ( "Scanner is not ready on the page after multiple retries. after run" ) ) ;
261308 }
262309 } catch ( error ) {
263- browserStackLog ( `Error in saving results with error: ${ error . message } ` ) ;
264- return resolve ( ) ;
310+ browserStackLog ( `Error in saving results with error: ${ error . message } ` ) ;
311+ finish ( ) ;
265312 }
266313
267314} )
@@ -317,6 +364,17 @@ commandToOverwrite.forEach((command) => {
317364} ) ;
318365
319366afterEach ( ( ) => {
367+ // Nothing that happens inside this accessibility hook may fail the user's
368+ // test or abort the remaining tests in the spec. Cypress chains have no .catch, so
369+ // suppress any failure raised while this hook's commands run (e.g. cy.window() on a
370+ // cross-origin page after an SSO redirect, or a cy.task that is not registered) via
371+ // the per-test 'fail' listener. Returning false prevents Cypress from failing the
372+ // test; the listener is scoped to the current test and auto-removed afterwards.
373+ cy . on ( 'fail' , ( err ) => {
374+ // eslint-disable-next-line no-console
375+ console . warn ( `BrowserStack Accessibility: suppressed afterEach error: ${ err && err . message } ` ) ;
376+ return false ;
377+ } ) ;
320378 const attributes = Cypress . mocha . getRunner ( ) . suite . ctx . currentTest ;
321379 cy . window ( ) . then ( async ( win ) => {
322380 let shouldScanTestForAccessibility = shouldScanForAccessibility ( attributes ) ;
0 commit comments