import findDisplayed from './lib/findDisplayed' ; import * as fs from 'fs' ; import Locator , { Strategy , toW3cLocator } from './lib/Locator' ; import waitForDeleted from './lib/waitForDeleted' ; import { manualFindByLinkText , sleep } from './lib/util' ; import { Task , CancellablePromise } from '@theintern/common' ; import Session from './Session' ; import JSZip from 'jszip' ; import { basename } from 'path' ; /**
 * The Element class represents a DOM or UI element within the remote
 * environment.
 */ export default class Element extends Locator < CancellablePromise < Element > , CancellablePromise < Element [ ] > , CancellablePromise < void > > { private _elementId : string ; private _session : Session ; constructor param elementId param session constructor ( elementId , session ) { super ( ) ; this . _elementId = elementId . ELEMENT || elementId . elementId || elementId [ 'element-6066-11e4-a52e-4f735466cecf' ] || elementId ; this . _session = session ; } member readonly get elementId ( ) { return this . _elementId ; } readonly get session ( ) { return this . _session ; } private _get < T > ( path , requestData ? , pathParts ? ) { path = 'element/' + encodeURIComponent ( this . _elementId ) + '/' + path ; return this . _session . serverGet < T > ( path , requestData , pathParts ) ; } private _post < T > ( path , requestData ? , pathParts ? ) { path = 'element/' + encodeURIComponent ( this . _elementId ) + '/' + path ; return this . _session . serverPost < T > ( path , requestData , pathParts ) ; } toJSON ( ) { return { ELEMENT : this . _elementId , 'element-6066-11e4-a52e-4f735466cecf' : this . _elementId , } ; } param text returns private _normalizeWhitespace ( text ) { if ( text ) { text = text . replace ( /^\s+/gm , '' ) . replace ( /\s+$/gm , '' ) . replace ( /\s*\r\n\s*/g , '\n' ) . replace ( / +/g , ' ' ) ; } return text ; } /**
     * Uploads a file to a remote Selenium server for use when testing file
     * uploads. This API is not part of the WebDriver specification and should
     * not be used directly. To send a file to a server that supports file
     * uploads, use [[Element.Element.type]] to type the name of the local file
     * into a file input field and the file will be transparently transmitted
     * and used by the server.
     */ private _uploadFile ( filename ) { return new Task < string > ( ( resolve ) => { const content = fs . readFileSync ( filename ) ; let zip = new JSZip ( ) ; zip . file ( basename ( filename ) , content ) ; zip . generateAsync ( { type : 'base64' } ) . then ( ( file ) => { resolve ( this . session . serverPost ( 'file' , { file } ) ) ; } ) ; } ) ; } param using param value find ( using , value ) { const session = this . _session ; const capabilities = session . capabilities ; if ( capabilities . usesWebDriverLocators ) { const locator = toW3cLocator ( using , value ) ; using = locator . using ; value = locator . value ; } if ( using . indexOf ( 'link text' ) !== - 1 && ( capabilities . brokenWhitespaceNormalization || capabilities . brokenLinkTextLocator ) ) { return session . execute < ElementOrElementId > ( manualFindByLinkText , [ using , value , false , this , ] ) . then ( function ( element ) { if ( ! element ) { const error = new Error ( ) ; error . name = 'NoSuchElement' ; throw error ; } return new Element ( element , session ) ; } ) ; } return this . _post < ElementOrElementId > ( 'element' , { using , value , } ) . then ( function ( element ) { return new Element ( element , session ) ; } ) . catch ( function ( error ) { if ( error . name === 'UnknownCommand' && error . message . indexOf ( 'Unable to locate element:' ) !== - 1 ) { const newError = new Error ( ) ; newError . name = 'NoSuchElement' ; newError . message = error . message ; throw newError ; } throw error ; } ) ; } param using param value findAll ( using , value ) { const session = this . _session ; const capabilities = session . capabilities ; if ( capabilities . usesWebDriverLocators ) { const locator = toW3cLocator ( using , value ) ; using = locator . using ; value = locator . value ; } let task ; if ( using . indexOf ( 'link text' ) !== - 1 && ( capabilities . brokenWhitespaceNormalization || capabilities . brokenLinkTextLocator ) ) { task = session . execute < ElementOrElementId [ ] > ( manualFindByLinkText , [ using , value , true , this , ] ) ; } else { task = this . _post < ElementOrElementId [ ] > ( 'elements' , { using : using , value : value , } ) ; } return task . then ( function ( elements ) { return elements . map ( function ( element ) { return new Element ( element , session ) ; } ) ; } ) ; } /**
     * Clicks the element. This method works on both mouse and touch platforms.
     */ click ( ) { if ( this . session . capabilities . brokenClick ) { return ( this . session . moveMouseTo ( this ) . then ( ( ) => this . session . pressMouseButton ( 1 ) ) . then ( ( ) => this . session . releaseMouseButton ( 1 ) ) . then ( ( ) => this . session . execute < void > ( function ( element ) { element . click ( ) ; element . focus ( ) ; } , [ this ] ) ) ) ; } return this . _post < void > ( 'click' ) . then ( ( ) => { if ( this . session . capabilities . touchEnabled || this . session . capabilities . returnsFromClickImmediately ) { return sleep ( 500 ) ; } } ) ; } /**
     * Submits the element, if it is a form, or the form belonging to the
     * element, if it is a form element.
     */ submit ( ) { if ( this . session . capabilities . brokenSubmitElement ) { return this . session . execute < void > ( ( element ) => { if ( element . submit ) { element . submit ( ) ; } else if ( element . type === 'submit' && element . click ) { element . click ( ) ; } } , [ this ] ) ; } return this . _post < void > ( 'submit' ) ; } /**
     * Gets the visible text within the element. `<br>` elements are converted
     * to line breaks in the returned text, and whitespace is normalised per
     * the usual XML/HTML whitespace normalisation rules.
     */ getVisibleText ( ) { if ( this . session . capabilities . brokenVisibleText ) { return this . session . execute < string > ( ( element ) => { return element . innerText ; } , [ this ] ) ; } const result = this . _get < string > ( 'text' ) ; if ( this . session . capabilities . brokenWhitespaceNormalization ) { return result . then ( ( text ) => this . _normalizeWhitespace ( text ) ) ; } return result ; } param value type ( value ) { const getPostData = ( value ) : { value ? : string [ ] ; text ? : string ; } => { if ( this . session . capabilities . usesWebDriverElementValue ) { return { text : value . join ( '' ) } ; } else if ( this . session . capabilities . usesFlatKeysArray ) { return { value : value . join ( '' ) . split ( '' ) } ; } else { return { value } ; } } ; const handleError = ( reason ) => { if ( reason . detail . error === 'invalid argument' && ! this . session . capabilities . usesWebDriverElementValue ) { this . session . capabilities . usesWebDriverElementValue = true ; return this . type ( value ) ; } throw reason ; } ; if ( ! Array . isArray ( value ) ) { value = [ value ] ; } if ( this . session . capabilities . remoteFiles ) { const filename = value . join ( '' ) ; try { if ( fs . statSync ( filename ) . isFile ( ) ) { return this . _uploadFile ( filename ) . then ( ( uploadedFilename ) => { return this . _post ( 'value' , getPostData ( [ uploadedFilename ] ) ) . then ( noop ) . catch ( handleError ) ; } ) ; } } catch ( error ) { } } return this . _post ( 'value' , getPostData ( value ) ) . then ( noop ) . catch ( handleError ) ; } /**
     * Gets the tag name of the element. For HTML documents, the value is
     * always lowercase.
     */ getTagName ( ) { return this . _get < string > ( 'name' ) . then ( ( name ) => { if ( this . session . capabilities . brokenHtmlTagName ) { return this . session . execute < boolean > ( 'return document.body && document.body.tagName === document.body.tagName.toUpperCase();' ) . then ( function ( isHtml ) { return isHtml ? name . toLowerCase ( ) : name ; } ) ; } return name ; } ) ; } /**
     * Clears the value of a form element.
     */ clearValue ( ) { return this . _post ( 'clear' ) . then ( noop ) ; } /**
     * Returns whether or not a form element is currently selected (for
     * drop-down options and radio buttons), or whether or not the element is
     * currently checked (for checkboxes).
     */ isSelected ( ) { return this . _get < boolean > ( 'selected' ) ; } /**
     * Returns whether or not a form element can be interacted with.
     */ isEnabled ( ) { if ( this . session . capabilities . brokenElementEnabled ) { return this . session . execute < boolean > ( function ( element ) { return ! Boolean ( element . hasAttribute ( 'disabled' ) ) ; } , [ this ] ) ; } return this . _get < boolean > ( 'enabled' ) ; } param name returns getSpecAttribute ( name ) { return this . _get < string | undefined > ( 'attribute/$0' , null , [ name ] ) . then ( ( value ) => { if ( this . session . capabilities . brokenNullGetSpecAttribute && ( value === '' || value === undefined ) ) { return this . session . execute < boolean > ( function ( element , name ) { return element . hasAttribute ( name ) ; } , [ this , name ] ) . then ( function ( hasAttribute ) { return hasAttribute ? < string > value : null ; } ) ; } return value || null ; } ) . then ( function ( value ) { if ( typeof value === 'boolean' ) { value = value ? 'true' : null ; } return value ; } ) ; } param name returns getAttribute ( name ) { if ( this . session . capabilities . usesWebDriverElementAttribute ) { return this . _get < string | null > ( 'attribute/$0' , null , [ name ] ) ; } return this . session . execute < string | null > ( 'return arguments[0].getAttribute(arguments[1]);' , [ this , name ] ) ; } param name returns getProperty < T = any > ( name ) { if ( this . session . capabilities . brokenElementProperty ) { return this . session . execute < T > ( 'return arguments[0][arguments[1]];' , [ this , name , ] ) ; } return this . _get < T > ( 'property/$0' , null , [ name ] ) . catch ( ( ) => { this . session . capabilities . brokenElementProperty = true ; return this . getProperty < T > ( name ) ; } ) ; } /**
     * Determines if this element is equal to another element.
     */ equals ( other ) { if ( this . session . capabilities . noElementEquals ) { return this . session . execute < boolean > ( 'return arguments[0] === arguments[1];' , [ this , other ] ) ; } const elementId = other . elementId || other ; return this . _get < boolean > ( 'equals/$0' , null , [ elementId ] ) . catch ( ( error ) => { if ( ! this . session . capabilities . noElementEquals && ( error . name === 'UnknownCommand' || error . name === 'UnknownError' ) ) { this . session . capabilities . noElementEquals = true ; return this . equals ( other ) ; } throw error ; } ) ; } /**
     * Returns whether or not the element would be visible to an actual user.
     * This means that the following types of elements are considered to be not
     * displayed:
     *
     * 1. Elements with `display: none`
     * 2. Elements with `visibility: hidden`
     * 3. Elements positioned outside of the viewport that cannot be scrolled
     *    into view
     * 4. Elements with `opacity: 0`
     * 5. Elements with no `offsetWidth` or `offsetHeight`
     */ isDisplayed ( ) { return this . _get < boolean > ( 'displayed' ) . then ( ( isDisplayed ) => { if ( isDisplayed && ( this . session . capabilities . brokenElementDisplayedOpacity || this . session . capabilities . brokenElementDisplayedOffscreen ) ) { return this . session . execute < boolean > ( ( element ) => { const scrollX = document . documentElement ! . scrollLeft || document . body . scrollLeft ; const scrollY = document . documentElement ! . scrollTop || document . body . scrollTop ; do { if ( window . getComputedStyle ( element ) . opacity === '0' ) { return false ; } const bbox = element . getBoundingClientRect ( ) ; if ( bbox . right + scrollX <= 0 || bbox . bottom + scrollY <= 0 ) { return false ; } } while ( ( element = < HTMLElement > element . parentNode ) && element . nodeType === 1 ) ; return true ; } , [ this ] ) ; } return isDisplayed ; } ) ; } /**
     * Gets the position of the element relative to the top-left corner of the
     * document, taking into account scrolling and CSS transformations (if they
     * are supported).
     */ getPosition ( ) { if ( this . session . capabilities . brokenElementPosition ) { return this . session . execute < { x : number ; y : number ; } > ( function ( element ) { const bbox = element . getBoundingClientRect ( ) ; const scrollX = document . documentElement ! . scrollLeft || document . body . scrollLeft ; const scrollY = document . documentElement ! . scrollTop || document . body . scrollTop ; return { x : scrollX + bbox . left , y : scrollY + bbox . top } ; } , [ this ] ) ; } return this . _get < { x : number ; y : number ; } > ( 'location' ) . then ( function ( { x , y , } ) { return { x , y } ; } ) ; } /**
     * Gets the size of the element, taking into account CSS transformations
     * (if they are supported).
     */ getSize ( ) { const getUsingExecute = ( ) => { return this . session . execute < { width : number ; height : number ; } > ( function ( element ) { const bbox = element . getBoundingClientRect ( ) ; return { width : bbox . right - bbox . left , height : bbox . bottom - bbox . top , } ; } , [ this ] ) ; } ; if ( this . session . capabilities . brokenCssTransformedSize ) { return getUsingExecute ( ) ; } return this . _get < { width : number ; height : number ; } > ( 'size' ) . catch ( function ( error ) { if ( error . name === 'UnknownCommand' ) { return getUsingExecute ( ) ; } throw error ; } ) . then ( function ( { width , height } ) { return { width , height } ; } ) ; } param propertyName getComputedStyle ( propertyName ) { const manualGetStyle = ( ) => { return this . session . execute < string > ( ( element , propertyName ) => { return ( < any > window . getComputedStyle ( element ) ) [ propertyName ] ; } , [ this , propertyName ] ) ; } ; let promise ; if ( this . session . capabilities . brokenComputedStyles ) { promise = manualGetStyle ( ) ; } else { promise = this . _get < string > ( 'css/$0' , null , [ propertyName ] ) . catch ( function ( error ) { if ( error . name === 'UnknownCommand' ) { return manualGetStyle ( ) ; } else if ( error . name === 'UnknownError' && error . message . indexOf ( 'failed to parse value' ) > - 1 ) { return '' ; } throw error ; } ) ; } return promise . then ( function ( value ) { if ( value ) { value = value . replace ( /(.*\b)rgb\((\d+,\s*\d+,\s*\d+)\)(.*)/g , function ( _ , prefix , rgb , suffix ) { return prefix + 'rgba(' + rgb + ', 1)' + suffix ; } ) ; } return value != null ? value : '' ; } ) ; } since param using param value findDisplayed ( using , value ) { return findDisplayed ( this . session , this , using , value ) ; } param using param value waitForDeleted ( strategy , value ) { return waitForDeleted ( this . session , this , strategy , value ) ; } } function noop ( ) { } export type ElementOrElementId = { ELEMENT : string ; } | { 'element-6066-11e4-a52e-4f735466cecf' : string ; } | Element | string ;