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 { Events } from './events' ;
import { Events as CommonEvents } from '../events' ;
import { assert , helper } from '../helper' ;
import { BrowserContext , BrowserContextOptions } from '../browserContext' ;
import { CRConnection , ConnectionEvents , CRSession } from './crConnection' ;
2020-01-07 12:59:01 -08:00
import { Page , Worker } from '../page' ;
2019-12-19 16:53:24 -08:00
import { CRTarget } from './crTarget' ;
import { Protocol } from './protocol' ;
2019-12-23 11:39:57 -08:00
import { CRPage } from './crPage' ;
2020-01-08 14:04:33 -08:00
import { Browser } from '../browser' ;
2019-12-19 16:53:24 -08:00
import * as network from '../network' ;
2020-01-03 10:14:50 -08:00
import * as types from '../types' ;
2020-01-07 11:55:24 -08:00
import * as platform from '../platform' ;
2020-01-07 15:27:45 -08:00
import { ConnectionTransport , SlowMoTransport } from '../transport' ;
2019-12-19 17:03:27 -08:00
import { readProtocolStream } from './crProtocolHelper' ;
2019-12-19 16:53:24 -08:00
2020-01-07 15:27:45 -08:00
export type CRConnectOptions = {
slowMo? : number ,
browserWSEndpoint? : string ;
browserURL? : string ;
transport? : ConnectionTransport ;
} ;
2020-01-08 14:04:33 -08:00
export class CRBrowser extends platform . EventEmitter implements Browser {
2019-12-19 16:53:24 -08:00
_connection : CRConnection ;
_client : CRSession ;
private _defaultContext : BrowserContext ;
private _contexts = new Map < string , BrowserContext > ( ) ;
_targets = new Map < string , CRTarget > ( ) ;
private _tracingRecording = false ;
2020-01-13 13:33:25 -08:00
private _tracingPath : string | null = '' ;
2019-12-19 16:53:24 -08:00
private _tracingClient : CRSession | undefined ;
2020-01-07 15:27:45 -08:00
static async connect ( options : CRConnectOptions ) : Promise < CRBrowser > {
const transport = await createTransport ( options ) ;
2019-12-19 16:53:24 -08:00
const connection = new CRConnection ( transport ) ;
const { browserContextIds } = await connection . rootSession . send ( 'Target.getBrowserContexts' ) ;
const browser = new CRBrowser ( connection , browserContextIds ) ;
await connection . rootSession . send ( 'Target.setDiscoverTargets' , { discover : true } ) ;
await browser . waitForTarget ( t = > t . type ( ) === 'page' ) ;
return browser ;
}
constructor ( connection : CRConnection , contextIds : string [ ] ) {
super ( ) ;
this . _connection = connection ;
this . _client = connection . rootSession ;
this . _defaultContext = this . _createBrowserContext ( null , { } ) ;
for ( const contextId of contextIds )
this . _contexts . set ( contextId , this . _createBrowserContext ( contextId , { } ) ) ;
this . _connection . on ( ConnectionEvents . Disconnected , ( ) = > this . emit ( CommonEvents . Browser . Disconnected ) ) ;
this . _client . on ( 'Target.targetCreated' , this . _targetCreated . bind ( this ) ) ;
this . _client . on ( 'Target.targetDestroyed' , this . _targetDestroyed . bind ( this ) ) ;
this . _client . on ( 'Target.targetInfoChanged' , this . _targetInfoChanged . bind ( this ) ) ;
}
_createBrowserContext ( contextId : string | null , options : BrowserContextOptions ) : BrowserContext {
const context = new BrowserContext ( {
pages : async ( ) : Promise < Page [ ] > = > {
const targets = this . _allTargets ( ) . filter ( target = > target . browserContext ( ) === context && target . type ( ) === 'page' ) ;
const pages = await Promise . all ( targets . map ( target = > target . page ( ) ) ) ;
2020-01-13 13:33:25 -08:00
return pages . filter ( page = > ! ! page ) as Page [ ] ;
2019-12-19 16:53:24 -08:00
} ,
newPage : async ( ) : Promise < Page > = > {
const { targetId } = await this . _client . send ( 'Target.createTarget' , { url : 'about:blank' , browserContextId : contextId || undefined } ) ;
2020-01-13 13:33:25 -08:00
const target = this . _targets . get ( targetId ) ! ;
2019-12-19 16:53:24 -08:00
assert ( await target . _initializedPromise , 'Failed to create target for page' ) ;
2020-01-13 13:33:25 -08:00
const page = await target . page ( ) ;
return page ! ;
2019-12-19 16:53:24 -08:00
} ,
close : async ( ) : Promise < void > = > {
assert ( contextId , 'Non-incognito profiles cannot be closed!' ) ;
2020-01-13 13:33:25 -08:00
await this . _client . send ( 'Target.disposeBrowserContext' , { browserContextId : contextId ! } ) ;
this . _contexts . delete ( contextId ! ) ;
2019-12-19 16:53:24 -08:00
} ,
cookies : async ( ) : Promise < network.NetworkCookie [ ] > = > {
const { cookies } = await this . _client . send ( 'Storage.getCookies' , { browserContextId : contextId || undefined } ) ;
return cookies . map ( c = > {
const copy : any = { sameSite : 'None' , . . . c } ;
delete copy . size ;
delete copy . priority ;
return copy as network . NetworkCookie ;
} ) ;
} ,
clearCookies : async ( ) : Promise < void > = > {
await this . _client . send ( 'Storage.clearCookies' , { browserContextId : contextId || undefined } ) ;
} ,
setCookies : async ( cookies : network.SetNetworkCookieParam [ ] ) : Promise < void > = > {
await this . _client . send ( 'Storage.setCookies' , { cookies , browserContextId : contextId || undefined } ) ;
} ,
2019-12-20 13:07:14 -08:00
setPermissions : async ( origin : string , permissions : string [ ] ) : Promise < void > = > {
const webPermissionToProtocol = new Map < string , Protocol.Browser.PermissionType > ( [
[ 'geolocation' , 'geolocation' ] ,
[ 'midi' , 'midi' ] ,
[ 'notifications' , 'notifications' ] ,
[ 'camera' , 'videoCapture' ] ,
[ 'microphone' , 'audioCapture' ] ,
[ 'background-sync' , 'backgroundSync' ] ,
[ 'ambient-light-sensor' , 'sensors' ] ,
[ 'accelerometer' , 'sensors' ] ,
[ 'gyroscope' , 'sensors' ] ,
[ 'magnetometer' , 'sensors' ] ,
[ 'accessibility-events' , 'accessibilityEvents' ] ,
[ 'clipboard-read' , 'clipboardReadWrite' ] ,
[ 'clipboard-write' , 'clipboardSanitizedWrite' ] ,
[ 'payment-handler' , 'paymentHandler' ] ,
// chrome-specific permissions we have.
[ 'midi-sysex' , 'midiSysex' ] ,
] ) ;
const filtered = permissions . map ( permission = > {
const protocolPermission = webPermissionToProtocol . get ( permission ) ;
if ( ! protocolPermission )
throw new Error ( 'Unknown permission: ' + permission ) ;
return protocolPermission ;
} ) ;
await this . _client . send ( 'Browser.grantPermissions' , { origin , browserContextId : contextId || undefined , permissions : filtered } ) ;
} ,
2019-12-20 15:32:30 -08:00
2019-12-20 13:07:14 -08:00
clearPermissions : async ( ) = > {
await this . _client . send ( 'Browser.resetPermissions' , { browserContextId : contextId || undefined } ) ;
2020-01-03 10:14:50 -08:00
} ,
2019-12-20 15:32:30 -08:00
2020-01-03 10:14:50 -08:00
setGeolocation : async ( geolocation : types.Geolocation | null ) : Promise < void > = > {
for ( const page of await context . pages ( ) )
await ( page . _delegate as CRPage ) . _client . send ( 'Emulation.setGeolocationOverride' , geolocation || { } ) ;
}
2019-12-19 16:53:24 -08:00
} , options ) ;
return context ;
}
async newContext ( options : BrowserContextOptions = { } ) : Promise < BrowserContext > {
const { browserContextId } = await this . _client . send ( 'Target.createBrowserContext' ) ;
const context = this . _createBrowserContext ( browserContextId , options ) ;
2020-01-13 13:32:44 -08:00
await context . _initialize ( ) ;
2019-12-19 16:53:24 -08:00
this . _contexts . set ( browserContextId , context ) ;
return context ;
}
browserContexts ( ) : BrowserContext [ ] {
return [ this . _defaultContext , . . . Array . from ( this . _contexts . values ( ) ) ] ;
}
defaultContext ( ) : BrowserContext {
return this . _defaultContext ;
}
async _targetCreated ( event : Protocol.Target.targetCreatedPayload ) {
const targetInfo = event . targetInfo ;
const { browserContextId } = targetInfo ;
2020-01-13 13:33:25 -08:00
const context = ( browserContextId && this . _contexts . has ( browserContextId ) ) ? this . _contexts . get ( browserContextId ) ! : this . _defaultContext ;
2019-12-19 16:53:24 -08:00
const target = new CRTarget ( this , targetInfo , context , ( ) = > this . _connection . createSession ( targetInfo ) ) ;
assert ( ! this . _targets . has ( event . targetInfo . targetId ) , 'Target should not exist before targetCreated' ) ;
this . _targets . set ( event . targetInfo . targetId , target ) ;
if ( target . _isInitialized || await target . _initializedPromise )
2019-12-20 13:07:14 -08:00
this . emit ( Events . CRBrowser . TargetCreated , target ) ;
2019-12-19 16:53:24 -08:00
}
async _targetDestroyed ( event : { targetId : string ; } ) {
2020-01-13 13:33:25 -08:00
const target = this . _targets . get ( event . targetId ) ! ;
2019-12-19 16:53:24 -08:00
target . _initializedCallback ( false ) ;
this . _targets . delete ( event . targetId ) ;
target . _didClose ( ) ;
if ( await target . _initializedPromise )
2019-12-20 13:07:14 -08:00
this . emit ( Events . CRBrowser . TargetDestroyed , target ) ;
2019-12-19 16:53:24 -08:00
}
_targetInfoChanged ( event : Protocol.Target.targetInfoChangedPayload ) {
2020-01-13 13:33:25 -08:00
const target = this . _targets . get ( event . targetInfo . targetId ) ! ;
2019-12-19 16:53:24 -08:00
assert ( target , 'target should exist before targetInfoChanged' ) ;
const previousURL = target . url ( ) ;
const wasInitialized = target . _isInitialized ;
target . _targetInfoChanged ( event . targetInfo ) ;
if ( wasInitialized && previousURL !== target . url ( ) )
2019-12-20 13:07:14 -08:00
this . emit ( Events . CRBrowser . TargetChanged , target ) ;
2019-12-19 16:53:24 -08:00
}
async _closePage ( page : Page ) {
await this . _client . send ( 'Target.closeTarget' , { targetId : CRTarget.fromPage ( page ) . _targetId } ) ;
}
_allTargets ( ) : CRTarget [ ] {
return Array . from ( this . _targets . values ( ) ) . filter ( target = > target . _isInitialized ) ;
}
async waitForTarget ( predicate : ( arg0 : CRTarget ) = > boolean , options : { timeout? : number ; } | undefined = { } ) : Promise < CRTarget > {
const {
timeout = 30000
} = options ;
const existingTarget = this . _allTargets ( ) . find ( predicate ) ;
if ( existingTarget )
return existingTarget ;
let resolve : ( target : CRTarget ) = > void ;
const targetPromise = new Promise < CRTarget > ( x = > resolve = x ) ;
2019-12-20 13:07:14 -08:00
this . on ( Events . CRBrowser . TargetCreated , check ) ;
this . on ( Events . CRBrowser . TargetChanged , check ) ;
2019-12-19 16:53:24 -08:00
try {
if ( ! timeout )
return await targetPromise ;
return await helper . waitWithTimeout ( targetPromise , 'target' , timeout ) ;
} finally {
2019-12-20 13:07:14 -08:00
this . removeListener ( Events . CRBrowser . TargetCreated , check ) ;
this . removeListener ( Events . CRBrowser . TargetChanged , check ) ;
2019-12-19 16:53:24 -08:00
}
function check ( target : CRTarget ) {
if ( predicate ( target ) )
resolve ( target ) ;
}
}
async close() {
2020-01-08 13:55:38 -08:00
const disconnected = new Promise ( f = > this . _connection . once ( ConnectionEvents . Disconnected , f ) ) ;
2019-12-19 16:53:24 -08:00
await this . _connection . rootSession . send ( 'Browser.close' ) ;
2020-01-08 13:55:38 -08:00
await disconnected ;
2019-12-19 16:53:24 -08:00
}
browserTarget ( ) : CRTarget {
2020-01-13 13:33:25 -08:00
return [ . . . this . _targets . values ( ) ] . find ( t = > t . type ( ) === 'browser' ) ! ;
2019-12-19 16:53:24 -08:00
}
2020-01-07 12:59:01 -08:00
serviceWorker ( target : CRTarget ) : Promise < Worker | null > {
2019-12-19 16:53:24 -08:00
return target . _worker ( ) ;
}
async startTracing ( page : Page | undefined , options : { path? : string ; screenshots? : boolean ; categories? : string [ ] ; } = { } ) {
assert ( ! this . _tracingRecording , 'Cannot start recording trace while already recording trace.' ) ;
2019-12-23 11:39:57 -08:00
this . _tracingClient = page ? ( page . _delegate as CRPage ) . _client : this._client ;
2019-12-19 16:53:24 -08:00
const defaultCategories = [
'-*' , 'devtools.timeline' , 'v8.execute' , 'disabled-by-default-devtools.timeline' ,
'disabled-by-default-devtools.timeline.frame' , 'toplevel' ,
'blink.console' , 'blink.user_timing' , 'latencyInfo' , 'disabled-by-default-devtools.timeline.stack' ,
'disabled-by-default-v8.cpu_profiler' , 'disabled-by-default-v8.cpu_profiler.hires'
] ;
const {
path = null ,
screenshots = false ,
categories = defaultCategories ,
} = options ;
if ( screenshots )
categories . push ( 'disabled-by-default-devtools.screenshot' ) ;
this . _tracingPath = path ;
this . _tracingRecording = true ;
await this . _tracingClient . send ( 'Tracing.start' , {
transferMode : 'ReturnAsStream' ,
categories : categories.join ( ',' )
} ) ;
}
2020-01-07 11:55:24 -08:00
async stopTracing ( ) : Promise < platform.BufferType > {
2019-12-19 16:53:24 -08:00
assert ( this . _tracingClient , 'Tracing was not started.' ) ;
2020-01-07 11:55:24 -08:00
let fulfill : ( buffer : platform.BufferType ) = > void ;
const contentPromise = new Promise < platform.BufferType > ( x = > fulfill = x ) ;
2020-01-13 13:33:25 -08:00
this . _tracingClient ! . once ( 'Tracing.tracingComplete' , event = > {
readProtocolStream ( this . _tracingClient ! , event . stream ! , this . _tracingPath ) . then ( fulfill ) ;
2019-12-19 16:53:24 -08:00
} ) ;
2020-01-13 13:33:25 -08:00
await this . _tracingClient ! . send ( 'Tracing.end' ) ;
2019-12-19 16:53:24 -08:00
this . _tracingRecording = false ;
return contentPromise ;
}
targets ( context? : BrowserContext ) : CRTarget [ ] {
const targets = this . _allTargets ( ) ;
return context ? targets . filter ( t = > t . browserContext ( ) === context ) : targets ;
}
pageTarget ( page : Page ) : CRTarget {
return CRTarget . fromPage ( page ) ;
}
disconnect() {
this . _connection . dispose ( ) ;
}
isConnected ( ) : boolean {
return ! this . _connection . _closed ;
}
}
2020-01-07 15:27:45 -08:00
export async function createTransport ( options : CRConnectOptions ) : Promise < ConnectionTransport > {
2020-01-07 16:13:49 -08:00
assert ( Number ( ! ! options . browserWSEndpoint ) + Number ( ! ! options . browserURL ) + Number ( ! ! options . transport ) === 1 , 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to connect' ) ;
2020-01-07 15:27:45 -08:00
let transport : ConnectionTransport | undefined ;
if ( options . transport ) {
transport = options . transport ;
} else if ( options . browserWSEndpoint ) {
transport = await platform . createWebSocketTransport ( options . browserWSEndpoint ) ;
} else if ( options . browserURL ) {
2020-01-07 16:13:49 -08:00
let connectionURL : string ;
2020-01-07 15:27:45 -08:00
try {
const data = await platform . fetchUrl ( new URL ( '/json/version' , options . browserURL ) . href ) ;
connectionURL = JSON . parse ( data ) . webSocketDebuggerUrl ;
} catch ( e ) {
e . message = ` Failed to fetch browser webSocket url from ${ options . browserURL } : ` + e . message ;
throw e ;
}
transport = await platform . createWebSocketTransport ( connectionURL ) ;
}
2020-01-13 13:33:25 -08:00
return SlowMoTransport . wrap ( transport ! , options . slowMo ) ;
2020-01-07 15:27:45 -08:00
}