diff --git a/lib/element.ts b/lib/element.ts index 5dc5c7c06..f18587553 100644 --- a/lib/element.ts +++ b/lib/element.ts @@ -4,6 +4,7 @@ import {ElementHelper, ProtractorBrowser} from './browser'; import {IError} from './exitCodes'; import {Locator} from './locators'; import {Logger} from './logger'; +import {falseIfMissing} from './util'; let clientSideScripts = require('./clientsidescripts'); @@ -17,6 +18,8 @@ let WEB_ELEMENT_FUNCTIONS = [ 'getLocation', 'isEnabled', 'isSelected', 'submit', 'clear', 'isDisplayed', 'getId', 'serialize', 'takeScreenshot' ] as (keyof WebdriverWebElement)[]; +let FALSE_IF_MISSING_WEB_ELEMENT_FUNCTIONS = + ['isEnabled', 'isSelected', 'isDisplayed'] as (keyof WebdriverWebElement)[]; /** * ElementArrayFinder is used for operations on an array of elements (as opposed @@ -82,7 +85,8 @@ export class ElementArrayFinder extends WebdriverWebElement { constructor( public browser_: ProtractorBrowser, public getWebElements: () => wdpromise.Promise = null, public locator_?: any, - public actionResults_: wdpromise.Promise = null) { + public actionResults_: wdpromise.Promise = null, + public falseIfMissing_: boolean = false) { super(); // TODO(juliemr): might it be easier to combine this with our docs and just @@ -92,7 +96,8 @@ export class ElementArrayFinder extends WebdriverWebElement { let actionFn = (webElem: any) => { return webElem[fnName].apply(webElem, args); }; - return this.applyAction_(actionFn); + return this.applyAction_( + actionFn, FALSE_IF_MISSING_WEB_ELEMENT_FUNCTIONS.indexOf(fnName) !== -1); }; }); } @@ -458,8 +463,9 @@ export class ElementArrayFinder extends WebdriverWebElement { * @private */ // map(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]; - private applyAction_(actionFn: (value: WebElement, index: number, array: WebElement[]) => any): - ElementArrayFinder { + private applyAction_( + actionFn: (value: WebElement, index: number, array: WebElement[]) => any, + falseIfMissing?: boolean): ElementArrayFinder { let callerError = new Error(); let actionResults = this.getWebElements() .then((arr: any) => wdpromise.all(arr.map(actionFn))) @@ -474,7 +480,8 @@ export class ElementArrayFinder extends WebdriverWebElement { } throw noSuchErr; }); - return new ElementArrayFinder(this.browser_, this.getWebElements, this.locator_, actionResults); + return new ElementArrayFinder( + this.browser_, this.getWebElements, this.locator_, actionResults, falseIfMissing); } /** @@ -801,12 +808,22 @@ export class ElementFinder extends WebdriverWebElement { // Access the underlying actionResult of ElementFinder. this.then = (fn: (value: any) => any | wdpromise.IThenable, errorFn?: (error: any) => any) => { - return this.elementArrayFinder_.then((actionResults: any) => { - if (!fn) { - return actionResults[0]; - } - return fn(actionResults[0]); - }, errorFn); + return this.elementArrayFinder_ + .then( + null, + (error) => { + if (this.elementArrayFinder_.falseIfMissing_) { + return falseIfMissing(error); + } else { + throw error; + } + }) + .then((actionResults: any) => { + if (!fn) { + return actionResults[0]; + } + return fn(actionResults[0]); + }, errorFn); }; } @@ -1055,30 +1072,14 @@ export class ElementFinder extends WebdriverWebElement { * the element is present on the page. */ isPresent(): wdpromise.Promise { - return this.parentElementArrayFinder.getWebElements().then( - (arr: any[]) => { - if (arr.length === 0) { - return false; - } - return arr[0].isEnabled().then( - () => { - return true; // is present, whether it is enabled or not - }, - (err: any) => { - if (err instanceof wderror.StaleElementReferenceError) { - return false; - } else { - throw err; - } - }); - }, - (err: Error) => { - if (err instanceof wderror.NoSuchElementError) { - return false; - } else { - throw err; - } - }); + return this.parentElementArrayFinder.getWebElements().then((arr: any[]) => { + if (arr.length === 0) { + return false; + } + return arr[0].isEnabled().then(() => { + return true; // is present, whether it is enabled or not + }); + }, falseIfMissing); } /** diff --git a/lib/expectedConditions.ts b/lib/expectedConditions.ts index c92eaa3c6..4700ef05e 100644 --- a/lib/expectedConditions.ts +++ b/lib/expectedConditions.ts @@ -1,6 +1,7 @@ import {error as wderror} from 'selenium-webdriver'; import {ProtractorBrowser} from './browser'; import {ElementFinder} from './element'; +import {falseIfMissing} from './util'; /** * Represents a library of canned expected conditions that are useful for @@ -210,7 +211,7 @@ export class ProtractorExpectedConditions { // MSEdge does not properly remove newlines, which causes false // negatives return actualText.replace(/\r?\n|\r/g, '').indexOf(text) > -1; - }); + }, falseIfMissing); }; return this.and(this.presenceOf(elementFinder), hasText); } @@ -235,7 +236,7 @@ export class ProtractorExpectedConditions { let hasText = () => { return elementFinder.getAttribute('value').then((actualText: string): boolean => { return actualText.indexOf(text) > -1; - }); + }, falseIfMissing); }; return this.and(this.presenceOf(elementFinder), hasText); } @@ -388,15 +389,7 @@ export class ProtractorExpectedConditions { * representing whether the element is visible. */ visibilityOf(elementFinder: ElementFinder): Function { - return this.and(this.presenceOf(elementFinder), () => { - return elementFinder.isDisplayed().then((displayed: boolean) => displayed, (err: any) => { - if (err instanceof wderror.NoSuchElementError) { - return false; - } else { - throw err; - } - }); - }); + return this.and(this.presenceOf(elementFinder), elementFinder.isDisplayed.bind(elementFinder)); } /** diff --git a/lib/util.ts b/lib/util.ts index ec22c527c..c002bba36 100644 --- a/lib/util.ts +++ b/lib/util.ts @@ -1,5 +1,6 @@ import {resolve} from 'path'; import {Promise, when} from 'q'; +import {error as wderror} from 'selenium-webdriver'; let STACK_SUBSTRINGS_TO_FILTER = [ 'node_modules/jasmine/', 'node_modules/selenium-webdriver', 'at Module.', 'at Object.Module.', @@ -75,3 +76,21 @@ export function joinTestLogs(log1: any, log2: any): any { specResults: (log1.specResults || []).concat(log2.specResults || []) }; } + +/** + * Returns false if an error indicates a missing or stale element, re-throws + * the error otherwise + * + * @param {*} The error to check + * @throws {*} The error it was passed if it doesn't indicate a missing or stale + * element + * @return {boolean} false, if it doesn't re-throw the error + */ +export function falseIfMissing(error: any) { + if ((error instanceof wderror.NoSuchElementError) || + (error instanceof wderror.StaleElementReferenceError)) { + return false; + } else { + throw error; + } +}