import { ListensToDomainEvents , Stage , StageCrewMemberBuilder } from '@serenity-js/core' ; import { AssertionError } from '@serenity-js/core/lib' ; import { ActivityRelatedArtifactGenerated , DomainEvent , InteractionFinished , InteractionStarts , SceneFinished , SceneStarts , TaskFinished , TaskStarts , TestRunFinished , } from '@serenity-js/core/lib/events' ; import { OutputStream } from '@serenity-js/core/lib/io' ; import { AssertionReport , CorrelationId , Duration , ExecutionCompromised , ExecutionFailedWithAssertionError , ExecutionFailedWithError , ExecutionIgnored , ExecutionSkipped , ExecutionSuccessful , ImplementationPending , LogEntry , Name , Outcome , ProblemIndication , Timestamp , } from '@serenity-js/core/lib/model' ; import { Instance as ChalkInstance } from 'chalk' ; import { ensure , isDefined , match } from 'tiny-types' ; import { Printer } from './Printer' ; import { Summary } from './Summary' ; import { SummaryFormatter } from './SummaryFormatter' ; import { TerminalTheme , ThemeForDarkTerminals , ThemeForLightTerminals , ThemeForMonochromaticTerminals } from './themes' ; desc example example example public implements serenity-js export class ConsoleReporter implements ListensToDomainEvents { private startTimes = new StartTimes ( ) ; private artifacts = new ActivityRelatedArtifacts ( ) ; private summary = new Summary ( ) ; private firstError : FirstError = new FirstError ( ) ; private readonly summaryFormatter : SummaryFormatter ; desc Instantiates a `ConsoleReporter` that auto-detects
     *  your terminal's support for colours and use a colour theme
     *  for dark terminals if successful.
     *
     *  Please note that spawning your test process from another process
     *  (by using [npm-failsafe](https://www.npmjs.com/package/npm-failsafe), for example)
     *  causes the `ConsoleReporter` to use the monochromatic colour scheme,
     *  as colour support can't be detected in child processes.
     *
     *  If the above describes your setup, use  ConsoleReporter *  or  ConsoleReporter to make the sub-process produce colour output.
     *
     *  returns { serenity-js static withDefaultColourSupport ( ) { return new ConsoleReporterBuilder ( new ThemeForDarkTerminals ( new ChalkInstance ( ) ) ) ; } desc returns { serenity-js static forMonochromaticTerminals ( ) { return new ConsoleReporterBuilder ( new ThemeForMonochromaticTerminals ( ) ) ; } desc returns { serenity-js static forDarkTerminals ( ) { return new ConsoleReporterBuilder ( new ThemeForDarkTerminals ( new ChalkInstance ( { level : 2 } ) ) ) ; } desc returns { serenity-js static forLightTerminals ( ) { return new ConsoleReporterBuilder ( new ThemeForLightTerminals ( new ChalkInstance ( { level : 2 } ) ) ) ; } param { Printer } printer param { TerminalTheme } theme param { serenity-js constructor ( private readonly printer , private readonly theme , private readonly stage ? ) { ensure ( 'printer' , printer , isDefined ( ) ) ; ensure ( 'theme' , theme , isDefined ( ) ) ; this . summaryFormatter = new SummaryFormatter ( this . theme ) ; } desc Handles  {@link @serenity-js/core/lib/events~DomainEvent} objects emitted by the  {@link @serenity-js/core/lib/stage~StageCrewMember} .
     *
     *  see {@link @serenity-js/core/lib/stage~StageCrewMember} listens param { serenity-js returns { void } notifyOf ( event ) { match ( event ) . when ( SceneStarts , ( e ) => { this . firstError = new FirstError ( ) ; this . startTimes . recordStartOf ( e ) ; this . printer . println ( this . theme . separator ( '-' ) ) ; this . printer . println ( e . details . location . path . value , e . details . location . line ? `:${ e . details . location . line }` : '' ) ; this . printer . println ( ) ; this . printer . println ( this . theme . heading ( e . details . category . value , ': ' , e . details . name . value ) ) ; this . printer . println ( ) ; } ) . when ( TaskStarts , ( e ) => { this . printer . indent ( ) ; if ( ! this . firstError . alreadyRecorded ( ) ) { this . printer . println ( e . details . name . value ) ; } } ) . when ( InteractionStarts , ( e ) => { this . startTimes . recordStartOf ( e ) ; } ) . when ( InteractionFinished , ( e ) => { this . printer . indent ( ) ; this . printer . println ( this . formattedOutcome ( e ) ) ; this . printer . indent ( ) ; if ( e . outcome instanceof ProblemIndication ) { this . firstError . recordIfNeeded ( e . outcome . error ) ; if ( ! ( e . outcome . error instanceof AssertionError ) ) { this . printer . println ( this . theme . outcome ( e . outcome , `${ e . outcome . error }` ) ) ; } } const artifactGeneratedEvents = this . artifacts . recordedFor ( e . activityId ) ; if ( artifactGeneratedEvents . some ( a => a instanceof AssertionReport || a instanceof LogEntry ) ) { this . printer . println ( ) ; } artifactGeneratedEvents . forEach ( artifactGenerated => { if ( artifactGenerated . artifact instanceof AssertionReport ) { const details = artifactGenerated . artifact . map ( ( artifactContents ) => this . theme . diff ( artifactContents . expected , artifactContents . actual ) ) ; this . printer . println ( ) ; this . printer . println ( details ) ; this . printer . println ( ) ; } if ( artifactGenerated . artifact instanceof LogEntry ) { const details = artifactGenerated . artifact . map ( ( artifactContents ) => artifactContents . data ) ; if ( artifactGenerated . name . value !== details ) { this . printer . println ( this . theme . log ( artifactGenerated . name . value , ':' ) ) ; } this . printer . println ( details ) ; this . printer . println ( ) ; } } ) ; this . printer . outdent ( ) ; this . printer . outdent ( ) ; } ) . when ( ActivityRelatedArtifactGenerated , ( e ) => { this . artifacts . record ( e ) ; } ) . when ( TaskFinished , ( e ) => { this . printer . outdent ( ) ; if ( e . outcome instanceof ProblemIndication ) { this . printer . indent ( ) ; this . printer . indent ( ) ; if ( ! this . firstError . alreadyRecorded ( ) ) { this . printer . println ( this . theme . outcome ( e . outcome , this . iconFrom ( e . outcome ) , `${ e . outcome . error }` ) ) ; } this . printer . outdent ( ) ; this . printer . outdent ( ) ; this . firstError . recordIfNeeded ( e . outcome . error ) ; } else if ( ! ( e . outcome instanceof ExecutionSuccessful ) ) { this . printer . indent ( ) ; this . printer . println ( this . iconFrom ( e . outcome ) , e . details . name . value ) ; this . printer . outdent ( ) ; } } ) . when ( SceneFinished , ( e ) => { this . summary . record ( e . details , e . outcome , this . startTimes . eventDurationOf ( e ) ) ; this . printer . println ( ) ; this . printer . println ( this . theme . outcome ( e . outcome , this . formattedOutcome ( e , this . deCamelCased ( e . outcome . constructor . name ) ) ) ) ; if ( e . outcome instanceof ProblemIndication ) { this . printer . println ( ) ; this . printer . indent ( ) ; if ( e . outcome instanceof ImplementationPending ) { this . printer . println ( `${ e . outcome . error . name }: ${ e . outcome . error . message }` ) ; } else if ( e . outcome . error ?. stack ) { this . printer . println ( e . outcome . error . stack ) ; } this . printer . outdent ( ) ; } this . artifacts . clear ( ) ; } ) . when ( TestRunFinished , ( _ ) => { this . printer . println ( this . theme . separator ( '=' ) ) ; this . printer . print ( this . summaryFormatter . format ( this . summary . aggregated ( ) ) ) ; this . printer . println ( this . theme . separator ( '=' ) ) ; } ) . else ( ( _ ) => { return void 0 ; } ) ; } private formattedOutcome ( event , description = event . details . name . value ) { const icon = `${ this . iconFrom ( event . outcome ) }` , message = `${ description } (${ this . startTimes . eventDurationOf ( event ) })` ; return ( event . outcome instanceof ProblemIndication ) ? this . theme . outcome ( event . outcome , `${ icon }${ message }` ) : `${ this . theme . outcome ( event . outcome , icon ) }${ message }` ; } private deCamelCased ( name ) { const deCamelCased = name . replace ( /([^A-Z])([A-Z])/g , '$1 $2' ) ; return deCamelCased . charAt ( 0 ) . toUpperCase ( ) + deCamelCased . slice ( 1 ) . toLocaleLowerCase ( ) ; } private iconFrom ( outcome ) { switch ( outcome . constructor ) { case ExecutionCompromised : case ExecutionFailedWithError : case ExecutionFailedWithAssertionError : return '✗ ' ; case ImplementationPending : return '☕' ; case ExecutionSkipped : return '⇢ ' ; case ExecutionIgnored : return '? ' ; case ExecutionSuccessful : return '✓ ' ; default : return '' ; } } } class ConsoleReporterBuilder implements StageCrewMemberBuilder < ConsoleReporter > { constructor ( private readonly theme ) { } build ( { stage , outputStream } ) { return new ConsoleReporter ( new Printer ( outputStream ) , this . theme , stage ) ; } } interface IdentifiableEvent { details : { name : Name ; toString ( ) : string ; } ; timestamp : Timestamp ; } class StartTimes { private times : { [ correlationId ] : Timestamp ; } = { } ; recordStartOf ( event ) { this . times [ event . details . toString ( ) ] = event . timestamp ; } eventDurationOf ( event ) { return event . timestamp . diff ( this . times [ event . details . toString ( ) ] ) ; } } class FirstError { private error : Error ; recordIfNeeded ( error ) { if ( ! this . error ) { this . error = error ; } } alreadyRecorded ( ) { return ! ! this . error ; } get ( ) { return this . error ; } } class ActivityRelatedArtifacts { private events : ActivityRelatedArtifactGenerated [ ] = [ ] ; record ( event ) { this . events . push ( event ) ; } recordedFor ( activityId ) { return this . events . filter ( event => event . activityId . equals ( activityId ) ) ; } clear ( ) { this . events = [ ] ; } }