import { ElementHandle as PElementHandle , Page , ChromiumBrowserContext , CDPSession , Frame , } from 'playwright' ; import { ElementHandle as IElementHandle , Locator , EvaluateFn , ScreenshotOptions , ClickOptions , } from './types' ; import { ErrorInterpreter , AnyErrorData , ActionErrorData , EmptyErrorData , interpretError , } from '../runtime/errors/Types' ; import { StructuredError } from '../utils/StructuredError' ; import { Key } from './Enums' ; import debugFactory from 'debug' ; import { Point } from './Point' ; import { CSSLocator } from './locators/index' ; const debug = debugFactory ( 'element:page:element-handle' ) ; internal async function getProperty < T > ( element , prop ) { if ( ! element ) return null ; const handle = await element . getProperty ( prop ) ; const value = ( await handle . jsonValue ( ) ) as T ; handle . dispose ( ) ; return value ; } internal function wrapDescriptiveError ( ... errorInterpreters ) { return function ( target , propertyKey , descriptor ) { const originalFn = descriptor . value ; descriptor . value = async function ( ... args ) { const calltimeError = new Error ( ) ; Error . captureStackTrace ( calltimeError ) ; const calltimeStack = calltimeError . stack ; try { return await originalFn . apply ( this , args ) ; } catch ( e ) { debug ( 'interpreting' , propertyKey , e ) ; const newError = interpretError < ElementHandle , AnyErrorData > ( errorInterpreters , e , this , propertyKey , args ) ; const sErr = StructuredError . liftWithSource ( newError , 'elementHandle' , `${ this . toErrorString ( ) }.${ propertyKey }` ) ; sErr . stack = calltimeStack ; throw sErr ; } } ; } ; } internal function domError ( err , target , key ) { if ( err . message . includes ( 'Node is detached from document' ) ) { return new StructuredError < ActionErrorData > ( 'dom error during action' , { _kind : 'action' , action : key , kind : 'node-detached' , } , err ) ; } } internal interface FilesystemAccessor { saveScreenshot ( fn ) : void ; testData ( filename ) : string ; } /**
 * ElementHandle represents a remote element in the DOM of the browser. It implements useful methods for querying and interacting with this DOM element.
 *
 * All methods on this class are asynchronous and must be used with `await` to wait for the result to fulfill from the browser.
 */ export class ElementHandle implements IElementHandle , Locator { internal private fs : FilesystemAccessor ; internal public errorString = '<element-handle>' ; internal public element : PElementHandle ; public page : Page ; private frame : Frame ; constructor ( elt , page , frame ? ) { this . element = elt ; this . page = page ; this . frame = frame || page . mainFrame ( ) ; } public async initErrorString ( foundVia ? ) { debug ( 'initErrorString' , foundVia ) ; let tag = await this . tagName ( ) ; const id = await this . getId ( ) ; if ( tag === null ) tag = 'element-tag' ; let estr = `<${ tag . toLowerCase ( ) }` ; if ( id !== null ) { estr += ` id='#${ id }'` ; } if ( foundVia !== null ) { estr += ` found using '${ foundVia }'` ; } estr += '>' ; this . errorString = estr ; return this ; } public bindBrowser ( filesystem ) { this . fs = filesystem ; } public toErrorString ( ) { return this . errorString ; } async find ( ) { return this ; } async findMany ( ) { return [ this ] ; } get pageFuncArgs ( ) : PElementHandle [ ] { return [ this . element ] ; } get pageFunc ( ) : EvaluateFn { return ( element ) => element ; } get pageFuncMany ( ) : EvaluateFn { return ( element ) => [ element ] ; } /**
     * Sends a click event to the element attached to this handle. If the element is
     * currently outside the viewport it will first scroll to that element.
     */ @ wrapDescriptiveError ( domError ) public async click ( options ? ) { return this . element . click ( options ) ; } /**
     * Sends a click event to the element attached to this handle. If the element is
     * currently outside the viewport it will first scroll to that element.
     */ @ wrapDescriptiveError ( domError ) public async doubleClick ( options ? ) { return this . element . click ( { clickCount : 2 , ... options } ) ; } /**
     * Schedules a command to clear the value of this element.
     * This command has no effect if the underlying DOM element is neither a text
     * INPUT, SELECT, or a TEXTAREA element.
     */ @ wrapDescriptiveError ( ) public async clear ( ) { const tagName = await this . tagName ( ) ; if ( tagName === 'SELECT' ) { await this . element . evaluate ( ( element ) => ( element . selectedIndex = - 1 ) , this . element ) ; } else if ( tagName === 'INPUT' ) { await this . element . evaluate ( ( element ) => ( element . value = '' ) , this . element ) ; } } /**
     * Sends focus to this element so that it receives keyboard inputs.
     */ @ wrapDescriptiveError ( ) public async focus ( ) { return await this . element . focus ( ) ; } /**
     * Clears focus from this element so that it will no longer receive keyboard inputs.
     */ @ wrapDescriptiveError ( ) public async blur ( ) { return await this . element . evaluate ( ( node ) => node . blur ( ) , this . element ) ; } /**
     * Sends a series of key modifiers to the element.
     */ @ wrapDescriptiveError ( ) public async sendKeys ( ... keys ) { const handle = this . element . asElement ( ) ; if ( ! handle ) return ; for ( const key of keys ) { if ( Object . values ( Key ) . includes ( key ) ) { await handle . press ( key ) ; } else { await handle . type ( key ) ; } } } /**
     * Sends a series of key presses to the element to simulate a user typing on the keyboard. Use this to fill in input fields.
     */ @ wrapDescriptiveError ( ) public async type ( text ) { await this . focus ( ) ; const handle = this . element . asElement ( ) ; if ( ! handle ) return ; return handle . type ( text ) ; } param names @ wrapDescriptiveError ( ) public async uploadFile ( ... names ) { return this . element . setInputFiles ( [ ... names . map ( ( name ) => this . fs . testData ( name ) ) ] ) ; } /**
     * Takes a screenshot of this element and saves it to the results folder with a random name.
     */ @ wrapDescriptiveError ( ) public async takeScreenshot ( options ? ) { return this . fs . saveScreenshot ( async ( path ) => { debug ( `Saving screenshot to: ${ path }` ) ; const handle = this . element . asElement ( ) ; if ( ! handle ) return false ; await handle . screenshot ( { path , ... options } ) ; return true ; } ) ; } public async findElement ( locator ) { if ( typeof locator === 'string' ) { const { BaseLocator } = await import ( './Locator' ) ; locator = new BaseLocator ( new CSSLocator ( locator ) , 'handle.findElement' ) ; } return locator . find ( this . page , this . element , this . frame ) ; } /**
     * Locates all elements using the supplied <[Locator]>, returning an array of <[ElementHandle]>'s
     */ public async findElements ( locator ) { if ( typeof locator === 'string' ) { const { BaseLocator } = await import ( './Locator' ) ; locator = new BaseLocator ( new CSSLocator ( locator ) , 'handle.findElements' ) ; } return locator . findMany ( this . page , this . element , this . frame ) ; } /**
     * Fetches the remote elements `tagName` property.
     */ public async tagName ( ) { return getProperty < string > ( this . element , 'tagName' ) ; } /**
     * Fetches the remote elements `id` attribute.
     */ public async getId ( ) { return this . getAttribute ( 'id' ) ; } /**
     * Fetches the value of an attribute on this element
     */ public async getAttribute ( key ) { const handle = this . element . asElement ( ) ; if ( ! handle ) return null ; return handle . evaluate ( ( element , key ) => element . getAttribute ( key ) , key ) ; } /**
     * getProperty
     */ public async getProperty ( key ) { return getProperty < string > ( this . element , key ) ; } /**
     * If the remote element is selectable (such as an `<option>` or `input[type="checkbox"]`) this methos will indicate whether it is selected.
     */ public async isSelected ( ) { if ( ! this . isSelectable ( ) ) { throw new Error ( 'Element is not selectable' ) ; } let propertyName = 'selected' ; const type = await this . getAttribute ( 'type' ) ; if ( 'checkbox' === type || 'radio' === type ) { propertyName = 'checked' ; } const value = await getProperty < string > ( this . element , propertyName ) ; return ! ! value ; } /**
     * Checks whether the remote element is selectable. An element is selectable if it is an `<option>` or `input[type="checkbox"]` or radio button.
     */ public async isSelectable ( ) { const tagName = await this . tagName ( ) ; if ( tagName === 'OPTION' ) { return true ; } if ( tagName === 'INPUT' ) { const type = await this . getAttribute ( 'type' ) ; return type === 'checkbox' || type === 'radio' ; } return false ; } /**
     * Checks whether the remote element is displayed in the DOM and is visible to the user without being hidden by CSS or occluded by another element.
     */ public async isDisplayed ( ) { const box = await this . element . boundingBox ( ) ; return box !== null ; } /**
     * Checks whether the remote element is enabled. Typically this means it does not have a `disabled` property or attribute applied.
     */ public async isEnabled ( ) { const disabled = await this . getAttribute ( 'disabled' ) ; return disabled === null ; } returns { Promise < string > } memberof public async text ( ) { return this . element . evaluate ( ( element ) => ( element . textContent ? element . textContent . trim ( ) : '' ) , this . element ) ; } /**
     * Returns a promise that will be resolved with the element's size
     * as a {width:number, height:number} object
     */ public async size ( ) { const box = await this . element . boundingBox ( ) ; if ( ! box ) return { width : 0 , height : 0 } ; const { width , height } = box ; return { width , height } ; } returns public async centerPoint ( ) { const box = await this . element . boundingBox ( ) ; if ( box == null ) return [ 0 , 0 ] ; const { x , y , height , width } = box ; const cx = Math . round ( x + width / 2 ) ; const cy = Math . round ( y + height / 2 ) ; return [ cx , cy ] ; } /**
     * Returns a promise that will be resolved to the element's location
     * as a {x:number, y:number} object.
     */ public async location ( ) { const box = await this . element . boundingBox ( ) ; if ( ! box ) return { x : 0 , y : 0 } ; const { x , y } = box ; return { x , y } ; } private async elementClient ( ) { try { const context = this . page . context ( ) as ChromiumBrowserContext ; return context . newCDPSession ( this . page ) ; } catch ( err ) { return null ; } } private get elementRemoteObject ( ) : any { return this . element [ '_objectId' ] ; } public async dispose ( ) { return this . element . dispose ( ) ; } public async highlight ( ) { const client = await this . elementClient ( ) ; if ( client ) { await client . send ( 'Overlay.highlightNode' , { highlightConfig : { showInfo : true , borderColor : { r : 76 , g : 175 , b : 80 , a : 1 } , contentColor : { r : 76 , g : 175 , b : 80 , a : 0.24 } , shapeColor : { r : 76 , g : 175 , b : 80 , a : 0.24 } , } , objectId : this . elementRemoteObject , } ) ; } } public async clearHighlights ( ) { const client = await this . elementClient ( ) ; if ( client ) { await client . send ( 'Overlay.hideHighlight' , { } ) ; } } public async drag ( to ) { const dataTransfer = await this . page . evaluateHandle ( ( ) => new DataTransfer ( ) ) ; await this . element . dispatchEvent ( 'dragstart' , { dataTransfer } ) ; await to . element . dispatchEvent ( 'drop' , { dataTransfer } ) ; } }