@@ -182,6 +182,13 @@ export default class Maestro {
182182 return result ;
183183 } catch ( error ) {
184184 logger . error ( error instanceof Error ? error . message : error ) ;
185+ // Display the cause if available
186+ if ( error instanceof Error && error . cause ) {
187+ const causeMessage = this . extractErrorMessage ( error . cause ) ;
188+ if ( causeMessage ) {
189+ logger . error ( ` Reason: ${ causeMessage } ` ) ;
190+ }
191+ }
185192 return { success : false , runs : [ ] } ;
186193 }
187194 }
@@ -446,7 +453,6 @@ export default class Maestro {
446453 try {
447454 const capabilities = this . options . getCapabilities ( this . detectedPlatform ) ;
448455 const maestroOptions = this . options . getMaestroOptions ( ) ;
449-
450456 const response = await axios . post (
451457 `${ this . URL } /${ this . appId } /run` ,
452458 {
@@ -471,13 +477,19 @@ export default class Maestro {
471477
472478 const result = response . data ;
473479 if ( result . success === false ) {
480+ // API returns errors as an array
481+ const errorMessage =
482+ result . errors ?. join ( '\n' ) || result . error || 'Unknown error' ;
474483 throw new TestingBotError ( `Running Maestro test failed` , {
475- cause : result . error ,
484+ cause : errorMessage ,
476485 } ) ;
477486 }
478487
479488 return true ;
480489 } catch ( error ) {
490+ if ( error instanceof TestingBotError ) {
491+ throw error ;
492+ }
481493 throw new TestingBotError ( `Running Maestro test failed` , {
482494 cause : error ,
483495 } ) ;
@@ -506,21 +518,31 @@ export default class Maestro {
506518
507519 private async waitForCompletion ( ) : Promise < MaestroResult > {
508520 let attempts = 0 ;
521+ const startTime = Date . now ( ) ;
522+ const previousStatus : Map < number , MaestroRunInfo [ 'status' ] > = new Map ( ) ;
509523
510524 while ( attempts < this . MAX_POLL_ATTEMPTS ) {
511525 const status = await this . getStatus ( ) ;
512526
513527 // Log current status of runs (unless quiet mode)
514528 if ( ! this . options . quiet ) {
515- for ( const run of status . runs ) {
516- const statusEmoji = this . getStatusEmoji ( run . status ) ;
517- logger . info (
518- ` ${ statusEmoji } Run ${ run . id } (${ run . capabilities . deviceName } ): ${ run . status } ` ,
519- ) ;
520- }
529+ this . displayRunStatus ( status . runs , startTime , previousStatus ) ;
521530 }
522531
523532 if ( status . completed ) {
533+ // Clear the updating line and print final status
534+ if ( ! this . options . quiet ) {
535+ this . clearLine ( ) ;
536+ for ( const run of status . runs ) {
537+ const statusEmoji = run . success === 1 ? '✅' : '❌' ;
538+ const statusText =
539+ run . success === 1 ? 'Test completed successfully' : 'Test failed' ;
540+ console . log (
541+ ` ${ statusEmoji } Run ${ run . id } (${ run . capabilities . deviceName } ): ${ statusText } ` ,
542+ ) ;
543+ }
544+ }
545+
524546 const allSucceeded = status . runs . every ( ( run ) => run . success === 1 ) ;
525547
526548 if ( allSucceeded ) {
@@ -531,7 +553,9 @@ export default class Maestro {
531553 const failedRuns = status . runs . filter ( ( run ) => run . success !== 1 ) ;
532554 logger . error ( `${ failedRuns . length } test run(s) failed:` ) ;
533555 for ( const run of failedRuns ) {
534- logger . error ( ` - Run ${ run . id } (${ run . capabilities . deviceName } )` ) ;
556+ logger . error (
557+ ` - Run ${ run . id } (${ run . capabilities . deviceName } ): ${ run . report } ` ,
558+ ) ;
535559 }
536560 }
537561
@@ -555,6 +579,75 @@ export default class Maestro {
555579 ) ;
556580 }
557581
582+ private displayRunStatus (
583+ runs : MaestroRunInfo [ ] ,
584+ startTime : number ,
585+ previousStatus : Map < number , MaestroRunInfo [ 'status' ] > ,
586+ ) : void {
587+ const elapsedSeconds = Math . floor ( ( Date . now ( ) - startTime ) / 1000 ) ;
588+ const elapsedStr = this . formatElapsedTime ( elapsedSeconds ) ;
589+
590+ for ( const run of runs ) {
591+ const prevStatus = previousStatus . get ( run . id ) ;
592+ const statusChanged = prevStatus !== run . status ;
593+
594+ // If status changed from WAITING/READY to something else, clear the updating line
595+ if (
596+ statusChanged &&
597+ prevStatus &&
598+ ( prevStatus === 'WAITING' || prevStatus === 'READY' )
599+ ) {
600+ this . clearLine ( ) ;
601+ }
602+
603+ previousStatus . set ( run . id , run . status ) ;
604+
605+ const statusInfo = this . getStatusInfo ( run . status ) ;
606+
607+ if ( run . status === 'WAITING' || run . status === 'READY' ) {
608+ // Update the same line for WAITING and READY states
609+ const message = ` ${ statusInfo . emoji } Run ${ run . id } (${ run . capabilities . deviceName } ): ${ statusInfo . text } (${ elapsedStr } )` ;
610+ process . stdout . write ( `\r${ message } ` ) ;
611+ } else if ( statusChanged ) {
612+ // For other states (DONE, FAILED), print on a new line only when status changes
613+ console . log (
614+ ` ${ statusInfo . emoji } Run ${ run . id } (${ run . capabilities . deviceName } ): ${ statusInfo . text } ` ,
615+ ) ;
616+ }
617+ }
618+ }
619+
620+ private clearLine ( ) : void {
621+ process . stdout . write ( '\r\x1b[K' ) ;
622+ }
623+
624+ private formatElapsedTime ( seconds : number ) : string {
625+ if ( seconds < 60 ) {
626+ return `${ seconds } s` ;
627+ }
628+ const minutes = Math . floor ( seconds / 60 ) ;
629+ const remainingSeconds = seconds % 60 ;
630+ return `${ minutes } m ${ remainingSeconds } s` ;
631+ }
632+
633+ private getStatusInfo ( status : MaestroRunInfo [ 'status' ] ) : {
634+ emoji : string ;
635+ text : string ;
636+ } {
637+ switch ( status ) {
638+ case 'WAITING' :
639+ return { emoji : '⏳' , text : 'Waiting for test to start' } ;
640+ case 'READY' :
641+ return { emoji : '🔄' , text : 'Running test' } ;
642+ case 'DONE' :
643+ return { emoji : '✅' , text : 'Test has finished running' } ;
644+ case 'FAILED' :
645+ return { emoji : '❌' , text : 'Test failed' } ;
646+ default :
647+ return { emoji : '❓' , text : status } ;
648+ }
649+ }
650+
558651 private async fetchReports ( runs : MaestroRunInfo [ ] ) : Promise < void > {
559652 const reportFormat = this . options . report ;
560653 const outputDir = this . options . reportOutputDir ;
@@ -581,15 +674,24 @@ export default class Maestro {
581674 username : this . credentials . userName ,
582675 password : this . credentials . accessKey ,
583676 } ,
584- responseType : 'arraybuffer' ,
585677 } ,
586678 ) ;
587679
680+ // Extract the report content from the JSON response
681+ const reportKey =
682+ reportFormat === 'junit' ? 'junit_report' : 'html_report' ;
683+ const reportContent = response . data [ reportKey ] ;
684+
685+ if ( ! reportContent ) {
686+ logger . error ( `No ${ reportFormat } report found for run ${ run . id } ` ) ;
687+ continue ;
688+ }
689+
588690 const fileExtension = reportFormat === 'junit' ? 'xml' : 'html' ;
589691 const fileName = `report_run_${ run . id } .${ fileExtension } ` ;
590692 const filePath = path . join ( outputDir , fileName ) ;
591693
592- await fs . promises . writeFile ( filePath , response . data ) ;
694+ await fs . promises . writeFile ( filePath , reportContent , 'utf-8' ) ;
593695
594696 if ( ! this . options . quiet ) {
595697 logger . info ( ` Saved report for run ${ run . id } : ${ filePath } ` ) ;
@@ -602,22 +704,60 @@ export default class Maestro {
602704 }
603705 }
604706
605- private getStatusEmoji ( status : MaestroRunInfo [ 'status' ] ) : string {
606- switch ( status ) {
607- case 'WAITING' :
608- return '⏳' ;
609- case 'READY' :
610- return '🔄' ;
611- case 'DONE' :
612- return '✅' ;
613- case 'FAILED' :
614- return '❌' ;
615- default :
616- return '❓' ;
617- }
618- }
619-
620707 private sleep ( ms : number ) : Promise < void > {
621708 return new Promise ( ( resolve ) => setTimeout ( resolve , ms ) ) ;
622709 }
710+
711+ private extractErrorMessage ( cause : unknown ) : string | null {
712+ if ( typeof cause === 'string' ) {
713+ return cause ;
714+ }
715+
716+ // Handle arrays of errors
717+ if ( Array . isArray ( cause ) ) {
718+ return cause . join ( '\n' ) ;
719+ }
720+
721+ if ( cause && typeof cause === 'object' ) {
722+ // Handle axios errors which have response.data
723+ const axiosError = cause as {
724+ response ?: {
725+ data ?: { error ?: string ; errors ?: string [ ] ; message ?: string } ;
726+ } ;
727+ message ?: string ;
728+ } ;
729+ if ( axiosError . response ?. data ?. errors ) {
730+ return axiosError . response . data . errors . join ( '\n' ) ;
731+ }
732+ if ( axiosError . response ?. data ?. error ) {
733+ return axiosError . response . data . error ;
734+ }
735+ if ( axiosError . response ?. data ?. message ) {
736+ return axiosError . response . data . message ;
737+ }
738+
739+ // Handle standard Error objects
740+ if ( cause instanceof Error ) {
741+ return cause . message ;
742+ }
743+
744+ // Handle plain objects with errors array, error, or message property
745+ const obj = cause as {
746+ errors ?: string [ ] ;
747+ error ?: string ;
748+ message ?: string ;
749+ } ;
750+ if ( obj . errors ) {
751+ return obj . errors . join ( '\n' ) ;
752+ }
753+ if ( obj . error ) {
754+ return obj . error ;
755+ }
756+ if ( obj . message ) {
757+ return obj . message ;
758+ }
759+ }
760+
761+ return null ;
762+ }
623763}
0 commit comments