2019-11-18 18:18:28 -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-04-01 14:42:47 -07:00
import * as fs from 'fs' ;
import * as util from 'util' ;
import { ConsoleMessage } from './console' ;
2019-11-27 16:02:31 -08:00
import * as dom from './dom' ;
2020-04-18 18:29:31 -07:00
import { TimeoutError , NotConnectedError } from './errors' ;
2019-12-11 07:17:32 -08:00
import { Events } from './events' ;
2020-05-08 10:37:54 -07:00
import { assert , helper , RegisteredListener , assertMaxArguments , debugAssert } from './helper' ;
2020-04-01 14:42:47 -07:00
import * as js from './javascript' ;
import * as network from './network' ;
2019-12-11 12:36:42 -08:00
import { Page } from './page' ;
2020-03-25 14:08:46 -07:00
import { selectors } from './selectors' ;
2020-04-01 14:42:47 -07:00
import * as types from './types' ;
2020-04-27 12:08:29 -07:00
import { waitForTimeoutWasUsed } from './hints' ;
2020-05-18 14:28:06 -07:00
import { BrowserContext } from './browserContext' ;
2020-05-28 16:33:31 -07:00
import { rewriteErrorMessage } from './debug/stackTrace' ;
2019-11-18 18:18:28 -08:00
2019-12-12 21:11:52 -08:00
type ContextType = 'main' | 'utility' ;
type ContextData = {
contextPromise : Promise < dom.FrameExecutionContext > ;
contextResolveCallback : ( c : dom.FrameExecutionContext ) = > void ;
context : dom.FrameExecutionContext | null ;
2020-05-30 15:00:53 -07:00
rerunnableTasks : Set < RerunnableTask < any > > ;
2019-11-26 15:37:25 -08:00
} ;
2020-03-06 08:24:32 -08:00
export type GotoOptions = types . NavigateOptions & {
2019-11-26 16:19:43 -08:00
referer? : string ,
} ;
2019-12-16 22:02:33 -08:00
export type GotoResult = {
newDocumentId? : string ,
} ;
2019-11-26 16:19:43 -08:00
2020-01-27 16:51:52 -08:00
type ConsoleTagHandler = ( ) = > void ;
2019-12-14 19:13:22 -08:00
2020-05-18 14:28:06 -07:00
export type FunctionWithSource = ( source : { context : BrowserContext , page : Page , frame : Frame } , . . . args : any ) = > any ;
2019-12-16 15:56:11 -08:00
export class FrameManager {
private _page : Page ;
private _frames = new Map < string , Frame > ( ) ;
private _mainFrame : Frame ;
2020-01-27 16:51:52 -08:00
readonly _consoleMessageTags = new Map < string , ConsoleTagHandler > ( ) ;
2020-04-16 13:09:24 -07:00
readonly _signalBarriers = new Set < SignalBarrier > ( ) ;
2019-12-16 15:56:11 -08:00
constructor ( page : Page ) {
this . _page = page ;
2020-01-13 13:33:25 -08:00
this . _mainFrame = undefined as any as Frame ;
2019-12-16 15:56:11 -08:00
}
mainFrame ( ) : Frame {
return this . _mainFrame ;
}
frames() {
const frames : Frame [ ] = [ ] ;
collect ( this . _mainFrame ) ;
return frames ;
function collect ( frame : Frame ) {
frames . push ( frame ) ;
for ( const subframe of frame . childFrames ( ) )
collect ( subframe ) ;
}
}
frame ( frameId : string ) : Frame | null {
return this . _frames . get ( frameId ) || null ;
}
frameAttached ( frameId : string , parentFrameId : string | null | undefined ) : Frame {
2020-01-13 13:33:25 -08:00
const parentFrame = parentFrameId ? this . _frames . get ( parentFrameId ) ! : null ;
2019-12-16 15:56:11 -08:00
if ( ! parentFrame ) {
if ( this . _mainFrame ) {
// Update frame id to retain frame identity on cross-process navigation.
this . _frames . delete ( this . _mainFrame . _id ) ;
this . _mainFrame . _id = frameId ;
} else {
assert ( ! this . _frames . has ( frameId ) ) ;
this . _mainFrame = new Frame ( this . _page , frameId , parentFrame ) ;
}
this . _frames . set ( frameId , this . _mainFrame ) ;
return this . _mainFrame ;
} else {
assert ( ! this . _frames . has ( frameId ) ) ;
const frame = new Frame ( this . _page , frameId , parentFrame ) ;
this . _frames . set ( frameId , frame ) ;
this . _page . emit ( Events . Page . FrameAttached , frame ) ;
return frame ;
}
}
2020-04-16 13:09:24 -07:00
async waitForSignalsCreatedBy < T > ( action : ( ) = > Promise < T > , deadline : number , options : types.NavigatingActionWaitOptions = { } , input? : boolean ) : Promise < T > {
2020-04-16 20:31:04 -07:00
if ( options . noWaitAfter )
2020-03-06 14:32:15 -08:00
return action ( ) ;
2020-04-16 20:31:04 -07:00
const barrier = new SignalBarrier ( options , deadline ) ;
2020-04-16 13:09:24 -07:00
this . _signalBarriers . add ( barrier ) ;
2020-03-04 19:15:01 -08:00
try {
const result = await action ( ) ;
2020-03-07 08:19:31 -08:00
if ( input )
await this . _page . _delegate . inputActionEpilogue ( ) ;
2020-03-05 14:47:04 -08:00
await barrier . waitFor ( ) ;
// Resolve in the next task, after all waitForNavigations.
2020-04-01 14:42:47 -07:00
await new Promise ( helper . makeWaitForNextTask ( ) ) ;
2020-03-04 19:15:01 -08:00
return result ;
} finally {
2020-04-16 13:09:24 -07:00
this . _signalBarriers . delete ( barrier ) ;
2020-03-04 19:15:01 -08:00
}
}
2020-03-05 14:47:04 -08:00
frameWillPotentiallyRequestNavigation() {
2020-04-16 13:09:24 -07:00
for ( const barrier of this . _signalBarriers )
2020-03-05 14:47:04 -08:00
barrier . retain ( ) ;
2020-03-04 19:15:01 -08:00
}
2020-03-05 14:47:04 -08:00
frameDidPotentiallyRequestNavigation() {
2020-04-16 13:09:24 -07:00
for ( const barrier of this . _signalBarriers )
2020-03-05 14:47:04 -08:00
barrier . release ( ) ;
}
2020-04-09 19:03:06 -07:00
frameRequestedNavigation ( frameId : string , documentId : string ) {
2020-03-05 14:47:04 -08:00
const frame = this . _frames . get ( frameId ) ;
if ( ! frame )
return ;
2020-04-16 13:09:24 -07:00
for ( const barrier of this . _signalBarriers )
barrier . addFrameNavigation ( frame ) ;
2020-04-09 19:03:06 -07:00
frame . _pendingDocumentId = documentId ;
}
frameUpdatedDocumentIdForNavigation ( frameId : string , documentId : string ) {
const frame = this . _frames . get ( frameId ) ;
if ( ! frame )
return ;
frame . _pendingDocumentId = documentId ;
2020-03-04 19:15:01 -08:00
}
2019-12-16 15:56:11 -08:00
frameCommittedNewDocumentNavigation ( frameId : string , url : string , name : string , documentId : string , initial : boolean ) {
2020-01-13 13:33:25 -08:00
const frame = this . _frames . get ( frameId ) ! ;
2020-04-06 15:09:43 -07:00
this . removeChildFramesRecursively ( frame ) ;
2019-12-16 15:56:11 -08:00
frame . _url = url ;
frame . _name = name ;
2020-05-08 10:37:54 -07:00
debugAssert ( ! frame . _pendingDocumentId || frame . _pendingDocumentId === documentId ) ;
2019-12-16 15:56:11 -08:00
frame . _lastDocumentId = documentId ;
2020-04-09 19:03:06 -07:00
frame . _pendingDocumentId = '' ;
2020-03-21 13:02:37 -07:00
for ( const task of frame . _frameTasks )
task . onNewDocument ( documentId ) ;
2020-01-27 16:51:52 -08:00
this . clearFrameLifecycle ( frame ) ;
2020-02-10 18:35:47 -08:00
if ( ! initial )
2020-01-08 15:32:13 -08:00
this . _page . emit ( Events . Page . FrameNavigated , frame ) ;
2019-12-16 15:56:11 -08:00
}
frameCommittedSameDocumentNavigation ( frameId : string , url : string ) {
const frame = this . _frames . get ( frameId ) ;
if ( ! frame )
return ;
frame . _url = url ;
2020-03-21 13:02:37 -07:00
for ( const task of frame . _frameTasks )
task . onSameDocument ( ) ;
2019-12-16 15:56:11 -08:00
this . _page . emit ( Events . Page . FrameNavigated , frame ) ;
}
frameDetached ( frameId : string ) {
const frame = this . _frames . get ( frameId ) ;
if ( frame )
this . _removeFramesRecursively ( frame ) ;
}
frameStoppedLoading ( frameId : string ) {
const frame = this . _frames . get ( frameId ) ;
if ( ! frame )
return ;
const hasDOMContentLoaded = frame . _firedLifecycleEvents . has ( 'domcontentloaded' ) ;
const hasLoad = frame . _firedLifecycleEvents . has ( 'load' ) ;
frame . _firedLifecycleEvents . add ( 'domcontentloaded' ) ;
frame . _firedLifecycleEvents . add ( 'load' ) ;
2020-03-21 13:02:37 -07:00
this . _notifyLifecycle ( frame ) ;
2019-12-16 15:56:11 -08:00
if ( frame === this . mainFrame ( ) && ! hasDOMContentLoaded )
this . _page . emit ( Events . Page . DOMContentLoaded ) ;
if ( frame === this . mainFrame ( ) && ! hasLoad )
this . _page . emit ( Events . Page . Load ) ;
}
2020-03-06 08:24:32 -08:00
frameLifecycleEvent ( frameId : string , event : types.LifecycleEvent ) {
2019-12-16 15:56:11 -08:00
const frame = this . _frames . get ( frameId ) ;
if ( ! frame )
return ;
2020-01-27 16:51:52 -08:00
frame . _firedLifecycleEvents . add ( event ) ;
2020-03-21 13:02:37 -07:00
this . _notifyLifecycle ( frame ) ;
2019-12-16 15:56:11 -08:00
if ( frame === this . _mainFrame && event === 'load' )
this . _page . emit ( Events . Page . Load ) ;
if ( frame === this . _mainFrame && event === 'domcontentloaded' )
this . _page . emit ( Events . Page . DOMContentLoaded ) ;
}
2020-01-27 16:51:52 -08:00
clearFrameLifecycle ( frame : Frame ) {
frame . _firedLifecycleEvents . clear ( ) ;
2020-01-03 12:59:27 -08:00
// Keep the current navigation request if any.
frame . _inflightRequests = new Set ( Array . from ( frame . _inflightRequests ) . filter ( request = > request . _documentId === frame . _lastDocumentId ) ) ;
2020-04-20 16:52:26 -07:00
frame . _stopNetworkIdleTimer ( ) ;
2020-01-03 12:59:27 -08:00
if ( frame . _inflightRequests . size === 0 )
2020-04-20 16:52:26 -07:00
frame . _startNetworkIdleTimer ( ) ;
2020-01-03 12:59:27 -08:00
}
2019-12-16 16:32:04 -08:00
requestStarted ( request : network.Request ) {
2020-01-03 12:59:27 -08:00
this . _inflightRequestStarted ( request ) ;
2020-03-21 13:02:37 -07:00
for ( const task of request . frame ( ) . _frameTasks )
task . onRequest ( request ) ;
2020-05-15 15:22:29 -07:00
if ( request . _isFavicon ) {
const route = request . _route ( ) ;
if ( route )
route . continue ( ) ;
return ;
}
this . _page . _requestStarted ( request ) ;
2019-12-16 16:32:04 -08:00
}
requestReceivedResponse ( response : network.Response ) {
2020-01-17 17:14:39 -08:00
if ( ! response . request ( ) . _isFavicon )
this . _page . emit ( Events . Page . Response , response ) ;
2019-12-16 16:32:04 -08:00
}
requestFinished ( request : network.Request ) {
2020-01-03 12:59:27 -08:00
this . _inflightRequestFinished ( request ) ;
2020-01-17 17:14:39 -08:00
if ( ! request . _isFavicon )
this . _page . emit ( Events . Page . RequestFinished , request ) ;
2019-12-16 16:32:04 -08:00
}
requestFailed ( request : network.Request , canceled : boolean ) {
2020-01-03 12:59:27 -08:00
this . _inflightRequestFinished ( request ) ;
2020-03-10 11:39:35 -07:00
if ( request . _documentId ) {
2020-04-09 19:03:06 -07:00
const isPendingDocument = request . frame ( ) . _pendingDocumentId === request . _documentId ;
if ( isPendingDocument ) {
request . frame ( ) . _pendingDocumentId = '' ;
2020-01-13 13:33:25 -08:00
let errorText = request . failure ( ) ! . errorText ;
2019-12-16 16:32:04 -08:00
if ( canceled )
errorText += '; maybe frame was detached?' ;
2020-03-21 13:02:37 -07:00
for ( const task of request . frame ( ) . _frameTasks )
task . onNewDocument ( request . _documentId , new Error ( errorText ) ) ;
2019-12-16 16:32:04 -08:00
}
}
2020-01-17 17:14:39 -08:00
if ( ! request . _isFavicon )
this . _page . emit ( Events . Page . RequestFailed , request ) ;
2019-12-16 16:32:04 -08:00
}
2020-02-10 18:35:47 -08:00
provisionalLoadFailed ( frame : Frame , documentId : string , error : string ) {
2020-03-21 13:02:37 -07:00
for ( const task of frame . _frameTasks )
task . onNewDocument ( documentId , new Error ( error ) ) ;
}
private _notifyLifecycle ( frame : Frame ) {
for ( let parent : Frame | null = frame ; parent ; parent = parent . parentFrame ( ) ) {
for ( const frameTask of parent . _frameTasks )
frameTask . onLifecycle ( ) ;
}
2020-01-14 11:46:08 -08:00
}
2020-04-06 15:09:43 -07:00
removeChildFramesRecursively ( frame : Frame ) {
2019-12-16 15:56:11 -08:00
for ( const child of frame . childFrames ( ) )
this . _removeFramesRecursively ( child ) ;
2020-04-06 15:09:43 -07:00
}
private _removeFramesRecursively ( frame : Frame ) {
this . removeChildFramesRecursively ( frame ) ;
2019-12-16 15:56:11 -08:00
frame . _onDetached ( ) ;
this . _frames . delete ( frame . _id ) ;
this . _page . emit ( Events . Page . FrameDetached , frame ) ;
}
2019-12-16 16:32:04 -08:00
2020-01-03 12:59:27 -08:00
private _inflightRequestFinished ( request : network.Request ) {
const frame = request . frame ( ) ;
2020-03-10 11:39:35 -07:00
if ( request . _isFavicon )
2020-01-13 13:33:25 -08:00
return ;
2020-01-03 12:59:27 -08:00
if ( ! frame . _inflightRequests . has ( request ) )
return ;
frame . _inflightRequests . delete ( request ) ;
if ( frame . _inflightRequests . size === 0 )
2020-04-20 16:52:26 -07:00
frame . _startNetworkIdleTimer ( ) ;
2019-12-16 16:32:04 -08:00
}
2020-01-03 12:59:27 -08:00
private _inflightRequestStarted ( request : network.Request ) {
const frame = request . frame ( ) ;
2020-03-10 11:39:35 -07:00
if ( request . _isFavicon )
2020-01-13 13:33:25 -08:00
return ;
2020-01-03 12:59:27 -08:00
frame . _inflightRequests . add ( request ) ;
if ( frame . _inflightRequests . size === 1 )
2020-04-20 16:52:26 -07:00
frame . _stopNetworkIdleTimer ( ) ;
2019-12-16 16:32:04 -08:00
}
2020-01-27 16:51:52 -08:00
interceptConsoleMessage ( message : ConsoleMessage ) : boolean {
if ( message . type ( ) !== 'debug' )
return false ;
const tag = message . text ( ) ;
const handler = this . _consoleMessageTags . get ( tag ) ;
if ( ! handler )
return false ;
this . _consoleMessageTags . delete ( tag ) ;
handler ( ) ;
return true ;
}
2019-12-16 15:56:11 -08:00
}
2019-11-27 16:02:31 -08:00
export class Frame {
2019-12-13 10:52:33 -08:00
_id : string ;
2020-03-06 08:24:32 -08:00
readonly _firedLifecycleEvents : Set < types.LifecycleEvent > ;
2020-02-10 18:35:47 -08:00
_lastDocumentId = '' ;
2020-04-09 19:03:06 -07:00
_pendingDocumentId = '' ;
2020-03-21 13:02:37 -07:00
_frameTasks = new Set < FrameTask > ( ) ;
2019-12-11 07:17:32 -08:00
readonly _page : Page ;
2020-01-13 13:33:25 -08:00
private _parentFrame : Frame | null ;
2019-12-16 15:56:11 -08:00
_url = '' ;
2019-11-18 18:18:28 -08:00
private _detached = false ;
2019-12-12 21:11:52 -08:00
private _contextData = new Map < ContextType , ContextData > ( ) ;
2019-11-27 16:02:31 -08:00
private _childFrames = new Set < Frame > ( ) ;
2020-01-13 13:33:25 -08:00
_name = '' ;
2020-01-03 12:59:27 -08:00
_inflightRequests = new Set < network.Request > ( ) ;
2020-04-20 16:52:26 -07:00
private _networkIdleTimer : NodeJS.Timer | undefined ;
2020-01-27 16:51:52 -08:00
private _setContentCounter = 0 ;
2020-03-18 20:05:35 -07:00
readonly _detachedPromise : Promise < void > ;
2020-02-10 18:35:47 -08:00
private _detachedCallback = ( ) = > { } ;
2019-11-18 18:18:28 -08:00
2019-12-13 10:52:33 -08:00
constructor ( page : Page , id : string , parentFrame : Frame | null ) {
this . _id = id ;
2019-12-09 16:34:42 -08:00
this . _firedLifecycleEvents = new Set ( ) ;
2019-12-11 07:17:32 -08:00
this . _page = page ;
2019-11-18 18:18:28 -08:00
this . _parentFrame = parentFrame ;
2020-02-10 18:35:47 -08:00
this . _detachedPromise = new Promise < void > ( x = > this . _detachedCallback = x ) ;
2019-12-12 21:11:52 -08:00
this . _contextData . set ( 'main' , { contextPromise : new Promise ( ( ) = > { } ) , contextResolveCallback : ( ) = > { } , context : null , rerunnableTasks : new Set ( ) } ) ;
this . _contextData . set ( 'utility' , { contextPromise : new Promise ( ( ) = > { } ) , contextResolveCallback : ( ) = > { } , context : null , rerunnableTasks : new Set ( ) } ) ;
2019-11-26 15:37:25 -08:00
this . _setContext ( 'main' , null ) ;
this . _setContext ( 'utility' , null ) ;
2019-11-18 18:18:28 -08:00
if ( this . _parentFrame )
this . _parentFrame . _childFrames . add ( this ) ;
}
2020-02-10 18:35:47 -08:00
async goto ( url : string , options : GotoOptions = { } ) : Promise < network.Response | null > {
2020-02-13 12:24:17 -08:00
const headers = ( this . _page . _state . extraHTTPHeaders || { } ) ;
let referer = headers [ 'referer' ] || headers [ 'Referer' ] ;
2020-02-10 18:35:47 -08:00
if ( options . referer !== undefined ) {
2020-01-14 11:46:08 -08:00
if ( referer !== undefined && referer !== options . referer )
throw new Error ( '"referer" is already specified as extra HTTP header' ) ;
2019-12-18 18:03:02 -08:00
referer = options . referer ;
2020-01-14 11:46:08 -08:00
}
2020-02-04 19:39:52 -08:00
url = helper . completeUserURL ( url ) ;
2020-02-10 18:35:47 -08:00
2020-03-18 20:05:35 -07:00
const frameTask = new FrameTask ( this , options , url ) ;
const sameDocumentPromise = frameTask . waitForSameDocumentNavigation ( ) ;
const navigateResult = await frameTask . raceAgainstFailures ( this . _page . _delegate . navigateFrame ( this , url , referer ) ) . catch ( e = > {
// Do not leave sameDocumentPromise unhandled.
sameDocumentPromise . catch ( e = > { } ) ;
throw e ;
} ) ;
if ( navigateResult . newDocumentId ) {
// Do not leave sameDocumentPromise unhandled.
sameDocumentPromise . catch ( e = > { } ) ;
await frameTask . waitForSpecificDocument ( navigateResult . newDocumentId ) ;
} else {
await sameDocumentPromise ;
2020-02-10 18:35:47 -08:00
}
2020-03-18 20:05:35 -07:00
const request = ( navigateResult && navigateResult . newDocumentId ) ? frameTask . request ( navigateResult . newDocumentId ) : null ;
2020-03-23 13:51:11 -07:00
await frameTask . waitForLifecycle ( options . waitUntil === undefined ? 'load' : options . waitUntil ) ;
2020-03-18 20:05:35 -07:00
frameTask . done ( ) ;
return request ? request . _finalRequest ( ) . response ( ) : null ;
2020-02-10 18:35:47 -08:00
}
2020-03-06 08:24:32 -08:00
async waitForNavigation ( options : types.WaitForNavigationOptions = { } ) : Promise < network.Response | null > {
2020-04-16 20:31:04 -07:00
return this . _waitForNavigation ( options ) ;
}
async _waitForNavigation ( options : types.ExtendedWaitForNavigationOptions = { } ) : Promise < network.Response | null > {
2020-03-18 20:05:35 -07:00
const frameTask = new FrameTask ( this , options ) ;
let documentId : string | undefined ;
await Promise . race ( [
frameTask . waitForNewDocument ( options . url ) . then ( id = > documentId = id ) ,
frameTask . waitForSameDocumentNavigation ( options . url ) ,
2019-12-16 22:02:33 -08:00
] ) ;
2020-03-18 20:05:35 -07:00
const request = documentId ? frameTask . request ( documentId ) : null ;
2020-04-16 20:31:04 -07:00
if ( options . waitUntil !== 'commit' )
await frameTask . waitForLifecycle ( options . waitUntil === undefined ? 'load' : options . waitUntil ) ;
2020-03-18 20:05:35 -07:00
frameTask . done ( ) ;
2020-03-16 13:31:06 -07:00
return request ? request . _finalRequest ( ) . response ( ) : null ;
2019-11-18 18:18:28 -08:00
}
2020-03-23 13:51:11 -07:00
async waitForLoadState ( state : types.LifecycleEvent = 'load' , options : types.TimeoutOptions = { } ) : Promise < void > {
2020-03-18 20:05:35 -07:00
const frameTask = new FrameTask ( this , options ) ;
2020-03-23 13:51:11 -07:00
await frameTask . waitForLifecycle ( state ) ;
2020-03-18 20:05:35 -07:00
frameTask . done ( ) ;
2019-12-20 15:32:30 -08:00
}
2020-02-05 17:20:23 -08:00
async frameElement ( ) : Promise < dom.ElementHandle > {
return this . _page . _delegate . getFrameElement ( this ) ;
}
2019-12-17 14:30:02 -08:00
_context ( contextType : ContextType ) : Promise < dom.FrameExecutionContext > {
2019-11-26 15:37:25 -08:00
if ( this . _detached )
throw new Error ( ` Execution Context is not available in detached frame " ${ this . url ( ) } " (are you trying to evaluate?) ` ) ;
2020-01-13 13:33:25 -08:00
return this . _contextData . get ( contextType ) ! . contextPromise ;
2019-12-17 14:30:02 -08:00
}
_mainContext ( ) : Promise < dom.FrameExecutionContext > {
return this . _context ( 'main' ) ;
2019-11-26 15:37:25 -08:00
}
2019-12-12 21:11:52 -08:00
_utilityContext ( ) : Promise < dom.FrameExecutionContext > {
2019-12-17 14:30:02 -08:00
return this . _context ( 'utility' ) ;
2019-11-28 12:50:52 -08:00
}
2020-03-20 15:08:17 -07:00
async evaluateHandle < R , Arg > ( pageFunction : types.Func1 < Arg , R > , arg : Arg ) : Promise < types.SmartHandle < R > > ;
async evaluateHandle < R > ( pageFunction : types.Func1 < void , R > , arg? : any ) : Promise < types.SmartHandle < R > > ;
async evaluateHandle < R , Arg > ( pageFunction : types.Func1 < Arg , R > , arg : Arg ) : Promise < types.SmartHandle < R > > {
2020-04-29 18:35:04 -07:00
assertMaxArguments ( arguments . length , 2 ) ;
2019-11-26 15:37:25 -08:00
const context = await this . _mainContext ( ) ;
2020-03-20 15:08:17 -07:00
return context . evaluateHandleInternal ( pageFunction , arg ) ;
2019-11-18 18:18:28 -08:00
}
2020-03-20 15:08:17 -07:00
async evaluate < R , Arg > ( pageFunction : types.Func1 < Arg , R > , arg : Arg ) : Promise < R > ;
async evaluate < R > ( pageFunction : types.Func1 < void , R > , arg? : any ) : Promise < R > ;
async evaluate < R , Arg > ( pageFunction : types.Func1 < Arg , R > , arg : Arg ) : Promise < R > {
2020-04-29 18:35:04 -07:00
assertMaxArguments ( arguments . length , 2 ) ;
2019-11-26 15:37:25 -08:00
const context = await this . _mainContext ( ) ;
2020-03-20 15:08:17 -07:00
return context . evaluateInternal ( pageFunction , arg ) ;
2019-11-18 18:18:28 -08:00
}
2019-12-18 14:28:16 -08:00
async $ ( selector : string ) : Promise < dom.ElementHandle < Element > | null > {
2020-03-25 14:08:46 -07:00
return selectors . _query ( this , selector ) ;
2019-12-18 14:28:16 -08:00
}
2020-03-06 15:02:42 -08:00
async waitForSelector ( selector : string , options? : types.WaitForElementOptions ) : Promise < dom.ElementHandle < Element > | null > {
2020-03-05 17:45:41 -08:00
if ( options && ( options as any ) . visibility )
2020-05-04 11:03:44 -07:00
throw new Error ( 'options.visibility is not supported, did you mean options.state?' ) ;
if ( options && ( options as any ) . waitFor && ( options as any ) . waitFor !== 'visible' )
throw new Error ( 'options.waitFor is not supported, did you mean options.state?' ) ;
const { state = 'visible' } = ( options || { } ) ;
if ( ! [ 'attached' , 'detached' , 'visible' , 'hidden' ] . includes ( state ) )
throw new Error ( ` Unsupported waitFor option " ${ state } " ` ) ;
2020-03-06 16:24:21 -08:00
2020-04-07 14:35:34 -07:00
const deadline = this . _page . _timeoutSettings . computeDeadline ( options ) ;
2020-05-30 15:00:53 -07:00
const { world , task } = selectors . _waitForSelectorTask ( selector , state ) ;
2020-05-19 17:47:46 -07:00
const result = await this . _scheduleRerunnableTask ( task , world , deadline , ` selector " ${ selector } " ${ state === 'attached' ? '' : ' to be ' + state } ` ) ;
2020-03-06 16:24:21 -08:00
if ( ! result . asElement ( ) ) {
result . dispose ( ) ;
return null ;
}
const handle = result . asElement ( ) as dom . ElementHandle < Element > ;
2019-12-18 14:28:16 -08:00
const mainContext = await this . _mainContext ( ) ;
if ( handle && handle . _context !== mainContext ) {
2020-03-06 16:24:21 -08:00
const adopted = await this . _page . _delegate . adoptElementHandle ( handle , mainContext ) ;
2020-03-04 17:57:35 -08:00
handle . dispose ( ) ;
2019-12-18 14:28:16 -08:00
return adopted ;
}
return handle ;
2019-11-18 18:18:28 -08:00
}
2020-04-23 14:58:37 -07:00
async dispatchEvent ( selector : string , type : string , eventInit? : Object , options? : types.TimeoutOptions ) : Promise < void > {
const deadline = this . _page . _timeoutSettings . computeDeadline ( options ) ;
2020-05-30 15:00:53 -07:00
const task = selectors . _dispatchEventTask ( selector , type , eventInit || { } ) ;
2020-05-19 17:47:46 -07:00
const result = await this . _scheduleRerunnableTask ( task , 'main' , deadline , ` selector " ${ selector } " ` ) ;
2020-04-23 14:58:37 -07:00
result . dispose ( ) ;
}
2020-03-20 15:08:17 -07:00
async $eval < R , Arg > ( selector : string , pageFunction : types.FuncOn < Element , Arg , R > , arg : Arg ) : Promise < R > ;
async $eval < R > ( selector : string , pageFunction : types.FuncOn < Element , void , R > , arg? : any ) : Promise < R > ;
async $eval < R , Arg > ( selector : string , pageFunction : types.FuncOn < Element , Arg , R > , arg : Arg ) : Promise < R > {
2020-04-29 18:35:04 -07:00
assertMaxArguments ( arguments . length , 3 ) ;
2020-03-25 14:08:46 -07:00
const handle = await this . $ ( selector ) ;
if ( ! handle )
2019-12-17 14:30:02 -08:00
throw new Error ( ` Error: failed to find element matching selector " ${ selector } " ` ) ;
2020-03-25 14:08:46 -07:00
const result = await handle . evaluate ( pageFunction , arg ) ;
handle . dispose ( ) ;
2019-12-17 14:30:02 -08:00
return result ;
2019-11-18 18:18:28 -08:00
}
2020-03-20 15:08:17 -07:00
async $ $eval < R , Arg > ( selector : string , pageFunction : types.FuncOn < Element [ ] , Arg , R > , arg : Arg ) : Promise < R > ;
async $ $eval < R > ( selector : string , pageFunction : types.FuncOn < Element [ ] , void , R > , arg? : any ) : Promise < R > ;
async $ $eval < R , Arg > ( selector : string , pageFunction : types.FuncOn < Element [ ] , Arg , R > , arg : Arg ) : Promise < R > {
2020-04-29 18:35:04 -07:00
assertMaxArguments ( arguments . length , 3 ) ;
2020-03-25 14:08:46 -07:00
const arrayHandle = await selectors . _queryArray ( this , selector ) ;
2020-03-20 15:08:17 -07:00
const result = await arrayHandle . evaluate ( pageFunction , arg ) ;
2020-03-04 17:57:35 -08:00
arrayHandle . dispose ( ) ;
2019-12-17 14:30:02 -08:00
return result ;
2019-11-18 18:18:28 -08:00
}
2019-12-17 14:30:02 -08:00
async $ $ ( selector : string ) : Promise < dom.ElementHandle < Element > [ ] > {
2020-03-25 14:08:46 -07:00
return selectors . _queryAll ( this , selector ) ;
2019-11-18 18:18:28 -08:00
}
async content ( ) : Promise < string > {
2019-11-26 15:37:25 -08:00
const context = await this . _utilityContext ( ) ;
2020-03-20 15:08:17 -07:00
return context . evaluateInternal ( ( ) = > {
2019-11-26 08:57:53 -08:00
let retVal = '' ;
if ( document . doctype )
retVal = new XMLSerializer ( ) . serializeToString ( document . doctype ) ;
if ( document . documentElement )
retVal += document . documentElement . outerHTML ;
return retVal ;
} ) ;
2019-11-18 18:18:28 -08:00
}
2020-03-06 08:24:32 -08:00
async setContent ( html : string , options? : types.NavigateOptions ) : Promise < void > {
2020-01-27 16:51:52 -08:00
const tag = ` --playwright--set--content-- ${ this . _id } -- ${ ++ this . _setContentCounter } -- ` ;
2019-12-16 22:02:33 -08:00
const context = await this . _utilityContext ( ) ;
2020-03-13 13:09:06 -07:00
const lifecyclePromise = new Promise ( ( resolve , reject ) = > {
2020-02-10 18:35:47 -08:00
this . _page . _frameManager . _consoleMessageTags . set ( tag , ( ) = > {
// Clear lifecycle right after document.open() - see 'tag' below.
this . _page . _frameManager . clearFrameLifecycle ( this ) ;
2020-03-23 13:51:11 -07:00
this . waitForLoadState ( options ? options . waitUntil : 'load' , options ) . then ( resolve ) . catch ( reject ) ;
2020-02-10 18:35:47 -08:00
} ) ;
2020-01-27 16:51:52 -08:00
} ) ;
2020-03-20 15:08:17 -07:00
const contentPromise = context . evaluateInternal ( ( { html , tag } ) = > {
2019-12-20 15:30:12 -08:00
window . stop ( ) ;
2019-12-16 22:02:33 -08:00
document . open ( ) ;
2020-01-27 16:51:52 -08:00
console . debug ( tag ) ; // eslint-disable-line no-console
2019-12-16 22:02:33 -08:00
document . write ( html ) ;
document . close ( ) ;
2020-03-20 15:08:17 -07:00
} , { html , tag } ) ;
2020-02-10 18:35:47 -08:00
await Promise . all ( [ contentPromise , lifecyclePromise ] ) ;
2019-11-18 18:18:28 -08:00
}
name ( ) : string {
return this . _name || '' ;
}
url ( ) : string {
return this . _url ;
}
2019-11-27 16:02:31 -08:00
parentFrame ( ) : Frame | null {
2019-11-18 18:18:28 -08:00
return this . _parentFrame ;
}
2019-11-27 16:02:31 -08:00
childFrames ( ) : Frame [ ] {
2019-11-18 18:18:28 -08:00
return Array . from ( this . _childFrames ) ;
}
isDetached ( ) : boolean {
return this . _detached ;
}
async addScriptTag ( options : {
url? : string ; path? : string ;
content? : string ;
2019-11-26 08:57:53 -08:00
type ? : string ;
2019-11-27 16:02:31 -08:00
} ) : Promise < dom.ElementHandle > {
2019-11-26 08:57:53 -08:00
const {
url = null ,
path = null ,
content = null ,
type = ''
} = options ;
2019-12-11 13:51:03 -08:00
if ( ! url && ! path && ! content )
throw new Error ( 'Provide an object with a `url`, `path` or `content` property' ) ;
const context = await this . _mainContext ( ) ;
return this . _raceWithCSPError ( async ( ) = > {
if ( url !== null )
2020-03-20 15:08:17 -07:00
return ( await context . evaluateHandleInternal ( addScriptUrl , { url , type } ) ) . asElement ( ) ! ;
2020-04-17 08:51:54 -07:00
let result ;
2019-12-11 13:51:03 -08:00
if ( path !== null ) {
2020-04-01 14:42:47 -07:00
let contents = await util . promisify ( fs . readFile ) ( path , 'utf8' ) ;
2019-12-11 13:51:03 -08:00
contents += '//# sourceURL=' + path . replace ( /\n/g , '' ) ;
2020-04-17 08:51:54 -07:00
result = ( await context . evaluateHandleInternal ( addScriptContent , { content : contents , type } ) ) . asElement ( ) ! ;
} else {
result = ( await context . evaluateHandleInternal ( addScriptContent , { content : content ! , type } ) ) . asElement ( ) ! ;
2019-11-26 08:57:53 -08:00
}
2020-04-17 08:51:54 -07:00
// Another round trip to the browser to ensure that we receive CSP error messages
// (if any) logged asynchronously in a separate task on the content main thread.
if ( this . _page . _delegate . cspErrorsAsynchronousForInlineScipts )
await context . evaluateInternal ( ( ) = > true ) ;
return result ;
2019-12-11 13:51:03 -08:00
} ) ;
2019-11-26 08:57:53 -08:00
2020-03-20 15:08:17 -07:00
async function addScriptUrl ( options : { url : string , type : string } ) : Promise < HTMLElement > {
2019-11-26 08:57:53 -08:00
const script = document . createElement ( 'script' ) ;
2020-03-20 15:08:17 -07:00
script . src = options . url ;
if ( options . type )
script . type = options . type ;
2019-11-26 08:57:53 -08:00
const promise = new Promise ( ( res , rej ) = > {
script . onload = res ;
2020-04-12 18:46:53 -07:00
script . onerror = e = > rej ( typeof e === 'string' ? new Error ( e ) : new Error ( ` Failed to load script at ${ script . src } ` ) ) ;
2019-11-26 08:57:53 -08:00
} ) ;
document . head . appendChild ( script ) ;
await promise ;
return script ;
}
2020-03-20 15:08:17 -07:00
function addScriptContent ( options : { content : string , type : string } ) : HTMLElement {
2019-11-26 08:57:53 -08:00
const script = document . createElement ( 'script' ) ;
2020-03-20 15:08:17 -07:00
script . type = options . type || 'text/javascript' ;
script . text = options . content ;
2019-11-26 08:57:53 -08:00
let error = null ;
script . onerror = e = > error = e ;
document . head . appendChild ( script ) ;
if ( error )
throw error ;
return script ;
}
2019-11-18 18:18:28 -08:00
}
2019-11-27 16:02:31 -08:00
async addStyleTag ( options : { url? : string ; path? : string ; content? : string ; } ) : Promise < dom.ElementHandle > {
2019-11-26 08:57:53 -08:00
const {
url = null ,
path = null ,
content = null
} = options ;
2019-12-11 13:51:03 -08:00
if ( ! url && ! path && ! content )
throw new Error ( 'Provide an object with a `url`, `path` or `content` property' ) ;
const context = await this . _mainContext ( ) ;
return this . _raceWithCSPError ( async ( ) = > {
if ( url !== null )
2020-03-20 15:08:17 -07:00
return ( await context . evaluateHandleInternal ( addStyleUrl , url ) ) . asElement ( ) ! ;
2019-12-12 20:21:29 -08:00
2019-12-11 13:51:03 -08:00
if ( path !== null ) {
2020-04-01 14:42:47 -07:00
let contents = await util . promisify ( fs . readFile ) ( path , 'utf8' ) ;
2019-12-11 13:51:03 -08:00
contents += '/*# sourceURL=' + path . replace ( /\n/g , '' ) + '*/' ;
2020-03-20 15:08:17 -07:00
return ( await context . evaluateHandleInternal ( addStyleContent , contents ) ) . asElement ( ) ! ;
2019-11-26 08:57:53 -08:00
}
2019-12-12 20:21:29 -08:00
2020-03-20 15:08:17 -07:00
return ( await context . evaluateHandleInternal ( addStyleContent , content ! ) ) . asElement ( ) ! ;
2019-12-11 13:51:03 -08:00
} ) ;
2019-12-12 20:21:29 -08:00
2019-11-26 08:57:53 -08:00
async function addStyleUrl ( url : string ) : Promise < HTMLElement > {
const link = document . createElement ( 'link' ) ;
link . rel = 'stylesheet' ;
link . href = url ;
const promise = new Promise ( ( res , rej ) = > {
link . onload = res ;
link . onerror = rej ;
} ) ;
document . head . appendChild ( link ) ;
await promise ;
return link ;
}
async function addStyleContent ( content : string ) : Promise < HTMLElement > {
const style = document . createElement ( 'style' ) ;
style . type = 'text/css' ;
style . appendChild ( document . createTextNode ( content ) ) ;
const promise = new Promise ( ( res , rej ) = > {
style . onload = res ;
style . onerror = rej ;
} ) ;
document . head . appendChild ( style ) ;
await promise ;
return style ;
}
2019-11-18 18:18:28 -08:00
}
2019-12-11 13:51:03 -08:00
private async _raceWithCSPError ( func : ( ) = > Promise < dom.ElementHandle > ) : Promise < dom.ElementHandle > {
const listeners : RegisteredListener [ ] = [ ] ;
2020-01-13 13:33:25 -08:00
let result : dom.ElementHandle ;
2019-12-11 13:51:03 -08:00
let error : Error | undefined ;
let cspMessage : ConsoleMessage | undefined ;
2019-12-12 20:21:29 -08:00
const actionPromise = new Promise < dom.ElementHandle > ( async resolve = > {
2019-12-11 13:51:03 -08:00
try {
result = await func ( ) ;
} catch ( e ) {
error = e ;
}
resolve ( ) ;
} ) ;
const errorPromise = new Promise ( resolve = > {
listeners . push ( helper . addEventListener ( this . _page , Events . Page . Console , ( message : ConsoleMessage ) = > {
if ( message . type ( ) === 'error' && message . text ( ) . includes ( 'Content Security Policy' ) ) {
cspMessage = message ;
resolve ( ) ;
}
} ) ) ;
} ) ;
await Promise . race ( [ actionPromise , errorPromise ] ) ;
helper . removeEventListeners ( listeners ) ;
if ( cspMessage )
throw new Error ( cspMessage . text ( ) ) ;
if ( error )
throw error ;
2020-01-13 13:33:25 -08:00
return result ! ;
2019-12-11 13:51:03 -08:00
}
2020-04-18 18:29:31 -07:00
private async _retryWithSelectorIfNotConnected < R > (
2020-05-04 21:44:33 -07:00
actionName : string ,
2020-04-18 18:29:31 -07:00
selector : string , options : types.TimeoutOptions ,
action : ( handle : dom.ElementHandle < Element > , deadline : number ) = > Promise < R > ) : Promise < R > {
const deadline = this . _page . _timeoutSettings . computeDeadline ( options ) ;
2020-05-04 21:44:33 -07:00
this . _page . _log ( dom . inputLog , ` (page|frame). ${ actionName } (" ${ selector } ") ` ) ;
2020-04-18 18:29:31 -07:00
while ( ! helper . isPastDeadline ( deadline ) ) {
try {
2020-05-30 15:00:53 -07:00
const { world , task } = selectors . _waitForSelectorTask ( selector , 'attached' ) ;
2020-05-04 21:44:33 -07:00
this . _page . _log ( dom . inputLog , ` waiting for the selector " ${ selector } " ` ) ;
2020-04-18 18:29:31 -07:00
const handle = await this . _scheduleRerunnableTask ( task , world , deadline , ` selector " ${ selector } " ` ) ;
2020-05-04 21:44:33 -07:00
this . _page . _log ( dom . inputLog , ` ...got element for the selector ` ) ;
2020-04-18 18:29:31 -07:00
const element = handle . asElement ( ) as dom . ElementHandle < Element > ;
try {
return await action ( element , deadline ) ;
} finally {
element . dispose ( ) ;
}
} catch ( e ) {
if ( ! ( e instanceof NotConnectedError ) )
throw e ;
2020-04-20 07:52:26 -07:00
this . _page . _log ( dom . inputLog , 'Element was detached from the DOM, retrying' ) ;
2020-04-18 18:29:31 -07:00
}
}
2020-05-04 21:44:33 -07:00
throw new TimeoutError ( ` waiting for selector " ${ selector } " failed: timeout exceeded. Re-run with the DEBUG=pw:input env variable to see the debug log. ` ) ;
2020-04-18 18:29:31 -07:00
}
2020-04-07 14:35:34 -07:00
async click ( selector : string , options : dom.ClickOptions & types . PointerActionWaitOptions & types . NavigatingActionWaitOptions = { } ) {
2020-05-04 21:44:33 -07:00
await this . _retryWithSelectorIfNotConnected ( 'click' , selector , options ,
2020-04-18 18:29:31 -07:00
( handle , deadline ) = > handle . click ( helper . optionsWithUpdatedTimeout ( options , deadline ) ) ) ;
2019-11-18 18:18:28 -08:00
}
2020-04-07 14:35:34 -07:00
async dblclick ( selector : string , options : dom.MultiClickOptions & types . PointerActionWaitOptions & types . NavigatingActionWaitOptions = { } ) {
2020-05-04 21:44:33 -07:00
await this . _retryWithSelectorIfNotConnected ( 'dblclick' , selector , options ,
2020-04-18 18:29:31 -07:00
( handle , deadline ) = > handle . dblclick ( helper . optionsWithUpdatedTimeout ( options , deadline ) ) ) ;
2019-11-18 18:18:28 -08:00
}
2020-04-07 14:35:34 -07:00
async fill ( selector : string , value : string , options : types.NavigatingActionWaitOptions = { } ) {
2020-05-04 21:44:33 -07:00
await this . _retryWithSelectorIfNotConnected ( 'fill' , selector , options ,
2020-04-18 18:29:31 -07:00
( handle , deadline ) = > handle . fill ( value , helper . optionsWithUpdatedTimeout ( options , deadline ) ) ) ;
2019-11-18 18:18:28 -08:00
}
2020-04-18 18:29:31 -07:00
async focus ( selector : string , options : types.TimeoutOptions = { } ) {
2020-05-04 21:44:33 -07:00
await this . _retryWithSelectorIfNotConnected ( 'focus' , selector , options ,
2020-04-18 18:29:31 -07:00
( handle , deadline ) = > handle . focus ( ) ) ;
2019-11-18 18:18:28 -08:00
}
2020-05-18 17:58:23 -07:00
async textContent ( selector : string , options : types.TimeoutOptions = { } ) : Promise < null | string > {
return await this . _retryWithSelectorIfNotConnected ( 'textContent' , selector , options ,
( handle , deadline ) = > handle . textContent ( ) ) ;
}
async innerText ( selector : string , options : types.TimeoutOptions = { } ) : Promise < string > {
return await this . _retryWithSelectorIfNotConnected ( 'innerText' , selector , options ,
( handle , deadline ) = > handle . innerText ( ) ) ;
}
async innerHTML ( selector : string , options : types.TimeoutOptions = { } ) : Promise < string > {
return await this . _retryWithSelectorIfNotConnected ( 'innerHTML' , selector , options ,
( handle , deadline ) = > handle . innerHTML ( ) ) ;
}
async getAttribute ( selector : string , name : string , options : types.TimeoutOptions = { } ) : Promise < string | null > {
return await this . _retryWithSelectorIfNotConnected ( 'getAttribute' , selector , options ,
( handle , deadline ) = > handle . getAttribute ( name ) as Promise < string > ) ;
}
2020-04-18 18:29:31 -07:00
async hover ( selector : string , options : dom.PointerActionOptions & types . PointerActionWaitOptions = { } ) {
2020-05-04 21:44:33 -07:00
await this . _retryWithSelectorIfNotConnected ( 'hover' , selector , options ,
2020-04-18 18:29:31 -07:00
( handle , deadline ) = > handle . hover ( helper . optionsWithUpdatedTimeout ( options , deadline ) ) ) ;
2019-11-18 18:18:28 -08:00
}
2020-04-18 18:29:31 -07:00
async selectOption ( selector : string , values : string | dom . ElementHandle | types . SelectOption | string [ ] | dom . ElementHandle [ ] | types . SelectOption [ ] , options : types.NavigatingActionWaitOptions = { } ) : Promise < string [ ] > {
2020-05-04 21:44:33 -07:00
return await this . _retryWithSelectorIfNotConnected ( 'selectOption' , selector , options ,
2020-04-18 18:29:31 -07:00
( handle , deadline ) = > handle . selectOption ( values , helper . optionsWithUpdatedTimeout ( options , deadline ) ) ) ;
2020-04-16 10:25:28 -07:00
}
2020-04-18 18:29:31 -07:00
async setInputFiles ( selector : string , files : string | types . FilePayload | string [ ] | types . FilePayload [ ] , options : types.NavigatingActionWaitOptions = { } ) : Promise < void > {
2020-05-04 21:44:33 -07:00
await this . _retryWithSelectorIfNotConnected ( 'setInputFiles' , selector , options ,
2020-04-18 18:29:31 -07:00
( handle , deadline ) = > handle . setInputFiles ( files , helper . optionsWithUpdatedTimeout ( options , deadline ) ) ) ;
2019-11-18 18:18:28 -08:00
}
2020-04-18 18:29:31 -07:00
async type ( selector : string , text : string , options : { delay? : number } & types . NavigatingActionWaitOptions = { } ) {
2020-05-04 21:44:33 -07:00
await this . _retryWithSelectorIfNotConnected ( 'type' , selector , options ,
2020-04-18 18:29:31 -07:00
( handle , deadline ) = > handle . type ( text , helper . optionsWithUpdatedTimeout ( options , deadline ) ) ) ;
2019-11-18 18:18:28 -08:00
}
2020-04-18 18:29:31 -07:00
async press ( selector : string , key : string , options : { delay? : number } & types . NavigatingActionWaitOptions = { } ) {
2020-05-04 21:44:33 -07:00
await this . _retryWithSelectorIfNotConnected ( 'press' , selector , options ,
2020-04-18 18:29:31 -07:00
( handle , deadline ) = > handle . press ( key , helper . optionsWithUpdatedTimeout ( options , deadline ) ) ) ;
2020-03-06 09:38:08 -08:00
}
2020-04-18 18:29:31 -07:00
async check ( selector : string , options : types.PointerActionWaitOptions & types . NavigatingActionWaitOptions = { } ) {
2020-05-04 21:44:33 -07:00
await this . _retryWithSelectorIfNotConnected ( 'check' , selector , options ,
2020-04-18 18:29:31 -07:00
( handle , deadline ) = > handle . check ( helper . optionsWithUpdatedTimeout ( options , deadline ) ) ) ;
2020-02-04 14:39:11 -08:00
}
2020-04-18 18:29:31 -07:00
async uncheck ( selector : string , options : types.PointerActionWaitOptions & types . NavigatingActionWaitOptions = { } ) {
2020-05-04 21:44:33 -07:00
await this . _retryWithSelectorIfNotConnected ( 'uncheck' , selector , options ,
2020-04-18 18:29:31 -07:00
( handle , deadline ) = > handle . uncheck ( helper . optionsWithUpdatedTimeout ( options , deadline ) ) ) ;
2020-02-04 14:39:11 -08:00
}
2020-04-27 12:08:29 -07:00
async waitForTimeout ( timeout : number ) {
waitForTimeoutWasUsed ( this . _page ) ;
await new Promise ( fulfill = > setTimeout ( fulfill , timeout ) ) ;
2019-11-18 18:18:28 -08:00
}
2020-03-20 15:08:17 -07:00
async waitForFunction < R , Arg > ( pageFunction : types.Func1 < Arg , R > , arg : Arg , options? : types.WaitForFunctionOptions ) : Promise < types.SmartHandle < R > > ;
async waitForFunction < R > ( pageFunction : types.Func1 < void , R > , arg? : any , options? : types.WaitForFunctionOptions ) : Promise < types.SmartHandle < R > > ;
async waitForFunction < R , Arg > ( pageFunction : types.Func1 < Arg , R > , arg : Arg , options : types.WaitForFunctionOptions = { } ) : Promise < types.SmartHandle < R > > {
2020-04-07 14:35:34 -07:00
const { polling = 'raf' } = options ;
const deadline = this . _page . _timeoutSettings . computeDeadline ( options ) ;
2020-03-20 15:08:17 -07:00
if ( helper . isString ( polling ) )
2020-04-29 21:34:14 -07:00
assert ( polling === 'raf' , 'Unknown polling option: ' + polling ) ;
2020-03-20 15:08:17 -07:00
else if ( helper . isNumber ( polling ) )
assert ( polling > 0 , 'Cannot poll with non-positive interval: ' + polling ) ;
else
throw new Error ( 'Unknown polling options: ' + polling ) ;
const predicateBody = helper . isString ( pageFunction ) ? 'return (' + pageFunction + ')' : 'return (' + pageFunction + ')(arg)' ;
2020-05-30 15:00:53 -07:00
const task = async ( context : dom.FrameExecutionContext ) = > context . evaluateHandleInternal ( ( { injected , predicateBody , polling , arg } ) = > {
const innerPredicate = new Function ( 'arg' , predicateBody ) as ( arg : any ) = > R ;
return injected . poll ( polling , ( ) = > innerPredicate ( arg ) ) ;
} , { injected : await context . injectedScript ( ) , predicateBody , polling , arg } ) ;
return this . _scheduleRerunnableTask ( task , 'main' , deadline ) ;
2019-12-18 18:11:02 -08:00
}
2019-11-18 18:18:28 -08:00
async title ( ) : Promise < string > {
2019-11-26 15:37:25 -08:00
const context = await this . _utilityContext ( ) ;
2020-03-20 15:08:17 -07:00
return context . evaluateInternal ( ( ) = > document . title ) ;
2019-11-18 18:18:28 -08:00
}
2019-12-11 07:17:32 -08:00
_onDetached() {
2019-11-18 18:18:28 -08:00
this . _detached = true ;
2020-02-10 18:35:47 -08:00
this . _detachedCallback ( ) ;
2019-12-12 21:11:52 -08:00
for ( const data of this . _contextData . values ( ) ) {
for ( const rerunnableTask of data . rerunnableTasks )
2019-12-03 10:51:41 -08:00
rerunnableTask . terminate ( new Error ( 'waitForFunction failed: frame got detached.' ) ) ;
2019-11-26 15:37:25 -08:00
}
2019-11-18 18:18:28 -08:00
if ( this . _parentFrame )
this . _parentFrame . _childFrames . delete ( this ) ;
this . _parentFrame = null ;
}
2019-11-26 15:37:25 -08:00
2020-05-30 15:00:53 -07:00
private _scheduleRerunnableTask < T > ( task : SchedulableTask < T > , contextType : ContextType , deadline : number , title? : string ) : Promise < types.SmartHandle < T > > {
2020-01-13 13:33:25 -08:00
const data = this . _contextData . get ( contextType ) ! ;
2020-04-07 14:35:34 -07:00
const rerunnableTask = new RerunnableTask ( data , task , deadline , title ) ;
2019-12-12 21:11:52 -08:00
if ( data . context )
rerunnableTask . rerun ( data . context ) ;
2019-12-03 10:51:41 -08:00
return rerunnableTask . promise ;
2019-11-26 15:37:25 -08:00
}
2019-12-12 21:11:52 -08:00
private _setContext ( contextType : ContextType , context : dom.FrameExecutionContext | null ) {
2020-01-13 13:33:25 -08:00
const data = this . _contextData . get ( contextType ) ! ;
2019-12-12 21:11:52 -08:00
data . context = context ;
2019-11-26 15:37:25 -08:00
if ( context ) {
2019-12-12 21:11:52 -08:00
data . contextResolveCallback . call ( null , context ) ;
for ( const rerunnableTask of data . rerunnableTasks )
rerunnableTask . rerun ( context ) ;
2019-11-26 15:37:25 -08:00
} else {
2019-12-12 21:11:52 -08:00
data . contextPromise = new Promise ( fulfill = > {
data . contextResolveCallback = fulfill ;
2019-11-26 15:37:25 -08:00
} ) ;
}
}
2019-12-12 21:11:52 -08:00
_contextCreated ( contextType : ContextType , context : dom.FrameExecutionContext ) {
2020-01-13 13:33:25 -08:00
const data = this . _contextData . get ( contextType ) ! ;
2019-11-26 15:37:25 -08:00
// In case of multiple sessions to the same target, there's a race between
// connections so we might end up creating multiple isolated worlds.
// We can use either.
2019-12-12 21:11:52 -08:00
if ( data . context )
this . _setContext ( contextType , null ) ;
this . _setContext ( contextType , context ) ;
2019-11-26 15:37:25 -08:00
}
2019-12-12 21:11:52 -08:00
_contextDestroyed ( context : dom.FrameExecutionContext ) {
for ( const [ contextType , data ] of this . _contextData ) {
if ( data . context === context )
this . _setContext ( contextType , null ) ;
2019-11-26 15:37:25 -08:00
}
}
2020-04-20 16:52:26 -07:00
_startNetworkIdleTimer() {
assert ( ! this . _networkIdleTimer ) ;
if ( this . _firedLifecycleEvents . has ( 'networkidle' ) )
return ;
this . _networkIdleTimer = setTimeout ( ( ) = > { this . _page . _frameManager . frameLifecycleEvent ( this . _id , 'networkidle' ) ; } , 500 ) ;
}
_stopNetworkIdleTimer() {
if ( this . _networkIdleTimer )
clearTimeout ( this . _networkIdleTimer ) ;
this . _networkIdleTimer = undefined ;
}
2019-11-18 18:18:28 -08:00
}
2019-12-03 10:51:41 -08:00
2020-05-30 15:00:53 -07:00
export type SchedulableTask < T > = ( context : dom.FrameExecutionContext ) = > Promise < js.JSHandle < types.CancelablePoll < T > > > ;
2020-03-20 15:08:17 -07:00
2020-05-30 15:00:53 -07:00
class RerunnableTask < T > {
readonly promise : Promise < types.SmartHandle < T > > ;
terminate : ( reason : Error ) = > void = ( ) = > { } ;
private _task : SchedulableTask < T > ;
private _resolve : ( result : types.SmartHandle < T > ) = > void = ( ) = > { } ;
2020-01-13 13:33:25 -08:00
private _reject : ( reason : Error ) = > void = ( ) = > { } ;
2020-05-30 15:00:53 -07:00
private _terminatedPromise : Promise < Error > ;
2019-12-03 10:51:41 -08:00
2020-05-30 15:00:53 -07:00
constructor ( data : ContextData , task : SchedulableTask < T > , deadline : number , title? : string ) {
2019-12-03 10:51:41 -08:00
this . _task = task ;
2020-05-30 15:00:53 -07:00
data . rerunnableTasks . add ( this ) ;
2019-12-03 10:51:41 -08:00
// Since page navigation requires us to re-install the pageScript, we should track
// timeout on our end.
2020-05-04 21:44:33 -07:00
const timeoutError = new TimeoutError ( ` waiting for ${ title || 'function' } failed: timeout exceeded. Re-run with the DEBUG=pw:input env variable to see the debug log. ` ) ;
2020-05-30 15:00:53 -07:00
let timeoutTimer : NodeJS.Timer | undefined ;
this . _terminatedPromise = new Promise ( resolve = > {
timeoutTimer = setTimeout ( ( ) = > resolve ( timeoutError ) , helper . timeUntilDeadline ( deadline ) ) ;
this . terminate = resolve ;
} ) ;
// This promise is either resolved with the task result, or rejected with a meaningful
// evaluation error.
const resultPromise = new Promise < types.SmartHandle < T > > ( ( resolve , reject ) = > {
this . _resolve = resolve ;
this . _reject = reject ;
} ) ;
const failPromise = this . _terminatedPromise . then ( error = > Promise . reject ( error ) ) ;
2019-12-03 10:51:41 -08:00
2020-05-30 15:00:53 -07:00
this . promise = Promise . race ( [ resultPromise , failPromise ] ) . finally ( ( ) = > {
if ( timeoutTimer )
clearTimeout ( timeoutTimer ) ;
data . rerunnableTasks . delete ( this ) ;
} ) ;
2019-12-03 10:51:41 -08:00
}
2019-12-12 21:11:52 -08:00
async rerun ( context : dom.FrameExecutionContext ) {
2020-05-30 15:00:53 -07:00
let poll : js.JSHandle < types.CancelablePoll < T > > | null = null ;
// On timeout or error, cancel current poll.
const cancelPoll = ( ) = > {
if ( ! poll )
return ;
const copy = poll ;
poll = null ;
copy . evaluate ( p = > p . cancel ( ) ) . catch ( e = > { } ) . then ( ( ) = > copy . dispose ( ) ) ;
} ;
this . _terminatedPromise . then ( cancelPoll ) ;
2019-12-03 10:51:41 -08:00
try {
2020-05-30 15:00:53 -07:00
poll = await this . _task ( context ) ;
const result = await poll . evaluateHandle ( poll = > poll . result ) ;
cancelPoll ( ) ;
this . _resolve ( result ) ;
2019-12-03 10:51:41 -08:00
} catch ( e ) {
2020-05-30 15:00:53 -07:00
cancelPoll ( ) ;
2019-12-03 10:51:41 -08:00
2020-05-30 15:00:53 -07:00
// When the page is navigated, the promise is rejected.
// We will try again in the new execution context.
if ( e . message . includes ( 'Execution context was destroyed' ) )
return ;
2019-12-03 10:51:41 -08:00
2020-05-30 15:00:53 -07:00
// We could have tried to evaluate in a context which was already
// destroyed.
if ( e . message . includes ( 'Cannot find context with specified id' ) )
return ;
2019-12-03 10:51:41 -08:00
2020-05-30 15:00:53 -07:00
this . _reject ( e ) ;
}
2019-12-03 10:51:41 -08:00
}
}
2019-12-11 07:17:32 -08:00
2020-04-16 13:09:24 -07:00
export class SignalBarrier {
2020-04-07 14:35:34 -07:00
private _options : types.NavigatingActionWaitOptions ;
2020-03-05 14:47:04 -08:00
private _protectCount = 0 ;
private _promise : Promise < void > ;
private _promiseCallback = ( ) = > { } ;
2020-04-07 14:35:34 -07:00
private _deadline : number ;
2020-03-05 14:47:04 -08:00
2020-04-07 14:35:34 -07:00
constructor ( options : types.NavigatingActionWaitOptions , deadline : number ) {
2020-03-06 14:32:15 -08:00
this . _options = options ;
2020-04-07 14:35:34 -07:00
this . _deadline = deadline ;
2020-03-05 14:47:04 -08:00
this . _promise = new Promise ( f = > this . _promiseCallback = f ) ;
this . retain ( ) ;
}
waitFor ( ) : Promise < void > {
this . release ( ) ;
return this . _promise ;
}
2020-04-16 13:09:24 -07:00
async addFrameNavigation ( frame : Frame ) {
2020-03-05 14:47:04 -08:00
this . retain ( ) ;
2020-04-16 20:31:04 -07:00
const options = helper . optionsWithUpdatedTimeout ( this . _options , this . _deadline ) ;
await frame . _waitForNavigation ( { . . . options , waitUntil : 'commit' } ) . catch ( e = > { } ) ;
2020-03-05 14:47:04 -08:00
this . release ( ) ;
}
retain() {
++ this . _protectCount ;
}
release() {
-- this . _protectCount ;
this . _maybeResolve ( ) ;
}
private async _maybeResolve() {
2020-05-13 17:20:33 -07:00
if ( ! this . _protectCount )
2020-03-05 14:47:04 -08:00
this . _promiseCallback ( ) ;
}
}
2020-03-18 20:05:35 -07:00
export class FrameTask {
private _frame : Frame ;
private _failurePromise : Promise < Error > ;
private _requestMap = new Map < string , network.Request > ( ) ;
2020-03-21 13:02:37 -07:00
private _timer? : NodeJS.Timer ;
2020-03-18 20:05:35 -07:00
private _url : string | undefined ;
2020-03-21 13:02:37 -07:00
onNewDocument : ( documentId : string , error? : Error ) = > void = ( ) = > { } ;
onSameDocument = ( ) = > { } ;
onLifecycle = ( ) = > { } ;
2020-03-18 20:05:35 -07:00
constructor ( frame : Frame , options : types.TimeoutOptions , url? : string ) {
this . _frame = frame ;
this . _url = url ;
// Process timeouts
let timeoutPromise = new Promise < TimeoutError > ( ( ) = > { } ) ;
const { timeout = frame . _page . _timeoutSettings . navigationTimeout ( ) } = options ;
if ( timeout ) {
2020-04-07 14:35:34 -07:00
const errorMessage = 'Navigation timeout exceeded' ;
2020-03-21 13:02:37 -07:00
timeoutPromise = new Promise ( fulfill = > this . _timer = setTimeout ( fulfill , timeout ) )
2020-03-18 20:05:35 -07:00
. then ( ( ) = > { throw new TimeoutError ( errorMessage ) ; } ) ;
}
// Process detached frames
this . _failurePromise = Promise . race ( [
timeoutPromise ,
this . _frame . _page . _disconnectedPromise . then ( ( ) = > { throw new Error ( 'Navigation failed because browser has disconnected!' ) ; } ) ,
this . _frame . _detachedPromise . then ( ( ) = > { throw new Error ( 'Navigating frame was detached!' ) ; } ) ,
] ) ;
2020-03-21 13:02:37 -07:00
frame . _frameTasks . add ( this ) ;
}
onRequest ( request : network.Request ) {
if ( ! request . _documentId || request . redirectedFrom ( ) )
return ;
this . _requestMap . set ( request . _documentId , request ) ;
2020-03-18 20:05:35 -07:00
}
async raceAgainstFailures < T > ( promise : Promise < T > ) : Promise < T > {
let result : T ;
let error : Error | undefined ;
await Promise . race ( [
this . _failurePromise . catch ( e = > error = e ) ,
promise . then ( r = > result = r ) . catch ( e = > error = e )
] ) ;
if ( ! error )
return result ! ;
this . done ( ) ;
if ( this . _url )
2020-05-28 16:33:31 -07:00
rewriteErrorMessage ( error , error . message + ` while navigating to ${ this . _url } ` ) ;
2020-03-18 20:05:35 -07:00
throw error ;
}
2020-03-21 13:02:37 -07:00
request ( documentId : string ) : network . Request | undefined {
return this . _requestMap . get ( documentId ) ;
2020-03-18 20:05:35 -07:00
}
2020-03-21 13:02:37 -07:00
waitForSameDocumentNavigation ( url? : types.URLMatch ) : Promise < void > {
return this . raceAgainstFailures ( new Promise ( ( resolve , reject ) = > {
this . onSameDocument = ( ) = > {
if ( helper . urlMatches ( this . _frame . url ( ) , url ) )
2020-03-18 20:05:35 -07:00
resolve ( ) ;
2020-03-21 13:02:37 -07:00
} ;
} ) ) ;
}
waitForSpecificDocument ( expectedDocumentId : string ) : Promise < void > {
return this . raceAgainstFailures ( new Promise ( ( resolve , reject ) = > {
this . onNewDocument = ( documentId : string , error? : Error ) = > {
if ( documentId === expectedDocumentId ) {
if ( ! error )
resolve ( ) ;
else
reject ( error ) ;
} else if ( ! error ) {
reject ( new Error ( 'Navigation interrupted by another one' ) ) ;
}
} ;
} ) ) ;
2020-03-18 20:05:35 -07:00
}
waitForNewDocument ( url? : types.URLMatch ) : Promise < string > {
2020-03-21 13:02:37 -07:00
return this . raceAgainstFailures ( new Promise ( ( resolve , reject ) = > {
this . onNewDocument = ( documentId : string , error? : Error ) = > {
if ( ! error && ! helper . urlMatches ( this . _frame . url ( ) , url ) )
return ;
if ( error )
reject ( error ) ;
else
resolve ( documentId ) ;
} ;
} ) ) ;
2020-03-18 20:05:35 -07:00
}
2020-03-23 13:51:11 -07:00
waitForLifecycle ( waitUntil : types.LifecycleEvent ) : Promise < void > {
2020-04-22 10:52:01 -07:00
if ( waitUntil as unknown === 'networkidle0' )
waitUntil = 'networkidle' ;
2020-03-18 20:05:35 -07:00
if ( ! types . kLifecycleEvents . has ( waitUntil ) )
throw new Error ( ` Unsupported waitUntil option ${ String ( waitUntil ) } ` ) ;
2020-03-21 13:02:37 -07:00
return this . raceAgainstFailures ( new Promise ( ( resolve , reject ) = > {
this . onLifecycle = ( ) = > {
if ( ! checkLifecycleRecursively ( this . _frame ) )
return ;
resolve ( ) ;
} ;
this . onLifecycle ( ) ;
} ) ) ;
2020-03-18 20:05:35 -07:00
function checkLifecycleRecursively ( frame : Frame ) : boolean {
if ( ! frame . _firedLifecycleEvents . has ( waitUntil ) )
return false ;
for ( const child of frame . childFrames ( ) ) {
if ( ! checkLifecycleRecursively ( child ) )
return false ;
}
return true ;
}
}
done() {
2020-03-21 13:02:37 -07:00
this . _frame . _frameTasks . delete ( this ) ;
if ( this . _timer )
clearTimeout ( this . _timer ) ;
2020-03-18 20:05:35 -07:00
this . _failurePromise . catch ( e = > { } ) ;
}
}