/**
 * Copyright 2019 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.
 */ import { assert } from './assert.js' ; import { helper , debugError } from './helper.js' ; import { LifecycleWatcher , PuppeteerLifeCycleEvent , } from './LifecycleWatcher.js' ; import { TimeoutError } from './Errors.js' ; import { JSHandle , ElementHandle } from './JSHandle.js' ; import { ExecutionContext } from './ExecutionContext.js' ; import { TimeoutSettings } from './TimeoutSettings.js' ; import { MouseButton } from './Input.js' ; import { FrameManager , Frame } from './FrameManager.js' ; import { getQueryHandlerAndSelector } from './QueryHandler.js' ; import { SerializableOrJSHandle , EvaluateHandleFn , WrapElementHandle , EvaluateFn , EvaluateFnReturnType , UnwrapPromiseLike , } from './EvalTypes.js' ; import { isNode } from '../environment.js' ; import { Protocol } from 'devtools-protocol' ; import { CDPSession } from './Connection.js' ; declare const predicateQueryHandler ; declare const checkWaitForOptions ; public export interface WaitForSelectorOptions { visible ? : boolean ; hidden ? : boolean ; timeout ? : number ; root ? : ElementHandle ; } internal export interface PageBinding { name : string ; pptrFunction : Function ; } internal export class DOMWorld { private _frameManager : FrameManager ; private _client : CDPSession ; private _frame : Frame ; private _timeoutSettings : TimeoutSettings ; private _documentPromise ? : Promise < ElementHandle > = null ; private _contextPromise ? : Promise < ExecutionContext > = null ; private _contextResolveCallback ? : ( x ? : ExecutionContext ) => void = null ; private _detached = false ; internal _waitTasks = new Set < WaitTask > ( ) ; internal _boundFunctions = new Map < string , Function > ( ) ; private _ctxBindings = new Set < string > ( ) ; private static bindingIdentifier = ( name , contextId ) => `${ name }_${ contextId }` ; constructor ( client , frameManager , frame , timeoutSettings ) { this . _client = client ; this . _frameManager = frameManager ; this . _frame = frame ; this . _timeoutSettings = timeoutSettings ; this . _setContext ( null ) ; this . _client . on ( 'Runtime.bindingCalled' , ( event ) => this . _onBindingCalled ( event ) ) ; } frame ( ) { return this . _frame ; } async _setContext ( context ? ) { if ( context ) { assert ( this . _contextResolveCallback , 'Execution Context has already been set.' ) ; this . _ctxBindings . clear ( ) ; this . _contextResolveCallback . call ( null , context ) ; this . _contextResolveCallback = null ; for ( const waitTask of this . _waitTasks ) waitTask . rerun ( ) ; } else { this . _documentPromise = null ; this . _contextPromise = new Promise ( ( fulfill ) => { this . _contextResolveCallback = fulfill ; } ) ; } } _hasContext ( ) { return ! this . _contextResolveCallback ; } _detach ( ) { this . _detached = true ; for ( const waitTask of this . _waitTasks ) waitTask . terminate ( new Error ( 'waitForFunction failed: frame got detached.' ) ) ; } executionContext ( ) { if ( this . _detached ) throw new Error ( `Execution context is not available in detached frame "${ this . _frame . url ( ) }" (are you trying to evaluate?)` ) ; return this . _contextPromise ; } async evaluateHandle < HandlerType extends JSHandle = JSHandle > ( pageFunction , ... args ) { const context = await this . executionContext ( ) ; return context . evaluateHandle ( pageFunction , ... args ) ; } async evaluate < T extends EvaluateFn > ( pageFunction , ... args ) { const context = await this . executionContext ( ) ; return context . evaluate < UnwrapPromiseLike < EvaluateFnReturnType < T > > > ( pageFunction , ... args ) ; } async $ < T extends Element = Element > ( selector ) { const document = await this . _document ( ) ; const value = await document . $ < T > ( selector ) ; return value ; } async _document ( ) { if ( this . _documentPromise ) return this . _documentPromise ; this . _documentPromise = this . executionContext ( ) . then ( async ( context ) => { const document = await context . evaluateHandle ( 'document' ) ; return document . asElement ( ) ; } ) ; return this . _documentPromise ; } async $x ( expression ) { const document = await this . _document ( ) ; const value = await document . $x ( expression ) ; return value ; } async $eval < ReturnType > ( selector , pageFunction , ... args ) { const document = await this . _document ( ) ; return document . $eval < ReturnType > ( selector , pageFunction , ... args ) ; } async $$eval < ReturnType > ( selector , pageFunction , ... args ) { const document = await this . _document ( ) ; const value = await document . $$eval < ReturnType > ( selector , pageFunction , ... args ) ; return value ; } async $$ < T extends Element = Element > ( selector ) { const document = await this . _document ( ) ; const value = await document . $$ < T > ( selector ) ; return value ; } 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 ; } ) ; } async setContent ( html , options = { } ) { const { waitUntil = [ 'load' ] , timeout = this . _timeoutSettings . navigationTimeout ( ) , } = options ; await this . evaluate < ( x : string ) => void > ( ( html ) => { document . open ( ) ; document . write ( html ) ; document . close ( ) ; } , html ) ; const watcher = new LifecycleWatcher ( this . _frameManager , this . _frame , waitUntil , timeout ) ; const error = await Promise . race ( [ watcher . timeoutOrTerminationPromise ( ) , watcher . lifecyclePromise ( ) , ] ) ; watcher . dispose ( ) ; if ( error ) throw error ; } remarks async addScriptTag ( options ) { const { url = null , path = null , content = null , id = '' , type = '' , } = options ; if ( url !== null ) { try { const context = await this . executionContext ( ) ; return ( await context . evaluateHandle ( addScriptUrl , url , id , type ) ) . asElement ( ) ; } catch ( error ) { throw new Error ( `Loading script from ${ url } failed` ) ; } } if ( path !== null ) { if ( ! isNode ) { throw new Error ( 'Cannot pass a filepath to addScriptTag in the browser environment.' ) ; } const fs = await helper . importFSModule ( ) ; let contents = await fs . promises . readFile ( path , 'utf8' ) ; contents += '//# sourceURL=' + path . replace ( /\n/g , '' ) ; const context = await this . executionContext ( ) ; return ( await context . evaluateHandle ( addScriptContent , contents , id , type ) ) . asElement ( ) ; } if ( content !== null ) { const context = await this . executionContext ( ) ; return ( await context . evaluateHandle ( addScriptContent , content , id , type ) ) . asElement ( ) ; } throw new Error ( 'Provide an object with a `url`, `path` or `content` property' ) ; async function addScriptUrl ( url , id , type ) { const script = document . createElement ( 'script' ) ; script . src = url ; if ( id ) script . id = id ; if ( type ) script . type = type ; const promise = new Promise ( ( res , rej ) => { script . onload = res ; script . onerror = rej ; } ) ; document . head . appendChild ( script ) ; await promise ; return script ; } function addScriptContent ( content , id , type = 'text/javascript' ) { const script = document . createElement ( 'script' ) ; script . type = type ; script . text = content ; if ( id ) script . id = id ; let error = null ; script . onerror = ( e ) => ( error = e ) ; document . head . appendChild ( script ) ; if ( error ) throw error ; return script ; } } remarks async addStyleTag ( options ) { const { url = null , path = null , content = null } = options ; if ( url !== null ) { try { const context = await this . executionContext ( ) ; return ( await context . evaluateHandle ( addStyleUrl , url ) ) . asElement ( ) ; } catch ( error ) { throw new Error ( `Loading style from ${ url } failed` ) ; } } if ( path !== null ) { if ( ! isNode ) { throw new Error ( 'Cannot pass a filepath to addStyleTag in the browser environment.' ) ; } const fs = await helper . importFSModule ( ) ; let contents = await fs . promises . readFile ( path , 'utf8' ) ; contents += '/*# sourceURL=' + path . replace ( /\n/g , '' ) + '*/' ; const context = await this . executionContext ( ) ; return ( await context . evaluateHandle ( addStyleContent , contents ) ) . asElement ( ) ; } if ( content !== null ) { const context = await this . executionContext ( ) ; return ( await context . evaluateHandle ( addStyleContent , content ) ) . asElement ( ) ; } throw new Error ( 'Provide an object with a `url`, `path` or `content` property' ) ; async function addStyleUrl ( url ) { 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 ) { 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 ; } } async click ( selector , options ) { const handle = await this . $ ( selector ) ; assert ( handle , 'No node found for selector: ' + selector ) ; await handle . click ( options ) ; await handle . dispose ( ) ; } async focus ( selector ) { const handle = await this . $ ( selector ) ; assert ( handle , 'No node found for selector: ' + selector ) ; await handle . focus ( ) ; await handle . dispose ( ) ; } async hover ( selector ) { const handle = await this . $ ( selector ) ; assert ( handle , 'No node found for selector: ' + selector ) ; await handle . hover ( ) ; await handle . dispose ( ) ; } async select ( selector , ... values ) { const handle = await this . $ ( selector ) ; assert ( handle , 'No node found for selector: ' + selector ) ; const result = await handle . select ( ... values ) ; await handle . dispose ( ) ; return result ; } async tap ( selector ) { const handle = await this . $ ( selector ) ; await handle . tap ( ) ; await handle . dispose ( ) ; } async type ( selector , text , options ? ) { const handle = await this . $ ( selector ) ; assert ( handle , 'No node found for selector: ' + selector ) ; await handle . type ( text , options ) ; await handle . dispose ( ) ; } async waitForSelector ( selector , options ) { const { updatedSelector , queryHandler } = getQueryHandlerAndSelector ( selector ) ; return queryHandler . waitFor ( this , updatedSelector , options ) ; } private _settingUpBinding : Promise < void > | null = null ; internal async addBindingToContext ( context , name ) { if ( this . _ctxBindings . has ( DOMWorld . bindingIdentifier ( name , context . _contextId ) ) ) { return ; } if ( this . _settingUpBinding ) { await this . _settingUpBinding ; return this . addBindingToContext ( context , name ) ; } const bind = async ( name ) => { const expression = helper . pageBindingInitString ( 'internal' , name ) ; try { await context . _client . send ( 'Runtime.addBinding' , { name , executionContextName : context . _contextName , } ) ; await context . evaluate ( expression ) ; } catch ( error ) { const ctxDestroyed = error . message . includes ( 'Execution context was destroyed' ) ; const ctxNotFound = error . message . includes ( 'Cannot find context with specified id' ) ; if ( ctxDestroyed || ctxNotFound ) { return ; } else { debugError ( error ) ; return ; } } this . _ctxBindings . add ( DOMWorld . bindingIdentifier ( name , context . _contextId ) ) ; } ; this . _settingUpBinding = bind ( name ) ; await this . _settingUpBinding ; this . _settingUpBinding = null ; } private async _onBindingCalled ( event ) { let payload ; if ( ! this . _hasContext ( ) ) return ; const context = await this . executionContext ( ) ; try { payload = JSON . parse ( event . payload ) ; } catch { return ; } const { type , name , seq , args } = payload ; if ( type !== 'internal' || ! this . _ctxBindings . has ( DOMWorld . bindingIdentifier ( name , context . _contextId ) ) ) return ; if ( context . _contextId !== event . executionContextId ) return ; try { const result = await this . _boundFunctions . get ( name ) ( ... args ) ; await context . evaluate ( deliverResult , name , seq , result ) ; } catch ( error ) { if ( error . message . includes ( 'Protocol error' ) ) return ; debugError ( error ) ; } function deliverResult ( name , seq , result ) { globalThis [ name ] . callbacks . get ( seq ) . resolve ( result ) ; globalThis [ name ] . callbacks . delete ( seq ) ; } } internal async waitForSelectorInPage ( queryOne , selector , options , binding ? ) { const { visible : waitForVisible = false , hidden : waitForHidden = false , timeout = this . _timeoutSettings . timeout ( ) , } = options ; const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation' ; const title = `selector \`${ selector }\`${ waitForHidden ? ' to be hidden' : '' }` ; async function predicate ( root , selector , waitForVisible , waitForHidden ) { const node = predicateQueryHandler ? ( ( await predicateQueryHandler ( root , selector ) ) as Element ) : root . querySelector ( selector ) ; return checkWaitForOptions ( node , waitForVisible , waitForHidden ) ; } const waitTaskOptions = { domWorld : this , predicateBody : helper . makePredicateString ( predicate , queryOne ) , predicateAcceptsContextElement : true , title , polling , timeout , args : [ selector , waitForVisible , waitForHidden ] , binding , root : options . root , } ; const waitTask = new WaitTask ( waitTaskOptions ) ; const jsHandle = await waitTask . promise ; const elementHandle = jsHandle . asElement ( ) ; if ( ! elementHandle ) { await jsHandle . dispose ( ) ; return null ; } return elementHandle ; } async waitForXPath ( xpath , options ) { const { visible : waitForVisible = false , hidden : waitForHidden = false , timeout = this . _timeoutSettings . timeout ( ) , } = options ; const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation' ; const title = `XPath \`${ xpath }\`${ waitForHidden ? ' to be hidden' : '' }` ; function predicate ( root , xpath , waitForVisible , waitForHidden ) { const node = document . evaluate ( xpath , root , null , XPathResult . FIRST_ORDERED_NODE_TYPE , null ) . singleNodeValue ; return checkWaitForOptions ( node , waitForVisible , waitForHidden ) ; } const waitTaskOptions = { domWorld : this , predicateBody : helper . makePredicateString ( predicate ) , predicateAcceptsContextElement : true , title , polling , timeout , args : [ xpath , waitForVisible , waitForHidden ] , root : options . root , } ; const waitTask = new WaitTask ( waitTaskOptions ) ; const jsHandle = await waitTask . promise ; const elementHandle = jsHandle . asElement ( ) ; if ( ! elementHandle ) { await jsHandle . dispose ( ) ; return null ; } return elementHandle ; } waitForFunction ( pageFunction , options = { } , ... args ) { const { polling = 'raf' , timeout = this . _timeoutSettings . timeout ( ) } = options ; const waitTaskOptions = { domWorld : this , predicateBody : pageFunction , predicateAcceptsContextElement : false , title : 'function' , polling , timeout , args , } ; const waitTask = new WaitTask ( waitTaskOptions ) ; return waitTask . promise ; } async title ( ) { return this . evaluate ( ( ) => document . title ) ; } } internal export interface WaitTaskOptions { domWorld : DOMWorld ; predicateBody : Function | string ; predicateAcceptsContextElement : boolean ; title : string ; polling : string | number ; timeout : number ; binding ? : PageBinding ; args : SerializableOrJSHandle [ ] ; root ? : ElementHandle ; } internal export class WaitTask { _domWorld : DOMWorld ; _polling : string | number ; _timeout : number ; _predicateBody : string ; _predicateAcceptsContextElement : boolean ; _args : SerializableOrJSHandle [ ] ; _binding : PageBinding ; _runCount = 0 ; promise : Promise < JSHandle > ; _resolve : ( x : JSHandle ) => void ; _reject : ( x : Error ) => void ; _timeoutTimer ? : NodeJS . Timeout ; _terminated = false ; _root : ElementHandle = null ; constructor ( options ) { if ( helper . isString ( options . polling ) ) assert ( options . polling === 'raf' || options . polling === 'mutation' , 'Unknown polling option: ' + options . polling ) ; else if ( helper . isNumber ( options . polling ) ) assert ( options . polling > 0 , 'Cannot poll with non-positive interval: ' + options . polling ) ; else throw new Error ( 'Unknown polling options: ' + options . polling ) ; function getPredicateBody ( predicateBody ) { if ( helper . isString ( predicateBody ) ) return `return (${ predicateBody });` ; return `return (${ predicateBody })(...args);` ; } this . _domWorld = options . domWorld ; this . _polling = options . polling ; this . _timeout = options . timeout ; this . _root = options . root ; this . _predicateBody = getPredicateBody ( options . predicateBody ) ; this . _predicateAcceptsContextElement = options . predicateAcceptsContextElement ; this . _args = options . args ; this . _binding = options . binding ; this . _runCount = 0 ; this . _domWorld . _waitTasks . add ( this ) ; if ( this . _binding ) { this . _domWorld . _boundFunctions . set ( this . _binding . name , this . _binding . pptrFunction ) ; } this . promise = new Promise < JSHandle > ( ( resolve , reject ) => { this . _resolve = resolve ; this . _reject = reject ; } ) ; if ( options . timeout ) { const timeoutError = new TimeoutError ( `waiting for ${ options . title } failed: timeout ${ options . timeout }ms exceeded` ) ; this . _timeoutTimer = setTimeout ( ( ) => this . terminate ( timeoutError ) , options . timeout ) ; } this . rerun ( ) ; } terminate ( error ) { this . _terminated = true ; this . _reject ( error ) ; this . _cleanup ( ) ; } async rerun ( ) { const runCount = ++ this . _runCount ; let success = null ; let error = null ; const context = await this . _domWorld . executionContext ( ) ; if ( this . _terminated || runCount !== this . _runCount ) return ; if ( this . _binding ) { await this . _domWorld . addBindingToContext ( context , this . _binding . name ) ; } if ( this . _terminated || runCount !== this . _runCount ) return ; try { success = await context . evaluateHandle ( waitForPredicatePageFunction , this . _root || null , this . _predicateBody , this . _predicateAcceptsContextElement , this . _polling , this . _timeout , ... this . _args ) ; } catch ( error_ ) { error = error_ ; } if ( this . _terminated || runCount !== this . _runCount ) { if ( success ) await success . dispose ( ) ; return ; } if ( ! error && ( await this . _domWorld . evaluate ( ( s ) => ! s , success ) . catch ( ( ) => true ) ) ) { await success . dispose ( ) ; return ; } if ( error ) { if ( error . message . includes ( 'TypeError: binding is not a function' ) ) { return this . rerun ( ) ; } if ( error . message . includes ( 'Execution context is not available in detached frame' ) ) { this . terminate ( new Error ( 'waitForFunction failed: frame got detached.' ) ) ; return ; } if ( error . message . includes ( 'Execution context was destroyed' ) ) return ; if ( error . message . includes ( 'Cannot find context with specified id' ) ) return ; this . _reject ( error ) ; } else { this . _resolve ( success ) ; } this . _cleanup ( ) ; } _cleanup ( ) { clearTimeout ( this . _timeoutTimer ) ; this . _domWorld . _waitTasks . delete ( this ) ; } } async function waitForPredicatePageFunction ( root , predicateBody , predicateAcceptsContextElement , polling , timeout , ... args ) { root = root || document ; const predicate = new Function ( '...args' , predicateBody ) ; let timedOut = false ; if ( timeout ) setTimeout ( ( ) => ( timedOut = true ) , timeout ) ; if ( polling === 'raf' ) return await pollRaf ( ) ; if ( polling === 'mutation' ) return await pollMutation ( ) ; if ( typeof polling === 'number' ) return await pollInterval ( polling ) ; returns { ! Promise < * > } async function pollMutation ( ) { const success = predicateAcceptsContextElement ? await predicate ( root , ... args ) : await predicate ( ... args ) ; if ( success ) return Promise . resolve ( success ) ; let fulfill ; const result = new Promise ( ( x ) => ( fulfill = x ) ) ; const observer = new MutationObserver ( async ( ) => { if ( timedOut ) { observer . disconnect ( ) ; fulfill ( ) ; } const success = predicateAcceptsContextElement ? await predicate ( root , ... args ) : await predicate ( ... args ) ; if ( success ) { observer . disconnect ( ) ; fulfill ( success ) ; } } ) ; observer . observe ( root , { childList : true , subtree : true , attributes : true , } ) ; return result ; } async function pollRaf ( ) { let fulfill ; const result = new Promise ( ( x ) => ( fulfill = x ) ) ; await onRaf ( ) ; return result ; async function onRaf ( ) { if ( timedOut ) { fulfill ( ) ; return ; } const success = predicateAcceptsContextElement ? await predicate ( root , ... args ) : await predicate ( ... args ) ; if ( success ) fulfill ( success ) ; else requestAnimationFrame ( onRaf ) ; } } async function pollInterval ( pollInterval ) { let fulfill ; const result = new Promise ( ( x ) => ( fulfill = x ) ) ; await onTimeout ( ) ; return result ; async function onTimeout ( ) { if ( timedOut ) { fulfill ( ) ; return ; } const success = predicateAcceptsContextElement ? await predicate ( root , ... args ) : await predicate ( ... args ) ; if ( success ) fulfill ( success ) ; else setTimeout ( onTimeout , pollInterval ) ; } } }