2019-01-15 04:34:50 +00:00
/ * *
* 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 .
* /
2020-07-13 09:22:26 +00:00
import { assert } from './assert.js' ;
import { helper , debugError } from './helper.js' ;
import { ExecutionContext } from './ExecutionContext.js' ;
import { Page } from './Page.js' ;
import { CDPSession } from './Connection.js' ;
import { KeyInput } from './USKeyboardLayout.js' ;
import { FrameManager , Frame } from './FrameManager.js' ;
import { getQueryHandlerAndSelector } from './QueryHandler.js' ;
2020-07-10 10:51:52 +00:00
import { Protocol } from 'devtools-protocol' ;
2020-06-25 12:38:01 +00:00
import {
EvaluateFn ,
SerializableOrJSHandle ,
EvaluateFnReturnType ,
2020-07-01 11:44:08 +00:00
EvaluateHandleFn ,
2020-07-02 09:09:34 +00:00
WrapElementHandle ,
2020-07-10 10:52:13 +00:00
UnwrapPromiseLike ,
2020-07-13 09:22:26 +00:00
} from './EvalTypes.js' ;
2020-09-28 09:35:35 +00:00
import { isNode } from '../environment.js' ;
2020-04-21 11:11:06 +00:00
2020-06-22 15:21:57 +00:00
export interface BoxModel {
2020-05-07 10:54:55 +00:00
content : Array < { x : number ; y : number } > ;
padding : Array < { x : number ; y : number } > ;
border : Array < { x : number ; y : number } > ;
margin : Array < { x : number ; y : number } > ;
2020-04-21 11:11:06 +00:00
width : number ;
height : number ;
}
2019-01-15 04:34:50 +00:00
2020-06-22 15:21:57 +00:00
/ * *
* @public
* /
export interface BoundingBox {
/ * *
* the x coordinate of the element in pixels .
* /
x : number ;
/ * *
* the y coordinate of the element in pixels .
* /
y : number ;
/ * *
* the width of the element in pixels .
* /
width : number ;
/ * *
* the height of the element in pixels .
* /
height : number ;
}
/ * *
* @internal
* /
2020-05-07 10:54:55 +00:00
export function createJSHandle (
context : ExecutionContext ,
remoteObject : Protocol.Runtime.RemoteObject
) : JSHandle {
2019-01-15 04:34:50 +00:00
const frame = context . frame ( ) ;
if ( remoteObject . subtype === 'node' && frame ) {
const frameManager = frame . _frameManager ;
2020-05-07 10:54:55 +00:00
return new ElementHandle (
context ,
context . _client ,
remoteObject ,
frameManager . page ( ) ,
frameManager
) ;
2019-01-15 04:34:50 +00:00
}
return new JSHandle ( context , context . _client , remoteObject ) ;
}
2020-06-22 15:21:57 +00:00
/ * *
2020-06-25 14:49:35 +00:00
* Represents an in - page JavaScript object . JSHandles can be created with the
* { @link Page . evaluateHandle | page . evaluateHandle } method .
*
* @example
* ` ` ` js
* const windowHandle = await page . evaluateHandle ( ( ) = > window ) ;
* ` ` `
*
* JSHandle prevents the referenced JavaScript object from being garbage - collected
* unless the handle is { @link JSHandle . dispose | disposed } . JSHandles are auto -
* disposed when their origin frame gets navigated or the parent context gets destroyed .
*
* JSHandle instances can be used as arguments for { @link Page . $eval } ,
* { @link Page . evaluate } , and { @link Page . evaluateHandle } .
*
2020-06-22 15:21:57 +00:00
* @public
* /
2020-04-21 11:11:06 +00:00
export class JSHandle {
2020-06-30 14:56:37 +00:00
/ * *
* @internal
* /
chore: migrate src/ExecutionContext (#5705)
* chore: migrate src/ExecutionContext to TypeScript
I spent a while trying to decide on the best course of action for
typing the `evaluate` function.
Ideally I wanted to use generics so that as a user you could type
something like:
```
handle.evaluate<HTMLElement, number, boolean>((node, x) => true, 5)
```
And have TypeScript know the arguments of `node` and `x` based on those
generics. But I hit two problems with that:
* you have to have n overloads of `evaluate` to cope for as many number
of arguments as you can be bothered too (e.g. we'd need an overload for
1 arg, 2 args, 3 args, etc)
* I decided it's actually confusing because you don't know as a user
what those generics actually map to.
So in the end I went with one generic which is the return type of the
function:
```
handle.evaluate<boolean>((node, x) => true, 5)
```
And `node` and `x` get typed as `any` which means you can tell TS
yourself:
```
handle.evaluate<boolean>((node: HTMLElement, x: number) => true, 5)
```
I'd like to find a way to force that the arguments after the function do
match the arguments you've given (in the above example, TS would moan if
I swapped that `5` for `"foo"`), but I tried a few things and to be
honest the complexity of the types wasn't worth it, I don't think.
I'm very open to tweaking these but I'd rather ship this and tweak going
forwards rather than spend hours now tweaking. Once we ship these
typedefs and get feedback from the community I'm sure we can improve
them.
2020-04-22 09:33:44 +00:00
_context : ExecutionContext ;
2020-06-30 14:56:37 +00:00
/ * *
* @internal
* /
2020-04-21 11:11:06 +00:00
_client : CDPSession ;
2020-06-30 14:56:37 +00:00
/ * *
* @internal
* /
2020-04-21 11:11:06 +00:00
_remoteObject : Protocol.Runtime.RemoteObject ;
2020-06-30 14:56:37 +00:00
/ * *
* @internal
* /
2020-04-21 11:11:06 +00:00
_disposed = false ;
2020-06-30 14:56:37 +00:00
/ * *
* @internal
* /
2020-05-07 10:54:55 +00:00
constructor (
context : ExecutionContext ,
client : CDPSession ,
remoteObject : Protocol.Runtime.RemoteObject
) {
2019-01-15 04:34:50 +00:00
this . _context = context ;
this . _client = client ;
this . _remoteObject = remoteObject ;
}
2020-06-25 14:49:35 +00:00
/ * * R e t u r n s t h e e x e c u t i o n c o n t e x t t h e h a n d l e b e l o n g s t o .
* /
chore: migrate src/ExecutionContext (#5705)
* chore: migrate src/ExecutionContext to TypeScript
I spent a while trying to decide on the best course of action for
typing the `evaluate` function.
Ideally I wanted to use generics so that as a user you could type
something like:
```
handle.evaluate<HTMLElement, number, boolean>((node, x) => true, 5)
```
And have TypeScript know the arguments of `node` and `x` based on those
generics. But I hit two problems with that:
* you have to have n overloads of `evaluate` to cope for as many number
of arguments as you can be bothered too (e.g. we'd need an overload for
1 arg, 2 args, 3 args, etc)
* I decided it's actually confusing because you don't know as a user
what those generics actually map to.
So in the end I went with one generic which is the return type of the
function:
```
handle.evaluate<boolean>((node, x) => true, 5)
```
And `node` and `x` get typed as `any` which means you can tell TS
yourself:
```
handle.evaluate<boolean>((node: HTMLElement, x: number) => true, 5)
```
I'd like to find a way to force that the arguments after the function do
match the arguments you've given (in the above example, TS would moan if
I swapped that `5` for `"foo"`), but I tried a few things and to be
honest the complexity of the types wasn't worth it, I don't think.
I'm very open to tweaking these but I'd rather ship this and tweak going
forwards rather than spend hours now tweaking. Once we ship these
typedefs and get feedback from the community I'm sure we can improve
them.
2020-04-22 09:33:44 +00:00
executionContext ( ) : ExecutionContext {
2019-01-15 04:34:50 +00:00
return this . _context ;
}
2020-06-22 15:21:57 +00:00
/ * *
* This method passes this handle as the first argument to ` pageFunction ` .
* If ` pageFunction ` returns a Promise , then ` handle.evaluate ` would wait
* for the promise to resolve and return its value .
*
* @example
* ` ` ` js
* const tweetHandle = await page . $ ( '.tweet .retweets' ) ;
* expect ( await tweetHandle . evaluate ( node = > node . innerText ) ) . toBe ( '10' ) ;
* ` ` `
* /
2020-06-25 12:38:01 +00:00
async evaluate < T extends EvaluateFn > (
pageFunction : T | string ,
. . . args : SerializableOrJSHandle [ ]
2020-07-10 10:52:13 +00:00
) : Promise < UnwrapPromiseLike < EvaluateFnReturnType < T > >> {
return await this . executionContext ( ) . evaluate <
UnwrapPromiseLike < EvaluateFnReturnType < T > >
> ( pageFunction , this , . . . args ) ;
2019-09-04 22:19:34 +00:00
}
2020-06-22 15:21:57 +00:00
/ * *
* This method passes this handle as the first argument to ` pageFunction ` .
*
* @remarks
*
2020-06-25 14:49:35 +00:00
* The only difference between ` jsHandle.evaluate ` and
* ` jsHandle.evaluateHandle ` is that ` jsHandle.evaluateHandle `
* returns an in - page object ( JSHandle ) .
2020-06-22 15:21:57 +00:00
*
2020-06-25 14:49:35 +00:00
* If the function passed to ` jsHandle.evaluateHandle ` returns a Promise ,
* then ` evaluateHandle.evaluateHandle ` waits for the promise to resolve and
* returns its value .
2020-06-22 15:21:57 +00:00
*
* See { @link Page . evaluateHandle } for more details .
* /
2020-07-02 09:09:34 +00:00
async evaluateHandle < HandleType extends JSHandle = JSHandle > (
2020-07-01 11:44:08 +00:00
pageFunction : EvaluateHandleFn ,
. . . args : SerializableOrJSHandle [ ]
) : Promise < HandleType > {
2020-05-07 10:54:55 +00:00
return await this . executionContext ( ) . evaluateHandle (
pageFunction ,
this ,
. . . args
) ;
2019-09-04 22:19:34 +00:00
}
2020-06-25 14:49:35 +00:00
/ * * F e t c h e s a s i n g l e p r o p e r t y f r o m t h e r e f e r e n c e d o b j e c t .
* /
2020-04-21 11:11:06 +00:00
async getProperty ( propertyName : string ) : Promise < JSHandle | undefined > {
2020-05-07 10:54:55 +00:00
const objectHandle = await this . evaluateHandle (
( object : HTMLElement , propertyName : string ) = > {
const result = { __proto__ : null } ;
result [ propertyName ] = object [ propertyName ] ;
return result ;
} ,
propertyName
) ;
2019-01-15 04:34:50 +00:00
const properties = await objectHandle . getProperties ( ) ;
const result = properties . get ( propertyName ) || null ;
await objectHandle . dispose ( ) ;
return result ;
}
2020-06-22 15:21:57 +00:00
/ * *
* The method returns a map with property names as keys and JSHandle
* instances for the property values .
*
* @example
* ` ` ` js
* const listHandle = await page . evaluateHandle ( ( ) = > document . body . children ) ;
* const properties = await listHandle . getProperties ( ) ;
* const children = [ ] ;
* for ( const property of properties . values ( ) ) {
* const element = property . asElement ( ) ;
* if ( element )
* children . push ( element ) ;
* }
* children ; // holds elementHandles to all children of document.body
* ` ` `
* /
2020-04-21 11:11:06 +00:00
async getProperties ( ) : Promise < Map < string , JSHandle > > {
2019-01-15 04:34:50 +00:00
const response = await this . _client . send ( 'Runtime.getProperties' , {
objectId : this._remoteObject.objectId ,
2020-05-07 10:54:55 +00:00
ownProperties : true ,
2019-01-15 04:34:50 +00:00
} ) ;
2020-04-21 11:11:06 +00:00
const result = new Map < string , JSHandle > ( ) ;
2019-01-15 04:34:50 +00:00
for ( const property of response . result ) {
2020-05-07 10:54:55 +00:00
if ( ! property . enumerable ) continue ;
2019-01-15 04:34:50 +00:00
result . set ( property . name , createJSHandle ( this . _context , property . value ) ) ;
}
return result ;
}
2020-06-22 15:21:57 +00:00
/ * *
* Returns a JSON representation of the object .
*
* @remarks
*
* The JSON is generated by running { @link https : //developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify | JSON.stringify}
* on the object in page and consequent { @link https : //developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse | JSON.parse} in puppeteer.
2020-06-25 14:49:35 +00:00
* * * NOTE * * The method throws if the referenced object is not stringifiable .
2020-06-22 15:21:57 +00:00
* /
2020-10-12 09:30:35 +00:00
async jsonValue ( ) : Promise < Record < string , unknown > > {
2019-01-15 04:34:50 +00:00
if ( this . _remoteObject . objectId ) {
const response = await this . _client . send ( 'Runtime.callFunctionOn' , {
functionDeclaration : 'function() { return this; }' ,
objectId : this._remoteObject.objectId ,
returnByValue : true ,
awaitPromise : true ,
} ) ;
return helper . valueFromRemoteObject ( response . result ) ;
}
return helper . valueFromRemoteObject ( this . _remoteObject ) ;
}
2020-06-25 14:49:35 +00:00
/ * *
* Returns either ` null ` or the object handle itself , if the object handle is
* an instance of { @link ElementHandle } .
* /
2020-04-21 11:11:06 +00:00
asElement ( ) : ElementHandle | null {
2020-06-25 14:49:35 +00:00
// This always returns null, but subclasses can override this and return an
// ElementHandle.
2019-01-15 04:34:50 +00:00
return null ;
}
2020-06-22 15:21:57 +00:00
/ * *
2020-06-25 14:49:35 +00:00
* Stops referencing the element handle , and resolves when the object handle is
* successfully disposed of .
2020-06-22 15:21:57 +00:00
* /
2020-04-21 11:11:06 +00:00
async dispose ( ) : Promise < void > {
2020-05-07 10:54:55 +00:00
if ( this . _disposed ) return ;
2019-01-15 04:34:50 +00:00
this . _disposed = true ;
await helper . releaseObject ( this . _client , this . _remoteObject ) ;
}
2020-06-25 14:49:35 +00:00
/ * *
* Returns a string representation of the JSHandle .
*
* @remarks Useful during debugging .
* /
2020-04-21 11:11:06 +00:00
toString ( ) : string {
2019-01-15 04:34:50 +00:00
if ( this . _remoteObject . objectId ) {
2020-05-07 10:54:55 +00:00
const type = this . _remoteObject . subtype || this . _remoteObject . type ;
2019-01-15 04:34:50 +00:00
return 'JSHandle@' + type ;
}
return 'JSHandle:' + helper . valueFromRemoteObject ( this . _remoteObject ) ;
}
}
2020-06-22 15:21:57 +00:00
/ * *
* ElementHandle represents an in - page DOM element .
*
* @remarks
*
* ElementHandles can be created with the { @link Page . $ } method .
*
* ` ` ` js
* const puppeteer = require ( 'puppeteer' ) ;
*
* ( async ( ) = > {
* const browser = await puppeteer . launch ( ) ;
* const page = await browser . newPage ( ) ;
* await page . goto ( 'https://example.com' ) ;
* const hrefElement = await page . $ ( 'a' ) ;
* await hrefElement . click ( ) ;
* // ...
* } ) ( ) ;
* ` ` `
*
2020-06-25 14:49:35 +00:00
* ElementHandle prevents the DOM element from being garbage - collected unless the
* handle is { @link JSHandle . dispose | disposed } . ElementHandles are auto - disposed
* when their origin frame gets navigated .
2020-06-22 15:21:57 +00:00
*
* ElementHandle instances can be used as arguments in { @link Page . $eval } and
* { @link Page . evaluate } methods .
*
2020-07-02 09:09:34 +00:00
* If you ' re using TypeScript , ElementHandle takes a generic argument that
* denotes the type of element the handle is holding within . For example , if you
* have a handle to a ` <select> ` element , you can type it as
* ` ElementHandle<HTMLSelectElement> ` and you get some nicer type checks .
*
2020-06-22 15:21:57 +00:00
* @public
* /
2020-07-02 09:09:34 +00:00
export class ElementHandle <
ElementType extends Element = Element
> extends JSHandle {
2020-06-22 15:21:57 +00:00
private _page : Page ;
private _frameManager : FrameManager ;
/ * *
* @internal
* /
2020-05-07 10:54:55 +00:00
constructor (
context : ExecutionContext ,
client : CDPSession ,
remoteObject : Protocol.Runtime.RemoteObject ,
page : Page ,
frameManager : FrameManager
) {
2019-01-15 04:34:50 +00:00
super ( context , client , remoteObject ) ;
this . _client = client ;
this . _remoteObject = remoteObject ;
this . _page = page ;
this . _frameManager = frameManager ;
}
2020-07-02 09:09:34 +00:00
asElement ( ) : ElementHandle < ElementType > | null {
2019-01-15 04:34:50 +00:00
return this ;
}
2020-06-22 15:21:57 +00:00
/ * *
* Resolves to the content frame for element handles referencing
* iframe nodes , or null otherwise
* /
2020-04-29 11:28:16 +00:00
async contentFrame ( ) : Promise < Frame | null > {
2019-01-15 04:34:50 +00:00
const nodeInfo = await this . _client . send ( 'DOM.describeNode' , {
2020-05-07 10:54:55 +00:00
objectId : this._remoteObject.objectId ,
2019-01-15 04:34:50 +00:00
} ) ;
2020-05-07 10:54:55 +00:00
if ( typeof nodeInfo . node . frameId !== 'string' ) return null ;
2019-01-15 04:34:50 +00:00
return this . _frameManager . frame ( nodeInfo . node . frameId ) ;
}
2020-06-22 15:21:57 +00:00
private async _scrollIntoViewIfNeeded ( ) : Promise < void > {
2020-06-25 12:38:01 +00:00
const error = await this . evaluate <
(
2020-07-02 09:09:34 +00:00
element : Element ,
2020-06-25 12:38:01 +00:00
pageJavascriptEnabled : boolean
) = > Promise < string | false >
> ( async ( element , pageJavascriptEnabled ) = > {
if ( ! element . isConnected ) return 'Node is detached from document' ;
if ( element . nodeType !== Node . ELEMENT_NODE )
return 'Node is not of type HTMLElement' ;
// force-scroll if page's javascript is disabled.
if ( ! pageJavascriptEnabled ) {
element . scrollIntoView ( {
block : 'center' ,
inline : 'center' ,
2020-10-12 09:30:35 +00:00
// @ts-expect-error Chrome still supports behavior: instant but
// it's not in the spec so TS shouts We don't want to make this
// breaking change in Puppeteer yet so we'll ignore the line.
2020-06-25 12:38:01 +00:00
behavior : 'instant' ,
2019-01-15 04:34:50 +00:00
} ) ;
2020-05-07 10:54:55 +00:00
return false ;
2020-06-25 12:38:01 +00:00
}
const visibleRatio = await new Promise ( ( resolve ) = > {
const observer = new IntersectionObserver ( ( entries ) = > {
resolve ( entries [ 0 ] . intersectionRatio ) ;
observer . disconnect ( ) ;
} ) ;
observer . observe ( element ) ;
} ) ;
if ( visibleRatio !== 1.0 ) {
element . scrollIntoView ( {
block : 'center' ,
inline : 'center' ,
2020-10-12 09:30:35 +00:00
// @ts-expect-error Chrome still supports behavior: instant but
// it's not in the spec so TS shouts We don't want to make this
// breaking change in Puppeteer yet so we'll ignore the line.
2020-06-25 12:38:01 +00:00
behavior : 'instant' ,
} ) ;
}
return false ;
} , this . _page . isJavaScriptEnabled ( ) ) ;
2020-04-21 11:11:06 +00:00
2020-05-07 10:54:55 +00:00
if ( error ) throw new Error ( error ) ;
2019-01-15 04:34:50 +00:00
}
2020-06-22 15:21:57 +00:00
private async _clickablePoint ( ) : Promise < { x : number ; y : number } > {
2019-04-12 01:11:20 +00:00
const [ result , layoutMetrics ] = await Promise . all ( [
2020-05-07 10:54:55 +00:00
this . _client
. send ( 'DOM.getContentQuads' , {
objectId : this._remoteObject.objectId ,
} )
. catch ( debugError ) ,
2019-04-12 01:11:20 +00:00
this . _client . send ( 'Page.getLayoutMetrics' ) ,
] ) ;
2019-01-15 04:34:50 +00:00
if ( ! result || ! result . quads . length )
throw new Error ( 'Node is either not visible or not an HTMLElement' ) ;
// Filter out quads that have too small area to click into.
2020-05-07 10:54:55 +00:00
const { clientWidth , clientHeight } = layoutMetrics . layoutViewport ;
const quads = result . quads
. map ( ( quad ) = > this . _fromProtocolQuad ( quad ) )
. map ( ( quad ) = >
this . _intersectQuadWithViewport ( quad , clientWidth , clientHeight )
)
. filter ( ( quad ) = > computeQuadArea ( quad ) > 1 ) ;
2019-01-15 04:34:50 +00:00
if ( ! quads . length )
throw new Error ( 'Node is either not visible or not an HTMLElement' ) ;
// Return the middle point of the first quad.
const quad = quads [ 0 ] ;
let x = 0 ;
let y = 0 ;
for ( const point of quad ) {
x += point . x ;
y += point . y ;
}
return {
x : x / 4 ,
2020-05-07 10:54:55 +00:00
y : y / 4 ,
2019-01-15 04:34:50 +00:00
} ;
}
2020-07-10 10:51:52 +00:00
private _getBoxModel ( ) : Promise < void | Protocol.DOM.GetBoxModelResponse > {
const params : Protocol.DOM.GetBoxModelRequest = {
objectId : this._remoteObject.objectId ,
} ;
2020-05-07 10:54:55 +00:00
return this . _client
2020-07-10 10:51:52 +00:00
. send ( 'DOM.getBoxModel' , params )
2020-05-07 10:54:55 +00:00
. catch ( ( error ) = > debugError ( error ) ) ;
2019-01-15 04:34:50 +00:00
}
2020-06-22 15:21:57 +00:00
private _fromProtocolQuad ( quad : number [ ] ) : Array < { x : number ; y : number } > {
2019-01-15 04:34:50 +00:00
return [
2020-05-07 10:54:55 +00:00
{ x : quad [ 0 ] , y : quad [ 1 ] } ,
{ x : quad [ 2 ] , y : quad [ 3 ] } ,
{ x : quad [ 4 ] , y : quad [ 5 ] } ,
{ x : quad [ 6 ] , y : quad [ 7 ] } ,
2019-01-15 04:34:50 +00:00
] ;
}
2020-06-22 15:21:57 +00:00
private _intersectQuadWithViewport (
2020-05-07 10:54:55 +00:00
quad : Array < { x : number ; y : number } > ,
width : number ,
height : number
) : Array < { x : number ; y : number } > {
return quad . map ( ( point ) = > ( {
2019-04-12 01:11:20 +00:00
x : Math.min ( Math . max ( point . x , 0 ) , width ) ,
y : Math.min ( Math . max ( point . y , 0 ) , height ) ,
} ) ) ;
}
2020-06-22 15:21:57 +00:00
/ * *
* This method scrolls element into view if needed , and then
* uses { @link Page . mouse } to hover over the center of the element .
* If the element is detached from DOM , the method throws an error .
* /
2020-04-21 11:11:06 +00:00
async hover ( ) : Promise < void > {
2019-01-15 04:34:50 +00:00
await this . _scrollIntoViewIfNeeded ( ) ;
2020-05-07 10:54:55 +00:00
const { x , y } = await this . _clickablePoint ( ) ;
2019-01-15 04:34:50 +00:00
await this . _page . mouse . move ( x , y ) ;
}
2020-06-22 15:21:57 +00:00
/ * *
* This method scrolls element into view if needed , and then
* uses { @link Page . mouse } to click in the center of the element .
* If the element is detached from DOM , the method throws an error .
* /
2020-06-23 05:18:46 +00:00
async click ( options : ClickOptions = { } ) : Promise < void > {
2019-01-15 04:34:50 +00:00
await this . _scrollIntoViewIfNeeded ( ) ;
2020-05-07 10:54:55 +00:00
const { x , y } = await this . _clickablePoint ( ) ;
2019-01-15 04:34:50 +00:00
await this . _page . mouse . click ( x , y , options ) ;
}
2020-06-22 15:21:57 +00:00
/ * *
* Triggers a ` change ` and ` input ` event once all the provided options have been
* selected . If there ' s no ` <select> ` element matching ` selector ` , the method
* throws an error .
*
* @example
* ` ` ` js
* handle . select ( 'blue' ) ; // single selection
* handle . select ( 'red' , 'green' , 'blue' ) ; // multiple selections
* ` ` `
* @param values - Values of options to select . If the ` <select> ` has the
* ` multiple ` attribute , all values are considered , otherwise only the first
* one is taken into account .
* /
2020-04-21 11:11:06 +00:00
async select ( . . . values : string [ ] ) : Promise < string [ ] > {
2019-09-04 22:19:34 +00:00
for ( const value of values )
2020-05-07 10:54:55 +00:00
assert (
helper . isString ( value ) ,
'Values must be strings. Found value "' +
value +
'" of type "' +
typeof value +
'"'
) ;
2020-04-21 11:11:06 +00:00
2020-07-02 09:09:34 +00:00
return this . evaluate <
( element : HTMLSelectElement , values : string [ ] ) = > string [ ]
> ( ( element , values ) = > {
2019-09-04 22:19:34 +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 ;
for ( const option of options ) {
option . selected = values . includes ( option . value ) ;
2020-05-07 10:54:55 +00:00
if ( option . selected && ! element . multiple ) break ;
2019-09-04 22:19:34 +00:00
}
2020-05-07 10:54:55 +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 ) ;
2019-09-04 22:19:34 +00:00
} , values ) ;
}
2020-06-22 15:21:57 +00:00
/ * *
* This method expects ` elementHandle ` to point to an
* { @link https : //developer.mozilla.org/en-US/docs/Web/HTML/Element/input | input element}.
* @param filePaths - Sets the value of the file input to these paths .
* If some of the ` filePaths ` are relative paths , then they are resolved
* relative to the { @link https : //nodejs.org/api/process.html#process_process_cwd | current working directory}
* /
2020-04-21 11:11:06 +00:00
async uploadFile ( . . . filePaths : string [ ] ) : Promise < void > {
2020-06-25 12:38:01 +00:00
const isMultiple = await this . evaluate <
( element : HTMLInputElement ) = > boolean
> ( ( element ) = > element . multiple ) ;
2020-05-07 10:54:55 +00:00
assert (
filePaths . length <= 1 || isMultiple ,
'Multiple file uploads only work with <input type=file multiple>'
) ;
2020-04-16 15:22:52 +00:00
2020-09-28 09:35:35 +00:00
if ( ! isNode ) {
throw new Error (
` JSHandle#uploadFile can only be used in Node environments. `
) ;
}
2020-04-16 15:22:52 +00:00
// This import is only needed for `uploadFile`, so keep it scoped here to avoid paying
// the cost unnecessarily.
2020-09-28 09:35:35 +00:00
const path = await import ( 'path' ) ;
2020-04-21 11:11:06 +00:00
// eslint-disable-next-line @typescript-eslint/no-var-requires
2020-09-28 09:35:35 +00:00
const fs = await import ( 'fs' ) ;
2020-04-24 11:36:46 +00:00
// Locate all files and confirm that they exist.
2020-05-07 10:54:55 +00:00
const files = await Promise . all (
filePaths . map ( async ( filePath ) = > {
const resolvedPath : string = path . resolve ( filePath ) ;
try {
2020-09-28 09:35:35 +00:00
await fs . promises . access ( resolvedPath , fs . constants . R_OK ) ;
2020-05-07 10:54:55 +00:00
} catch ( error ) {
if ( error . code === 'ENOENT' )
throw new Error ( ` ${ filePath } does not exist or is not readable ` ) ;
}
return resolvedPath ;
} )
) ;
const { objectId } = this . _remoteObject ;
const { node } = await this . _client . send ( 'DOM.describeNode' , { objectId } ) ;
const { backendNodeId } = node ;
2020-04-16 15:22:52 +00:00
// The zero-length array is a special case, it seems that DOM.setFileInputFiles does
// not actually update the files in that case, so the solution is to eval the element
// value to a new FileList directly.
if ( files . length === 0 ) {
2020-07-02 09:09:34 +00:00
await this . evaluate < ( element : HTMLInputElement ) = > void > ( ( element ) = > {
2020-04-16 15:22:52 +00:00
element . files = new DataTransfer ( ) . files ;
// Dispatch events for this case because it should behave akin to a user action.
2020-05-07 10:54:55 +00:00
element . dispatchEvent ( new Event ( 'input' , { bubbles : true } ) ) ;
element . dispatchEvent ( new Event ( 'change' , { bubbles : true } ) ) ;
2020-04-16 15:22:52 +00:00
} ) ;
} else {
2020-05-07 10:54:55 +00:00
await this . _client . send ( 'DOM.setFileInputFiles' , {
objectId ,
files ,
backendNodeId ,
} ) ;
2019-12-03 08:18:18 +00:00
}
2019-01-15 04:34:50 +00:00
}
2020-06-22 15:21:57 +00:00
/ * *
* This method scrolls element into view if needed , and then uses
* { @link Touchscreen . tap } to tap in the center of the element .
* If the element is detached from DOM , the method throws an error .
* /
2020-04-21 11:11:06 +00:00
async tap ( ) : Promise < void > {
2019-01-15 04:34:50 +00:00
await this . _scrollIntoViewIfNeeded ( ) ;
2020-05-07 10:54:55 +00:00
const { x , y } = await this . _clickablePoint ( ) ;
2019-01-15 04:34:50 +00:00
await this . _page . touchscreen . tap ( x , y ) ;
}
2020-06-22 15:21:57 +00:00
/ * *
* Calls { @link https : //developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus | focus} on the element.
* /
2020-04-21 11:11:06 +00:00
async focus ( ) : Promise < void > {
2020-07-10 10:52:13 +00:00
await this . evaluate < ( element : HTMLElement ) = > void > ( ( element ) = >
element . focus ( )
) ;
2019-01-15 04:34:50 +00:00
}
2020-06-22 15:21:57 +00:00
/ * *
* Focuses the element , and then sends a ` keydown ` , ` keypress ` / ` input ` , and
* ` keyup ` event for each character in the text .
*
* To press a special key , like ` Control ` or ` ArrowDown ` ,
* use { @link ElementHandle . press } .
*
* @example
* ` ` ` js
* await elementHandle . type ( 'Hello' ) ; // Types instantly
* await elementHandle . type ( 'World' , { delay : 100 } ) ; // Types slower, like a user
* ` ` `
*
* @example
* An example of typing into a text field and then submitting the form :
*
* ` ` ` js
* const elementHandle = await page . $ ( 'input' ) ;
* await elementHandle . type ( 'some text' ) ;
* await elementHandle . press ( 'Enter' ) ;
* ` ` `
* /
2020-05-07 10:54:55 +00:00
async type ( text : string , options ? : { delay : number } ) : Promise < void > {
2019-01-15 04:34:50 +00:00
await this . focus ( ) ;
await this . _page . keyboard . type ( text , options ) ;
}
2020-06-22 15:21:57 +00:00
/ * *
* Focuses the element , and then uses { @link Keyboard . down } and { @link Keyboard . up } .
*
* @remarks
* If ` key ` is a single character and no modifier keys besides ` Shift `
* are being held down , a ` keypress ` / ` input ` event will also be generated .
* The ` text ` option can be specified to force an input event to be generated .
*
* * * NOTE * * Modifier keys DO affect ` elementHandle.press ` . Holding down ` Shift `
* will type the text in upper case .
*
* @param key - Name of key to press , such as ` ArrowLeft ` .
* See { @link KeyInput } for a list of all key names .
* /
async press ( key : KeyInput , options? : PressOptions ) : Promise < void > {
2019-01-15 04:34:50 +00:00
await this . focus ( ) ;
await this . _page . keyboard . press ( key , options ) ;
}
2020-06-22 15:21:57 +00:00
/ * *
* This method returns the bounding box of the element ( relative to the main frame ) ,
* or ` null ` if the element is not visible .
* /
async boundingBox ( ) : Promise < BoundingBox | null > {
2019-01-15 04:34:50 +00:00
const result = await this . _getBoxModel ( ) ;
2020-05-07 10:54:55 +00:00
if ( ! result ) return null ;
2019-01-15 04:34:50 +00:00
const quad = result . model . border ;
const x = Math . min ( quad [ 0 ] , quad [ 2 ] , quad [ 4 ] , quad [ 6 ] ) ;
const y = Math . min ( quad [ 1 ] , quad [ 3 ] , quad [ 5 ] , quad [ 7 ] ) ;
const width = Math . max ( quad [ 0 ] , quad [ 2 ] , quad [ 4 ] , quad [ 6 ] ) - x ;
const height = Math . max ( quad [ 1 ] , quad [ 3 ] , quad [ 5 ] , quad [ 7 ] ) - y ;
2020-05-07 10:54:55 +00:00
return { x , y , width , height } ;
2019-01-15 04:34:50 +00:00
}
/ * *
2020-06-22 15:21:57 +00:00
* This method returns boxes of the element , or ` null ` if the element is not visible .
*
* @remarks
*
* Boxes are represented as an array of points ;
* Each Point is an object ` {x, y} ` . Box points are sorted clock - wise .
2019-01-15 04:34:50 +00:00
* /
2020-04-21 11:11:06 +00:00
async boxModel ( ) : Promise < BoxModel | null > {
2019-01-15 04:34:50 +00:00
const result = await this . _getBoxModel ( ) ;
2020-05-07 10:54:55 +00:00
if ( ! result ) return null ;
2019-01-15 04:34:50 +00:00
2020-05-07 10:54:55 +00:00
const { content , padding , border , margin , width , height } = result . model ;
2019-01-15 04:34:50 +00:00
return {
content : this._fromProtocolQuad ( content ) ,
padding : this._fromProtocolQuad ( padding ) ,
border : this._fromProtocolQuad ( border ) ,
margin : this._fromProtocolQuad ( margin ) ,
width ,
2020-05-07 10:54:55 +00:00
height ,
2019-01-15 04:34:50 +00:00
} ;
}
2020-06-22 15:21:57 +00:00
/ * *
* This method scrolls element into view if needed , and then uses
* { @link Page . screenshot } to take a screenshot of the element .
* If the element is detached from DOM , the method throws an error .
* /
2020-05-07 13:49:42 +00:00
async screenshot ( options = { } ) : Promise < string | Buffer | void > {
2019-01-15 04:34:50 +00:00
let needsViewportReset = false ;
let boundingBox = await this . boundingBox ( ) ;
assert ( boundingBox , 'Node is either not visible or not an HTMLElement' ) ;
const viewport = this . _page . viewport ( ) ;
2020-05-07 10:54:55 +00:00
if (
viewport &&
( boundingBox . width > viewport . width ||
boundingBox . height > viewport . height )
) {
2019-01-15 04:34:50 +00:00
const newViewport = {
width : Math.max ( viewport . width , Math . ceil ( boundingBox . width ) ) ,
height : Math.max ( viewport . height , Math . ceil ( boundingBox . height ) ) ,
} ;
await this . _page . setViewport ( Object . assign ( { } , viewport , newViewport ) ) ;
needsViewportReset = true ;
}
await this . _scrollIntoViewIfNeeded ( ) ;
boundingBox = await this . boundingBox ( ) ;
assert ( boundingBox , 'Node is either not visible or not an HTMLElement' ) ;
assert ( boundingBox . width !== 0 , 'Node has 0 width.' ) ;
assert ( boundingBox . height !== 0 , 'Node has 0 height.' ) ;
2020-05-07 10:54:55 +00:00
const {
layoutViewport : { pageX , pageY } ,
} = await this . _client . send ( 'Page.getLayoutMetrics' ) ;
2019-01-15 04:34:50 +00:00
const clip = Object . assign ( { } , boundingBox ) ;
clip . x += pageX ;
clip . y += pageY ;
2020-05-07 10:54:55 +00:00
const imageData = await this . _page . screenshot (
Object . assign (
{ } ,
{
clip ,
} ,
options
)
) ;
2019-01-15 04:34:50 +00:00
2020-05-07 10:54:55 +00:00
if ( needsViewportReset ) await this . _page . setViewport ( viewport ) ;
2019-01-15 04:34:50 +00:00
return imageData ;
}
2020-06-22 15:21:57 +00:00
/ * *
2020-06-25 14:49:35 +00:00
* Runs ` element.querySelector ` within the page . If no element matches the selector ,
* the return value resolves to ` null ` .
2020-06-22 15:21:57 +00:00
* /
2020-04-21 11:11:06 +00:00
async $ ( selector : string ) : Promise < ElementHandle | null > {
2020-05-07 10:54:55 +00:00
const { updatedSelector , queryHandler } = getQueryHandlerAndSelector (
2020-07-17 05:29:42 +00:00
selector
2020-05-07 10:54:55 +00:00
) ;
2020-09-23 14:02:22 +00:00
return queryHandler . queryOne ( this , updatedSelector ) ;
2019-01-15 04:34:50 +00:00
}
2020-06-22 15:21:57 +00:00
/ * *
2020-06-25 14:49:35 +00:00
* Runs ` element.querySelectorAll ` within the page . If no elements match the selector ,
* the return value resolves to ` [] ` .
2020-06-22 15:21:57 +00:00
* /
2020-04-21 11:11:06 +00:00
async $ $ ( selector : string ) : Promise < ElementHandle [ ] > {
2020-05-07 10:54:55 +00:00
const { updatedSelector , queryHandler } = getQueryHandlerAndSelector (
2020-07-17 05:29:42 +00:00
selector
2020-05-07 10:54:55 +00:00
) ;
2020-09-23 14:02:22 +00:00
return queryHandler . queryAll ( this , updatedSelector ) ;
2019-01-15 04:34:50 +00:00
}
2020-06-22 15:21:57 +00:00
/ * *
* This method runs ` document.querySelector ` within the element and passes it as
* the first argument to ` pageFunction ` . If there ' s no element matching ` selector ` ,
* the method throws an error .
*
* If ` pageFunction ` returns a Promise , then ` frame. $ eval ` would wait for the promise
* to resolve and return its value .
*
* @example
* ` ` ` js
* const tweetHandle = await page . $ ( '.tweet' ) ;
* expect ( await tweetHandle . $eval ( '.like' , node = > node . innerText ) ) . toBe ( '100' ) ;
* expect ( await tweetHandle . $eval ( '.retweets' , node = > node . innerText ) ) . toBe ( '10' ) ;
* ` ` `
* /
2020-07-02 09:09:34 +00:00
async $eval < ReturnType > (
2020-05-07 10:54:55 +00:00
selector : string ,
2020-07-02 09:09:34 +00:00
pageFunction : (
element : Element ,
. . . args : unknown [ ]
) = > ReturnType | Promise < ReturnType > ,
2020-06-25 12:38:01 +00:00
. . . args : SerializableOrJSHandle [ ]
2020-07-02 09:09:34 +00:00
) : Promise < WrapElementHandle < ReturnType > > {
2019-01-15 04:34:50 +00:00
const elementHandle = await this . $ ( selector ) ;
if ( ! elementHandle )
2020-05-07 10:54:55 +00:00
throw new Error (
` Error: failed to find element matching selector " ${ selector } " `
) ;
2020-07-02 09:09:34 +00:00
const result = await elementHandle . evaluate <
(
element : Element ,
. . . args : SerializableOrJSHandle [ ]
) = > ReturnType | Promise < ReturnType >
> ( pageFunction , . . . args ) ;
2019-01-15 04:34:50 +00:00
await elementHandle . dispose ( ) ;
2020-07-02 09:09:34 +00:00
/ * *
2020-07-17 05:29:42 +00:00
* This ` as ` is a little unfortunate but helps TS understand the behavior of
* ` elementHandle.evaluate ` . If evaluate returns an element it will return an
2020-07-02 09:09:34 +00:00
* ElementHandle instance , rather than the plain object . All the
* WrapElementHandle type does is wrap ReturnType into
* ElementHandle < ReturnType > if it is an ElementHandle , or leave it alone as
* ReturnType if it isn ' t .
* /
return result as WrapElementHandle < ReturnType > ;
2019-01-15 04:34:50 +00:00
}
2020-06-22 15:21:57 +00:00
/ * *
* This method runs ` document.querySelectorAll ` within the element and passes it as
* the first argument to ` pageFunction ` . If there ' s no element matching ` selector ` ,
* the method throws an error .
*
* If ` pageFunction ` returns a Promise , then ` frame. $ $ eval ` would wait for the
* promise to resolve and return its value .
*
* @example
* ` ` ` html
* < div class = "feed" >
* < div class = "tweet" > Hello ! < / div >
* < div class = "tweet" > Hi ! < / div >
* < / div >
* ` ` `
*
* @example
* ` ` ` js
* const feedHandle = await page . $ ( '.feed' ) ;
* expect ( await feedHandle . $ $eval ( '.tweet' , nodes = > nodes . map ( n = > n . innerText ) ) )
* . toEqual ( [ 'Hello!' , 'Hi!' ] ) ;
* ` ` `
* /
2020-07-03 14:23:51 +00:00
async $ $eval < ReturnType > (
2020-05-07 10:54:55 +00:00
selector : string ,
2020-07-03 14:23:51 +00:00
pageFunction : (
elements : Element [ ] ,
. . . args : unknown [ ]
) = > ReturnType | Promise < ReturnType > ,
2020-06-25 12:38:01 +00:00
. . . args : SerializableOrJSHandle [ ]
2020-07-03 14:23:51 +00:00
) : Promise < WrapElementHandle < ReturnType > > {
2020-05-07 10:54:55 +00:00
const { updatedSelector , queryHandler } = getQueryHandlerAndSelector (
2020-07-17 05:29:42 +00:00
selector
2020-05-07 10:54:55 +00:00
) ;
2020-09-23 14:02:22 +00:00
const arrayHandle = await queryHandler . queryAllArray ( this , updatedSelector ) ;
2020-07-03 14:23:51 +00:00
const result = await arrayHandle . evaluate <
(
elements : Element [ ] ,
. . . args : unknown [ ]
) = > ReturnType | Promise < ReturnType >
> ( pageFunction , . . . args ) ;
2019-01-15 04:34:50 +00:00
await arrayHandle . dispose ( ) ;
2020-07-17 05:29:42 +00:00
/ * T h i s ` a s ` e x i s t s f o r t h e s a m e r e a s o n a s t h e ` a s ` i n $ e v a l a b o v e .
* See the comment there for a full explanation .
2020-07-03 14:23:51 +00:00
* /
return result as WrapElementHandle < ReturnType > ;
2019-01-15 04:34:50 +00:00
}
2020-06-22 15:21:57 +00:00
/ * *
* The method evaluates the XPath expression relative to the elementHandle .
* If there are no such elements , the method will resolve to an empty array .
* @param expression - Expression to { @link https : //developer.mozilla.org/en-US/docs/Web/API/Document/evaluate | evaluate}
* /
2020-04-21 11:11:06 +00:00
async $x ( expression : string ) : Promise < ElementHandle [ ] > {
2020-07-01 11:44:08 +00:00
const arrayHandle = await this . evaluateHandle (
( element : Document , expression : string ) = > {
const document = element . ownerDocument || element ;
const iterator = document . evaluate (
expression ,
element ,
null ,
XPathResult . ORDERED_NODE_ITERATOR_TYPE
) ;
const array = [ ] ;
let item ;
while ( ( item = iterator . iterateNext ( ) ) ) array . push ( item ) ;
return array ;
} ,
expression
) ;
2019-01-15 04:34:50 +00:00
const properties = await arrayHandle . getProperties ( ) ;
await arrayHandle . dispose ( ) ;
const result = [ ] ;
for ( const property of properties . values ( ) ) {
const elementHandle = property . asElement ( ) ;
2020-05-07 10:54:55 +00:00
if ( elementHandle ) result . push ( elementHandle ) ;
2019-01-15 04:34:50 +00:00
}
return result ;
}
2020-06-22 15:21:57 +00:00
/ * *
* Resolves to true if the element is visible in the current viewport .
* /
chore: migrate src/ExecutionContext (#5705)
* chore: migrate src/ExecutionContext to TypeScript
I spent a while trying to decide on the best course of action for
typing the `evaluate` function.
Ideally I wanted to use generics so that as a user you could type
something like:
```
handle.evaluate<HTMLElement, number, boolean>((node, x) => true, 5)
```
And have TypeScript know the arguments of `node` and `x` based on those
generics. But I hit two problems with that:
* you have to have n overloads of `evaluate` to cope for as many number
of arguments as you can be bothered too (e.g. we'd need an overload for
1 arg, 2 args, 3 args, etc)
* I decided it's actually confusing because you don't know as a user
what those generics actually map to.
So in the end I went with one generic which is the return type of the
function:
```
handle.evaluate<boolean>((node, x) => true, 5)
```
And `node` and `x` get typed as `any` which means you can tell TS
yourself:
```
handle.evaluate<boolean>((node: HTMLElement, x: number) => true, 5)
```
I'd like to find a way to force that the arguments after the function do
match the arguments you've given (in the above example, TS would moan if
I swapped that `5` for `"foo"`), but I tried a few things and to be
honest the complexity of the types wasn't worth it, I don't think.
I'm very open to tweaking these but I'd rather ship this and tweak going
forwards rather than spend hours now tweaking. Once we ship these
typedefs and get feedback from the community I'm sure we can improve
them.
2020-04-22 09:33:44 +00:00
async isIntersectingViewport ( ) : Promise < boolean > {
2020-06-25 12:38:01 +00:00
return await this . evaluate < ( element : Element ) = > Promise < boolean > > (
async ( element ) = > {
const visibleRatio = await new Promise ( ( resolve ) = > {
const observer = new IntersectionObserver ( ( entries ) = > {
resolve ( entries [ 0 ] . intersectionRatio ) ;
observer . disconnect ( ) ;
} ) ;
observer . observe ( element ) ;
2019-01-15 04:34:50 +00:00
} ) ;
2020-06-25 12:38:01 +00:00
return visibleRatio > 0 ;
}
) ;
2019-01-15 04:34:50 +00:00
}
}
2020-06-22 15:21:57 +00:00
/ * *
* @public
* /
export interface ClickOptions {
/ * *
* Time to wait between ` mousedown ` and ` mouseup ` in milliseconds .
*
* @defaultValue 0
* /
delay? : number ;
/ * *
* @defaultValue 'left'
* /
button ? : 'left' | 'right' | 'middle' ;
/ * *
* @defaultValue 1
* /
clickCount? : number ;
}
/ * *
* @public
* /
export interface PressOptions {
/ * *
* Time to wait between ` keydown ` and ` keyup ` in milliseconds . Defaults to 0 .
* /
delay? : number ;
/ * *
* If specified , generates an input event with this text .
* /
text? : string ;
}
2020-05-07 10:54:55 +00:00
function computeQuadArea ( quad : Array < { x : number ; y : number } > ) : number {
2019-01-15 04:34:50 +00:00
// Compute sum of all directed areas of adjacent triangles
// https://en.wikipedia.org/wiki/Polygon#Simple_polygons
let area = 0 ;
for ( let i = 0 ; i < quad . length ; ++ i ) {
const p1 = quad [ i ] ;
const p2 = quad [ ( i + 1 ) % quad . length ] ;
area += ( p1 . x * p2 . y - p2 . x * p1 . y ) / 2 ;
}
return Math . abs ( area ) ;
}