import { Readable } from 'stream' ; import { createHash } from 'crypto' ; import { readFile , writeFile } from 'fs' ; import { promisify } from 'util' ; import puppeteer from 'puppeteer' ; import compareImages from 'resemblejs/compareImages' ; import { test } from 'mocha' ; import { assert } from 'chai' ; import Storage from '@google-cloud/storage' ; import comparisonOptions from './screenshot-comparison-options' ; import axios from 'axios' ; import path from 'path' ; import mkdirp from 'mkdirp' ; const readFilePromise = promisify ( readFile ) ; const writeFilePromise = promisify ( writeFile ) ; const serviceAccountKey = process . env . MDC_GCLOUD_SERVICE_ACCOUNT_KEY || '' ; const branchName = process . env . MDC_BRANCH_NAME ; const commitHash = process . env . MDC_COMMIT_HASH ; const goldenFilePath = './test/screenshot/golden.json' ; const bucketName = 'screenshot-uploads' ; const defaultMetadata = { commit : commitHash , branch : branchName , } ; const NO_MATCH_DIRECTORY = 'no_match' ; let storage = null ; let bucket = null ; if ( serviceAccountKey ) { storage = new Storage ( { credentials : JSON . parse ( serviceAccountKey ) , } ) ; bucket = storage . bucket ( bucketName ) ; } export default class Screenshot { urlPath_ : string ; param { string } urlPath constructor ( urlPath ) { private this . urlPath_ = urlPath ; } /**
     * Captures a screenshot of the test URL and marks it as the new golden image
     */ capture ( ) { test ( this . urlPath_ , async ( ) => { const golden = await this . takeScreenshot_ ( ) ; const goldenHash = this . generateImageHash_ ( golden ) ; const goldenPath = this . getImagePath_ ( goldenHash , 'golden' ) ; if ( ! goldenPath ) { console . error ( 'no goldenPath found' ) ; return ; } await Promise . all ( [ this . saveImage_ ( goldenPath , golden ) , this . saveGoldenHash_ ( goldenHash ) , ] ) ; return ; } ) ; } /**
     * Diffs a screenshot of the test URL with the existing golden screenshot
     */ diff ( ) { test ( this . urlPath_ , async ( ) => { const goldenHash = await this . getGoldenHash_ ( ) ; const goldenPath = this . getImagePath_ ( goldenHash , 'golden' ) ; if ( ! goldenPath ) { console . error ( 'no goldenPath found' ) ; return ; } const [ snapshot , golden ] = await Promise . all ( [ this . takeScreenshot_ ( ) , this . readImage_ ( goldenPath ) , ] ) ; const data = await compareImages ( snapshot , golden , comparisonOptions ) ; const diff = data . getBuffer ( ) ; const snapshotHash = this . generateImageHash_ ( snapshot ) ; const snapshotPath = this . getImagePath_ ( snapshotHash , 'snapshot' ) ; const diffPath = this . getImagePath_ ( snapshotHash , 'diff' ) ; const metadata = { golden : goldenHash } ; if ( ! snapshotPath ) { console . error ( 'no snapshotPath found' ) ; return ; } if ( ! diffPath ) { console . error ( 'no diffPath found' ) ; return ; } if ( storage ) { await Promise . all ( [ this . saveImage_ ( snapshotPath , snapshot , metadata ) , this . saveImage_ ( diffPath , diff , metadata ) , ] ) ; } if ( Number ( data . misMatchPercentage ) > 0 ) { mkdirp . sync ( path . dirname ( `${ NO_MATCH_DIRECTORY }/${ diffPath }` ) ) ; await writeFilePromise ( `${ NO_MATCH_DIRECTORY }/${ diffPath }` , diff ) ; } return assert . equal ( Number ( data . misMatchPercentage ) , 0 ) ; } ) ; } param { ! Buffer } imageBuffer return { string } private generateImageHash_ ( imageBuffer ) { return createHash ( 'sha256' ) . update ( imageBuffer ) . digest ( 'hex' ) ; } return { string | undefined } private async getGoldenHash_ ( ) { const goldenFile = await readFilePromise ( goldenFilePath ) ; const goldenJSON = JSON . parse ( goldenFile . toString ( ) ) ; return goldenJSON [ this . urlPath_ ] ; } param { string } imageHash param { string } imageType return { string | undefined } private getImagePath_ ( imageHash , imageType ) { if ( imageType === 'golden' ) { return `${ this . urlPath_ }/${ imageHash }.golden.png` ; } if ( [ 'snapshot' , 'diff' ] . includes ( imageType ) ) { return `${ this . urlPath_ }/${ commitHash }/${ imageHash }.${ imageType }.png` ; } return undefined ; } param { string } gcsFilePath return { Buffer } private async readImage_ ( gcsFilePath ) { const url = `https://storage.googleapis.com/screenshot-uploads/${ gcsFilePath }` ; const response = await axios . request ( { url , responseType : 'arraybuffer' , } ) ; return response . data ; } param { string } goldenHash private async saveGoldenHash_ ( goldenHash ) { const goldenFile = await readFilePromise ( goldenFilePath ) ; const goldenJSON = JSON . parse ( goldenFile . toString ( ) ) ; goldenJSON [ this . urlPath_ ] = goldenHash ; const goldenContent = JSON . stringify ( goldenJSON , null , '  ' ) ; await writeFilePromise ( goldenFilePath , `${ goldenContent }\r\n` ) ; } param { string } imagePath param { ! Buffer } imageBuffer param { ! Object = } customMetadata private async saveImage_ ( imagePath , imageBuffer , customMetadata = { } ) { const metadata = Object . assign ( { } , defaultMetadata , customMetadata ) ; if ( ! bucket ) { throw new Error ( 'GCS is not configured' ) ; } const file = bucket . file ( imagePath ) ; const [ exists ] = await file . exists ( ) ; if ( exists ) { console . log ( '✔︎ No changes to' , imagePath ) ; return ; } const stream = new Readable ( ) ; stream . push ( imageBuffer ) ; stream . push ( null ) ; return new Promise ( ( resolve , reject ) => { stream . pipe ( file . createWriteStream ( ) ) . on ( 'error' , ( err ) => { reject ( err ) ; } ) . on ( 'finish' , async ( ) => { try { await file . makePublic ( ) ; await file . setMetadata ( { metadata } ) ; console . log ( '✔︎ Uploaded' , imagePath ) ; resolve ( ) ; } catch ( err ) { reject ( err ) ; } } ) ; } ) ; } return { ! Buffer } private async takeScreenshot_ ( ) { const browser = await puppeteer . launch ( { executablePath : 'google-chrome-unstable' , args : [ '--disable-dev-shm-usage' ] , } ) ; const page = await browser . newPage ( ) ; await page . goto ( `http://localhost:8080/#/${ this . urlPath_ }` , { waitUntil : [ 'networkidle2' ] , timeout : 600000 , } ) ; const imageBuffer = await page . screenshot ( { fullPage : true } ) ; await browser . close ( ) ; return imageBuffer ; } }