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

Commit 40bbeca

Browse files
tilmanschweitzerheathkit
authored andcommitted
fix(expectedConditions): Add tests and fix race conditions around visibility (#4006)
Add test cases to reproduce the missing element race conditions possible in expected condition methods `visibilityOf`, `textToBePresentInElement`, `textToBePresentInValue` and `elementToBeClickable`. Add error handler `falseIfMissing` to all expected conditions that depend on the presence of an element. Expected conditions check the presence of an element before other checks, but when an element is removed exactly in the moment after the `isPresent` and before `isDisplayed` in `visibilityOf` the condition used to fail. This solution does not handle missing elements in (`isEnable`, `isDisplayed`, `isSelected`) and focused only on expected conditions (see #3972) This problem was also referenced in #3578 and #3777
1 parent 3d98a16 commit 40bbeca

File tree

4 files changed

+105
-55
lines changed

4 files changed

+105
-55
lines changed

lib/element.ts

+9-24
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

@@ -1071,30 +1072,14 @@ export class ElementFinder extends WebdriverWebElement {
10711072
* the element is present on the page.
10721073
*/
10731074
isPresent(): wdpromise.Promise<boolean> {
1074-
return this.parentElementArrayFinder.getWebElements().then(
1075-
(arr: any[]) => {
1076-
if (arr.length === 0) {
1077-
return false;
1078-
}
1079-
return arr[0].isEnabled().then(
1080-
() => {
1081-
return true; // is present, whether it is enabled or not
1082-
},
1083-
(err: any) => {
1084-
if (err instanceof wderror.StaleElementReferenceError) {
1085-
return false;
1086-
} else {
1087-
throw err;
1088-
}
1089-
});
1090-
},
1091-
(err: Error) => {
1092-
if (err instanceof wderror.NoSuchElementError) {
1093-
return false;
1094-
} else {
1095-
throw err;
1096-
}
1097-
});
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+
}, falseIfMissing);
1082+
}, falseIfMissing);
10981083
}
10991084

11001085
/**

lib/expectedConditions.ts

+10-11
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, passBoolean} from './util';
45

56
/**
67
* Represents a library of canned expected conditions that are useful for
@@ -185,7 +186,9 @@ export class ProtractorExpectedConditions {
185186
* representing whether the element is clickable.
186187
*/
187188
elementToBeClickable(elementFinder: ElementFinder): Function {
188-
return this.and(this.visibilityOf(elementFinder), elementFinder.isEnabled.bind(elementFinder));
189+
return this.and(this.visibilityOf(elementFinder), () => {
190+
return elementFinder.isEnabled().then(passBoolean, falseIfMissing);
191+
});
189192
}
190193

191194
/**
@@ -210,7 +213,7 @@ export class ProtractorExpectedConditions {
210213
// MSEdge does not properly remove newlines, which causes false
211214
// negatives
212215
return actualText.replace(/\r?\n|\r/g, '').indexOf(text) > -1;
213-
});
216+
}, falseIfMissing);
214217
};
215218
return this.and(this.presenceOf(elementFinder), hasText);
216219
}
@@ -235,7 +238,7 @@ export class ProtractorExpectedConditions {
235238
let hasText = () => {
236239
return elementFinder.getAttribute('value').then((actualText: string): boolean => {
237240
return actualText.indexOf(text) > -1;
238-
});
241+
}, falseIfMissing);
239242
};
240243
return this.and(this.presenceOf(elementFinder), hasText);
241244
}
@@ -389,13 +392,7 @@ export class ProtractorExpectedConditions {
389392
*/
390393
visibilityOf(elementFinder: ElementFinder): Function {
391394
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-
});
395+
return elementFinder.isDisplayed().then(passBoolean, falseIfMissing);
399396
});
400397
}
401398

@@ -433,6 +430,8 @@ export class ProtractorExpectedConditions {
433430
* representing whether the element is selected.
434431
*/
435432
elementToBeSelected(elementFinder: ElementFinder): Function {
436-
return this.and(this.presenceOf(elementFinder), elementFinder.isSelected.bind(elementFinder));
433+
return this.and(this.presenceOf(elementFinder), () => {
434+
return elementFinder.isSelected().then(passBoolean, falseIfMissing);
435+
});
437436
}
438437
}

