@@ -4,7 +4,27 @@ const browserStackLog = (message) => {
44 if ( ! Cypress . env ( 'BROWSERSTACK_LOGS' ) ) return ;
55 cy . task ( 'browserstack_log' , message ) ;
66}
7-
7+
8+ // SDK-6463: 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' ] ;
@@ -50,10 +70,14 @@ new Promise((resolve) => {
5070 // the rest of the spec. Failure modes guarded here:
5171 // - the injected scanner never dispatches A11Y_SCAN_FINISHED (page mid-navigation / slow scan)
5272 // - 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)" ) ;
76+ }
5377 let settled = false ;
5478 const finish = ( val ) => { if ( ! settled ) { settled = true ; clearTimeout ( overallTimer ) ; resolve ( val ) ; } } ;
5579 const overallTimeout = parseInt ( Cypress . env ( 'ACCESSIBILITY_SCAN_TIMEOUT' ) ) || 25000 ;
56- const overallTimer = setTimeout ( ( ) => finish ( "Accessibility scan timed out" ) , overallTimeout ) ;
80+ const overallTimer = setTimeout ( ( ) => { noteA11yTimeout ( ) ; finish ( "Accessibility scan timed out" ) ; } , overallTimeout ) ;
5781
5882 try {
5983 const isHttpOrHttps = / ^ ( h t t p | h t t p s ) : $ / . test ( win . location . protocol ) ;
@@ -89,6 +113,7 @@ new Promise((resolve) => {
89113 function startScan ( ) {
90114 function onScanComplete ( ) {
91115 win . removeEventListener ( "A11Y_SCAN_FINISHED" , onScanComplete ) ;
116+ if ( ! settled ) noteA11ySuccess ( ) ;
92117 return finish ( ) ;
93118 }
94119
@@ -222,10 +247,13 @@ const saveTestResults = (win, payloadToSend) =>
222247new Promise ( ( resolve ) => {
223248 // SDK-6463: must always settle (see performScan note) so a slow/absent A11Y_RESULTS_SAVED
224249 // 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+ }
225253 let settled = false ;
226254 const finish = ( val ) => { if ( ! settled ) { settled = true ; clearTimeout ( overallTimer ) ; resolve ( val ) ; } } ;
227255 const overallTimeout = parseInt ( Cypress . env ( 'ACCESSIBILITY_SCAN_TIMEOUT' ) ) || 25000 ;
228- const overallTimer = setTimeout ( ( ) => finish ( "Accessibility results save timed out" ) , overallTimeout ) ;
256+ const overallTimer = setTimeout ( ( ) => { noteA11yTimeout ( ) ; finish ( "Accessibility results save timed out" ) ; } , overallTimeout ) ;
229257 try {
230258 const isHttpOrHttps = / ^ ( h t t p | h t t p s ) : $ / . test ( win . location . protocol ) ;
231259 if ( ! isHttpOrHttps ) {
@@ -261,6 +289,7 @@ new Promise((resolve) => {
261289 function saveResults ( ) {
262290 function onResultsSaved ( event ) {
263291 win . removeEventListener ( "A11Y_RESULTS_SAVED" , onResultsSaved ) ;
292+ if ( ! settled ) noteA11ySuccess ( ) ;
264293 return finish ( ) ;
265294 }
266295 win . addEventListener ( "A11Y_RESULTS_SAVED" , onResultsSaved ) ;
@@ -335,6 +364,17 @@ commandToOverwrite.forEach((command) => {
335364} ) ;
336365
337366afterEach ( ( ) => {
367+ // SDK-6463: 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+ } ) ;
338378 const attributes = Cypress . mocha . getRunner ( ) . suite . ctx . currentTest ;
339379 cy . window ( ) . then ( async ( win ) => {
340380 let shouldScanTestForAccessibility = shouldScanForAccessibility ( attributes ) ;
0 commit comments