import { FileManager , Logger } from './utils' ; import { CRIConnection , RetryStrategy } from './cdp' ; import { EntryBuilder , HarBuilder , NetworkObserver , NetworkRequest } from './network' ; import { Entry , Har } from 'har-format' ; import { join } from 'path' ; export interface SaveOptions { fileName : string ; outDir : string ; } export interface RecordOptions { content : boolean ; excludePaths : string [ ] ; } export class Plugin { private rdpPort ? : number ; private entries : Entry [ ] = [ ] ; private connection ? : CRIConnection ; constructor ( private readonly logger , private readonly fileManager ) { } public ensureBrowserFlags ( browser , args ) { if ( ! this . isSupportedBrowser ( browser ) ) { throw new Error ( `An unsupported browser family was used: ${ browser . name }` ) ; } const browserFlags = this . ensureRdpPort ( this . ensureTestingFlags ( args ) ) ; return browserFlags . filter ( ( x ) : boolean => ! args . includes ( x ) ) ; } public async recordHar ( options ) { await this . closeConnection ( ) ; this . connection = new CRIConnection ( { port : this . rdpPort } , this . logger , new RetryStrategy ( 20 , 5 , 100 ) ) ; this . entries = [ ] ; await this . connection . open ( ) ; await this . listenNetworkEvents ( options ) ; return null ; } public async saveHar ( options ) { const filePath = join ( options . outDir , options . fileName ) ; this . assertFilePath ( filePath ) ; if ( ! this . connection ) { this . logger . err ( `Failed to save HAR. First you should start recording.` ) ; return null ; } try { await this . fileManager . createFolder ( options . outDir ) ; const har = this . buildHar ( ) ; if ( har ) { await this . fileManager . writeFile ( filePath , har ) ; } } catch ( e ) { this . logger . err ( `Failed to save HAR: ${ e . message }` ) ; } finally { this . entries = [ ] ; } return null ; } private buildHar ( ) { if ( this . entries . length ) { const har = new HarBuilder ( this . entries ) . build ( ) ; return JSON . stringify ( har , null , 2 ) ; } } private async listenNetworkEvents ( options ) { const networkObservable = new NetworkObserver ( options , this . connection , this . logger ) ; await networkObservable . subscribe ( async ( request ) : Promise < number > => this . entries . push ( await new EntryBuilder ( request ) . build ( ) ) ) ; } private async closeConnection ( ) { if ( this . connection ) { await this . connection . close ( ) ; delete this . connection ; } } private assertFilePath ( path ) { if ( typeof path !== 'string' ) { throw new Error ( 'File path must be a string.' ) ; } } private isSupportedBrowser ( browser ) { return [ 'chromium' ] . includes ( browser ?. family ) ; } private ensureTestingFlags ( args ) { return [ ... args , '--no-sandbox' , '--disable-background-networking' , '--reduce-security-for-testing' , '--allow-insecure-localhost' , '--ignore-certificate-errors' ] ; } private ensureRdpPort ( args ) { this . rdpPort = this . getRdpPortFromArgs ( args ) ; if ( this . rdpPort ) { return args ; } this . rdpPort = 40000 + Math . round ( Math . random ( ) * 25000 ) ; return [ ... args , `--remote-debugging-port=${ this . rdpPort }` ] ; } private getRdpPortFromArgs ( args ) { const existing = args . find ( ( arg ) : boolean => arg . startsWith ( '--remote-debugging-port=' ) ) ; if ( existing ) { return + existing . split ( '=' ) [ 1 ] ; } } }