lib/util.ts

+29
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,31 @@ 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+
}
97+
98+
/**
99+
* Return a boolean given boolean value.
100+
*
101+
* @param {boolean} value
102+
* @returns {boolean} given value
103+
*/
104+
export function passBoolean(value: boolean) {
105+
return value;
106+
}

spec/basic/expected_conditions_spec.js

+57-20
Original file line numberDiff line numberDiff line change
@@ -45,26 +45,6 @@ describe('expected conditions', function() {
4545
expect(visibilityOfHideable.call()).toBe(false);
4646
});
4747

48-
it('should have visibilityOf (handling race conditions)', function() {
49-
var disabledButton = $('#disabledButton[disabled="disabled"]');
50-
51-
// toggle presence (of .ng-hide) between visibility evaluation to simulate race condition
52-
var originalIsDisplayedFn = disabledButton.isDisplayed;
53-
disabledButton.isDisplayed = function () {
54-
element(by.model('disabled')).click();
55-
return originalIsDisplayedFn.call(this);
56-
};
57-
58-
var visibilityOfDisabledButtonWithInterceptor = EC.visibilityOf(disabledButton);
59-
60-
element(by.model('disabled')).click();
61-
62-
expect(originalIsDisplayedFn.call(disabledButton)).toBe(true);
63-
expect(disabledButton.isPresent()).toBe(true);
64-
65-
expect(visibilityOfDisabledButtonWithInterceptor.call()).toBe(false);
66-
});
67-
6848
it('should have invisibilityOf', function() {
6949
var invisibilityOfInvalid = EC.invisibilityOf($('#INVALID'));
7050
var invisibilityOfHideable = EC.invisibilityOf($('#shower'));
@@ -215,4 +195,61 @@ describe('expected conditions', function() {
215195
browser2.switchTo().alert().accept();
216196
});
217197
});
198+
199+
describe('race condition handling', function () {
200+
201+
var disabledButton;
202+
203+
beforeEach(function () {
204+
disabledButton = $('#disabledButton[disabled="disabled"]');
205+
});
206+
207+
function enableButtonBeforeCallToUnmatchSelector(testElement, fnName) {
208+
var originalFn = testElement[fnName];
209+
210+
testElement[fnName] = function () {
211+
element(by.model('disabled')).click();
212+
return originalFn.apply(this, arguments);
213+
};
214+
215+
// save original fn with _ prefix
216+
testElement['_' + fnName] = originalFn;
217+
}
218+
219+
it('can deal with missing elements in visibilityOf', function() {
220+
enableButtonBeforeCallToUnmatchSelector(disabledButton, 'isDisplayed');
221+
222+
element(by.model('disabled')).click();
223+
224+
expect(disabledButton._isDisplayed()).toBe(true);
225+
expect(EC.visibilityOf(disabledButton).call()).toBe(false);
226+
});
227+
228+
it('can deal with missing elements in textToBePresentInElement', function() {
229+
enableButtonBeforeCallToUnmatchSelector(disabledButton, 'getText');
230+
231+
element(by.model('disabled')).click();
232+
233+
expect(disabledButton._getText()).toBe('Dummy');
234+
expect(EC.textToBePresentInElement(disabledButton, 'Dummy').call()).toBe(false);
235+
});
236+
237+
it('can deal with missing elements in textToBePresentInValue', function() {
238+
enableButtonBeforeCallToUnmatchSelector(disabledButton, 'getAttribute');
239+
240+
element(by.model('disabled')).click();
241+
242+
expect(disabledButton._getAttribute('value')).toBe('');
243+
expect(EC.textToBePresentInElementValue(disabledButton, '').call()).toBe(false);
244+
});
245+
246+
it('can deal with missing elements in elementToBeClickable', function() {
247+
enableButtonBeforeCallToUnmatchSelector(disabledButton, 'isEnabled');
248+
249+
element(by.model('disabled')).click();
250+
251+
expect(disabledButton._isEnabled()).toBe(false);
252+
expect(EC.elementToBeClickable(disabledButton).call()).toBe(false);
253+
});
254+
});
218255
});

0 commit comments

Comments
 (0)