Skip to content
This repository was archived by the owner on Jul 29, 2024. It is now read-only.

Commit b7403bd

Browse files
committed
fix(ExpectedConditions): allow ExpectedConditions to handle missing elements
Expected conditions used `presenceOf` and `visibilityOf` to check that it's referencing elements which actually exist on the page, but there is a race condition with this strategy: an element could disappear after the `presenceOf`/`visibilityOf` check but before other checks, causing an error to be thrown. This PR handles this race condition in two ways: 1. `ElementFinder`'s `isEnabled`, `isDisplayed`, and `isSelected` functions now return false if no such element exists, rahter than throwing an error 2. `ExpectedConditions`'s `textToBePresent` and `textToBePresentInElementValue` now check for errors and also return false in those cases This is a general solution to the problem referenced in #3777 and #3958.
1 parent 6a4dc7a commit b7403bd

File tree

3 files changed

+59
-46
lines changed

3 files changed

+59
-46
lines changed

lib/element.ts

Lines changed: 36 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {ElementHelper, ProtractorBrowser} from './browser';
44
import {IError} from './exitCodes';
55
import {Locator} from './locators';
66
import {Logger} from './logger';
7+
import {falseIfMissing} from './util';
78

89
let clientSideScripts = require('./clientsidescripts');
910

@@ -17,6 +18,8 @@ let WEB_ELEMENT_FUNCTIONS = [
1718
'getLocation', 'isEnabled', 'isSelected', 'submit', 'clear', 'isDisplayed', 'getId', 'serialize',
1819
'takeScreenshot'
1920
] as (keyof WebdriverWebElement)[];
21+
let FALSE_IF_MISSING_WEB_ELEMENT_FUNCTIONS =
22+
['isEnabled', 'isSelected', 'isDisplayed'] as (keyof WebdriverWebElement)[];
2023

2124
/**
2225
* ElementArrayFinder is used for operations on an array of elements (as opposed
@@ -82,7 +85,8 @@ export class ElementArrayFinder extends WebdriverWebElement {
8285
constructor(
8386
public browser_: ProtractorBrowser,
8487
public getWebElements: () => wdpromise.Promise<WebElement[]> = null, public locator_?: any,
85-
public actionResults_: wdpromise.Promise<any> = null) {
88+
public actionResults_: wdpromise.Promise<any> = null,
89+
public falseIfMissing_: boolean = false) {
8690
super();
8791

8892
// TODO(juliemr): might it be easier to combine this with our docs and just
@@ -92,7 +96,8 @@ export class ElementArrayFinder extends WebdriverWebElement {
9296
let actionFn = (webElem: any) => {
9397
return webElem[fnName].apply(webElem, args);
9498
};
95-
return this.applyAction_(actionFn);
99+
return this.applyAction_(
100+
actionFn, FALSE_IF_MISSING_WEB_ELEMENT_FUNCTIONS.indexOf(fnName) !== -1);
96101
};
97102
});
98103
}
@@ -458,8 +463,9 @@ export class ElementArrayFinder extends WebdriverWebElement {
458463
* @private
459464
*/
460465
// map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];
461-
private applyAction_(actionFn: (value: WebElement, index: number, array: WebElement[]) => any):
462-
ElementArrayFinder {
466+
private applyAction_(
467+
actionFn: (value: WebElement, index: number, array: WebElement[]) => any,
468+
falseIfMissing?: boolean): ElementArrayFinder {
463469
let callerError = new Error();
464470
let actionResults = this.getWebElements()
465471
.then((arr: any) => wdpromise.all(arr.map(actionFn)))
@@ -474,7 +480,8 @@ export class ElementArrayFinder extends WebdriverWebElement {
474480
}
475481
throw noSuchErr;
476482
});
477-
return new ElementArrayFinder(this.browser_, this.getWebElements, this.locator_, actionResults);
483+
return new ElementArrayFinder(
484+
this.browser_, this.getWebElements, this.locator_, actionResults, falseIfMissing);
478485
}
479486

