2019-12-10 15:13:56 -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 .
* /
2020-06-30 11:02:12 -07:00
import { isUnderTest , helper , deprecate } from './helper' ;
2020-03-05 17:22:57 -08:00
import * as network from './network' ;
import { Page , PageBinding } from './page' ;
2020-02-13 14:18:18 -08:00
import { TimeoutSettings } from './timeoutSettings' ;
2020-06-11 18:18:33 -07:00
import * as frames from './frames' ;
2020-03-05 17:22:57 -08:00
import * as types from './types' ;
import { Events } from './events' ;
2020-04-02 17:56:14 -07:00
import { Download } from './download' ;
2020-04-20 07:52:26 -07:00
import { BrowserBase } from './browser' ;
2020-06-16 17:11:19 -07:00
import { Loggers , Logger } from './logger' ;
2020-06-10 15:12:50 -07:00
import { EventEmitter } from 'events' ;
import { ProgressController } from './progress' ;
2020-06-13 13:17:12 -07:00
import { DebugController } from './debug/debugController' ;
2020-06-23 14:51:06 -07:00
import { LoggerSink } from './loggerSink' ;
2019-12-11 07:18:43 -08:00
2020-06-09 16:11:17 -07:00
export interface BrowserContext {
2020-02-24 08:53:30 -08:00
setDefaultNavigationTimeout ( timeout : number ) : void ;
setDefaultTimeout ( timeout : number ) : void ;
2020-03-13 11:33:33 -07:00
pages ( ) : Page [ ] ;
2020-02-24 08:53:30 -08:00
newPage ( ) : Promise < Page > ;
2020-06-25 08:30:56 -07:00
cookies ( urls? : string | string [ ] ) : Promise < types.NetworkCookie [ ] > ;
addCookies ( cookies : types.SetNetworkCookieParam [ ] ) : Promise < void > ;
2020-02-24 08:53:30 -08:00
clearCookies ( ) : Promise < void > ;
2020-03-17 15:32:50 -07:00
grantPermissions ( permissions : string [ ] , options ? : { origin? : string } ) : Promise < void > ;
2020-02-24 08:53:30 -08:00
clearPermissions ( ) : Promise < void > ;
setGeolocation ( geolocation : types.Geolocation | null ) : Promise < void > ;
2020-06-25 08:30:56 -07:00
setExtraHTTPHeaders ( headers : types.Headers ) : Promise < void > ;
2020-03-04 17:58:12 -08:00
setOffline ( offline : boolean ) : Promise < void > ;
2020-03-06 13:50:42 -08:00
setHTTPCredentials ( httpCredentials : types.Credentials | null ) : Promise < void > ;
2020-03-20 15:08:17 -07:00
addInitScript ( script : Function | string | { path? : string , content? : string } , arg? : any ) : Promise < void > ;
2020-06-11 18:18:33 -07:00
exposeBinding ( name : string , playwrightBinding : frames.FunctionWithSource ) : Promise < void > ;
2020-03-03 16:46:06 -08:00
exposeFunction ( name : string , playwrightFunction : Function ) : Promise < void > ;
2020-03-09 21:02:54 -07:00
route ( url : types.URLMatch , handler : network.RouteHandler ) : Promise < void > ;
2020-04-15 19:55:22 -07:00
unroute ( url : types.URLMatch , handler? : network.RouteHandler ) : Promise < void > ;
2020-03-05 17:22:57 -08:00
waitForEvent ( event : string , optionsOrPredicate? : Function | ( types . TimeoutOptions & { predicate? : Function } ) ) : Promise < any > ;
2020-02-24 08:53:30 -08:00
close ( ) : Promise < void > ;
2020-03-05 17:22:57 -08:00
}
2020-01-03 10:14:50 -08:00
2020-06-25 08:30:56 -07:00
type BrowserContextOptions = types . BrowserContextOptions & { logger? : LoggerSink } ;
2020-06-10 15:12:50 -07:00
export abstract class BrowserContextBase extends EventEmitter implements BrowserContext {
2020-03-05 17:22:57 -08:00
readonly _timeoutSettings = new TimeoutSettings ( ) ;
readonly _pageBindings = new Map < string , PageBinding > ( ) ;
2020-02-24 08:53:30 -08:00
readonly _options : BrowserContextOptions ;
2020-04-15 19:55:22 -07:00
_routes : { url : types.URLMatch , handler : network.RouteHandler } [ ] = [ ] ;
2020-06-29 16:26:32 -07:00
private _isPersistentContext : boolean ;
2020-07-08 21:36:03 -07:00
private _closedStatus : 'open' | 'closing' | 'closed' = 'open' ;
2020-06-10 15:12:50 -07:00
readonly _closePromise : Promise < Error > ;
2020-03-05 17:22:57 -08:00
private _closePromiseFulfill : ( ( error : Error ) = > void ) | undefined ;
2020-03-20 19:45:35 -07:00
readonly _permissions = new Map < string , string [ ] > ( ) ;
2020-04-02 17:56:14 -07:00
readonly _downloads = new Set < Download > ( ) ;
2020-04-20 07:52:26 -07:00
readonly _browserBase : BrowserBase ;
2020-06-16 17:11:19 -07:00
readonly _apiLogger : Logger ;
2020-03-05 17:22:57 -08:00
2020-06-29 16:26:32 -07:00
constructor ( browserBase : BrowserBase , options : BrowserContextOptions , isPersistentContext : boolean ) {
2020-03-05 17:22:57 -08:00
super ( ) ;
2020-04-20 07:52:26 -07:00
this . _browserBase = browserBase ;
2020-03-05 17:22:57 -08:00
this . _options = options ;
2020-06-16 17:11:19 -07:00
const loggers = options . logger ? new Loggers ( options . logger ) : browserBase . _options . loggers ;
this . _apiLogger = loggers . api ;
2020-06-29 16:26:32 -07:00
this . _isPersistentContext = isPersistentContext ;
2020-03-05 17:22:57 -08:00
this . _closePromise = new Promise ( fulfill = > this . _closePromiseFulfill = fulfill ) ;
}
2020-05-27 22:16:54 -07:00
async _initialize() {
2020-07-16 13:13:26 -07:00
if ( helper . isDebugMode ( ) )
new DebugController ( this ) ;
2020-05-27 22:16:54 -07:00
}
2020-06-10 15:12:50 -07:00
async waitForEvent ( event : string , optionsOrPredicate : types.WaitForEventOptions = { } ) : Promise < any > {
const options = typeof optionsOrPredicate === 'function' ? { predicate : optionsOrPredicate } : optionsOrPredicate ;
2020-06-16 17:11:19 -07:00
const progressController = new ProgressController ( this . _apiLogger , this . _timeoutSettings . timeout ( options ) , 'browserContext.waitForEvent' ) ;
2020-06-10 15:12:50 -07:00
if ( event !== Events . BrowserContext . Close )
this . _closePromise . then ( error = > progressController . abort ( error ) ) ;
2020-07-07 15:22:05 -07:00
return progressController . run ( progress = > helper . waitForEvent ( progress , this , event , options . predicate ) . promise ) ;
2020-03-31 16:18:49 -07:00
}
2020-03-05 17:22:57 -08:00
_browserClosed() {
2020-03-13 11:33:33 -07:00
for ( const page of this . pages ( ) )
2020-03-05 17:22:57 -08:00
page . _didClose ( ) ;
2020-06-29 16:26:32 -07:00
this . _didCloseInternal ( ) ;
2020-03-05 17:22:57 -08:00
}
2020-06-29 16:26:32 -07:00
private _didCloseInternal() {
2020-07-08 21:36:03 -07:00
if ( this . _closedStatus === 'closed' ) {
// We can come here twice if we close browser context and browser
// at the same time.
return ;
}
this . _closedStatus = 'closed' ;
2020-04-02 17:56:14 -07:00
this . _downloads . clear ( ) ;
2020-06-29 16:26:32 -07:00
this . _closePromiseFulfill ! ( new Error ( 'Context closed' ) ) ;
this . emit ( Events . BrowserContext . Close ) ;
2020-03-05 17:22:57 -08:00
}
// BrowserContext methods.
2020-03-13 11:33:33 -07:00
abstract pages ( ) : Page [ ] ;
2020-03-05 17:22:57 -08:00
abstract newPage ( ) : Promise < Page > ;
2020-06-25 08:30:56 -07:00
abstract _doCookies ( urls : string [ ] ) : Promise < types.NetworkCookie [ ] > ;
abstract addCookies ( cookies : types.SetNetworkCookieParam [ ] ) : Promise < void > ;
2020-03-05 17:22:57 -08:00
abstract clearCookies ( ) : Promise < void > ;
2020-03-17 15:32:50 -07:00
abstract _doGrantPermissions ( origin : string , permissions : string [ ] ) : Promise < void > ;
abstract _doClearPermissions ( ) : Promise < void > ;
2020-03-05 17:22:57 -08:00
abstract setGeolocation ( geolocation : types.Geolocation | null ) : Promise < void > ;
2020-06-30 11:02:12 -07:00
abstract _doSetHTTPCredentials ( httpCredentials : types.Credentials | null ) : Promise < void > ;
2020-06-25 08:30:56 -07:00
abstract setExtraHTTPHeaders ( headers : types.Headers ) : Promise < void > ;
2020-03-05 17:22:57 -08:00
abstract setOffline ( offline : boolean ) : Promise < void > ;
2020-06-25 08:30:56 -07:00
abstract _doAddInitScript ( expression : string ) : Promise < void > ;
2020-05-18 14:28:06 -07:00
abstract _doExposeBinding ( binding : PageBinding ) : Promise < void > ;
2020-03-09 21:02:54 -07:00
abstract route ( url : types.URLMatch , handler : network.RouteHandler ) : Promise < void > ;
2020-04-15 19:55:22 -07:00
abstract unroute ( url : types.URLMatch , handler? : network.RouteHandler ) : Promise < void > ;
2020-06-29 16:26:32 -07:00
abstract _doClose ( ) : Promise < void > ;
2020-03-05 17:22:57 -08:00
2020-06-25 08:30:56 -07:00
async cookies ( urls : string | string [ ] | undefined = [ ] ) : Promise < types.NetworkCookie [ ] > {
if ( urls && ! Array . isArray ( urls ) )
urls = [ urls ] ;
return await this . _doCookies ( urls as string [ ] ) ;
}
2020-05-18 14:28:06 -07:00
async exposeFunction ( name : string , playwrightFunction : Function ) : Promise < void > {
await this . exposeBinding ( name , ( options , . . . args : any ) = > playwrightFunction ( . . . args ) ) ;
}
2020-06-30 11:02:12 -07:00
setHTTPCredentials ( httpCredentials : types.Credentials | null ) : Promise < void > {
if ( ! isUnderTest ( ) )
deprecate ( ` context.setHTTPCredentials ` , ` warning: method |context.setHTTPCredentials()| is deprecated. Instead of changing credentials, create another browser context with new credentials. ` ) ;
return this . _doSetHTTPCredentials ( httpCredentials ) ;
}
2020-06-11 18:18:33 -07:00
async exposeBinding ( name : string , playwrightBinding : frames.FunctionWithSource ) : Promise < void > {
2020-05-18 14:28:06 -07:00
for ( const page of this . pages ( ) ) {
if ( page . _pageBindings . has ( name ) )
throw new Error ( ` Function " ${ name } " has been already registered in one of the pages ` ) ;
}
if ( this . _pageBindings . has ( name ) )
throw new Error ( ` Function " ${ name } " has been already registered ` ) ;
const binding = new PageBinding ( name , playwrightBinding ) ;
this . _pageBindings . set ( name , binding ) ;
this . _doExposeBinding ( binding ) ;
}
2020-06-25 08:30:56 -07:00
async addInitScript ( script : string | Function | { path? : string | undefined ; content? : string | undefined ; } , arg? : any ) : Promise < void > {
const source = await helper . evaluationScript ( script , arg ) ;
await this . _doAddInitScript ( source ) ;
}
2020-03-17 15:32:50 -07:00
async grantPermissions ( permissions : string [ ] , options ? : { origin? : string } ) {
let origin = '*' ;
if ( options && options . origin ) {
const url = new URL ( options . origin ) ;
origin = url . origin ;
}
const existing = new Set ( this . _permissions . get ( origin ) || [ ] ) ;
permissions . forEach ( p = > existing . add ( p ) ) ;
const list = [ . . . existing . values ( ) ] ;
this . _permissions . set ( origin , list ) ;
await this . _doGrantPermissions ( origin , list ) ;
}
async clearPermissions() {
this . _permissions . clear ( ) ;
await this . _doClearPermissions ( ) ;
}
2020-03-05 17:22:57 -08:00
setDefaultNavigationTimeout ( timeout : number ) {
this . _timeoutSettings . setDefaultNavigationTimeout ( timeout ) ;
}
setDefaultTimeout ( timeout : number ) {
this . _timeoutSettings . setDefaultTimeout ( timeout ) ;
}
2020-04-20 07:52:26 -07:00
2020-05-10 15:23:53 -07:00
async _loadDefaultContext() {
if ( ! this . pages ( ) . length )
await this . waitForEvent ( 'page' ) ;
const pages = this . pages ( ) ;
await pages [ 0 ] . waitForLoadState ( ) ;
2020-05-21 15:13:16 -07:00
if ( pages . length !== 1 || pages [ 0 ] . url ( ) !== 'about:blank' )
2020-05-13 15:57:26 -07:00
throw new Error ( ` Arguments can not specify page to be opened (first url is ${ pages [ 0 ] . url ( ) } ) ` ) ;
2020-05-21 15:13:16 -07:00
if ( this . _options . isMobile || this . _options . locale ) {
// Workaround for:
// - chromium fails to change isMobile for existing page;
// - webkit fails to change locale for existing page.
const oldPage = pages [ 0 ] ;
await this . newPage ( ) ;
await oldPage . close ( ) ;
2020-05-10 15:23:53 -07:00
}
}
2020-06-05 13:50:15 -07:00
protected _authenticateProxyViaHeader() {
const proxy = this . _browserBase . _options . proxy || { username : undefined , password : undefined } ;
const { username , password } = proxy ;
if ( username ) {
this . _options . httpCredentials = { username , password : password ! } ;
this . _options . extraHTTPHeaders = this . _options . extraHTTPHeaders || { } ;
const token = Buffer . from ( ` ${ username } : ${ password } ` ) . toString ( 'base64' ) ;
this . _options . extraHTTPHeaders [ 'Proxy-Authorization' ] = ` Basic ${ token } ` ;
}
}
protected _authenticateProxyViaCredentials() {
const proxy = this . _browserBase . _options . proxy ;
if ( ! proxy )
return ;
const { username , password } = proxy ;
if ( username && password )
this . _options . httpCredentials = { username , password } ;
}
2020-06-29 16:26:32 -07:00
async close() {
if ( this . _isPersistentContext ) {
// Default context is only created in 'persistent' mode and closing it should close
// the browser.
await this . _browserBase . close ( ) ;
return ;
}
2020-07-08 21:36:03 -07:00
if ( this . _closedStatus === 'open' ) {
this . _closedStatus = 'closing' ;
2020-06-29 16:26:32 -07:00
await this . _doClose ( ) ;
await Promise . all ( [ . . . this . _downloads ] . map ( d = > d . delete ( ) ) ) ;
this . _didCloseInternal ( ) ;
}
await this . _closePromise ;
}
2020-02-24 08:53:30 -08:00
}
2020-01-13 17:16:05 -08:00
2020-03-05 17:22:57 -08:00
export function assertBrowserContextIsNotOwned ( context : BrowserContextBase ) {
2020-03-13 11:33:33 -07:00
for ( const page of context . pages ( ) ) {
2020-02-24 08:53:30 -08:00
if ( page . _ownedContext )
throw new Error ( 'Please use browser.newContext() for multi-page scripts that share the context.' ) ;
2020-01-13 17:16:05 -08:00
}
2020-02-24 08:53:30 -08:00
}
2020-02-11 10:27:19 -08:00
2020-02-24 08:53:30 -08:00
export function validateBrowserContextOptions ( options : BrowserContextOptions ) : BrowserContextOptions {
2020-05-21 15:13:16 -07:00
// Copy all fields manually to strip any extra junk.
// Especially useful when we share context and launch options for launchPersistent.
const result : BrowserContextOptions = {
ignoreHTTPSErrors : options.ignoreHTTPSErrors ,
bypassCSP : options.bypassCSP ,
locale : options.locale ,
timezoneId : options.timezoneId ,
offline : options.offline ,
colorScheme : options.colorScheme ,
acceptDownloads : options.acceptDownloads ,
viewport : options.viewport ,
javaScriptEnabled : options.javaScriptEnabled ,
userAgent : options.userAgent ,
geolocation : options.geolocation ,
permissions : options.permissions ,
extraHTTPHeaders : options.extraHTTPHeaders ,
httpCredentials : options.httpCredentials ,
deviceScaleFactor : options.deviceScaleFactor ,
isMobile : options.isMobile ,
hasTouch : options.hasTouch ,
logger : options.logger ,
} ;
2020-05-12 18:31:17 -07:00
if ( result . viewport === null && result . deviceScaleFactor !== undefined )
throw new Error ( ` "deviceScaleFactor" option is not supported with null "viewport" ` ) ;
if ( result . viewport === null && result . isMobile !== undefined )
throw new Error ( ` "isMobile" option is not supported with null "viewport" ` ) ;
2020-02-24 08:53:30 -08:00
if ( ! result . viewport && result . viewport !== null )
result . viewport = { width : 1280 , height : 720 } ;
if ( result . viewport )
result . viewport = { . . . result . viewport } ;
if ( result . geolocation )
result . geolocation = verifyGeolocation ( result . geolocation ) ;
2020-02-26 12:42:20 -08:00
if ( result . extraHTTPHeaders )
result . extraHTTPHeaders = network . verifyHeaders ( result . extraHTTPHeaders ) ;
2020-02-24 08:53:30 -08:00
return result ;
2019-12-10 15:13:56 -08:00
}
2020-01-13 15:39:13 -08:00
2020-02-24 08:53:30 -08:00
export function verifyGeolocation ( geolocation : types.Geolocation ) : types . Geolocation {
2020-01-13 15:39:13 -08:00
const result = { . . . geolocation } ;
result . accuracy = result . accuracy || 0 ;
const { longitude , latitude , accuracy } = result ;
if ( ! helper . isNumber ( longitude ) || longitude < - 180 || longitude > 180 )
throw new Error ( ` Invalid longitude " ${ longitude } ": precondition -180 <= LONGITUDE <= 180 failed. ` ) ;
if ( ! helper . isNumber ( latitude ) || latitude < - 90 || latitude > 90 )
throw new Error ( ` Invalid latitude " ${ latitude } ": precondition -90 <= LATITUDE <= 90 failed. ` ) ;
if ( ! helper . isNumber ( accuracy ) || accuracy < 0 )
throw new Error ( ` Invalid accuracy " ${ accuracy } ": precondition 0 <= ACCURACY failed. ` ) ;
return result ;
}
2020-06-05 13:50:15 -07:00
export function verifyProxySettings ( proxy : types.ProxySettings ) : types . ProxySettings {
let { server , bypass } = proxy ;
if ( ! helper . isString ( server ) )
throw new Error ( ` Invalid proxy.server: ` + server ) ;
let url = new URL ( server ) ;
if ( ! [ 'http:' , 'https:' , 'socks5:' ] . includes ( url . protocol ) ) {
url = new URL ( 'http://' + server ) ;
server = ` ${ url . protocol } // ${ url . host } ` ;
}
if ( bypass )
bypass = bypass . split ( ',' ) . map ( t = > t . trim ( ) ) . join ( ',' ) ;
return { . . . proxy , server , bypass } ;
}