2019-12-19 16:53:24 -08:00
/ * *
* Copyright 2017 Google Inc . All rights reserved .
* Modifications copyright ( c ) Microsoft Corporation .
*
* Licensed under the Apache License , Version 2.0 ( the "License" ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
*
* http : //www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an "AS IS" BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
* /
import * as http from 'http' ;
import * as https from 'https' ;
import * as URL from 'url' ;
2020-01-07 13:58:23 -08:00
import * as fs from 'fs' ;
import * as os from 'os' ;
import * as path from 'path' ;
import * as util from 'util' ;
2019-12-19 16:53:24 -08:00
import { BrowserFetcher , BrowserFetcherOptions , BrowserFetcherRevisionInfo , OnProgressCallback } from '../browserFetcher' ;
2020-01-07 12:53:06 -08:00
import { DeviceDescriptors } from '../deviceDescriptors' ;
2019-12-19 16:53:24 -08:00
import * as Errors from '../errors' ;
2020-01-07 12:53:06 -08:00
import * as types from '../types' ;
2019-12-19 16:53:24 -08:00
import { assert } from '../helper' ;
2020-01-07 13:58:23 -08:00
import { ConnectionTransport , WebSocketTransport , SlowMoTransport , PipeTransport } from '../transport' ;
2019-12-19 16:53:24 -08:00
import { CRBrowser } from './crBrowser' ;
2020-01-07 13:58:23 -08:00
import * as platform from '../platform' ;
import { TimeoutError } from '../errors' ;
import { launchProcess , waitForLine } from '../processLauncher' ;
2020-01-07 14:13:55 -08:00
import { ChildProcess } from 'child_process' ;
import { CRConnection } from './crConnection' ;
2020-01-07 13:58:23 -08:00
2020-01-07 14:13:55 -08:00
export type SlowMoOptions = {
slowMo? : number ,
} ;
export type ChromeArgOptions = {
2020-01-07 13:58:23 -08:00
headless? : boolean ,
args? : string [ ] ,
userDataDir? : string ,
devtools? : boolean ,
} ;
2020-01-07 14:13:55 -08:00
export type LaunchOptions = ChromeArgOptions & SlowMoOptions & {
2020-01-07 13:58:23 -08:00
executablePath? : string ,
ignoreDefaultArgs? : boolean | string [ ] ,
handleSIGINT? : boolean ,
handleSIGTERM? : boolean ,
handleSIGHUP? : boolean ,
timeout? : number ,
dumpio? : boolean ,
env ? : { [ key : string ] : string } | undefined ,
pipe? : boolean ,
} ;
2020-01-07 14:13:55 -08:00
export type ConnectOptions = SlowMoOptions & {
browserWSEndpoint? : string ;
browserURL? : string ;
transport? : ConnectionTransport ;
2020-01-07 13:58:23 -08:00
} ;
2019-12-19 16:53:24 -08:00
2020-01-07 14:13:55 -08:00
export class CRBrowserServer {
private _process : ChildProcess ;
private _connectOptions : ConnectOptions ;
constructor ( process : ChildProcess , connectOptions : ConnectOptions ) {
this . _process = process ;
this . _connectOptions = connectOptions ;
}
async connect ( ) : Promise < CRBrowser > {
const transport = await createTransport ( this . _connectOptions ) ;
return CRBrowser . create ( transport ) ;
}
process ( ) : ChildProcess {
return this . _process ;
}
wsEndpoint ( ) : string | null {
return this . _connectOptions . browserWSEndpoint || null ;
}
connectOptions ( ) : ConnectOptions {
return this . _connectOptions ;
}
async close ( ) : Promise < void > {
const transport = await createTransport ( this . _connectOptions ) ;
const connection = new CRConnection ( transport ) ;
await connection . rootSession . send ( 'Browser.close' ) ;
connection . dispose ( ) ;
}
}
2019-12-19 16:53:24 -08:00
export class CRPlaywright {
private _projectRoot : string ;
readonly _revision : string ;
constructor ( projectRoot : string , preferredRevision : string ) {
this . _projectRoot = projectRoot ;
this . _revision = preferredRevision ;
}
async downloadBrowser ( options? : BrowserFetcherOptions & { onProgress? : OnProgressCallback } ) : Promise < BrowserFetcherRevisionInfo > {
const fetcher = this . createBrowserFetcher ( options ) ;
const revisionInfo = fetcher . revisionInfo ( this . _revision ) ;
await fetcher . download ( this . _revision , options ? options.onProgress : undefined ) ;
return revisionInfo ;
}
2020-01-07 14:13:55 -08:00
async launch ( options? : LaunchOptions ) : Promise < CRBrowser > {
2020-01-07 13:58:23 -08:00
const server = await this . launchServer ( options ) ;
2019-12-19 16:53:24 -08:00
return server . connect ( ) ;
}
2020-01-07 14:13:55 -08:00
async launchServer ( options : LaunchOptions = { } ) : Promise < CRBrowserServer > {
2020-01-07 13:58:23 -08:00
const {
ignoreDefaultArgs = false ,
args = [ ] ,
dumpio = false ,
executablePath = null ,
pipe = false ,
env = process . env ,
handleSIGINT = true ,
handleSIGTERM = true ,
handleSIGHUP = true ,
slowMo = 0 ,
timeout = 30000
} = options ;
const chromeArguments = [ ] ;
if ( ! ignoreDefaultArgs )
chromeArguments . push ( . . . this . defaultArgs ( options ) ) ;
else if ( Array . isArray ( ignoreDefaultArgs ) )
chromeArguments . push ( . . . this . defaultArgs ( options ) . filter ( arg = > ignoreDefaultArgs . indexOf ( arg ) === - 1 ) ) ;
else
chromeArguments . push ( . . . args ) ;
let temporaryUserDataDir : string | null = null ;
if ( ! chromeArguments . some ( argument = > argument . startsWith ( '--remote-debugging-' ) ) )
chromeArguments . push ( pipe ? '--remote-debugging-pipe' : '--remote-debugging-port=0' ) ;
if ( ! chromeArguments . some ( arg = > arg . startsWith ( '--user-data-dir' ) ) ) {
temporaryUserDataDir = await mkdtempAsync ( CHROME_PROFILE_PATH ) ;
chromeArguments . push ( ` --user-data-dir= ${ temporaryUserDataDir } ` ) ;
}
let chromeExecutable = executablePath ;
if ( ! executablePath ) {
const { missingText , executablePath } = this . _resolveExecutablePath ( ) ;
if ( missingText )
throw new Error ( missingText ) ;
chromeExecutable = executablePath ;
}
const usePipe = chromeArguments . includes ( '--remote-debugging-pipe' ) ;
const launchedProcess = await launchProcess ( {
executablePath : chromeExecutable ,
args : chromeArguments ,
env ,
handleSIGINT ,
handleSIGTERM ,
handleSIGHUP ,
dumpio ,
pipe : usePipe ,
tempDir : temporaryUserDataDir
} , ( ) = > {
2020-01-07 14:13:55 -08:00
if ( temporaryUserDataDir || ! server )
2020-01-07 13:58:23 -08:00
return Promise . reject ( ) ;
2020-01-07 14:13:55 -08:00
return server . close ( ) ;
2020-01-07 13:58:23 -08:00
} ) ;
2020-01-07 14:13:55 -08:00
let server : CRBrowserServer | undefined ;
2020-01-07 13:58:23 -08:00
try {
2020-01-07 14:13:55 -08:00
let connectOptions : ConnectOptions | undefined ;
2020-01-07 13:58:23 -08:00
let browserWSEndpoint : string = '' ;
if ( ! usePipe ) {
const timeoutError = new TimeoutError ( ` Timed out after ${ timeout } ms while trying to connect to Chrome! The only Chrome revision guaranteed to work is r ${ this . _revision } ` ) ;
const match = await waitForLine ( launchedProcess , launchedProcess . stderr , /^DevTools listening on (ws:\/\/.*)$/ , timeout , timeoutError ) ;
browserWSEndpoint = match [ 1 ] ;
2020-01-07 14:13:55 -08:00
connectOptions = { browserWSEndpoint , slowMo } ;
2020-01-07 13:58:23 -08:00
} else {
2020-01-07 14:13:55 -08:00
const transport = new PipeTransport ( launchedProcess . stdio [ 3 ] as NodeJS . WritableStream , launchedProcess . stdio [ 4 ] as NodeJS . ReadableStream ) ;
connectOptions = { slowMo , transport } ;
2020-01-07 13:58:23 -08:00
}
2020-01-07 14:13:55 -08:00
server = new CRBrowserServer ( launchedProcess , connectOptions ) ;
return server ;
2020-01-07 13:58:23 -08:00
} catch ( e ) {
2020-01-07 14:13:55 -08:00
if ( server )
await server . close ( ) ;
2020-01-07 13:58:23 -08:00
throw e ;
}
2019-12-19 16:53:24 -08:00
}
2020-01-07 14:13:55 -08:00
async connect ( options : ConnectOptions ) : Promise < CRBrowser > {
const transport = await createTransport ( options ) ;
return CRBrowser . create ( transport ) ;
2019-12-19 16:53:24 -08:00
}
executablePath ( ) : string {
2020-01-07 13:58:23 -08:00
return this . _resolveExecutablePath ( ) . executablePath ;
2019-12-19 16:53:24 -08:00
}
2020-01-07 12:53:06 -08:00
get devices ( ) : types . Devices {
return DeviceDescriptors ;
2019-12-19 16:53:24 -08:00
}
get errors ( ) : any {
return Errors ;
}
2020-01-07 14:13:55 -08:00
defaultArgs ( options : ChromeArgOptions = { } ) : string [ ] {
2020-01-07 13:58:23 -08:00
const {
devtools = false ,
headless = ! devtools ,
args = [ ] ,
userDataDir = null
} = options ;
const chromeArguments = [ . . . DEFAULT_ARGS ] ;
if ( userDataDir )
chromeArguments . push ( ` --user-data-dir= ${ userDataDir } ` ) ;
if ( devtools )
chromeArguments . push ( '--auto-open-devtools-for-tabs' ) ;
if ( headless ) {
chromeArguments . push (
'--headless' ,
'--hide-scrollbars' ,
'--mute-audio'
) ;
}
if ( args . every ( arg = > arg . startsWith ( '-' ) ) )
chromeArguments . push ( 'about:blank' ) ;
chromeArguments . push ( . . . args ) ;
return chromeArguments ;
2019-12-19 16:53:24 -08:00
}
2020-01-07 13:58:23 -08:00
createBrowserFetcher ( options : BrowserFetcherOptions = { } ) : BrowserFetcher {
const downloadURLs = {
linux : '%s/chromium-browser-snapshots/Linux_x64/%d/%s.zip' ,
mac : '%s/chromium-browser-snapshots/Mac/%d/%s.zip' ,
win32 : '%s/chromium-browser-snapshots/Win/%d/%s.zip' ,
win64 : '%s/chromium-browser-snapshots/Win_x64/%d/%s.zip' ,
} ;
const defaultOptions = {
path : path.join ( this . _projectRoot , '.local-chromium' ) ,
host : 'https://storage.googleapis.com' ,
platform : ( ( ) = > {
const platform = os . platform ( ) ;
if ( platform === 'darwin' )
return 'mac' ;
if ( platform === 'linux' )
return 'linux' ;
if ( platform === 'win32' )
return os . arch ( ) === 'x64' ? 'win64' : 'win32' ;
return platform ;
} ) ( )
} ;
options = {
. . . defaultOptions ,
. . . options ,
} ;
assert ( ! ! ( downloadURLs as any ) [ options . platform ] , 'Unsupported platform: ' + options . platform ) ;
return new BrowserFetcher ( options . path , options . platform , ( platform : string , revision : string ) = > {
let archiveName = '' ;
let executablePath = '' ;
if ( platform === 'linux' ) {
archiveName = 'chrome-linux' ;
executablePath = path . join ( archiveName , 'chrome' ) ;
} else if ( platform === 'mac' ) {
archiveName = 'chrome-mac' ;
executablePath = path . join ( archiveName , 'Chromium.app' , 'Contents' , 'MacOS' , 'Chromium' ) ;
} else if ( platform === 'win32' || platform === 'win64' ) {
// Windows archive name changed at r591479.
archiveName = parseInt ( revision , 10 ) > 591479 ? 'chrome-win' : 'chrome-win32' ;
executablePath = path . join ( archiveName , 'chrome.exe' ) ;
}
return {
downloadUrl : util.format ( ( downloadURLs as any ) [ platform ] , options . host , revision , archiveName ) ,
executablePath
} ;
} ) ;
}
_resolveExecutablePath ( ) : { executablePath : string ; missingText : string | null ; } {
const browserFetcher = this . createBrowserFetcher ( ) ;
const revisionInfo = browserFetcher . revisionInfo ( this . _revision ) ;
const missingText = ! revisionInfo . local ? ` Chromium revision is not downloaded. Run "npm install" or "yarn install" ` : null ;
return { executablePath : revisionInfo.executablePath , missingText } ;
2019-12-19 16:53:24 -08:00
}
}
2020-01-07 13:58:23 -08:00
const mkdtempAsync = platform . promisify ( fs . mkdtemp ) ;
const CHROME_PROFILE_PATH = path . join ( os . tmpdir ( ) , 'playwright_dev_profile-' ) ;
const DEFAULT_ARGS = [
'--disable-background-networking' ,
'--enable-features=NetworkService,NetworkServiceInProcess' ,
'--disable-background-timer-throttling' ,
'--disable-backgrounding-occluded-windows' ,
'--disable-breakpad' ,
'--disable-client-side-phishing-detection' ,
'--disable-component-extensions-with-background-pages' ,
'--disable-default-apps' ,
'--disable-dev-shm-usage' ,
'--disable-extensions' ,
// BlinkGenPropertyTrees disabled due to crbug.com/937609
'--disable-features=TranslateUI,BlinkGenPropertyTrees' ,
'--disable-hang-monitor' ,
'--disable-ipc-flooding-protection' ,
'--disable-popup-blocking' ,
'--disable-prompt-on-repost' ,
'--disable-renderer-backgrounding' ,
'--disable-sync' ,
'--force-color-profile=srgb' ,
'--metrics-recording-only' ,
'--no-first-run' ,
'--enable-automation' ,
'--password-store=basic' ,
'--use-mock-keychain' ,
] ;
2019-12-19 16:53:24 -08:00
function getWSEndpoint ( browserURL : string ) : Promise < string > {
let resolve : ( url : string ) = > void ;
let reject : ( e : Error ) = > void ;
const promise = new Promise < string > ( ( res , rej ) = > { resolve = res ; reject = rej ; } ) ;
const endpointURL = URL . resolve ( browserURL , '/json/version' ) ;
const protocol = endpointURL . startsWith ( 'https' ) ? https : http ;
const requestOptions = Object . assign ( URL . parse ( endpointURL ) , { method : 'GET' } ) ;
const request = protocol . request ( requestOptions , res = > {
let data = '' ;
if ( res . statusCode !== 200 ) {
// Consume response data to free up memory.
res . resume ( ) ;
reject ( new Error ( 'HTTP ' + res . statusCode ) ) ;
return ;
}
res . setEncoding ( 'utf8' ) ;
res . on ( 'data' , chunk = > data += chunk ) ;
res . on ( 'end' , ( ) = > resolve ( JSON . parse ( data ) . webSocketDebuggerUrl ) ) ;
} ) ;
request . on ( 'error' , reject ) ;
request . end ( ) ;
return promise . catch ( e = > {
e . message = ` Failed to fetch browser webSocket url from ${ endpointURL } : ` + e . message ;
throw e ;
} ) ;
2020-01-07 13:58:23 -08:00
}
2020-01-07 14:13:55 -08:00
async function createTransport ( options : ConnectOptions ) : Promise < ConnectionTransport > {
assert ( Number ( ! ! options . browserWSEndpoint ) + Number ( ! ! options . browserURL ) + Number ( ! ! options . transport ) === 1 , 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to playwright.connect' ) ;
let transport : ConnectionTransport | undefined ;
let connectionURL : string = '' ;
if ( options . transport ) {
transport = options . transport ;
} else if ( options . browserWSEndpoint ) {
connectionURL = options . browserWSEndpoint ;
transport = await WebSocketTransport . create ( options . browserWSEndpoint ) ;
} else if ( options . browserURL ) {
connectionURL = await getWSEndpoint ( options . browserURL ) ;
transport = await WebSocketTransport . create ( connectionURL ) ;
}
return SlowMoTransport . wrap ( transport , options . slowMo ) ;
}