import path from "path" ; import { CoreConfig , WorkingDirectoryInfo , PluginCreateOptions , KeyGeneratorPlugin , PublisherPlugin , NotifierPlugin , NotifyParams , PluginLogger , ComparisonResult , } from "reg-suit-interface" ; import { EventEmitter } from "events" ; const compare = require ( "reg-cli" ) ; const rimraf = require ( "rimraf" ) ; const cpx = require ( "cpx" ) ; export interface ProcessorOptions { keyGenerator ? : KeyGeneratorPlugin < any > ; publisher ? : PublisherPlugin < any > ; notifiers : NotifierPlugin < any > [ ] ; userDirs : { actualDir : string ; } ; } export interface StepResultAfterExpectedKey { expectedKey : string | null ; } export interface StepResultAfterComparison extends StepResultAfterExpectedKey { comparisonResult : ComparisonResult ; } export interface StepResultAfterActualKey extends StepResultAfterComparison { actualKey : string ; } export interface StepResultAfterPublish extends StepResultAfterActualKey { reportUrl ? : string ; } export class RegProcessor { private _logger : PluginLogger ; private _config : CoreConfig ; private _directoryInfo : { workingDirs : WorkingDirectoryInfo ; userDirs : { actualDir : string ; } ; } ; private _keyGenerator ? : KeyGeneratorPlugin < any > ; private _publisher ? : PublisherPlugin < any > ; private _notifiers : NotifierPlugin < any > [ ] ; constructor ( opt ) { this . _logger = opt . logger ; this . _config = opt . coreConfig ; this . _directoryInfo = { workingDirs : opt . workingDirs , userDirs : opt . options . userDirs , } ; this . _keyGenerator = opt . options . keyGenerator ; this . _publisher = opt . options . publisher ; this . _notifiers = opt . options . notifiers ; } runAll ( ) { return this . getExpectedKey ( ) . then ( ctx => this . syncExpected ( ctx ) ) . then ( ctx => this . compare ( ctx ) ) . then ( ctx => this . getActualKey ( ctx ) ) . then ( ctx => this . publish ( ctx ) ) . then ( ctx => this . notify ( ctx ) ) ; } getExpectedKey ( ) { if ( this . _keyGenerator ) { return this . _keyGenerator . getExpectedKey ( ) . then ( key => { this . _logger . info ( `Detected the previous snapshot key: '${ key }'` ) ; return { expectedKey : key } ; } ) . catch ( reason => { this . _logger . warn ( "Failed to detect the previous snapshot key" ) ; if ( reason ) this . _logger . error ( reason ) ; return Promise . resolve ( { expectedKey : null } ) ; } ) ; } else { this . _logger . info ( "Skipped to detect the previous snapshot key because key generator plugin is not set up." ) ; return Promise . resolve ( { expectedKey : null } ) ; } } compare ( ctx ) { const { actualDir , expectedDir , diffDir } = this . _directoryInfo . workingDirs ; const json = path . join ( this . _directoryInfo . workingDirs . base , "out.json" ) ; const report = path . join ( this . _directoryInfo . workingDirs . base , "index.html" ) ; const ximgdiffConf = this . _config . ximgdiff || { invocationType : "cli" } ; rimraf . sync ( actualDir ) ; cpx . copySync ( `${ this . _directoryInfo . userDirs . actualDir }/**/*.{png,jpg,jpeg,tiff,bmp,gif}` , actualDir ) ; const emitter = compare ( { actualDir , expectedDir , diffDir , json , report , update : false , ignoreChange : true , urlPrefix : "" , threshold : this . _config . threshold , thresholdPixel : this . _config . thresholdPixel , thresholdRate : this . _config . thresholdRate , matchingThreshold : this . _config . matchingThreshold ?? 0 , enableAntialias : this . _config . enableAntialias , enableCliAdditionalDetection : ximgdiffConf . invocationType === "cli" , enableClientAdditionalDetection : ximgdiffConf . invocationType !== "none" , } ) as EventEmitter ; emitter . on ( "compare" , ( compareItem ) => { this . _logger . verbose ( `${ this . _logger . colors . red ( compareItem . type ) }: ${ this . _logger . colors . magenta ( compareItem . path ) }` ) ; } ) ; const comparisonResult = new Promise < ComparisonResult > ( ( resolve , reject ) => { emitter . once ( "complete" , ( result ) => resolve ( result ) ) ; emitter . once ( "error" , ( reason ) => reject ( reason ) ) ; } ) ; return comparisonResult . then ( result => { this . _logger . info ( "Comparison Complete" ) ; this . _logger . info ( this . _logger . colors . red ( "   Changed items: " + result . failedItems . length ) ) ; this . _logger . info ( this . _logger . colors . cyan ( "   New items: " + result . newItems . length ) ) ; this . _logger . info ( this . _logger . colors . redBright ( "   Deleted items: " + result . deletedItems . length ) ) ; this . _logger . info ( this . _logger . colors . green ( "   Passed items: " + result . passedItems . length ) ) ; this . _logger . verbose ( "Comparison details:" , result ) ; return { ... ctx , comparisonResult : result } ; } ) . catch ( reason => { this . _logger . error ( "An error occurs during compare images:" ) ; if ( reason ) this . _logger . error ( reason ) ; return Promise . reject < StepResultAfterComparison > ( reason ) ; } ) ; } getActualKey ( ctx ) { const fallbackFn = ( ) => "snapshot_" + ~ ~ ( new Date ( ) . getTime ( ) / 1000 ) ; if ( this . _keyGenerator ) { return this . _keyGenerator . getActualKey ( ) . then ( key => { if ( ! key ) { this . _logger . warn ( "Failed to generate the current snapshot key." ) ; return { ... ctx , actualKey : fallbackFn ( ) } ; } this . _logger . info ( `The current snapshot key: '${ key }'` ) ; return { ... ctx , actualKey : key } ; } ) . catch ( reason => { this . _logger . warn ( "Failed to gerenate the current snapshot key." ) ; if ( reason ) this . _logger . error ( reason ) ; return Promise . resolve ( { ... ctx , actualKey : fallbackFn ( ) } ) ; } ) ; } else { const fallbackKey = fallbackFn ( ) ; this . _logger . info ( `Use '${ fallbackKey }' as the current snapshot key because key generator plugin is not set up.` ) ; return Promise . resolve ( { ... ctx , actualKey : fallbackKey } ) ; } } syncExpected ( ctx ) { const keyForExpected = ctx . expectedKey ; if ( this . _publisher && keyForExpected ) { return this . _publisher . fetch ( keyForExpected ) ; } else if ( ! keyForExpected ) { this . _logger . info ( "Skipped to fetch the expected data because expected key is null." ) ; return Promise . resolve ( ctx ) ; } else if ( ! this . _publisher ) { this . _logger . info ( "Skipped to fetch the expected data because publisher plugin is not set up." ) ; return Promise . resolve ( ctx ) ; } else { return Promise . resolve ( ctx ) ; } } publish ( ctx ) { if ( this . _publisher ) { return this . _publisher . publish ( ctx . actualKey ) . then ( result => { this . _logger . info ( `Published snapshot '${ ctx . actualKey }' successfully.` ) ; if ( result . reportUrl ) { this . _logger . info ( `Report URL: ${ result . reportUrl }` ) ; } this . _logger . verbose ( "Publish result:" , result ) ; return { ... ctx , reportUrl : result . reportUrl } ; } ) . catch ( reason => { this . _logger . error ( "An error occurs during publishing snapshot:" ) ; if ( reason . code === "CredentialsError" ) { this . _logger . error ( "Failed to read AWS credentials." ) ; this . _logger . error ( `Create ${ this . _logger . colors . magenta ( "~/.aws/credentials" ) } or export ${ this . _logger . colors . green ( "$AWS_ACCESS_KEY_ID" ) } and ${ this . _logger . colors . green ( "$AWS_SECRET_ACCESS_KEY" ) }.` ) ; } if ( reason ) this . _logger . error ( reason ) ; return Promise . reject < StepResultAfterPublish > ( reason ) ; } ) ; } else { this . _logger . info ( "Skipped to publish the snapshot data because publisher plugin is not set up." ) ; return Promise . resolve ( ctx ) ; } } notify ( ctx ) { const notifyParams = { ... ctx , } ; if ( ! this . _notifiers . length ) { this . _logger . info ( "Skipped to notify result because notifier plugins are not set up." ) ; } this . _logger . verbose ( "Notify parameters:" , notifyParams ) ; return this . _notifiers . reduce ( ( queue , notifier ) => { return queue . then ( ( ) => notifier . notify ( notifyParams ) ) . catch ( reason => { this . _logger . error ( "An error occurs during notify:" ) ; this . _logger . error ( reason ) ; return Promise . resolve ( ) ; } ) ; } , Promise . resolve ( ) ) . then ( ( ) => ctx ) ; } }