11/**
22 * Timeout Diagnostics - Smart suggestions for timeout errors
3- *
3+ *
44 * This module provides contextual diagnostics and actionable suggestions
55 * when commands timeout, helping developers quickly identify and fix issues.
66 */
77
8- import _ from 'lodash'
9-
108interface TimeoutContext {
119 command : string
1210 selector ?: string
@@ -32,7 +30,7 @@ export class TimeoutDiagnostics {
3230 dynamicContent : / l o a d i n g | s p i n n e r | s k e l e t o n | p l a c e h o l d e r / i,
3331 asyncLoad : / f e t c h | a p i | g r a p h q l | a j a x / i,
3432 animation : / f a d e | s l i d e | a n i m a t e | t r a n s i t i o n / i,
35-
33+
3634 // Network patterns
3735 slowNetwork : 3000 , // threshold in ms
3836 manyRequests : 5 ,
@@ -41,7 +39,7 @@ export class TimeoutDiagnostics {
4139 /**
4240 * Generate diagnostic suggestions based on timeout context
4341 */
44- static analyze ( context : TimeoutContext ) : DiagnosticSuggestion [ ] {
42+ static analyze ( context : TimeoutContext ) : DiagnosticSuggestion [ ] {
4543 const suggestions : DiagnosticSuggestion [ ] = [ ]
4644
4745 // Check for common selector issues
@@ -72,13 +70,13 @@ export class TimeoutDiagnostics {
7270 return suggestions
7371 }
7472
75- private static analyzeSelectorIssues ( context : TimeoutContext ) : DiagnosticSuggestion [ ] {
73+ private static analyzeSelectorIssues ( context : TimeoutContext ) : DiagnosticSuggestion [ ] {
7674 const suggestions : DiagnosticSuggestion [ ] = [ ]
7775 const { selector = '' , command } = context
7876
7977 // Check for dynamic content indicators
8078 if ( this . COMMON_PATTERNS . dynamicContent . test ( selector ) ) {
81- const escapedSelector = selector . replace ( / ' / g , "\\'" ) ;
79+ const escapedSelector = this . escapeSelector ( selector )
8280
8381 suggestions . push ( {
8482 reason : 'The selector appears to target dynamic/loading content that may not be ready yet' ,
@@ -94,8 +92,8 @@ export class TimeoutDiagnostics {
9492
9593 // Check for potentially incorrect selectors
9694 if ( selector . includes ( ' ' ) && ! selector . includes ( '[' ) && command === 'get' ) {
97- const escapedFirst = selector . split ( ' ' ) [ 0 ] . replace ( / ' / g , "\\'" ) ;
98- const escapedRest = selector . split ( ' ' ) . slice ( 1 ) . join ( ' ' ) . replace ( / ' / g , "\\'" ) ;
95+ const escapedFirst = this . escapeSelector ( selector . split ( ' ' ) [ 0 ] )
96+ const escapedRest = this . escapeSelector ( selector . split ( ' ' ) . slice ( 1 ) . join ( ' ' ) )
9997
10098 suggestions . push ( {
10199 reason : 'Complex selector detected - might be fragile or incorrect' ,
@@ -109,24 +107,27 @@ export class TimeoutDiagnostics {
109107 }
110108
111109 // Check for ID selectors that might be dynamic
112- if ( selector . startsWith ( '#' ) && / \d { 3 , } / . test ( selector ) ) {
113- const prefix = selector . split ( / \d / ) [ 0 ] ;
114- const escapedPrefix = prefix . replace ( / ' / g, "\\'" ) ;
115-
116- suggestions . push ( {
117- reason : 'Selector uses an ID with numbers - might be dynamically generated' ,
118- suggestions : [
119- 'Dynamic IDs change between sessions and will cause flaky tests' ,
120- 'Use a data-cy attribute or a more stable selector instead' ,
121- `If the ID is dynamic, use a partial match: cy.get('[id^="${ escapedPrefix } "]').first()` ,
122- ] ,
123- } )
110+ if ( selector . startsWith ( '#' ) ) {
111+ const prefixMatch = selector . match ( / ^ # ( [ ^ \d ] + ) \d { 3 , } / )
112+
113+ if ( prefixMatch ) {
114+ const escapedPrefix = this . escapeSelector ( prefixMatch [ 1 ] )
115+
116+ suggestions . push ( {
117+ reason : 'Selector uses an ID with numbers - might be dynamically generated' ,
118+ suggestions : [
119+ 'Dynamic IDs change between sessions and will cause flaky tests' ,
120+ 'Use a data-cy attribute or a more stable selector instead' ,
121+ `If the ID is dynamic, use a partial match: cy.get('[id^="${ escapedPrefix } "]').first()` ,
122+ ] ,
123+ } )
124+ }
124125 }
125126
126127 return suggestions
127128 }
128129
129- private static analyzeNetworkIssues ( context : TimeoutContext ) : DiagnosticSuggestion [ ] {
130+ private static analyzeNetworkIssues ( context : TimeoutContext ) : DiagnosticSuggestion [ ] {
130131 const suggestions : DiagnosticSuggestion [ ] = [ ]
131132 const { networkRequests = 0 , timeout } = context
132133
@@ -161,7 +162,7 @@ export class TimeoutDiagnostics {
161162 return suggestions
162163 }
163164
164- private static analyzeAnimationIssues ( context : TimeoutContext ) : DiagnosticSuggestion [ ] {
165+ private static analyzeAnimationIssues ( context : TimeoutContext ) : DiagnosticSuggestion [ ] {
165166 return [ {
166167 reason : 'Animations are still running when the command timed out' ,
167168 suggestions : [
@@ -174,7 +175,7 @@ export class TimeoutDiagnostics {
174175 } ]
175176 }
176177
177- private static analyzeDOMMutationIssues ( context : TimeoutContext ) : DiagnosticSuggestion {
178+ private static analyzeDOMMutationIssues ( context : TimeoutContext ) : DiagnosticSuggestion {
178179 return {
179180 reason : `The DOM is changing rapidly (${ context . domMutations } mutations detected)` ,
180181 suggestions : [
@@ -187,20 +188,19 @@ export class TimeoutDiagnostics {
187188 }
188189 }
189190
190- private static getGeneralSuggestions ( context : TimeoutContext ) : DiagnosticSuggestion {
191+ private static getGeneralSuggestions ( context : TimeoutContext ) : DiagnosticSuggestion {
191192 const { command, timeout, selector } = context
193+ const escapedSelector = selector ? this . escapeSelector ( selector ) : undefined
192194
193195 const generalSuggestions = [
194- `Increase timeout if needed: cy.${ command } (${ selector ? `'${ selector } ', ` : '' } { timeout: ${ timeout * 2 } })` ,
196+ `Increase timeout if needed: cy.${ command } (${ escapedSelector ? `'${ escapedSelector } ', ` : '' } { timeout: ${ timeout * 2 } })` ,
195197 'Verify the element/condition you\'re waiting for actually appears' ,
196198 'Check the browser console and Network tab for errors' ,
197199 'Use .debug() before the failing command to inspect the state: cy.debug()' ,
198200 ]
199201
200202 // Add command-specific suggestions
201- if ( [ 'get' , 'contains' ] . includes ( command ) && selector ) {
202- const escapedSelector = selector . replace ( / ' / g, "\\'" ) ;
203-
203+ if ( command === 'get' && escapedSelector ) {
204204 generalSuggestions . unshift (
205205 `Verify selector in DevTools: document.querySelector('${ escapedSelector } ')` ,
206206 'Ensure the element is not hidden by CSS (display: none, visibility: hidden)' ,
@@ -221,17 +221,23 @@ export class TimeoutDiagnostics {
221221 }
222222 }
223223
224+ private static escapeSelector ( selector : string ) : string {
225+ return selector
226+ . replace ( / \\ / g, '\\\\' )
227+ . replace ( / ' / g, '\\\'' )
228+ }
229+
224230 /**
225231 * Format diagnostic suggestions into a readable message
226232 */
227- static formatSuggestions ( suggestions : DiagnosticSuggestion [ ] ) : string {
233+ static formatSuggestions ( suggestions : DiagnosticSuggestion [ ] ) : string {
228234 if ( suggestions . length === 0 ) return ''
229235
230236 let output = '\n\n🔍 Diagnostic Suggestions:\n'
231237
232238 suggestions . forEach ( ( suggestion , index ) => {
233239 output += `\n${ index + 1 } . ${ suggestion . reason } \n`
234-
240+
235241 suggestion . suggestions . forEach ( ( tip , tipIndex ) => {
236242 output += ` ${ String . fromCharCode ( 97 + tipIndex ) } ) ${ tip } \n`
237243 } )
@@ -247,10 +253,10 @@ export class TimeoutDiagnostics {
247253 /**
248254 * Enhanced error message with diagnostics
249255 */
250- static enhanceTimeoutError ( originalMessage : string , context : TimeoutContext ) : string {
256+ static enhanceTimeoutError ( originalMessage : string , context : TimeoutContext ) : string {
251257 const suggestions = this . analyze ( context )
252258 const diagnostics = this . formatSuggestions ( suggestions )
253-
259+
254260 return originalMessage + diagnostics
255261 }
256262}
0 commit comments