480487
/**
@@ -801,12 +808,22 @@ export class ElementFinder extends WebdriverWebElement {
801808
// Access the underlying actionResult of ElementFinder.
802809
this.then =
803810
(fn: (value: any) => any | wdpromise.IThenable<any>, errorFn?: (error: any) => any) => {
804-
return this.elementArrayFinder_.then((actionResults: any) => {
805-
if (!fn) {
806-
return actionResults[0];
807-
}
808-
return fn(actionResults[0]);
809-
}, errorFn);
811+
return this.elementArrayFinder_
812+
.then(
813+
null,
814+
(error) => {
815+
if (this.elementArrayFinder_.falseIfMissing_) {
816+
return falseIfMissing(error);
817+
} else {
818+
throw error;
819+
}
820+
})
821+
.then((actionResults: any) => {
822+
if (!fn) {
823+
return actionResults[0];
824+
}
825+
return fn(actionResults[0]);
826+
}, errorFn);
810827
};
811828
}
812829

@@ -1055,30 +1072,14 @@ export class ElementFinder extends WebdriverWebElement {
10551072
* the element is present on the page.
10561073
*/
10571074
isPresent(): wdpromise.Promise<boolean> {
1058-
return this.parentElementArrayFinder.getWebElements().then(
1059-
(arr: any[]) => {
1060-
if (arr.length === 0) {
1061-
return false;
1062-
}
1063-
return arr[0].isEnabled().then(
1064-
() => {
1065-
return true; // is present, whether it is enabled or not
1066-
},
1067-
(err: any) => {
1068-
if (err instanceof wderror.StaleElementReferenceError) {
1069-
return false;
1070-
} else {
1071-
throw err;
1072-
}
1073-
});
1074-
},
1075-
(err: Error) => {
1076-
if (err instanceof wderror.NoSuchElementError) {
1077-
return false;
1078-
} else {
1079-
throw err;
1080-
}
1081-
});
1075+
return this.parentElementArrayFinder.getWebElements().then((arr: any[]) => {
1076+
if (arr.length === 0) {
1077+
return false;
1078+
}
1079+
return arr[0].isEnabled().then(() => {
1080+
return true; // is present, whether it is enabled or not
1081+
});
1082+
}, falseIfMissing);
10821083
}
10831084

10841085
/**

lib/expectedConditions.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {error as wderror} from 'selenium-webdriver';
22
import {ProtractorBrowser} from './browser';
33
import {ElementFinder} from './element';
4+
import {falseIfMissing} from './util';
45

56
/**
67
* Represents a library of canned expected conditions that are useful for
@@ -210,7 +211,7 @@ export class ProtractorExpectedConditions {
210211
// MSEdge does not properly remove newlines, which causes false
211212
// negatives
212213
return actualText.replace(/\r?\n|\r/g, '').indexOf(text) > -1;
213-
});
214+
}, falseIfMissing);
214215
};
215216
return this.and(this.presenceOf(elementFinder), hasText);
216217
}
@@ -235,7 +236,7 @@ export class ProtractorExpectedConditions {
235236
let hasText = () => {
236237
return elementFinder.getAttribute('value').then((actualText: string): boolean => {
237238
return actualText.indexOf(text) > -1;
238-
});
239+
}, falseIfMissing);
239240
};
240241
return this.and(this.presenceOf(elementFinder), hasText);
241242
}
@@ -388,15 +389,7 @@ export class ProtractorExpectedConditions {
388389
* representing whether the element is visible.
389390
*/
390391
visibilityOf(elementFinder: ElementFinder): Function {
391-
return this.and(this.presenceOf(elementFinder), () => {
392-
return elementFinder.isDisplayed().then((displayed: boolean) => displayed, (err: any) => {
393-
if (err instanceof wderror.NoSuchElementError) {
394-
return false;
395-
} else {
396-
throw err;
397-
}
398-
});
399-
});
392+
return this.and(this.presenceOf(elementFinder), elementFinder.isDisplayed.bind(elementFinder));
400393
}
401394

402395
/**

lib/util.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {resolve} from 'path';
22
import {Promise, when} from 'q';
3+
import {error as wderror} from 'selenium-webdriver';
34

45
let STACK_SUBSTRINGS_TO_FILTER = [
56
'node_modules/jasmine/', 'node_modules/selenium-webdriver', 'at Module.', 'at Object.Module.',
@@ -75,3 +76,21 @@ export function joinTestLogs(log1: any, log2: any): any {
7576
specResults: (log1.specResults || []).concat(log2.specResults || [])
7677
};
7778
}
79+
80+
/**
81+
* Returns false if an error indicates a missing or stale element, re-throws
82+
* the error otherwise
83+
*
84+
* @param {*} The error to check
85+
* @throws {*} The error it was passed if it doesn't indicate a missing or stale
86+
* element
87+
* @return {boolean} false, if it doesn't re-throw the error
88+
*/
89+
export function falseIfMissing(error: any) {
90+
if ((error instanceof wderror.NoSuchElementError) ||
91+
(error instanceof wderror.StaleElementReferenceError)) {
92+
return false;
93+
} else {
94+
throw error;
95+
}
96+
}

0 commit comments

Comments
 (0)