import { RootConnection , SessionConnection } from 'chrome-debugging-client' ; import { TestServerApi } from './test-server-api' ; import Protocol from 'devtools-protocol' ; import { FrameStore } from './models/frame' ; import { ServiceWorkerState } from './models/service-worker-state' ; import { addTimeout } from './timeout' ; export interface EvaluateFunction { < T > ( toEvaluate ) : Promise < T > ; } export interface TypedRemoteObject < T > extends Protocol . Runtime . RemoteObject { value : T ; } function exceptionDetailsToError ( { exception , text , stackTrace } ) { const msg = ( exception ? exception . description : text ) || text ; const err = new Error ( `Client Evaluation Failed: ${ msg }` ) ; if ( stackTrace ) { err . stack = stackTrace . callFrames . join ( '\n' ) ; } return err ; } function isAbsolutePath ( url ) { if ( url . length < 8 || url . substr ( 0 , 1 ) === '/' ) { return false ; } else if ( url . substr ( 0 , 7 ) === 'http://' ) { return true ; } else if ( url . substr ( 0 , 8 ) === 'https://' ) { return true ; } return false ; } export interface NavigateOptions { targetUrl ? : string ; } export interface PageLoadResult { page : CapturedResponse ; childResponses : CapturedResponseHash ; frame : Protocol . Page . Frame ; } export interface CapturedResponseHash { [ url ] : CapturedResponse ; } export interface CapturedResponse { responseMeta : Protocol . Network . Response ; body : string ; } export interface PageNavigateResult { page : CapturedResponse ; frame : Protocol . Page . Frame ; } export interface TargetOptions { rootUrl : string ; log ? : boolean ; ignoreErrors ? : boolean ; handleError ? : ( err : Error ) => void ; } export class Target { private browserConnection : RootConnection ; private targetId : string ; private frameStore : FrameStore ; private options : TargetOptions ; public rootUrl : string ; public page : SessionConnection ; public swState : ServiceWorkerState ; private constructor ( browserConnection , page , targetId , options ) { this . browserConnection = browserConnection ; this . targetId = targetId ; this . frameStore = new FrameStore ( ) ; this . options = options ; this . rootUrl = options . rootUrl ; this . page = page ; this . swState = new ServiceWorkerState ( this . page , this . options ) ; this . page . on ( 'Network.responseReceived' , this . frameStore . onNetworkResponse . bind ( this . frameStore ) ) ; this . page . on ( 'Network.requestWillBeSent' , this . frameStore . onRequestWillBeSent . bind ( this . frameStore ) ) ; this . page . on ( 'Page.frameNavigated' , this . frameStore . onNavigationComplete . bind ( this . frameStore ) ) ; this . page . on ( 'Page.loadEventFired' , this . frameStore . onLoadEvent . bind ( this . frameStore ) ) ; } public async evaluate < T > ( code ) { const expression = typeof code === 'string' ? code : `(${ code . toString ( ) }())` ; const { result , exceptionDetails } = await this . page . send ( 'Runtime.evaluate' , { expression , awaitPromise : true , silent : false } ) ; if ( exceptionDetails ) { throw exceptionDetailsToError ( exceptionDetails ) ; } return result ; } public async activate ( ) { return this . browserConnection . send ( 'Target.activateTarget' , { targetId : this . targetId } ) ; } public static async create ( browserConnection , browserContextId , options ) { const { targetId } = await browserConnection . send ( 'Target.createTarget' , { url : 'about:blank' , browserContextId } ) ; const page = await browserConnection . attachToTarget ( targetId ) ; await Promise . all ( [ page . send ( 'Page.enable' ) , page . send ( 'Network.enable' ) , page . send ( 'ServiceWorker.enable' ) ] ) ; const target = new Target ( browserConnection , page , targetId , options ) ; return target ; } public waitForServiceWorkerRegistration ( ) { return this . evaluate ( function ( ) { return navigator . serviceWorker . ready . then ( ( ) => { return navigator . serviceWorker . getRegistration ( ) ; } ) ; } ) ; } public getStorageEstimate ( ) { return this . evaluate ( function getStorageEstimate ( ) { return ( navigator as any ) . storage . estimate ( ) . then ( ( stats ) => { return JSON . stringify ( stats ) ; } ) ; } ) ; } public async ensureMaximumStorageAvailable ( bytesAvailable ) { const functionString = function ( ) { function getAvailable ( ) { return ( navigator as any ) . storage . estimate ( ) . then ( ( stats ) => { return stats . quota - stats . usage ; } ) ; } function generateString ( size ) { let str = '' ; const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' ; for ( let i = 0 ; i < size ; i ++ ) { str += possible . charAt ( Math . floor ( Math . random ( ) * possible . length ) ) ; } return str ; } return caches . open ( 'test-cache' ) . then ( async ( cache ) => { let available = - 1 ; let iterations = 0 ; do { await cache . put ( generateString ( 20 ) , new Response ( new Blob ( [ generateString ( 999 ) ] ) ) ) ; available = await getAvailable ( ) ; iterations ++ ; } while ( available > bytesAvailable ) ; return JSON . stringify ( { iterations , available } ) ; } ) ; } . toString ( ) ; const paramsInjected = functionString . replace ( /bytesAvailable/ , String ( bytesAvailable ) ) ; return this . evaluate ( `(${ paramsInjected })();` ) ; } public async emulateOffline ( ) { throw new Error ( 'Offline emulation not working. See https://bugs.chromium.org/p/chromium/issues/detail?id=852127' ) ; } public async navigate ( arg ? ) { const url = arg ? this . getAbsoluteUrl ( arg ) : this . rootUrl ; await Promise . all ( [ this . page . until ( 'Page.loadEventFired' ) , this . page . send ( 'Page.navigate' , { url } ) , ] ) ; } private getAbsoluteUrl ( targetUrl ) { if ( isAbsolutePath ( targetUrl ) ) { return targetUrl ; } return this . rootUrl + targetUrl ; } private async buildCapturedResponse ( response ) { return { responseMeta : response . response , body : ( await this . page . send ( 'Network.getResponseBody' , { requestId : response . requestId } ) ) . body } ; } private async navigateWithAssets ( arg ? ) { const targetUrl = arg ? typeof arg === 'string' ? arg : arg . targetUrl : null ; const url = targetUrl ? this . getAbsoluteUrl ( targetUrl ) : this . rootUrl ; const tree = await this . page . send ( 'Page.getResourceTree' ) ; const frame = tree . frameTree . frame ; const navPromise = this . frameStore . start ( frame . id , true ) ; const { errorText } = await this . page . send ( 'Page.navigate' , { url } ) ; if ( errorText ) { console . error ( `Navigation Failed: ${ errorText }` ) ; } const responses = await navPromise ; const page = await this . buildCapturedResponse ( this . findPageResponse ( responses , url ) ) ; return { responses , page , frame } ; } private findResponse ( responses , url ) { return responses . find ( ( item ) => { return item . response . url === url ; } ) ; } private findPageResponse ( responses , url ) { const urlResult = this . findResponse ( responses , url ) ; if ( urlResult ) { return urlResult ; } const slashedUrl = `${ url }/` ; const slashedUrlResult = this . findResponse ( responses , slashedUrl ) ; if ( slashedUrlResult ) { return slashedUrlResult ; } throw new Error ( `Couldn't match shell url (${ url }) to a captured response` ) ; } /**
     * Load page in active tab. Resolves once both the page's load event is fired and all asset responses are recieved.
     * To wait only for initial HTTP response, use navigate() method instead.
     * * @return All asset responses and page frame data
     */ public async load ( arg ? ) { const { responses , page , frame } = await this . navigateWithAssets ( arg ) ; const capturedResponses = await Promise . all ( responses . map ( ( response ) => { return this . buildCapturedResponse ( response ) ; } ) ) ; const childResponses = capturedResponses . reduce ( ( responseHash , response ) => { if ( response . responseMeta . url !== page . responseMeta . url ) { responseHash [ response . responseMeta . url ] = response ; } return responseHash ; } , { } as CapturedResponseHash ) ; return { page , childResponses , frame } ; } public async close ( ) { if ( ! this . options . ignoreErrors ) { this . swState . ensureNoErrors ( this . options . handleError ) ; } this . swState . close ( ) ; await this . page . send ( 'ServiceWorker.stopAllWorkers' ) ; await this . page . send ( 'ServiceWorker.unregister' , { scopeURL : this . rootUrl } ) ; return this . browserConnection . send ( 'Target.closeTarget' , { targetId : this . targetId } ) ; } } export interface TabConfig { debug ? : boolean ; ignoreErrors ? : boolean ; handleError ? : ( err : Error ) => void ; } remarks public export class TestEnvironment < S extends TestServerApi = TestServerApi > { private testServer : S ; private browserConnection : RootConnection ; private browserContextId : string ; private tabIndex : Target [ ] ; private activeTarget : Target ; constructor ( browserConnection , testServer , browserContextId ) { this . testServer = testServer ; this . browserConnection = browserConnection ; this . browserContextId = browserContextId ; this . tabIndex = [ ] ; } public async createTab ( config = { } ) { const target = await addTimeout ( Target . create ( this . browserConnection , this . browserContextId , { rootUrl : this . testServer . rootUrl , log : config . debug , ignoreErrors : config . ignoreErrors , handleError : config . handleError } ) , 'New tab creation timeout' , 1000 ) ; this . tabIndex . push ( target ) ; await target . activate ( ) ; this . activeTarget = target ; return target ; } public getTestServer ( ) { return this . testServer ; } public async activateTabByIndex ( index ) { if ( index < 0 || index >= this . tabIndex . length || ! this . tabIndex [ index ] ) { throw new Error ( 'Invalid tab index' ) ; } await this . activateTarget ( this . tabIndex [ index ] ) ; } private async activateTarget ( target ) { await target . activate ( ) ; this . activeTarget = target ; } public async close ( ) { await Promise . all ( this . tabIndex . map ( ( target ) => target . close ( ) ) ) ; } public getActiveTab ( ) { if ( ! this . activeTarget ) { throw new Error ( `No active tab client exists. Have you tried creating one first?
        You can do so using TestEnvironment.createTarget()` ) ; } return this . activeTarget ; } public async activateTab ( target ) { const t = this . tabIndex . find ( ( tab ) => tab === target ) ; if ( t ) { await this . activateTarget ( t ) ; } } }