@@ -11,6 +11,7 @@ import type $Command from './command'
1111import type { StateFunc } from './state'
1212import type { $Cy } from './cy'
1313import type { IStability } from '../cy/stability'
14+ import { isJquery } from '../dom/jquery'
1415
1516const debugErrors = Debug ( 'cypress:driver:errors' )
1617
@@ -228,7 +229,7 @@ export class CommandQueue extends Queue<$Command> {
228229 this . state ( 'isStable' , true )
229230 }
230231
231- private runCommand ( command : $Command ) {
232+ private async runCommand ( command : $Command ) {
232233 const startTime = performance . now ( )
233234 const isQuery = command . get ( 'query' )
234235 const name = command . get ( 'name' )
@@ -244,177 +245,178 @@ export class CommandQueue extends Queue<$Command> {
244245 this . state ( 'current' , command )
245246 this . state ( 'chainerId' , command . get ( 'chainerId' ) )
246247
247- return this . stability . whenStable ( ( ) => {
248- this . state ( 'nestedIndex' , this . index )
249-
250- return command . get ( 'args' )
251- } )
252- . then ( ( args : any ) => {
253- // store this if we enqueue new commands
254- // to check for promise violations
255- let ret
256- let enqueuedCmd
257-
258- // Queries can invoke other queries - they are synchronous, and get added to the subject chain without
259- // issue. But they cannot contain commands, which are async.
260- // This callback watches to ensure users don't try and invoke any commands while inside a query.
261- const commandEnqueued = ( obj : Cypress . EnqueuedCommandAttributes ) => {
262- if ( isQuery && ! obj . query ) {
263- $errUtils . throwErrByPath (
264- 'query_command.invoked_action' , {
265- args : {
266- name,
267- action : obj . name ,
268- } ,
269- } ,
270- )
271- }
272-
273- return enqueuedCmd = obj
274- }
275-
276- // only check for command enqueuing when none
277- // of our args are functions else commands
278- // like cy.then or cy.each would always fail
279- // since they return promises and queue more
280- // new commands
281- if ( $utils . noArgsAreAFunction ( args ) ) {
282- Cypress . once ( 'command:enqueued' , commandEnqueued )
283- }
284-
285- args = [ command . get ( 'chainerId' ) , ...args ]
286-
287- // run the command's fn with runnable's context
248+ let args = await new Promise < any > ( ( resolve , reject ) => {
288249 try {
289- command . start ( )
290- ret = __stackReplacementMarker ( command . get ( 'fn' ) , args )
291-
292- // Queries return a function which takes the current subject and returns the next subject. We wrap this in
293- // retryQuery() - and let it retry until it passes, times out or is cancelled.
294- // We save the original return value on the $Command though - it's what gets added to the subject chain later.
295- if ( isQuery ) {
296- command . set ( 'queryFn' , ret )
297- ret = retryQuery ( command , ret , this . cy )
298- }
250+ this . stability . whenStable ( ( ) => {
251+ this . state ( 'nestedIndex' , this . index )
252+ resolve ( command . get ( 'args' ) )
253+ } )
299254 } catch ( err ) {
300- throw err
301- } finally {
302- // always remove this listener
303- Cypress . removeListener ( 'command:enqueued' , commandEnqueued )
255+ reject ( err )
304256 }
257+ } )
305258
306- this . state ( 'commandIntermediateValue' , ret )
307-
308- // we cannot pass our cypress instance or our chainer
309- // back into bluebird else it will create a thenable
310- // which is never resolved
311- if ( this . cy . isCy ( ret ) ) {
312- return null
313- }
259+ // store this if we enqueue new commands
260+ // to check for promise violations
261+ let subject
262+ let enqueuedCmd
314263
315- if ( ! ( ! enqueuedCmd || ! $utils . isPromiseLike ( ret ) ) ) {
264+ // Queries can invoke other queries - they are synchronous, and get added to the subject chain without
265+ // issue. But they cannot contain commands, which are async.
266+ // This callback watches to ensure users don't try and invoke any commands while inside a query.
267+ const commandEnqueued = ( obj : Cypress . EnqueuedCommandAttributes ) => {
268+ if ( isQuery && ! obj . query ) {
316269 $errUtils . throwErrByPath (
317- 'miscellaneous.command_returned_promise_and_commands ' , {
270+ 'query_command.invoked_action ' , {
318271 args : {
319- current : name ,
320- called : enqueuedCmd . name ,
272+ name,
273+ action : obj . name ,
321274 } ,
322275 } ,
323276 )
324277 }
325278
326- if ( ! ( ! enqueuedCmd || ! ! _ . isUndefined ( ret ) ) ) {
327- ret = _ . isFunction ( ret ) ?
328- ret . toString ( ) :
329- $utils . stringify ( ret )
330-
331- // if we got a return value and we enqueued
332- // a new command and we didn't return cy
333- // or an undefined value then throw
334- $errUtils . throwErrByPath (
335- 'miscellaneous.returned_value_and_commands_from_custom_command' , {
336- args : { current : name , returned : ret } ,
337- } ,
338- )
339- }
340-
341- return ret
342- } )
343- . then ( ( subject ) => {
344- // we may be given a regular array here so
345- // we need to re-wrap the array in jquery
346- // if that's the case if the first item
347- // in this subject is a jquery element.
348- // we want to do this because in 3.1.2 there
349- // was a regression when wrapping an array of elements
350- const firstSubject = $utils . unwrapFirst ( subject )
351-
352- // if ret is a DOM element and its not an instance of our own jQuery
353- if ( subject && $dom . isElement ( firstSubject ) && ! $utils . isInstanceOf ( subject , $ ) ) {
354- // set it back to our own jquery object
355- // to prevent it from being passed downstream
356- // TODO: enable turning this off
357- // wrapSubjectsInJquery: false
358- // which will just pass subjects downstream
359- // without modifying them
360- subject = $dom . wrap ( subject )
361- }
279+ return enqueuedCmd = obj
280+ }
362281
363- command . set ( { subject } )
364- command . pass ( )
282+ // only check for command enqueuing when none
283+ // of our args are functions else commands
284+ // like cy.then or cy.each would always fail
285+ // since they return promises and queue more
286+ // new commands
287+ if ( $utils . noArgsAreAFunction ( args ) ) {
288+ Cypress . once ( 'command:enqueued' , commandEnqueued )
289+ }
365290
366- const numElements = subject ? subject . length ?? 1 : 0
291+ args = [ command . get ( 'chainerId' ) , ... args ]
367292
368- // end / snapshot our logs if they need it
369- command . finishLogs ( )
293+ // run the command's fn with runnable's context
294+ try {
295+ command . start ( )
296+ subject = __stackReplacementMarker ( command . get ( 'fn' ) , args )
370297
298+ // Queries return a function which takes the current subject and returns the next subject. We wrap this in
299+ // retryQuery() - and let it retry until it passes, times out or is cancelled.
300+ // We save the original return value on the $Command though - it's what gets added to the subject chain later.
371301 if ( isQuery ) {
372- subject = command . get ( 'queryFn' )
373- // For queries, the "subject" here is the query's return value, which is a function which
374- // accepts a subject and returns a subject, and can be re-invoked at any time.
375-
376- subject . commandName = name
377- subject . args = command . get ( 'args' )
378-
379- // Even though we've snapshotted, we only end the logs a query's logs if we're at the end of a query
380- // chain - either there is no next command (end of a test), the next command is an action, or the next
381- // command belongs to another chainer (end of a chain).
382-
383- // This is done so that any query's logs remain in the 'pending' state until the subject chain is finished.
384- this . cy . addQueryToChainer ( command . get ( 'chainerId' ) , subject )
385- } else {
386- // For commands, the "subject" here is the command's return value, which replaces
387- // the current subject chain. We cannot re-invoke commands - the return value here is final.
388- this . cy . setSubjectForChainer ( command . get ( 'chainerId' ) , [ subject ] )
302+ command . set ( 'queryFn' , subject )
303+ subject = retryQuery ( command , subject , this . cy )
389304 }
305+ } catch ( err ) {
306+ throw err
307+ } finally {
308+ // always remove this listener
309+ Cypress . removeListener ( 'command:enqueued' , commandEnqueued )
310+ }
390311
391- // TODO: This line was causing subjects to be cleaned up prematurely in some instances (Specifically seen on the within command)
392- // The command log would print the yielded value as null if checked outside of the current command chain.
393- // this.cleanSubjects()
312+ this . state ( 'commandIntermediateValue' , subject )
394313
395- this . state ( {
396- commandIntermediateValue : undefined ,
397- // reset the nestedIndex back to null
398- nestedIndex : null ,
399- // we're finished with the current command so set it back to null
400- current : null ,
401- } )
314+ // we cannot pass our cypress instance or our chainer
315+ // back into bluebird else it will create a thenable
316+ // which is never resolved
317+ if ( this . cy . isCy ( subject ) ) {
318+ return null
319+ }
402320
403- const duration = performance . now ( ) - startTime
321+ if ( ! ( ! enqueuedCmd || ! $utils . isPromiseLike ( subject ) ) ) {
322+ $errUtils . throwErrByPath (
323+ 'miscellaneous.command_returned_promise_and_commands' , {
324+ args : {
325+ current : name ,
326+ called : enqueuedCmd . name ,
327+ } ,
328+ } ,
329+ )
330+ }
404331
405- Cypress . automation ( 'log:command:performance' , {
406- name : command ?. attributes ?. name ?? 'unknown' ,
407- startTime,
408- duration,
409- detail : {
410- runnableTitle : ( this . state ( 'runnable' ) ?? { } ) . title ?? 'unknown' ,
411- spec : Cypress . spec . relative ,
412- numElements,
332+ if ( ! ( ! enqueuedCmd || ! ! _ . isUndefined ( subject ) ) ) {
333+ subject = _ . isFunction ( subject ) ?
334+ subject . toString ( ) :
335+ $utils . stringify ( subject )
336+
337+ // if we got a return value and we enqueued
338+ // a new command and we didn't return cy
339+ // or an undefined value then throw
340+ $errUtils . throwErrByPath (
341+ 'miscellaneous.returned_value_and_commands_from_custom_command' , {
342+ args : { current : name , returned : subject } ,
413343 } ,
414- } )
344+ )
345+ }
346+
347+ // we may be given a regular array here so
348+ // we need to re-wrap the array in jquery
349+ // if that's the case if the first item
350+ // in this subject is a jquery element.
351+ // we want to do this because in 3.1.2 there
352+ // was a regression when wrapping an array of elements
353+ const firstSubject = $utils . unwrapFirst ( subject )
354+
355+ // if ret is a DOM element and its not an instance of our own jQuery
356+ if ( subject && $dom . isElement ( firstSubject ) && ! $utils . isInstanceOf ( subject , $ ) ) {
357+ // set it back to our own jquery object
358+ // to prevent it from being passed downstream
359+ // TODO: enable turning this off
360+ // wrapSubjectsInJquery: false
361+ // which will just pass subjects downstream
362+ // without modifying them
363+ subject = $dom . wrap ( subject )
364+ }
365+
366+ command . set ( { subject } )
367+ command . pass ( )
368+
369+ const numElements = isJquery ( subject ) ? subject . length : 0
415370
416- return subject
371+ // end / snapshot our logs if they need it
372+ command . finishLogs ( )
373+
374+ if ( isQuery ) {
375+ subject = command . get ( 'queryFn' )
376+ // For queries, the "subject" here is the query's return value, which is a function which
377+ // accepts a subject and returns a subject, and can be re-invoked at any time.
378+
379+ subject . commandName = name
380+ subject . args = command . get ( 'args' )
381+
382+ // Even though we've snapshotted, we only end the logs a query's logs if we're at the end of a query
383+ // chain - either there is no next command (end of a test), the next command is an action, or the next
384+ // command belongs to another chainer (end of a chain).
385+
386+ // This is done so that any query's logs remain in the 'pending' state until the subject chain is finished.
387+ this . cy . addQueryToChainer ( command . get ( 'chainerId' ) , subject )
388+ } else {
389+ // For commands, the "subject" here is the command's return value, which replaces
390+ // the current subject chain. We cannot re-invoke commands - the return value here is final.
391+ this . cy . setSubjectForChainer ( command . get ( 'chainerId' ) , [ subject ] )
392+ }
393+
394+ // TODO: This line was causing subjects to be cleaned up prematurely in some instances (Specifically seen on the within command)
395+ // The command log would print the yielded value as null if checked outside of the current command chain.
396+ // this.cleanSubjects()
397+
398+ this . state ( {
399+ commandIntermediateValue : undefined ,
400+ // reset the nestedIndex back to null
401+ nestedIndex : null ,
402+ // we're finished with the current command so set it back to null
403+ current : null ,
404+ } )
405+
406+ const duration = performance . now ( ) - startTime
407+
408+ await Cypress . automation ( 'log:command:performance' , {
409+ name : command ?. attributes ?. name ?? 'unknown' ,
410+ startTime,
411+ duration,
412+ detail : {
413+ runnableTitle : ( this . state ( 'runnable' ) ?? { } ) . title ?? 'unknown' ,
414+ spec : Cypress . spec . relative ,
415+ numElements,
416+ } ,
417417 } )
418+
419+ return subject
418420 }
419421
420422 // TypeScript doesn't allow overriding functions with different type signatures
0 commit comments