2017-06-17 21:27:51 +00:00
/ * *
* Copyright 2017 Google Inc . All rights reserved .
*
* 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 .
* /
2017-08-02 19:06:47 +00:00
const fs = require ( 'fs' ) ;
const EventEmitter = require ( 'events' ) ;
2018-05-31 23:53:51 +00:00
const { helper , assert } = require ( './helper' ) ;
2018-08-16 23:16:27 +00:00
const { ExecutionContext } = require ( './ExecutionContext' ) ;
2018-08-09 23:51:12 +00:00
const { TimeoutError } = require ( './Errors' ) ;
2018-09-19 20:12:28 +00:00
const { NetworkManager } = require ( './NetworkManager' ) ;
2018-11-20 22:21:13 +00:00
const { CDPSession } = require ( './Connection' ) ;
2017-06-17 21:27:51 +00:00
2017-10-12 08:26:44 +00:00
const readFileAsync = helper . promisify ( fs . readFile ) ;
2017-06-17 21:27:51 +00:00
class FrameManager extends EventEmitter {
2017-06-21 20:51:06 +00:00
/ * *
2018-01-11 03:33:22 +00:00
* @ param { ! Puppeteer . CDPSession } client
2018-04-08 00:58:52 +00:00
* @ param { ! Protocol . Page . FrameTree } frameTree
2017-10-10 05:31:40 +00:00
* @ param { ! Puppeteer . Page } page
2018-09-19 20:12:28 +00:00
* @ param { ! Puppeteer . NetworkManager } networkManager
2017-06-21 20:58:49 +00:00
* /
2018-09-19 20:12:28 +00:00
constructor ( client , frameTree , page , networkManager ) {
2017-06-21 20:51:06 +00:00
super ( ) ;
this . _client = client ;
2017-10-07 07:28:24 +00:00
this . _page = page ;
2018-09-19 20:12:28 +00:00
this . _networkManager = networkManager ;
this . _defaultNavigationTimeout = 30000 ;
2017-06-21 20:51:06 +00:00
/** @type {!Map<string, !Frame>} */
this . _frames = new Map ( ) ;
2018-04-08 00:58:52 +00:00
/** @type {!Map<number, !ExecutionContext>} */
2017-10-06 22:35:02 +00:00
this . _contextIdToContext = new Map ( ) ;
2017-06-21 20:51:06 +00:00
2017-06-27 19:40:46 +00:00
this . _client . on ( 'Page.frameAttached' , event => this . _onFrameAttached ( event . frameId , event . parentFrameId ) ) ;
this . _client . on ( 'Page.frameNavigated' , event => this . _onFrameNavigated ( event . frame ) ) ;
2018-04-10 06:38:20 +00:00
this . _client . on ( 'Page.navigatedWithinDocument' , event => this . _onFrameNavigatedWithinDocument ( event . frameId , event . url ) ) ;
2017-06-27 19:40:46 +00:00
this . _client . on ( 'Page.frameDetached' , event => this . _onFrameDetached ( event . frameId ) ) ;
2018-04-10 22:59:41 +00:00
this . _client . on ( 'Page.frameStoppedLoading' , event => this . _onFrameStoppedLoading ( event . frameId ) ) ;
2017-06-27 19:40:46 +00:00
this . _client . on ( 'Runtime.executionContextCreated' , event => this . _onExecutionContextCreated ( event . context ) ) ;
2017-11-19 00:27:52 +00:00
this . _client . on ( 'Runtime.executionContextDestroyed' , event => this . _onExecutionContextDestroyed ( event . executionContextId ) ) ;
this . _client . on ( 'Runtime.executionContextsCleared' , event => this . _onExecutionContextsCleared ( ) ) ;
2017-11-10 23:33:14 +00:00
this . _client . on ( 'Page.lifecycleEvent' , event => this . _onLifecycleEvent ( event ) ) ;
2017-10-18 02:14:57 +00:00
this . _handleFrameTree ( frameTree ) ;
}
2018-09-19 20:12:28 +00:00
/ * *
* @ param { number } timeout
* /
setDefaultNavigationTimeout ( timeout ) {
this . _defaultNavigationTimeout = timeout ;
}
/ * *
* @ param { ! Puppeteer . Frame } frame
* @ param { string } url
2018-11-12 20:59:21 +00:00
* @ param { ! { referer ? : string , timeout ? : number , waitUntil ? : string | ! Array < string > } = } options
2018-09-19 20:12:28 +00:00
* @ return { ! Promise < ? Puppeteer . Response > }
* /
async navigateFrame ( frame , url , options = { } ) {
2018-11-20 23:32:46 +00:00
assertNoLegacyNavigationOptions ( options ) ;
2018-11-12 20:59:21 +00:00
const {
referer = this . _networkManager . extraHTTPHeaders ( ) [ 'referer' ] ,
2018-11-20 23:32:46 +00:00
waitUntil = [ 'load' ] ,
2018-11-12 20:59:21 +00:00
timeout = this . _defaultNavigationTimeout ,
} = options ;
2018-09-19 20:12:28 +00:00
2018-11-20 23:32:46 +00:00
const watcher = new LifecycleWatcher ( this , frame , waitUntil , timeout ) ;
2018-09-19 20:12:28 +00:00
let ensureNewDocumentNavigation = false ;
let error = await Promise . race ( [
2018-11-12 20:59:21 +00:00
navigate ( this . _client , url , referer , frame . _id ) ,
2018-09-19 20:12:28 +00:00
watcher . timeoutOrTerminationPromise ( ) ,
] ) ;
if ( ! error ) {
error = await Promise . race ( [
watcher . timeoutOrTerminationPromise ( ) ,
ensureNewDocumentNavigation ? watcher . newDocumentNavigationPromise ( ) : watcher . sameDocumentNavigationPromise ( ) ,
] ) ;
}
watcher . dispose ( ) ;
if ( error )
throw error ;
return watcher . navigationResponse ( ) ;
/ * *
* @ param { ! Puppeteer . CDPSession } client
* @ param { string } url
* @ param { string } referrer
2018-09-20 18:31:19 +00:00
* @ param { string } frameId
2018-09-19 20:12:28 +00:00
* @ return { ! Promise < ? Error > }
* /
2018-09-20 18:31:19 +00:00
async function navigate ( client , url , referrer , frameId ) {
2018-09-19 20:12:28 +00:00
try {
2018-09-20 18:31:19 +00:00
const response = await client . send ( 'Page.navigate' , { url , referrer , frameId } ) ;
2018-09-19 20:12:28 +00:00
ensureNewDocumentNavigation = ! ! response . loaderId ;
return response . errorText ? new Error ( ` ${ response . errorText } at ${ url } ` ) : null ;
} catch ( error ) {
return error ;
}
}
}
/ * *
* @ param { ! Puppeteer . Frame } frame
2018-11-12 20:59:21 +00:00
* @ param { ! { timeout ? : number , waitUntil ? : string | ! Array < string > } = } options
2018-09-19 20:12:28 +00:00
* @ return { ! Promise < ? Puppeteer . Response > }
* /
2018-11-12 20:59:21 +00:00
async waitForFrameNavigation ( frame , options = { } ) {
2018-11-20 23:32:46 +00:00
assertNoLegacyNavigationOptions ( options ) ;
2018-11-12 20:59:21 +00:00
const {
2018-11-20 23:32:46 +00:00
waitUntil = [ 'load' ] ,
timeout = this . _defaultNavigationTimeout ,
2018-11-12 20:59:21 +00:00
} = options ;
2018-11-20 23:32:46 +00:00
const watcher = new LifecycleWatcher ( this , frame , waitUntil , timeout ) ;
2018-09-19 20:12:28 +00:00
const error = await Promise . race ( [
watcher . timeoutOrTerminationPromise ( ) ,
watcher . sameDocumentNavigationPromise ( ) ,
watcher . newDocumentNavigationPromise ( )
] ) ;
watcher . dispose ( ) ;
if ( error )
throw error ;
return watcher . navigationResponse ( ) ;
}
2017-11-10 23:33:14 +00:00
/ * *
2018-04-08 00:58:52 +00:00
* @ param { ! Protocol . Page . lifecycleEventPayload } event
2017-11-10 23:33:14 +00:00
* /
_onLifecycleEvent ( event ) {
const frame = this . _frames . get ( event . frameId ) ;
if ( ! frame )
return ;
frame . _onLifecycleEvent ( event . loaderId , event . name ) ;
this . emit ( FrameManager . Events . LifecycleEvent , frame ) ;
}
2018-04-10 22:59:41 +00:00
/ * *
* @ param { string } frameId
* /
_onFrameStoppedLoading ( frameId ) {
const frame = this . _frames . get ( frameId ) ;
if ( ! frame )
return ;
frame . _onLoadingStopped ( ) ;
this . emit ( FrameManager . Events . LifecycleEvent , frame ) ;
}
2017-10-18 02:14:57 +00:00
/ * *
2018-04-08 00:58:52 +00:00
* @ param { ! Protocol . Page . FrameTree } frameTree
2017-10-18 02:14:57 +00:00
* /
_handleFrameTree ( frameTree ) {
if ( frameTree . frame . parentId )
this . _onFrameAttached ( frameTree . frame . id , frameTree . frame . parentId ) ;
this . _onFrameNavigated ( frameTree . frame ) ;
if ( ! frameTree . childFrames )
return ;
for ( const child of frameTree . childFrames )
this . _handleFrameTree ( child ) ;
2017-06-21 20:51:06 +00:00
}
2018-08-16 23:16:27 +00:00
/ * *
* @ return { ! Puppeteer . Page }
* /
page ( ) {
return this . _page ;
}
2017-06-21 20:51:06 +00:00
/ * *
2017-06-21 20:58:49 +00:00
* @ return { ! Frame }
* /
2017-06-21 20:51:06 +00:00
mainFrame ( ) {
return this . _mainFrame ;
}
2017-06-21 20:36:04 +00:00
2017-06-21 20:51:06 +00:00
/ * *
2017-06-21 20:58:49 +00:00
* @ return { ! Array < ! Frame > }
* /
2017-06-21 20:51:06 +00:00
frames ( ) {
return Array . from ( this . _frames . values ( ) ) ;
}
2017-06-21 20:36:04 +00:00
2018-01-10 02:47:21 +00:00
/ * *
* @ param { ! string } frameId
* @ return { ? Frame }
* /
frame ( frameId ) {
return this . _frames . get ( frameId ) || null ;
}
2017-06-21 20:51:06 +00:00
/ * *
2017-06-21 20:58:49 +00:00
* @ param { string } frameId
* @ param { ? string } parentFrameId
* /
2017-06-27 19:40:46 +00:00
_onFrameAttached ( frameId , parentFrameId ) {
2017-06-21 20:51:06 +00:00
if ( this . _frames . has ( frameId ) )
return ;
2018-05-31 23:53:51 +00:00
assert ( parentFrameId ) ;
2017-08-21 23:39:04 +00:00
const parentFrame = this . _frames . get ( parentFrameId ) ;
2018-08-16 23:16:27 +00:00
const frame = new Frame ( this , this . _client , parentFrame , frameId ) ;
2017-06-21 20:51:06 +00:00
this . _frames . set ( frame . _id , frame ) ;
this . emit ( FrameManager . Events . FrameAttached , frame ) ;
}
/ * *
2018-04-07 01:20:48 +00:00
* @ param { ! Protocol . Page . Frame } framePayload
2017-06-21 20:58:49 +00:00
* /
2017-06-27 19:40:46 +00:00
_onFrameNavigated ( framePayload ) {
2017-07-28 23:52:38 +00:00
const isMainFrame = ! framePayload . parentId ;
let frame = isMainFrame ? this . _mainFrame : this . _frames . get ( framePayload . id ) ;
2018-05-31 23:53:51 +00:00
assert ( isMainFrame || frame , 'We either navigate top level or have old version of the navigated frame' ) ;
2017-07-28 23:52:38 +00:00
// Detach all child frames first.
if ( frame ) {
2017-08-21 23:39:04 +00:00
for ( const child of frame . childFrames ( ) )
2017-07-28 23:52:38 +00:00
this . _removeFramesRecursively ( child ) ;
}
// Update or create main frame.
if ( isMainFrame ) {
if ( frame ) {
// Update frame id to retain frame identity on cross-process navigation.
2017-10-10 05:31:40 +00:00
this . _frames . delete ( frame . _id ) ;
2017-07-28 23:52:38 +00:00
frame . _id = framePayload . id ;
} else {
// Initial main frame navigation.
2018-08-16 23:16:27 +00:00
frame = new Frame ( this , this . _client , null , framePayload . id ) ;
2017-07-28 23:52:38 +00:00
}
this . _frames . set ( framePayload . id , frame ) ;
this . _mainFrame = frame ;
2017-06-21 20:36:04 +00:00
}
2017-07-28 23:52:38 +00:00
// Update frame payload.
frame . _navigated ( framePayload ) ;
this . emit ( FrameManager . Events . FrameNavigated , frame ) ;
2017-06-21 20:51:06 +00:00
}
2017-06-21 20:36:04 +00:00
2018-04-10 06:38:20 +00:00
/ * *
* @ param { string } frameId
* @ param { string } url
* /
_onFrameNavigatedWithinDocument ( frameId , url ) {
const frame = this . _frames . get ( frameId ) ;
if ( ! frame )
return ;
frame . _navigatedWithinDocument ( url ) ;
this . emit ( FrameManager . Events . FrameNavigatedWithinDocument , frame ) ;
this . emit ( FrameManager . Events . FrameNavigated , frame ) ;
}
2017-06-21 20:51:06 +00:00
/ * *
2017-06-21 20:58:49 +00:00
* @ param { string } frameId
* /
2017-06-27 19:40:46 +00:00
_onFrameDetached ( frameId ) {
2017-08-21 23:39:04 +00:00
const frame = this . _frames . get ( frameId ) ;
2017-06-21 20:51:06 +00:00
if ( frame )
this . _removeFramesRecursively ( frame ) ;
}
2017-06-21 20:36:04 +00:00
2017-10-06 22:35:02 +00:00
_onExecutionContextCreated ( contextPayload ) {
2018-08-09 21:57:08 +00:00
const frameId = contextPayload . auxData ? contextPayload . auxData . frameId : null ;
const frame = this . _frames . get ( frameId ) || null ;
2018-08-01 20:53:08 +00:00
/** @type {!ExecutionContext} */
2018-08-16 23:16:27 +00:00
const context = new ExecutionContext ( this . _client , contextPayload , frame ) ;
2017-10-06 22:35:02 +00:00
this . _contextIdToContext . set ( contextPayload . id , context ) ;
2018-02-13 22:02:44 +00:00
if ( frame )
2018-08-09 21:57:08 +00:00
frame . _addExecutionContext ( context ) ;
2017-11-19 00:27:52 +00:00
}
/ * *
2018-04-08 00:58:52 +00:00
* @ param { number } executionContextId
2017-11-19 00:27:52 +00:00
* /
_onExecutionContextDestroyed ( executionContextId ) {
const context = this . _contextIdToContext . get ( executionContextId ) ;
if ( ! context )
2017-07-25 21:30:04 +00:00
return ;
2017-11-19 00:27:52 +00:00
this . _contextIdToContext . delete ( executionContextId ) ;
2018-08-09 21:57:08 +00:00
if ( context . frame ( ) )
context . frame ( ) . _removeExecutionContext ( context ) ;
2017-06-21 20:51:06 +00:00
}
2017-06-21 20:36:04 +00:00
2017-11-19 00:27:52 +00:00
_onExecutionContextsCleared ( ) {
2018-08-10 01:14:21 +00:00
for ( const context of this . _contextIdToContext . values ( ) ) {
2018-08-09 21:57:08 +00:00
if ( context . frame ( ) )
context . frame ( ) . _removeExecutionContext ( context ) ;
}
2018-08-10 01:14:21 +00:00
this . _contextIdToContext . clear ( ) ;
2017-10-06 22:35:02 +00:00
}
/ * *
2018-04-08 00:58:52 +00:00
* @ param { number } contextId
2018-08-01 20:53:08 +00:00
* @ return { ! ExecutionContext }
2017-10-06 22:35:02 +00:00
* /
2018-08-01 20:53:08 +00:00
executionContextById ( contextId ) {
2017-10-06 22:35:02 +00:00
const context = this . _contextIdToContext . get ( contextId ) ;
2018-05-31 23:53:51 +00:00
assert ( context , 'INTERNAL ERROR: missing context with id = ' + contextId ) ;
2018-08-01 20:53:08 +00:00
return context ;
}
2017-06-21 20:51:06 +00:00
/ * *
2017-06-21 20:58:49 +00:00
* @ param { ! Frame } frame
* /
2017-06-21 20:51:06 +00:00
_removeFramesRecursively ( frame ) {
2017-08-21 23:39:04 +00:00
for ( const child of frame . childFrames ( ) )
2017-06-21 20:51:06 +00:00
this . _removeFramesRecursively ( child ) ;
frame . _detach ( ) ;
this . _frames . delete ( frame . _id ) ;
this . emit ( FrameManager . Events . FrameDetached , frame ) ;
}
2017-06-17 21:27:51 +00:00
}
/** @enum {string} */
FrameManager . Events = {
2017-06-21 20:51:06 +00:00
FrameAttached : 'frameattached' ,
FrameNavigated : 'framenavigated' ,
2017-11-10 23:33:14 +00:00
FrameDetached : 'framedetached' ,
2018-04-10 06:38:20 +00:00
LifecycleEvent : 'lifecycleevent' ,
2018-08-09 21:57:08 +00:00
FrameNavigatedWithinDocument : 'framenavigatedwithindocument' ,
ExecutionContextCreated : 'executioncontextcreated' ,
ExecutionContextDestroyed : 'executioncontextdestroyed' ,
2017-06-17 21:27:51 +00:00
} ;
/ * *
* @ unrestricted
* /
class Frame {
2017-06-21 20:51:06 +00:00
/ * *
2018-08-16 23:16:27 +00:00
* @ param { ! FrameManager } frameManager
2018-01-11 03:33:22 +00:00
* @ param { ! Puppeteer . CDPSession } client
2017-06-21 20:58:49 +00:00
* @ param { ? Frame } parentFrame
* @ param { string } frameId
* /
2018-08-16 23:16:27 +00:00
constructor ( frameManager , client , parentFrame , frameId ) {
this . _frameManager = frameManager ;
2017-07-25 21:30:04 +00:00
this . _client = client ;
2017-06-21 20:51:06 +00:00
this . _parentFrame = parentFrame ;
this . _url = '' ;
this . _id = frameId ;
2018-08-24 22:17:36 +00:00
this . _detached = false ;
2017-11-19 00:27:52 +00:00
2018-08-16 23:16:27 +00:00
/** @type {?Promise<!Puppeteer.ElementHandle>} */
2017-12-20 00:23:45 +00:00
this . _documentPromise = null ;
2018-08-24 22:17:36 +00:00
/** @type {!Promise<!ExecutionContext>} */
this . _contextPromise ;
2017-11-19 00:27:52 +00:00
this . _contextResolveCallback = null ;
this . _setDefaultContext ( null ) ;
2018-08-24 22:17:36 +00:00
2017-07-24 16:58:51 +00:00
/** @type {!Set<!WaitTask>} */
this . _waitTasks = new Set ( ) ;
2017-11-10 23:33:14 +00:00
this . _loaderId = '' ;
/** @type {!Set<string>} */
this . _lifecycleEvents = new Set ( ) ;
2017-07-20 02:04:51 +00:00
2017-06-21 20:51:06 +00:00
/** @type {!Set<!Frame>} */
this . _childFrames = new Set ( ) ;
if ( this . _parentFrame )
this . _parentFrame . _childFrames . add ( this ) ;
}
2018-08-09 21:57:08 +00:00
/ * *
* @ param { ! ExecutionContext } context
* /
_addExecutionContext ( context ) {
2018-08-10 01:14:21 +00:00
if ( context . _isDefault )
2018-08-09 21:57:08 +00:00
this . _setDefaultContext ( context ) ;
}
/ * *
* @ param { ! ExecutionContext } context
* /
_removeExecutionContext ( context ) {
2018-08-10 01:14:21 +00:00
if ( context . _isDefault )
2018-08-09 21:57:08 +00:00
this . _setDefaultContext ( null ) ;
}
2017-10-06 22:35:02 +00:00
/ * *
2017-11-19 00:27:52 +00:00
* @ param { ? ExecutionContext } context
* /
_setDefaultContext ( context ) {
if ( context ) {
this . _contextResolveCallback . call ( null , context ) ;
this . _contextResolveCallback = null ;
for ( const waitTask of this . _waitTasks )
waitTask . rerun ( ) ;
} else {
2017-12-20 00:23:45 +00:00
this . _documentPromise = null ;
2017-11-19 00:27:52 +00:00
this . _contextPromise = new Promise ( fulfill => {
this . _contextResolveCallback = fulfill ;
} ) ;
}
}
2018-09-20 18:31:19 +00:00
/ * *
* @ param { string } url
2018-11-12 20:59:21 +00:00
* @ param { ! { referer ? : string , timeout ? : number , waitUntil ? : string | ! Array < string > } = } options
2018-09-20 18:31:19 +00:00
* @ return { ! Promise < ? Puppeteer . Response > }
* /
2018-11-12 20:59:21 +00:00
async goto ( url , options ) {
2018-09-20 18:31:19 +00:00
return await this . _frameManager . navigateFrame ( this , url , options ) ;
}
/ * *
2018-11-12 20:59:21 +00:00
* @ param { ! { timeout ? : number , waitUntil ? : string | ! Array < string > } = } options
2018-09-20 18:31:19 +00:00
* @ return { ! Promise < ? Puppeteer . Response > }
* /
2018-11-12 20:59:21 +00:00
async waitForNavigation ( options ) {
2018-09-20 18:31:19 +00:00
return await this . _frameManager . waitForFrameNavigation ( this , options ) ;
}
2017-11-19 00:27:52 +00:00
/ * *
* @ return { ! Promise < ! ExecutionContext > }
2017-10-06 22:35:02 +00:00
* /
executionContext ( ) {
2017-11-19 00:27:52 +00:00
return this . _contextPromise ;
2017-10-06 22:35:02 +00:00
}
2018-01-25 05:16:01 +00:00
/ * *
* @ param { function ( ) | string } pageFunction
* @ param { ! Array < * > } args
* @ return { ! Promise < ! Puppeteer . JSHandle > }
* /
async evaluateHandle ( pageFunction , ... args ) {
const context = await this . _contextPromise ;
return context . evaluateHandle ( pageFunction , ... args ) ;
}
2017-06-27 19:40:46 +00:00
/ * *
2017-10-10 05:31:40 +00:00
* @ param { Function | string } pageFunction
2017-06-27 19:40:46 +00:00
* @ param { ! Array < * > } args
2017-10-10 05:31:40 +00:00
* @ return { ! Promise < * > }
2017-06-27 19:40:46 +00:00
* /
2017-07-14 20:51:22 +00:00
async evaluate ( pageFunction , ... args ) {
2017-11-19 00:27:52 +00:00
const context = await this . _contextPromise ;
return context . evaluate ( pageFunction , ... args ) ;
2017-08-15 21:54:02 +00:00
}
/ * *
* @ param { string } selector
2018-08-16 23:16:27 +00:00
* @ return { ! Promise < ? Puppeteer . ElementHandle > }
2017-08-15 21:54:02 +00:00
* /
async $ ( selector ) {
2017-12-20 00:23:45 +00:00
const document = await this . _document ( ) ;
const value = await document . $ ( selector ) ;
return value ;
}
/ * *
2018-08-16 23:16:27 +00:00
* @ return { ! Promise < ! Puppeteer . ElementHandle > }
2017-12-20 00:23:45 +00:00
* /
async _document ( ) {
if ( this . _documentPromise )
return this . _documentPromise ;
this . _documentPromise = this . _contextPromise . then ( async context => {
const document = await context . evaluateHandle ( 'document' ) ;
return document . asElement ( ) ;
} ) ;
return this . _documentPromise ;
}
/ * *
* @ param { string } expression
2018-08-16 23:16:27 +00:00
* @ return { ! Promise < ! Array < ! Puppeteer . ElementHandle >> }
2017-12-20 00:23:45 +00:00
* /
2018-01-03 23:37:08 +00:00
async $x ( expression ) {
2017-12-20 00:23:45 +00:00
const document = await this . _document ( ) ;
2018-01-03 23:37:08 +00:00
const value = await document . $x ( expression ) ;
2017-12-20 00:23:45 +00:00
return value ;
2017-08-15 21:54:02 +00:00
}
2017-08-31 22:38:01 +00:00
/ * *
* @ param { string } selector
2017-10-10 05:31:40 +00:00
* @ param { Function | string } pageFunction
2017-08-31 22:38:01 +00:00
* @ param { ! Array < * > } args
* @ return { ! Promise < ( ! Object | undefined ) > }
* /
async $eval ( selector , pageFunction , ... args ) {
2018-05-09 01:17:59 +00:00
const document = await this . _document ( ) ;
2018-05-26 00:29:11 +00:00
return document . $eval ( selector , pageFunction , ... args ) ;
2017-08-31 22:38:01 +00:00
}
2017-10-11 06:23:14 +00:00
/ * *
* @ param { string } selector
* @ param { Function | string } pageFunction
* @ param { ! Array < * > } args
* @ return { ! Promise < ( ! Object | undefined ) > }
* /
async $$eval ( selector , pageFunction , ... args ) {
2018-05-25 23:56:51 +00:00
const document = await this . _document ( ) ;
const value = await document . $$eval ( selector , pageFunction , ... args ) ;
return value ;
2017-10-11 06:23:14 +00:00
}
2017-08-23 05:56:55 +00:00
/ * *
* @ param { string } selector
2018-08-16 23:16:27 +00:00
* @ return { ! Promise < ! Array < ! Puppeteer . ElementHandle >> }
2017-08-23 05:56:55 +00:00
* /
async $$ ( selector ) {
2017-12-20 00:23:45 +00:00
const document = await this . _document ( ) ;
const value = await document . $$ ( selector ) ;
return value ;
2017-08-23 05:56:55 +00:00
}
2017-11-23 02:44:33 +00:00
/ * *
* @ return { ! Promise < String > }
* /
async content ( ) {
return await this . evaluate ( ( ) => {
let retVal = '' ;
if ( document . doctype )
retVal = new XMLSerializer ( ) . serializeToString ( document . doctype ) ;
if ( document . documentElement )
retVal += document . documentElement . outerHTML ;
return retVal ;
} ) ;
}
/ * *
* @ param { string } html
2018-11-20 23:32:46 +00:00
* @ param { ! { timeout ? : number , waitUntil ? : string | ! Array < string > } = } options
2017-11-23 02:44:33 +00:00
* /
2018-11-20 23:32:46 +00:00
async setContent ( html , options = { } ) {
const {
waitUntil = [ 'load' ] ,
timeout = 30000 ,
} = options ;
// We rely upon the fact that document.open() will reset frame lifecycle with "init"
// lifecycle event. @see https://crrev.com/608658
2017-11-23 02:44:33 +00:00
await this . evaluate ( html => {
document . open ( ) ;
document . write ( html ) ;
document . close ( ) ;
} , html ) ;
2018-11-20 23:32:46 +00:00
const watcher = new LifecycleWatcher ( this . _frameManager , this , waitUntil , timeout ) ;
const error = await Promise . race ( [
watcher . timeoutOrTerminationPromise ( ) ,
watcher . lifecyclePromise ( ) ,
] ) ;
watcher . dispose ( ) ;
if ( error )
throw error ;
2017-11-23 02:44:33 +00:00
}
2017-06-21 20:51:06 +00:00
/ * *
2017-06-21 20:58:49 +00:00
* @ return { string }
* /
2017-06-21 20:51:06 +00:00
name ( ) {
return this . _name || '' ;
}
2017-06-21 20:36:04 +00:00
2017-06-21 20:51:06 +00:00
/ * *
2017-06-21 20:58:49 +00:00
* @ return { string }
* /
2017-06-21 20:51:06 +00:00
url ( ) {
return this . _url ;
}
2017-06-21 20:36:04 +00:00
2017-06-21 20:51:06 +00:00
/ * *
2017-06-21 20:58:49 +00:00
* @ return { ? Frame }
* /
2017-06-21 20:51:06 +00:00
parentFrame ( ) {
return this . _parentFrame ;
}
2017-06-21 20:36:04 +00:00
2017-06-21 20:51:06 +00:00
/ * *
2017-06-21 20:58:49 +00:00
* @ return { ! Array . < ! Frame > }
* /
2017-06-21 20:51:06 +00:00
childFrames ( ) {
return Array . from ( this . _childFrames ) ;
}
2017-06-21 20:36:04 +00:00
2017-06-21 20:51:06 +00:00
/ * *
2017-06-21 20:58:49 +00:00
* @ return { boolean }
* /
2017-06-21 20:51:06 +00:00
isDetached ( ) {
return this . _detached ;
}
2017-06-21 20:36:04 +00:00
2017-07-25 18:37:46 +00:00
/ * *
2018-11-12 20:59:21 +00:00
* @ param { ! { url ? : string , path ? : string , content ? : string , type ? : string } } options
2018-08-16 23:16:27 +00:00
* @ return { ! Promise < ! Puppeteer . ElementHandle > }
2017-07-25 18:37:46 +00:00
* /
2017-10-12 08:26:44 +00:00
async addScriptTag ( options ) {
2018-11-12 20:59:21 +00:00
const {
url = null ,
path = null ,
content = null ,
type = ''
} = options ;
if ( url !== null ) {
2017-11-07 06:04:40 +00:00
try {
2017-11-19 00:27:52 +00:00
const context = await this . _contextPromise ;
2018-11-12 20:59:21 +00:00
return ( await context . evaluateHandle ( addScriptUrl , url , type ) ) . asElement ( ) ;
2017-11-07 06:04:40 +00:00
} catch ( error ) {
throw new Error ( ` Loading script from ${ url } failed ` ) ;
}
}
2017-07-25 18:37:46 +00:00
2018-11-12 20:59:21 +00:00
if ( path !== null ) {
let contents = await readFileAsync ( path , 'utf8' ) ;
contents += '//# sourceURL=' + path . replace ( /\n/g , '' ) ;
2017-11-19 00:27:52 +00:00
const context = await this . _contextPromise ;
2018-11-12 20:59:21 +00:00
return ( await context . evaluateHandle ( addScriptContent , contents , type ) ) . asElement ( ) ;
2017-10-12 08:26:44 +00:00
}
2018-11-12 20:59:21 +00:00
if ( content !== null ) {
2017-11-19 00:27:52 +00:00
const context = await this . _contextPromise ;
2018-11-12 20:59:21 +00:00
return ( await context . evaluateHandle ( addScriptContent , content , type ) ) . asElement ( ) ;
2017-11-19 00:27:52 +00:00
}
2017-10-12 08:26:44 +00:00
throw new Error ( 'Provide an object with a `url`, `path` or `content` property' ) ;
2017-07-25 18:37:46 +00:00
/ * *
* @ param { string } url
2018-03-14 20:07:48 +00:00
* @ param { string } type
2017-11-03 07:05:38 +00:00
* @ return { ! Promise < ! HTMLElement > }
2017-07-25 18:37:46 +00:00
* /
2018-03-14 20:07:48 +00:00
async function addScriptUrl ( url , type ) {
2017-08-21 23:39:04 +00:00
const script = document . createElement ( 'script' ) ;
2017-07-25 18:37:46 +00:00
script . src = url ;
2018-03-14 20:07:48 +00:00
if ( type )
script . type = type ;
2018-04-06 20:17:55 +00:00
const promise = new Promise ( ( res , rej ) => {
2017-11-07 06:04:40 +00:00
script . onload = res ;
script . onerror = rej ;
} ) ;
2018-04-06 20:17:55 +00:00
document . head . appendChild ( script ) ;
await promise ;
2017-11-03 07:05:38 +00:00
return script ;
2017-07-25 18:37:46 +00:00
}
2017-10-12 08:26:44 +00:00
/ * *
* @ param { string } content
2018-03-14 20:07:48 +00:00
* @ param { string } type
2017-11-03 07:05:38 +00:00
* @ return { ! HTMLElement }
2017-10-12 08:26:44 +00:00
* /
2018-03-14 20:07:48 +00:00
function addScriptContent ( content , type = 'text/javascript' ) {
2017-10-12 08:26:44 +00:00
const script = document . createElement ( 'script' ) ;
2018-03-14 20:07:48 +00:00
script . type = type ;
2017-10-12 08:26:44 +00:00
script . text = content ;
2018-04-06 20:17:55 +00:00
let error = null ;
script . onerror = e => error = e ;
2017-10-12 08:26:44 +00:00
document . head . appendChild ( script ) ;
2018-04-06 20:17:55 +00:00
if ( error )
throw error ;
2017-11-03 07:05:38 +00:00
return script ;
2017-10-12 08:26:44 +00:00
}
2017-07-25 18:37:46 +00:00
}
2017-10-04 20:42:26 +00:00
/ * *
2018-11-12 20:59:21 +00:00
* @ param { ! { url ? : string , path ? : string , content ? : string } } options
2018-08-16 23:16:27 +00:00
* @ return { ! Promise < ! Puppeteer . ElementHandle > }
2017-10-04 20:42:26 +00:00
* /
2017-10-12 08:26:44 +00:00
async addStyleTag ( options ) {
2018-11-12 20:59:21 +00:00
const {
url = null ,
path = null ,
content = null
} = options ;
if ( url !== null ) {
2017-11-07 06:04:40 +00:00
try {
2017-11-19 00:27:52 +00:00
const context = await this . _contextPromise ;
2017-12-20 00:23:45 +00:00
return ( await context . evaluateHandle ( addStyleUrl , url ) ) . asElement ( ) ;
2017-11-07 06:04:40 +00:00
} catch ( error ) {
throw new Error ( ` Loading style from ${ url } failed ` ) ;
}
}
2017-10-12 08:26:44 +00:00
2018-11-12 20:59:21 +00:00
if ( path !== null ) {
let contents = await readFileAsync ( path , 'utf8' ) ;
contents += '/*# sourceURL=' + path . replace ( /\n/g , '' ) + '*/' ;
2017-11-19 00:27:52 +00:00
const context = await this . _contextPromise ;
2017-12-20 00:23:45 +00:00
return ( await context . evaluateHandle ( addStyleContent , contents ) ) . asElement ( ) ;
2017-10-12 08:26:44 +00:00
}
2018-11-12 20:59:21 +00:00
if ( content !== null ) {
2017-11-19 00:27:52 +00:00
const context = await this . _contextPromise ;
2018-11-12 20:59:21 +00:00
return ( await context . evaluateHandle ( addStyleContent , content ) ) . asElement ( ) ;
2017-11-19 00:27:52 +00:00
}
2017-10-12 08:26:44 +00:00
throw new Error ( 'Provide an object with a `url`, `path` or `content` property' ) ;
2017-10-04 20:42:26 +00:00
/ * *
* @ param { string } url
2017-11-03 07:05:38 +00:00
* @ return { ! Promise < ! HTMLElement > }
2017-10-04 20:42:26 +00:00
* /
2017-11-03 07:05:38 +00:00
async function addStyleUrl ( url ) {
2017-10-04 20:42:26 +00:00
const link = document . createElement ( 'link' ) ;
link . rel = 'stylesheet' ;
link . href = url ;
2018-04-06 20:17:55 +00:00
const promise = new Promise ( ( res , rej ) => {
2017-11-07 06:04:40 +00:00
link . onload = res ;
link . onerror = rej ;
} ) ;
2018-04-06 20:17:55 +00:00
document . head . appendChild ( link ) ;
await promise ;
2017-11-03 07:05:38 +00:00
return link ;
2017-10-04 20:42:26 +00:00
}
2017-10-12 08:26:44 +00:00
/ * *
* @ param { string } content
2018-04-06 20:17:55 +00:00
* @ return { ! Promise < ! HTMLElement > }
2017-10-12 08:26:44 +00:00
* /
2018-04-06 20:17:55 +00:00
async function addStyleContent ( content ) {
2017-10-12 08:26:44 +00:00
const style = document . createElement ( 'style' ) ;
style . type = 'text/css' ;
style . appendChild ( document . createTextNode ( content ) ) ;
2018-04-06 20:17:55 +00:00
const promise = new Promise ( ( res , rej ) => {
style . onload = res ;
style . onerror = rej ;
} ) ;
2017-10-12 08:26:44 +00:00
document . head . appendChild ( style ) ;
2018-04-06 20:17:55 +00:00
await promise ;
2017-11-03 07:05:38 +00:00
return style ;
2017-10-12 08:26:44 +00:00
}
2017-10-04 20:42:26 +00:00
}
2018-02-05 22:58:03 +00:00
/ * *
* @ param { string } selector
2018-11-12 20:59:21 +00:00
* @ param { ! { delay ? : number , button ? : "left" | "right" | "middle" , clickCount ? : number } = } options
2018-02-05 22:58:03 +00:00
* /
2018-11-12 20:59:21 +00:00
async click ( selector , options ) {
2018-02-05 22:58:03 +00:00
const handle = await this . $ ( selector ) ;
2018-05-31 23:53:51 +00:00
assert ( handle , 'No node found for selector: ' + selector ) ;
2018-02-05 22:58:03 +00:00
await handle . click ( options ) ;
await handle . dispose ( ) ;
}
/ * *
* @ param { string } selector
* /
async focus ( selector ) {
const handle = await this . $ ( selector ) ;
2018-05-31 23:53:51 +00:00
assert ( handle , 'No node found for selector: ' + selector ) ;
2018-02-05 22:58:03 +00:00
await handle . focus ( ) ;
await handle . dispose ( ) ;
}
/ * *
* @ param { string } selector
* /
async hover ( selector ) {
const handle = await this . $ ( selector ) ;
2018-05-31 23:53:51 +00:00
assert ( handle , 'No node found for selector: ' + selector ) ;
2018-02-05 22:58:03 +00:00
await handle . hover ( ) ;
await handle . dispose ( ) ;
}
2017-11-02 05:06:04 +00:00
/ * *
* @ param { string } selector
* @ param { ! Array < string > } values
* @ return { ! Promise < ! Array < string >> }
* /
2018-01-25 05:12:27 +00:00
select ( selector , ... values ) {
2017-11-04 02:20:12 +00:00
for ( const value of values )
2018-05-31 23:53:51 +00:00
assert ( helper . isString ( value ) , 'Values must be strings. Found value "' + value + '" of type "' + ( typeof value ) + '"' ) ;
2018-01-25 05:12:27 +00:00
return this . $eval ( selector , ( element , values ) => {
2017-11-02 05:06:04 +00:00
if ( element . nodeName . toLowerCase ( ) !== 'select' )
throw new Error ( 'Element is not a <select> element.' ) ;
const options = Array . from ( element . options ) ;
element . value = undefined ;
2018-01-25 05:12:27 +00:00
for ( const option of options ) {
2017-11-02 05:06:04 +00:00
option . selected = values . includes ( option . value ) ;
2018-01-25 05:12:27 +00:00
if ( option . selected && ! element . multiple )
break ;
}
2017-11-02 05:06:04 +00:00
element . dispatchEvent ( new Event ( 'input' , { 'bubbles' : true } ) ) ;
element . dispatchEvent ( new Event ( 'change' , { 'bubbles' : true } ) ) ;
return options . filter ( option => option . selected ) . map ( option => option . value ) ;
} , values ) ;
}
2018-02-05 22:58:03 +00:00
/ * *
* @ param { string } selector
* /
async tap ( selector ) {
const handle = await this . $ ( selector ) ;
2018-05-31 23:53:51 +00:00
assert ( handle , 'No node found for selector: ' + selector ) ;
2018-02-05 22:58:03 +00:00
await handle . tap ( ) ;
await handle . dispose ( ) ;
}
/ * *
* @ param { string } selector
* @ param { string } text
* @ param { { delay : ( number | undefined ) } = } options
* /
async type ( selector , text , options ) {
const handle = await this . $ ( selector ) ;
2018-05-31 23:53:51 +00:00
assert ( handle , 'No node found for selector: ' + selector ) ;
2018-02-05 22:58:03 +00:00
await handle . type ( text , options ) ;
await handle . dispose ( ) ;
}
2017-07-21 19:41:49 +00:00
/ * *
2017-10-10 05:31:40 +00:00
* @ param { ( string | number | Function ) } selectorOrFunctionOrTimeout
2017-07-21 19:41:49 +00:00
* @ param { ! Object = } options
2017-09-15 21:28:15 +00:00
* @ param { ! Array < * > } args
2018-11-08 06:48:43 +00:00
* @ return { ! Promise < ! Puppeteer . JSHandle > }
2017-07-21 19:41:49 +00:00
* /
2017-09-15 21:28:15 +00:00
waitFor ( selectorOrFunctionOrTimeout , options = { } , ... args ) {
2018-01-22 23:16:20 +00:00
const xPathPattern = '//' ;
if ( helper . isString ( selectorOrFunctionOrTimeout ) ) {
const string = /** @type {string} */ ( selectorOrFunctionOrTimeout ) ;
if ( string . startsWith ( xPathPattern ) )
return this . waitForXPath ( string , options ) ;
return this . waitForSelector ( string , options ) ;
}
2017-07-28 00:09:28 +00:00
if ( helper . isNumber ( selectorOrFunctionOrTimeout ) )
2018-10-02 20:38:06 +00:00
return new Promise ( fulfill => setTimeout ( fulfill , /** @type {number} */ ( selectorOrFunctionOrTimeout ) ) ) ;
2017-07-28 00:09:28 +00:00
if ( typeof selectorOrFunctionOrTimeout === 'function' )
2017-09-15 21:28:15 +00:00
return this . waitForFunction ( selectorOrFunctionOrTimeout , options , ... args ) ;
2017-07-28 00:09:28 +00:00
return Promise . reject ( new Error ( 'Unsupported target type: ' + ( typeof selectorOrFunctionOrTimeout ) ) ) ;
2017-07-21 19:41:49 +00:00
}
2017-07-07 22:39:02 +00:00
/ * *
* @ param { string } selector
2018-11-12 20:59:21 +00:00
* @ param { ! { visible ? : boolean , hidden ? : boolean , timeout ? : number } = } options
2018-11-08 06:48:43 +00:00
* @ return { ! Promise < ! Puppeteer . ElementHandle > }
2017-07-07 22:39:02 +00:00
* /
2018-11-12 20:59:21 +00:00
waitForSelector ( selector , options ) {
2018-01-22 23:16:20 +00:00
return this . _waitForSelectorOrXPath ( selector , false , options ) ;
}
/ * *
* @ param { string } xpath
2018-11-12 20:59:21 +00:00
* @ param { ! { visible ? : boolean , hidden ? : boolean , timeout ? : number } = } options
2018-11-08 06:48:43 +00:00
* @ return { ! Promise < ! Puppeteer . ElementHandle > }
2018-01-22 23:16:20 +00:00
* /
2018-11-12 20:59:21 +00:00
waitForXPath ( xpath , options ) {
2018-01-22 23:16:20 +00:00
return this . _waitForSelectorOrXPath ( xpath , true , options ) ;
}
/ * *
* @ param { Function | string } pageFunction
2018-11-12 20:59:21 +00:00
* @ param { ! { polling ? : string | number , timeout ? : number } = } options
2018-11-08 06:48:43 +00:00
* @ return { ! Promise < ! Puppeteer . JSHandle > }
2018-01-22 23:16:20 +00:00
* /
waitForFunction ( pageFunction , options = { } , ... args ) {
2018-11-12 20:59:21 +00:00
const {
polling = 'raf' ,
timeout = 30000
} = options ;
2018-03-30 19:37:56 +00:00
return new WaitTask ( this , pageFunction , 'function' , polling , timeout , ... args ) . promise ;
2018-01-22 23:16:20 +00:00
}
/ * *
* @ return { ! Promise < string > }
* /
async title ( ) {
2018-03-15 21:04:22 +00:00
return this . evaluate ( ( ) => document . title ) ;
2018-01-22 23:16:20 +00:00
}
/ * *
* @ param { string } selectorOrXPath
* @ param { boolean } isXPath
2018-11-12 20:59:21 +00:00
* @ param { ! { visible ? : boolean , hidden ? : boolean , timeout ? : number } = } options
2018-11-08 06:48:43 +00:00
* @ return { ! Promise < ! Puppeteer . ElementHandle > }
2018-01-22 23:16:20 +00:00
* /
_waitForSelectorOrXPath ( selectorOrXPath , isXPath , options = { } ) {
2018-11-12 20:59:21 +00:00
const {
visible : waitForVisible = false ,
hidden : waitForHidden = false ,
timeout = 30000 ,
} = options ;
2017-10-13 16:11:11 +00:00
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation' ;
2018-07-26 23:24:04 +00:00
const title = ` ${ isXPath ? 'XPath' : 'selector' } " ${ selectorOrXPath } " ${ waitForHidden ? ' to be hidden' : '' } ` ;
return new WaitTask ( this , predicate , title , polling , timeout , selectorOrXPath , isXPath , waitForVisible , waitForHidden ) . promise ;
2017-07-27 22:17:43 +00:00
/ * *
2018-01-22 23:16:20 +00:00
* @ param { string } selectorOrXPath
* @ param { boolean } isXPath
2017-07-27 22:17:43 +00:00
* @ param { boolean } waitForVisible
2017-10-13 16:11:11 +00:00
* @ param { boolean } waitForHidden
2018-01-04 22:49:13 +00:00
* @ return { ? Node | boolean }
2017-07-27 22:17:43 +00:00
* /
2018-01-22 23:16:20 +00:00
function predicate ( selectorOrXPath , isXPath , waitForVisible , waitForHidden ) {
const node = isXPath
? document . evaluate ( selectorOrXPath , document , null , XPathResult . FIRST _ORDERED _NODE _TYPE , null ) . singleNodeValue
: document . querySelector ( selectorOrXPath ) ;
2017-07-27 22:17:43 +00:00
if ( ! node )
2017-10-13 16:11:11 +00:00
return waitForHidden ;
if ( ! waitForVisible && ! waitForHidden )
2018-01-04 22:49:13 +00:00
return node ;
2018-01-22 23:16:20 +00:00
const element = /** @type {Element} */ ( node . nodeType === Node . TEXT _NODE ? node . parentElement : node ) ;
const style = window . getComputedStyle ( element ) ;
2017-11-10 23:44:01 +00:00
const isVisible = style && style . visibility !== 'hidden' && hasVisibleBoundingBox ( ) ;
2018-01-04 22:49:13 +00:00
const success = ( waitForVisible === isVisible || waitForHidden === ! isVisible ) ;
return success ? node : null ;
2017-11-10 23:44:01 +00:00
/ * *
* @ return { boolean }
* /
function hasVisibleBoundingBox ( ) {
2018-01-22 23:16:20 +00:00
const rect = element . getBoundingClientRect ( ) ;
2017-11-10 23:44:01 +00:00
return ! ! ( rect . top || rect . bottom || rect . width || rect . height ) ;
}
2017-07-27 22:17:43 +00:00
}
}
2017-06-21 20:51:06 +00:00
/ * *
2018-04-07 01:20:48 +00:00
* @ param { ! Protocol . Page . Frame } framePayload
2017-06-21 20:58:49 +00:00
* /
2017-07-25 21:58:10 +00:00
_navigated ( framePayload ) {
2017-06-21 20:51:06 +00:00
this . _name = framePayload . name ;
2018-04-28 04:15:40 +00:00
// TODO(lushnikov): remove this once requestInterception has loaderId exposed.
this . _navigationURL = framePayload . url ;
2017-06-21 20:51:06 +00:00
this . _url = framePayload . url ;
2017-11-10 23:33:14 +00:00
}
2018-04-10 06:38:20 +00:00
/ * *
* @ param { string } url
* /
_navigatedWithinDocument ( url ) {
this . _url = url ;
}
2017-11-10 23:33:14 +00:00
/ * *
* @ param { string } loaderId
* @ param { string } name
* /
_onLifecycleEvent ( loaderId , name ) {
if ( name === 'init' ) {
this . _loaderId = loaderId ;
this . _lifecycleEvents . clear ( ) ;
}
this . _lifecycleEvents . add ( name ) ;
2017-06-21 20:51:06 +00:00
}
2018-04-10 22:59:41 +00:00
_onLoadingStopped ( ) {
this . _lifecycleEvents . add ( 'DOMContentLoaded' ) ;
this . _lifecycleEvents . add ( 'load' ) ;
}
2017-06-21 20:51:06 +00:00
_detach ( ) {
2017-08-21 23:39:04 +00:00
for ( const waitTask of this . _waitTasks )
2018-01-22 23:16:20 +00:00
waitTask . terminate ( new Error ( 'waitForFunction failed: frame got detached.' ) ) ;
2017-06-21 20:51:06 +00:00
this . _detached = true ;
if ( this . _parentFrame )
this . _parentFrame . _childFrames . delete ( this ) ;
this . _parentFrame = null ;
}
2017-06-17 21:27:51 +00:00
}
2017-07-19 03:53:00 +00:00
helper . tracePublicAPI ( Frame ) ;
2017-06-17 21:27:51 +00:00
2017-07-24 16:58:51 +00:00
class WaitTask {
2017-07-20 02:04:51 +00:00
/ * *
2017-07-24 16:58:51 +00:00
* @ param { ! Frame } frame
2018-01-04 22:49:13 +00:00
* @ param { Function | string } predicateBody
2017-10-10 05:31:40 +00:00
* @ param { string | number } polling
2017-07-24 16:58:51 +00:00
* @ param { number } timeout
2018-01-04 22:49:13 +00:00
* @ param { ! Array < * > } args
2017-07-20 02:04:51 +00:00
* /
2018-03-30 19:37:56 +00:00
constructor ( frame , predicateBody , title , polling , timeout , ... args ) {
2017-07-27 22:17:43 +00:00
if ( helper . isString ( polling ) )
2018-05-31 23:53:51 +00:00
assert ( polling === 'raf' || polling === 'mutation' , 'Unknown polling option: ' + polling ) ;
2017-07-27 22:17:43 +00:00
else if ( helper . isNumber ( polling ) )
2018-05-31 23:53:51 +00:00
assert ( polling > 0 , 'Cannot poll with non-positive interval: ' + polling ) ;
2017-07-27 22:17:43 +00:00
else
throw new Error ( 'Unknown polling options: ' + polling ) ;
2017-07-24 16:58:51 +00:00
this . _frame = frame ;
2018-01-04 22:49:13 +00:00
this . _polling = polling ;
this . _timeout = timeout ;
2019-01-11 02:18:28 +00:00
this . _predicateBody = helper . isString ( predicateBody ) ? 'return (' + predicateBody + ')' : 'return (' + predicateBody + ')(...args)' ;
2018-01-04 22:49:13 +00:00
this . _args = args ;
2017-07-25 22:57:31 +00:00
this . _runCount = 0 ;
2017-07-25 21:58:10 +00:00
frame . _waitTasks . add ( this ) ;
2017-07-20 02:04:51 +00:00
this . promise = new Promise ( ( resolve , reject ) => {
this . _resolve = resolve ;
this . _reject = reject ;
} ) ;
2017-07-24 16:58:51 +00:00
// Since page navigation requires us to re-install the pageScript, we should track
// timeout on our end.
2018-07-04 23:39:09 +00:00
if ( timeout ) {
2018-08-09 23:51:12 +00:00
const timeoutError = new TimeoutError ( ` waiting for ${ title } failed: timeout ${ timeout } ms exceeded ` ) ;
2018-07-04 23:39:09 +00:00
this . _timeoutTimer = setTimeout ( ( ) => this . terminate ( timeoutError ) , timeout ) ;
}
2017-07-25 21:58:10 +00:00
this . rerun ( ) ;
2017-07-20 02:04:51 +00:00
}
/ * *
* @ param { ! Error } error
* /
terminate ( error ) {
2017-07-25 22:57:31 +00:00
this . _terminated = true ;
2017-07-20 02:04:51 +00:00
this . _reject ( error ) ;
2017-07-24 16:58:51 +00:00
this . _cleanup ( ) ;
2017-07-20 02:04:51 +00:00
}
2017-07-25 22:57:31 +00:00
async rerun ( ) {
const runCount = ++ this . _runCount ;
2018-08-16 23:16:27 +00:00
/** @type {?Puppeteer.JSHandle} */
2018-01-04 22:49:13 +00:00
let success = null ;
2017-07-25 22:57:31 +00:00
let error = null ;
try {
2018-01-04 22:49:13 +00:00
success = await ( await this . _frame . executionContext ( ) ) . evaluateHandle ( waitForPredicatePageFunction , this . _predicateBody , this . _polling , this . _timeout , ... this . _args ) ;
2017-07-25 22:57:31 +00:00
} catch ( e ) {
error = e ;
2017-07-24 16:58:51 +00:00
}
2017-07-25 22:57:31 +00:00
2018-01-04 22:49:13 +00:00
if ( this . _terminated || runCount !== this . _runCount ) {
if ( success )
await success . dispose ( ) ;
2017-07-25 22:57:31 +00:00
return ;
2018-01-04 22:49:13 +00:00
}
2017-07-25 22:57:31 +00:00
// Ignore timeouts in pageScript - we track timeouts ourselves.
2018-06-14 18:39:51 +00:00
// If the frame's execution context has already changed, `frame.evaluate` will
// throw an error - ignore this predicate run altogether.
if ( ! error && await this . _frame . evaluate ( s => ! s , success ) . catch ( e => true ) ) {
2018-01-04 22:49:13 +00:00
await success . dispose ( ) ;
2017-07-25 22:57:31 +00:00
return ;
2018-01-04 22:49:13 +00:00
}
2017-07-25 22:57:31 +00:00
2017-08-11 08:21:02 +00:00
// When the page is navigated, the promise is rejected.
// We will try again in the new execution context.
if ( error && error . message . includes ( 'Execution context was destroyed' ) )
return ;
2017-08-21 20:34:26 +00:00
// We could have tried to evaluate in a context which was already
// destroyed.
if ( error && error . message . includes ( 'Cannot find context with specified id' ) )
return ;
2017-07-25 22:57:31 +00:00
if ( error )
this . _reject ( error ) ;
else
2018-01-04 22:49:13 +00:00
this . _resolve ( success ) ;
2017-07-25 22:57:31 +00:00
this . _cleanup ( ) ;
2017-07-24 16:58:51 +00:00
}
_cleanup ( ) {
clearTimeout ( this . _timeoutTimer ) ;
2017-07-25 21:58:10 +00:00
this . _frame . _waitTasks . delete ( this ) ;
2017-07-24 16:58:51 +00:00
this . _runningTask = null ;
}
}
/ * *
2017-07-27 22:17:43 +00:00
* @ param { string } predicateBody
* @ param { string } polling
2017-07-24 16:58:51 +00:00
* @ param { number } timeout
2018-01-04 22:49:13 +00:00
* @ return { ! Promise < * > }
2017-07-24 16:58:51 +00:00
* /
2018-01-04 22:49:13 +00:00
async function waitForPredicatePageFunction ( predicateBody , polling , timeout , ... args ) {
const predicate = new Function ( '...args' , predicateBody ) ;
2017-07-25 22:57:31 +00:00
let timedOut = false ;
2018-05-25 23:45:04 +00:00
if ( timeout )
setTimeout ( ( ) => timedOut = true , timeout ) ;
2017-07-27 22:17:43 +00:00
if ( polling === 'raf' )
2018-01-04 22:49:13 +00:00
return await pollRaf ( ) ;
if ( polling === 'mutation' )
return await pollMutation ( ) ;
if ( typeof polling === 'number' )
return await pollInterval ( polling ) ;
2017-07-24 16:58:51 +00:00
/ * *
2018-01-04 22:49:13 +00:00
* @ return { ! Promise < * > }
2017-07-24 16:58:51 +00:00
* /
2017-07-27 22:17:43 +00:00
function pollMutation ( ) {
2018-01-04 22:49:13 +00:00
const success = predicate . apply ( null , args ) ;
if ( success )
return Promise . resolve ( success ) ;
2017-07-24 16:58:51 +00:00
let fulfill ;
const result = new Promise ( x => fulfill = x ) ;
const observer = new MutationObserver ( mutations => {
2018-01-04 22:49:13 +00:00
if ( timedOut ) {
2017-07-24 16:58:51 +00:00
observer . disconnect ( ) ;
2017-07-25 22:57:31 +00:00
fulfill ( ) ;
2017-07-24 16:58:51 +00:00
}
2018-01-04 22:49:13 +00:00
const success = predicate . apply ( null , args ) ;
if ( success ) {
observer . disconnect ( ) ;
fulfill ( success ) ;
}
2017-07-24 16:58:51 +00:00
} ) ;
observer . observe ( document , {
childList : true ,
2017-08-23 20:25:40 +00:00
subtree : true ,
attributes : true
2017-07-24 16:58:51 +00:00
} ) ;
return result ;
}
/ * *
2018-01-04 22:49:13 +00:00
* @ return { ! Promise < * > }
2017-07-24 16:58:51 +00:00
* /
2017-07-27 22:17:43 +00:00
function pollRaf ( ) {
2017-07-24 16:58:51 +00:00
let fulfill ;
const result = new Promise ( x => fulfill = x ) ;
onRaf ( ) ;
return result ;
2017-07-25 22:57:31 +00:00
function onRaf ( ) {
2018-01-04 22:49:13 +00:00
if ( timedOut ) {
2017-07-25 22:57:31 +00:00
fulfill ( ) ;
2018-01-04 22:49:13 +00:00
return ;
}
const success = predicate . apply ( null , args ) ;
if ( success )
fulfill ( success ) ;
2017-07-27 22:17:43 +00:00
else
2017-07-24 16:58:51 +00:00
requestAnimationFrame ( onRaf ) ;
2017-07-27 22:17:43 +00:00
}
}
/ * *
* @ param { number } pollInterval
2018-01-04 22:49:13 +00:00
* @ return { ! Promise < * > }
2017-07-27 22:17:43 +00:00
* /
function pollInterval ( pollInterval ) {
let fulfill ;
const result = new Promise ( x => fulfill = x ) ;
onTimeout ( ) ;
return result ;
function onTimeout ( ) {
2018-01-04 22:49:13 +00:00
if ( timedOut ) {
2017-07-27 22:17:43 +00:00
fulfill ( ) ;
2018-01-04 22:49:13 +00:00
return ;
}
const success = predicate . apply ( null , args ) ;
if ( success )
fulfill ( success ) ;
2017-07-27 22:17:43 +00:00
else
setTimeout ( onTimeout , pollInterval ) ;
2017-07-20 02:04:51 +00:00
}
}
}
2018-11-20 23:32:46 +00:00
function assertNoLegacyNavigationOptions ( options ) {
assert ( options [ 'networkIdleTimeout' ] === undefined , 'ERROR: networkIdleTimeout option is no longer supported.' ) ;
assert ( options [ 'networkIdleInflight' ] === undefined , 'ERROR: networkIdleInflight option is no longer supported.' ) ;
assert ( options . waitUntil !== 'networkidle' , 'ERROR: "networkidle" option is no longer supported. Use "networkidle2" instead' ) ;
}
class LifecycleWatcher {
2018-09-19 20:12:28 +00:00
/ * *
* @ param { ! FrameManager } frameManager
* @ param { ! Puppeteer . Frame } frame
2018-11-20 23:32:46 +00:00
* @ param { string | ! Array < string > } waitUntil
2018-09-19 20:12:28 +00:00
* @ param { number } timeout
2018-11-20 23:32:46 +00:00
* /
constructor ( frameManager , frame , waitUntil , timeout ) {
2018-11-12 20:59:21 +00:00
if ( Array . isArray ( waitUntil ) )
waitUntil = waitUntil . slice ( ) ;
else if ( typeof waitUntil === 'string' )
waitUntil = [ waitUntil ] ;
2018-09-19 20:12:28 +00:00
this . _expectedLifecycle = waitUntil . map ( value => {
const protocolEvent = puppeteerToProtocolLifecycle [ value ] ;
assert ( protocolEvent , 'Unknown value for options.waitUntil: ' + value ) ;
return protocolEvent ;
} ) ;
this . _frameManager = frameManager ;
2018-11-20 23:32:46 +00:00
this . _networkManager = frameManager . _networkManager ;
2018-09-19 20:12:28 +00:00
this . _frame = frame ;
this . _initialLoaderId = frame . _loaderId ;
this . _timeout = timeout ;
/** @type {?Puppeteer.Request} */
this . _navigationRequest = null ;
this . _eventListeners = [
2018-11-20 23:32:46 +00:00
helper . addEventListener ( frameManager . _client , CDPSession . Events . Disconnected , ( ) => this . _terminate ( new Error ( 'Navigation failed because browser has disconnected!' ) ) ) ,
2018-09-19 20:12:28 +00:00
helper . addEventListener ( this . _frameManager , FrameManager . Events . LifecycleEvent , this . _checkLifecycleComplete . bind ( this ) ) ,
helper . addEventListener ( this . _frameManager , FrameManager . Events . FrameNavigatedWithinDocument , this . _navigatedWithinDocument . bind ( this ) ) ,
2018-09-20 18:31:19 +00:00
helper . addEventListener ( this . _frameManager , FrameManager . Events . FrameDetached , this . _onFrameDetached . bind ( this ) ) ,
2018-09-19 20:12:28 +00:00
helper . addEventListener ( this . _networkManager , NetworkManager . Events . Request , this . _onRequest . bind ( this ) ) ,
] ;
this . _sameDocumentNavigationPromise = new Promise ( fulfill => {
this . _sameDocumentNavigationCompleteCallback = fulfill ;
} ) ;
2018-11-20 23:32:46 +00:00
this . _lifecyclePromise = new Promise ( fulfill => {
this . _lifecycleCallback = fulfill ;
} ) ;
2018-09-19 20:12:28 +00:00
this . _newDocumentNavigationPromise = new Promise ( fulfill => {
this . _newDocumentNavigationCompleteCallback = fulfill ;
} ) ;
this . _timeoutPromise = this . _createTimeoutPromise ( ) ;
this . _terminationPromise = new Promise ( fulfill => {
this . _terminationCallback = fulfill ;
} ) ;
2018-12-13 21:33:42 +00:00
this . _checkLifecycleComplete ( ) ;
2018-09-19 20:12:28 +00:00
}
/ * *
* @ param { ! Puppeteer . Request } request
* /
_onRequest ( request ) {
if ( request . frame ( ) !== this . _frame || ! request . isNavigationRequest ( ) )
return ;
this . _navigationRequest = request ;
}
2018-09-20 18:31:19 +00:00
/ * *
* @ param { ! Puppeteer . Frame } frame
* /
_onFrameDetached ( frame ) {
if ( this . _frame === frame ) {
this . _terminationCallback . call ( null , new Error ( 'Navigating frame was detached' ) ) ;
return ;
}
this . _checkLifecycleComplete ( ) ;
}
2018-09-19 20:12:28 +00:00
/ * *
* @ return { ? Puppeteer . Response }
* /
navigationResponse ( ) {
return this . _navigationRequest ? this . _navigationRequest . response ( ) : null ;
}
/ * *
* @ param { ! Error } error
* /
_terminate ( error ) {
this . _terminationCallback . call ( null , error ) ;
}
/ * *
* @ return { ! Promise < ? Error > }
* /
sameDocumentNavigationPromise ( ) {
return this . _sameDocumentNavigationPromise ;
}
/ * *
* @ return { ! Promise < ? Error > }
* /
newDocumentNavigationPromise ( ) {
return this . _newDocumentNavigationPromise ;
}
2018-11-20 23:32:46 +00:00
/ * *
* @ return { ! Promise }
* /
lifecyclePromise ( ) {
return this . _lifecyclePromise ;
}
2018-09-19 20:12:28 +00:00
/ * *
* @ return { ! Promise < ? Error > }
* /
timeoutOrTerminationPromise ( ) {
return Promise . race ( [ this . _timeoutPromise , this . _terminationPromise ] ) ;
}
/ * *
* @ return { ! Promise < ? Error > }
* /
_createTimeoutPromise ( ) {
if ( ! this . _timeout )
return new Promise ( ( ) => { } ) ;
const errorMessage = 'Navigation Timeout Exceeded: ' + this . _timeout + 'ms exceeded' ;
return new Promise ( fulfill => this . _maximumTimer = setTimeout ( fulfill , this . _timeout ) )
. then ( ( ) => new TimeoutError ( errorMessage ) ) ;
}
/ * *
* @ param { ! Puppeteer . Frame } frame
* /
_navigatedWithinDocument ( frame ) {
if ( frame !== this . _frame )
return ;
this . _hasSameDocumentNavigation = true ;
this . _checkLifecycleComplete ( ) ;
}
_checkLifecycleComplete ( ) {
// We expect navigation to commit.
if ( ! checkLifecycle ( this . _frame , this . _expectedLifecycle ) )
return ;
2018-11-20 23:32:46 +00:00
this . _lifecycleCallback ( ) ;
if ( this . _frame . _loaderId === this . _initialLoaderId && ! this . _hasSameDocumentNavigation )
return ;
2018-09-19 20:12:28 +00:00
if ( this . _hasSameDocumentNavigation )
this . _sameDocumentNavigationCompleteCallback ( ) ;
if ( this . _frame . _loaderId !== this . _initialLoaderId )
this . _newDocumentNavigationCompleteCallback ( ) ;
/ * *
* @ param { ! Puppeteer . Frame } frame
* @ param { ! Array < string > } expectedLifecycle
* @ return { boolean }
* /
function checkLifecycle ( frame , expectedLifecycle ) {
for ( const event of expectedLifecycle ) {
if ( ! frame . _lifecycleEvents . has ( event ) )
return false ;
}
for ( const child of frame . childFrames ( ) ) {
if ( ! checkLifecycle ( child , expectedLifecycle ) )
return false ;
}
return true ;
}
}
dispose ( ) {
helper . removeEventListeners ( this . _eventListeners ) ;
clearTimeout ( this . _maximumTimer ) ;
}
}
const puppeteerToProtocolLifecycle = {
'load' : 'load' ,
'domcontentloaded' : 'DOMContentLoaded' ,
'networkidle0' : 'networkIdle' ,
'networkidle2' : 'networkAlmostIdle' ,
} ;
2017-10-10 05:31:40 +00:00
module . exports = { FrameManager , Frame } ;