@@ -49,7 +49,7 @@ describe('Logger', () => {
4949 moveCursor : ( ) => true ,
5050 clearLine : ( ) => {
5151 const idx = output . lastIndexOf ( '\n' ) ;
52- output = idx >= 0 ? output . substring ( 0 , idx ) : '' ;
52+ output = idx >= 0 ? output . substring ( 0 , idx + 1 ) : '' ;
5353 return true ;
5454 } ,
5555 } ;
@@ -291,7 +291,7 @@ ${ansis.magenta('└')} ${ansis.green(`Total line coverage is ${ansis.bold('82%'
291291 performanceNowSpy . mockReturnValueOnce ( 0 ) . mockReturnValueOnce ( 42 ) ; // task duration: 42 ms
292292 } ) ;
293293
294- it ( 'should render spinner for async tasks' , async ( ) => {
294+ it ( 'should render dots spinner for async tasks' , async ( ) => {
295295 const task = new Logger ( ) . task (
296296 'Uploading report to portal' ,
297297 async ( ) => 'Uploaded report to portal' ,
@@ -397,6 +397,33 @@ ${logSymbols.success} Uploaded report to portal ${ansis.gray('(42 ms)')}
397397 ) ;
398398 } ) ;
399399
400+ it ( 'should print other logs once spinner fails' , async ( ) => {
401+ vi . stubEnv ( 'CP_VERBOSE' , 'true' ) ;
402+ const logger = new Logger ( ) ;
403+
404+ const task = logger . task ( 'Uploading report to portal' , async ( ) => {
405+ logger . debug ( 'Sent request to Portal API' ) ;
406+ await new Promise ( resolve => {
407+ setTimeout ( resolve , 42 ) ;
408+ } ) ;
409+ logger . debug ( 'Received response from Portal API' ) ;
410+ throw new Error ( 'GraphQL error: Invalid API key' ) ;
411+ } ) ;
412+
413+ expect ( output ) . toBe ( `${ ansis . cyan ( '⠋' ) } Uploading report to portal` ) ;
414+
415+ vi . advanceTimersByTime ( 42 ) ;
416+ await expect ( task ) . rejects . toThrow ( 'GraphQL error: Invalid API key' ) ;
417+
418+ expect ( output ) . toBe (
419+ `
420+ ${ logSymbols . error } Uploading report to portal → ${ ansis . red ( 'Error: GraphQL error: Invalid API key' ) }
421+ ${ ansis . gray ( 'Sent request to Portal API' ) }
422+ ${ ansis . gray ( 'Received response from Portal API' ) }
423+ ` . trimStart ( ) ,
424+ ) ;
425+ } ) ;
426+
400427 it ( 'should print other logs immediately in CI' , async ( ) => {
401428 vi . stubEnv ( 'CI' , 'true' ) ;
402429 vi . stubEnv ( 'CP_VERBOSE' , 'true' ) ;
@@ -466,4 +493,292 @@ ${logSymbols.success} Uploaded report to portal ${ansis.gray('(42 ms)')}
466493 ) ;
467494 } ) ;
468495 } ) ;
496+
497+ describe ( 'spinners + groups' , ( ) => {
498+ beforeEach ( ( ) => {
499+ performanceNowSpy
500+ . mockReturnValueOnce ( 0 )
501+ . mockReturnValueOnce ( 0 )
502+ . mockReturnValueOnce ( 42 ) // task duration: 42 ms
503+ . mockReturnValueOnce ( 50 ) ; // group duration: 50 ms;
504+ } ) ;
505+
506+ it ( 'should render line spinner for async tasks within group' , async ( ) => {
507+ const logger = new Logger ( ) ;
508+
509+ const group = logger . group ( 'Running plugin "ESLint"' , async ( ) => {
510+ await logger . command ( 'npx eslint . --format=json' , async ( ) => { } ) ;
511+ logger . warn ( 'Skipping unknown rule "deprecation/deprecation"' ) ;
512+ return 'ESLint reported 4 errors and 11 warnings' ;
513+ } ) ;
514+
515+ expect ( output ) . toBe (
516+ `
517+ ${ ansis . bold . cyan ( '❯ Running plugin "ESLint"' ) }
518+ ${ ansis . cyan ( '-' ) } ${ ansis . blue ( '$' ) } npx eslint . --format=json`,
519+ ) ;
520+
521+ vi . advanceTimersByTime ( cliSpinners . line . interval ) ;
522+ expect ( output ) . toBe (
523+ `
524+ ${ ansis . bold . cyan ( '❯ Running plugin "ESLint"' ) }
525+ ${ ansis . cyan ( '\\' ) } ${ ansis . blue ( '$' ) } npx eslint . --format=json`,
526+ ) ;
527+
528+ vi . advanceTimersByTime ( cliSpinners . line . interval ) ;
529+ expect ( output ) . toBe (
530+ `
531+ ${ ansis . bold . cyan ( '❯ Running plugin "ESLint"' ) }
532+ ${ ansis . cyan ( '|' ) } ${ ansis . blue ( '$' ) } npx eslint . --format=json`,
533+ ) ;
534+
535+ vi . advanceTimersByTime ( cliSpinners . line . interval ) ;
536+ expect ( output ) . toBe (
537+ `
538+ ${ ansis . bold . cyan ( '❯ Running plugin "ESLint"' ) }
539+ ${ ansis . cyan ( '/' ) } ${ ansis . blue ( '$' ) } npx eslint . --format=json`,
540+ ) ;
541+
542+ await expect ( group ) . resolves . toBeUndefined ( ) ;
543+
544+ expect ( output ) . toBe (
545+ `
546+ ${ ansis . bold . cyan ( '❯ Running plugin "ESLint"' ) }
547+ ${ ansis . cyan ( '│' ) } ${ ansis . green ( '$' ) } npx eslint . --format=json ${ ansis . gray ( '(42 ms)' ) }
548+ ${ ansis . cyan ( '│' ) } ${ ansis . yellow ( 'Skipping unknown rule "deprecation/deprecation"' ) }
549+ ${ ansis . cyan ( '└' ) } ${ ansis . green ( 'ESLint reported 4 errors and 11 warnings' ) } ${ ansis . gray ( '(50 ms)' ) }
550+
551+ ` ,
552+ ) ;
553+ } ) ;
554+
555+ it ( 'should colorize line spinner with same color as group' , async ( ) => {
556+ const logger = new Logger ( ) ;
557+
558+ const group1 = logger . group ( 'Running plugin "ESLint"' , async ( ) => {
559+ await logger . command ( 'npx eslint . --format=json' , async ( ) => { } ) ;
560+ return 'ESLint reported 4 errors and 11 warnings' ;
561+ } ) ;
562+
563+ expect ( output ) . toBe (
564+ `
565+ ${ ansis . bold . cyan ( '❯ Running plugin "ESLint"' ) }
566+ ${ ansis . cyan ( '-' ) } ${ ansis . blue ( '$' ) } npx eslint . --format=json`,
567+ ) ;
568+
569+ await group1 ;
570+
571+ performanceNowSpy
572+ . mockReturnValueOnce ( 0 )
573+ . mockReturnValueOnce ( 0 )
574+ . mockReturnValueOnce ( cliSpinners . line . interval ) // task duration
575+ . mockReturnValueOnce ( cliSpinners . line . interval ) ; // group duration
576+
577+ const group2 = logger . group ( 'Running plugin "Lighthouse"' , async ( ) => {
578+ await logger . task (
579+ `Executing ${ ansis . bold ( 'runLighthouse' ) } function` ,
580+ async ( ) => {
581+ await new Promise ( resolve => {
582+ setTimeout ( resolve , cliSpinners . line . interval ) ;
583+ } ) ;
584+ return `Executed ${ ansis . bold ( 'runLighthouse' ) } function` ;
585+ } ,
586+ ) ;
587+ return 'Calculated Lighthouse scores for 4 categories' ;
588+ } ) ;
589+
590+ expect ( output ) . toBe (
591+ `
592+ ${ ansis . bold . cyan ( '❯ Running plugin "ESLint"' ) }
593+ ${ ansis . cyan ( '│' ) } ${ ansis . green ( '$' ) } npx eslint . --format=json ${ ansis . gray ( '(42 ms)' ) }
594+ ${ ansis . cyan ( '└' ) } ${ ansis . green ( 'ESLint reported 4 errors and 11 warnings' ) } ${ ansis . gray ( '(50 ms)' ) }
595+
596+ ${ ansis . bold . magenta ( '❯ Running plugin "Lighthouse"' ) }
597+ ${ ansis . magenta ( '-' ) } Executing ${ ansis . bold ( 'runLighthouse' ) } function`,
598+ ) ;
599+
600+ vi . advanceTimersByTime ( cliSpinners . line . interval ) ;
601+ expect ( output ) . toBe (
602+ `
603+ ${ ansis . bold . cyan ( '❯ Running plugin "ESLint"' ) }
604+ ${ ansis . cyan ( '│' ) } ${ ansis . green ( '$' ) } npx eslint . --format=json ${ ansis . gray ( '(42 ms)' ) }
605+ ${ ansis . cyan ( '└' ) } ${ ansis . green ( 'ESLint reported 4 errors and 11 warnings' ) } ${ ansis . gray ( '(50 ms)' ) }
606+
607+ ${ ansis . bold . magenta ( '❯ Running plugin "Lighthouse"' ) }
608+ ${ ansis . magenta ( '\\' ) } Executing ${ ansis . bold ( 'runLighthouse' ) } function`,
609+ ) ;
610+
611+ await group2 ;
612+ expect ( output ) . toBe (
613+ `
614+ ${ ansis . bold . cyan ( '❯ Running plugin "ESLint"' ) }
615+ ${ ansis . cyan ( '│' ) } ${ ansis . green ( '$' ) } npx eslint . --format=json ${ ansis . gray ( '(42 ms)' ) }
616+ ${ ansis . cyan ( '└' ) } ${ ansis . green ( 'ESLint reported 4 errors and 11 warnings' ) } ${ ansis . gray ( '(50 ms)' ) }
617+
618+ ${ ansis . bold . magenta ( '❯ Running plugin "Lighthouse"' ) }
619+ ${ ansis . magenta ( '│' ) } Executed ${ ansis . bold ( 'runLighthouse' ) } function ${ ansis . gray ( '(130 ms)' ) }
620+ ${ ansis . magenta ( '└' ) } ${ ansis . green ( 'Calculated Lighthouse scores for 4 categories' ) } ${ ansis . gray ( '(130 ms)' ) }
621+
622+ ` ,
623+ ) ;
624+ } ) ;
625+
626+ it ( 'should skip interactive group spinner in CI' , async ( ) => {
627+ vi . stubEnv ( 'CI' , 'true' ) ;
628+ const logger = new Logger ( ) ;
629+
630+ const group = logger . group ( 'Running plugin "ESLint"' , async ( ) => {
631+ await logger . command ( 'npx eslint . --format=json' , async ( ) => { } ) ;
632+ return 'ESLint reported 4 errors and 11 warnings' ;
633+ } ) ;
634+
635+ expect ( output ) . toBe (
636+ `
637+ ${ ansis . bold . cyan ( '❯ Running plugin "ESLint"' ) }
638+ ${ ansis . cyan ( '│' ) } ${ ansis . blue ( '$' ) } npx eslint . --format=json
639+ ` ,
640+ ) ;
641+
642+ await group ;
643+
644+ expect ( output ) . toBe (
645+ `
646+ ${ ansis . bold . cyan ( '❯ Running plugin "ESLint"' ) }
647+ ${ ansis . cyan ( '│' ) } ${ ansis . blue ( '$' ) } npx eslint . --format=json
648+ ${ ansis . cyan ( '│' ) } ${ ansis . green ( '$' ) } npx eslint . --format=json ${ ansis . gray ( '(42 ms)' ) }
649+ ${ ansis . cyan ( '└' ) } ${ ansis . green ( 'ESLint reported 4 errors and 11 warnings' ) } ${ ansis . gray ( '(50 ms)' ) }
650+
651+ ` ,
652+ ) ;
653+ } ) ;
654+
655+ it ( 'should fail group if spinner task rejects' , async ( ) => {
656+ const logger = new Logger ( ) ;
657+
658+ const group = logger . group ( 'Running plugin "ESLint"' , async ( ) => {
659+ await logger . command ( 'npx eslint . --format=json' , async ( ) => {
660+ await new Promise ( resolve => {
661+ setTimeout ( resolve , 0 ) ;
662+ } ) ;
663+ throw new Error ( 'Process failed with exit code 1' ) ;
664+ } ) ;
665+ return 'ESLint reported 4 errors and 11 warnings' ;
666+ } ) ;
667+
668+ expect ( output ) . toBe (
669+ `
670+ ${ ansis . bold . cyan ( '❯ Running plugin "ESLint"' ) }
671+ ${ ansis . cyan ( '-' ) } ${ ansis . blue ( '$' ) } npx eslint . --format=json`,
672+ ) ;
673+
674+ vi . advanceTimersToNextTimer ( ) ;
675+ await expect ( group ) . rejects . toThrow ( 'Process failed with exit code 1' ) ;
676+
677+ expect ( output ) . toBe (
678+ `
679+ ${ ansis . bold . cyan ( '❯ Running plugin "ESLint"' ) }
680+ ${ ansis . cyan ( '│' ) } ${ ansis . red ( '$' ) } npx eslint . --format=json
681+ ${ ansis . cyan ( '└' ) } ${ ansis . red ( 'Error: Process failed with exit code 1' ) }
682+
683+ ` ,
684+ ) ;
685+ } ) ;
686+
687+ it ( 'should fail spinner, complete group and exit if SIGINT received' , async ( ) => {
688+ vi . spyOn ( process , 'exit' ) . mockReturnValue ( undefined as never ) ;
689+ vi . spyOn ( os , 'platform' ) . mockReturnValue ( 'win32' ) ;
690+ const logger = new Logger ( ) ;
691+
692+ logger . group ( 'Running plugin "ESLint"' , async ( ) => {
693+ await logger . command ( 'npx eslint . --format=json' , async ( ) => { } ) ;
694+ return 'ESLint reported 4 errors and 11 warnings' ;
695+ } ) ;
696+
697+ expect ( output ) . toBe (
698+ `
699+ ${ ansis . bold . cyan ( '❯ Running plugin "ESLint"' ) }
700+ ${ ansis . cyan ( '-' ) } ${ ansis . blue ( '$' ) } npx eslint . --format=json`,
701+ ) ;
702+
703+ process . emit ( 'SIGINT' ) ;
704+
705+ expect ( output ) . toBe (
706+ `
707+ ${ ansis . bold . cyan ( '❯ Running plugin "ESLint"' ) }
708+ ${ ansis . cyan ( '└' ) } ${ ansis . blue ( '$' ) } npx eslint . --format=json ${ ansis . red . bold ( '[SIGINT]' ) }
709+
710+ ${ ansis . red . bold ( 'Cancelled by SIGINT' ) }
711+ ` ,
712+ ) ;
713+
714+ expect ( process . exit ) . toHaveBeenCalledWith ( 2 ) ;
715+ } ) ;
716+
717+ it ( 'should indent other logs within group if they were logged while spinner was active' , async ( ) => {
718+ vi . stubEnv ( 'CP_VERBOSE' , 'true' ) ;
719+ const logger = new Logger ( ) ;
720+
721+ const group = logger . group ( 'Running plugin "ESLint"' , async ( ) => {
722+ await logger . command ( 'npx eslint . --format=json' , async ( ) => {
723+ logger . debug ( 'ESLint v9.0.0\n\nAll files pass linting.\n' ) ;
724+ } ) ;
725+ return 'ESLint reported 0 problems' ;
726+ } ) ;
727+
728+ expect ( ansis . strip ( output ) ) . toBe (
729+ `
730+ ❯ Running plugin "ESLint"
731+ - $ npx eslint . --format=json` ,
732+ ) ;
733+
734+ await expect ( group ) . resolves . toBeUndefined ( ) ;
735+
736+ expect ( ansis . strip ( output ) ) . toBe (
737+ `
738+ ❯ Running plugin "ESLint"
739+ │ $ npx eslint . --format=json (42 ms)
740+ │ ESLint v9.0.0
741+ │
742+ │ All files pass linting.
743+ │
744+ └ ESLint reported 0 problems (50 ms)
745+
746+ ` ,
747+ ) ;
748+ } ) ;
749+
750+ it ( 'should indent other logs from spinner in group when it fails in CI' , async ( ) => {
751+ vi . stubEnv ( 'CI' , 'true' ) ;
752+ const logger = new Logger ( ) ;
753+
754+ const group = logger . group ( 'Running plugin "ESLint"' , async ( ) => {
755+ await logger . command ( 'npx eslint . --format=json' , async ( ) => {
756+ logger . error (
757+ "\nOops! Something went wrong! :(\n\nESLint: 8.26.0\n\nESLint couldn't find a configuration file.\n" ,
758+ ) ;
759+ throw new Error ( 'Process failed with exit code 2' ) ;
760+ } ) ;
761+ return 'ESLint reported 0 problems' ;
762+ } ) ;
763+
764+ await expect ( group ) . rejects . toThrow ( 'Process failed with exit code 2' ) ;
765+
766+ expect ( ansis . strip ( output ) ) . toBe (
767+ `
768+ ❯ Running plugin "ESLint"
769+ │ $ npx eslint . --format=json
770+ │
771+ │ Oops! Something went wrong! :(
772+ │
773+ │ ESLint: 8.26.0
774+ │
775+ │ ESLint couldn't find a configuration file.
776+ │
777+ │ $ npx eslint . --format=json
778+ └ Error: Process failed with exit code 2
779+
780+ ` ,
781+ ) ;
782+ } ) ;
783+ } ) ;
469784} ) ;
0 commit comments