From 9bc1c53e40161521b0c125a810f86235c974f100 Mon Sep 17 00:00:00 2001 From: Hank Duan Date: Thu, 8 Jan 2015 10:50:54 -0800 Subject: [PATCH] feat(expectedConditions): add helper library for syncing with non-angular apps usage: ```javascript var EC = protractor.ExpectedConditions; var button = $('#xyz'); var isClickable = EC.elementToBeClickable(button); browser.get(URL); browser.wait(isClickable, 5000); //wait for an element to become clickable button.click(); ``` You can also customize the conditions: ```javascript var urlChanged = function() { return browser.getCurrentUrl().then(function(url) { return url != 'http://www.angularjs.org'; }); }; // condition to wait for url to change, title to contain 'foo', and $('abc') element to contain text 'bar' var condition = EC.and(urlChanged, EC.titleContains('foo'), EC.textToBePresentInElement($('abc'), 'bar')); $('navButton').click(); browser.wait(condition, 5000); //wait for condition to be true. // do other things ``` --- lib/expectedConditions.js | 293 +++++++++++++++++++++++++ lib/protractor.js | 6 + spec/basic/elements_spec.js | 8 +- spec/basic/expected_conditions_spec.js | 167 ++++++++++++++ testapp/form/form.html | 4 + 5 files changed, 474 insertions(+), 4 deletions(-) create mode 100644 lib/expectedConditions.js create mode 100644 spec/basic/expected_conditions_spec.js diff --git a/lib/expectedConditions.js b/lib/expectedConditions.js new file mode 100644 index 000000000..8f51e5801 --- /dev/null +++ b/lib/expectedConditions.js @@ -0,0 +1,293 @@ +var webdriver = require('selenium-webdriver'); + +/* globals browser */ + +/** + * Represents a library of canned expected conditions that are useful for + * protractor, especially when dealing with non-angular apps. + * + * Each condition returns a function that evaluates to a promise. You may mix + * multiple conditions using `and`, `or`, and/or `not`. You may also + * mix these conditions with any other conditions that you write. + * + * See https://selenium.googlecode.com/git/docs/api/java/org/openqa ... + * /selenium/support/ui/ExpectedConditions.html + * + * + * @example + * var EC = protractor.ExpectedConditions; + * var button = $('#xyz'); + * var isClickable = EC.elementToBeClickable(button); + * + * browser.get(URL); + * browser.wait(isClickable, 5000); //wait for an element to become clickable + * button.click(); + * + * // You can defined your own expected condition, which is a function that + * // takes no parameter and evaluates to a promise of a boolean. + * var urlChanged = function() { + * return browser.getCurrentUrl().then(function(url) { + * return url === 'http://www.angularjs.org'; + * }); + * }; + * + * // You can customize the conditions with EC.and, EC.or, and EC.not. + * // Here's a condition to wait for url to change, $('abc') element to contain + * // text 'bar', and button becomes clickable. + * var condition = EC.and(urlChanged, EC.textToBePresentInElement($('abc'), 'bar'), isClickable); + * browser.get(URL); + * browser.wait(condition, 5000); //wait for condition to be true. + * button.click(); + * + * @constructor + */ +var ExpectedConditions = function() {}; + +/** + * Negates the result of a promise. + * + * @param {!function} expectedCondition + * + * @return {!function} An expected condition that returns the negated value. + */ +ExpectedConditions.prototype.not = function(expectedCondition) { + return function() { + return expectedCondition.call().then(function(bool) { + return !bool; + }); + }; +}; + +/** + * Helper function that is equivalent to the logical_and if defaultRet==true, + * or logical_or if defaultRet==false + * + * @private + * @param {boolean} defaultRet + * @param {Array.} fns An array of expected conditions to chain. + * + * @return {!function} An expected condition that returns a promise which + * evaluates to the result of the logical chain. + */ +ExpectedConditions.prototype.logicalChain_ = function(defaultRet, fns) { + var self = this; + return function() { + if (fns.length === 0) { + return defaultRet; + } + var fn = fns[0]; + return fn().then(function(bool) { + if (bool === defaultRet) { + return self.logicalChain_(defaultRet, fns.slice(1))(); + } else { + return !defaultRet; + } + }); + }; +}; + +/** + * Chain a number of expected conditions using logical_and, short circuiting + * at the first expected condition that evaluates to false. + * + * @param {Array.} fns An array of expected conditions to 'and' together. + * + * @return {!function} An expected condition that returns a promise which + * evaluates to the result of the logical and. + */ +ExpectedConditions.prototype.and = function() { + var args = Array.prototype.slice.call(arguments); + return this.logicalChain_(true, args); +}; + +/** + * Chain a number of expected conditions using logical_or, short circuiting + * at the first expected condition that evaluates to true. + * + * @param {Array.} fns An array of expected conditions to 'or' together. + * + * @return {!function} An expected condition that returns a promise which + * evaluates to the result of the logical or. + */ +ExpectedConditions.prototype.or = function() { + var args = Array.prototype.slice.call(arguments); + return this.logicalChain_(false, args); +}; + +/** + * Expect an alert to be present. + * + * @return {!function} An expected condition that returns a promise + * representing whether an alert is present. + */ +ExpectedConditions.prototype.alertIsPresent = function() { + return function() { + return browser.switchTo().alert().then(function() { + return true; + }, function(err) { + if (err.code == webdriver.error.ErrorCode.NO_SUCH_ALERT) { + return false; + } else { + throw err; + } + }); + }; +}; + +/** + * An Expectation for checking an element is visible and enabled such that you + * can click it. + * + * @param {!ElementFinder} elementFinder The element to check + * + * @return {!function} An expected condition that returns a promise + * representing whether the element is clickable. + */ +ExpectedConditions.prototype.elementToBeClickable = function(elementFinder) { + return this.and( + this.visibilityOf(elementFinder), + elementFinder.isEnabled.bind(elementFinder)); +}; + +/** + * An expectation for checking if the given text is present in the + * element. Returns false if the elementFinder does not find an element. + * + * @param {!ElementFinder} elementFinder The element to check + * @param {!string} text The text to verify against + * + * @return {!function} An expected condition that returns a promise + * representing whether the text is present in the element. + */ +ExpectedConditions.prototype.textToBePresentInElement = function(elementFinder, text) { + var hasText = function() { + return elementFinder.getText().then(function(actualText) { + return actualText.indexOf(text) > -1; + }); + }; + return this.and(this.presenceOf(elementFinder), hasText); +}; + +/** + * An expectation for checking if the given text is present in the element’s + * value. Returns false if the elementFinder does not find an element. + * + * @param {!ElementFinder} elementFinder The element to check + * @param {!string} text The text to verify against + * + * @return {!function} An expected condition that returns a promise + * representing whether the text is present in the element's value. + */ +ExpectedConditions.prototype.textToBePresentInElementValue = function(elementFinder, text) { + var hasText = function() { + return elementFinder.getAttribute('value').then(function(actualText) { + return actualText.indexOf(text) > -1; + }); + }; + return this.and(this.presenceOf(elementFinder), hasText); +}; + +/** + * An expectation for checking that the title contains a case-sensitive + * substring. + * + * @param {!string} title The fragment of title expected + * + * @return {!function} An expected condition that returns a promise + * representing whether the title contains the string. + */ +ExpectedConditions.prototype.titleContains = function(title) { + return function() { + return browser.getTitle().then(function(actualTitle) { + return actualTitle.indexOf(title) > -1; + }); + }; +}; + +/** + * An expectation for checking the title of a page. + * + * @param {!string} title The expected title, which must be an exact match. + * + * @return {!function} An expected condition that returns a promise + * representing whether the title equals the string. + */ +ExpectedConditions.prototype.titleIs = function(title) { + return function() { + return browser.getTitle().then(function(actualTitle) { + return actualTitle === title; + }); + }; +}; + +/** + * An expectation for checking that an element is present on the DOM + * of a page. This does not necessarily mean that the element is visible. + * + * @param {!ElementFinder} elementFinder The element to check + * + * @return {!function} An expected condition that returns a promise + * representing whether the element is present. + */ +ExpectedConditions.prototype.presenceOf = function(elementFinder) { + return elementFinder.isPresent.bind(elementFinder); +}; + +/** + * An expectation for checking that an element is not attached to the DOM + * of a page. + * + * @param {!ElementFinder} elementFinder The element to check + * + * @return {!function} An expected condition that returns a promise + * representing whether the element is stale. + */ +ExpectedConditions.prototype.stalenessOf = function(elementFinder) { + return this.not(this.presenceOf(elementFinder)); +}; + +/** + * An expectation for checking that an element is present on the DOM of a + * page and visible. Visibility means that the element is not only displayed + * but also has a height and width that is greater than 0. + * + * @param {!ElementFinder} elementFinder The element to check + * + * @return {!function} An expected condition that returns a promise + * representing whether the element is visible. + */ +ExpectedConditions.prototype.visibilityOf = function(elementFinder) { + return this.and( + this.presenceOf(elementFinder), + elementFinder.isDisplayed.bind(elementFinder)); +}; + +/** + * An expectation for checking that an element is either invisible or not + * present on the DOM. + * + * @param {!ElementFinder} elementFinder The element to check + * + * @return {!function} An expected condition that returns a promise + * representing whether the element is invisible. + */ +ExpectedConditions.prototype.invisibilityOf = function(elementFinder) { + return this.not(this.visibilityOf(elementFinder)); +}; + +/** + * An expectation for checking the selection is selected. + * + * @param {!ElementFinder} elementFinder The element to check + * + * @return {!function} An expected condition that returns a promise + * representing whether the element is selected. + */ +ExpectedConditions.prototype.elementToBeSelected = function(elementFinder) { + return this.and( + this.presenceOf(elementFinder), + elementFinder.isSelected.bind(elementFinder)); +}; + +module.exports = ExpectedConditions; + diff --git a/lib/protractor.js b/lib/protractor.js index 33c5b93bd..1de58d9a2 100644 --- a/lib/protractor.js +++ b/lib/protractor.js @@ -7,6 +7,7 @@ var ElementFinder = require('./element').ElementFinder; var clientSideScripts = require('./clientsidescripts.js'); var ProtractorBy = require('./locators.js').ProtractorBy; +var ExpectedConditions = require('./expectedConditions.js'); /* global angular */ @@ -36,6 +37,11 @@ exports.ElementFinder = ElementFinder; */ exports.ElementArrayFinder = ElementArrayFinder; +/** + * @type {ExpectedConditions} + */ +exports.ExpectedConditions = new ExpectedConditions(); + /** * Mix a function from one object onto another. The function will still be * called in the context of the original object. diff --git a/spec/basic/elements_spec.js b/spec/basic/elements_spec.js index 572caf527..4f8706311 100644 --- a/spec/basic/elements_spec.js +++ b/spec/basic/elements_spec.js @@ -221,16 +221,16 @@ describe('ElementArrayFinder', function() { var checkboxesElms = $$('#checkboxes input'); browser.get('index.html'); - expect(checkboxesElms.isSelected()).toEqual([true, false, false]); + expect(checkboxesElms.isSelected()).toEqual([true, false, false, false]); checkboxesElms.click(); - expect(checkboxesElms.isSelected()).toEqual([false, true, true]); + expect(checkboxesElms.isSelected()).toEqual([false, true, true, true]); }); it('action should act on all elements selected by filter', function() { browser.get('index.html'); var multiElement = $$('#checkboxes input').filter(function(elem, index) { - return index == 1 || index == 2; + return index == 2 || index == 3; }); multiElement.click(); expect($('#letterlist').getText()).toEqual('wx'); @@ -240,7 +240,7 @@ describe('ElementArrayFinder', function() { browser.get('index.html'); var elem = $$('#checkboxes input').filter(function(elem, index) { - return index == 1 || index == 2; + return index == 2 || index == 3; }).last(); elem.click(); expect($('#letterlist').getText()).toEqual('x'); diff --git a/spec/basic/expected_conditions_spec.js b/spec/basic/expected_conditions_spec.js new file mode 100644 index 000000000..2ab8904ad --- /dev/null +++ b/spec/basic/expected_conditions_spec.js @@ -0,0 +1,167 @@ +var EC = protractor.ExpectedConditions; + +describe('expected conditions', function() { + beforeEach(function() { + browser.get('index.html#/form'); + }); + + it('should have alertIsPresent', function() { + var alertIsPresent = EC.alertIsPresent(); + expect(alertIsPresent.call()).toBe(false); + + var alertButton = $('#alertbutton'); + alertButton.click(); + expect(alertIsPresent.call()).toBe(true); + + browser.switchTo().alert().accept(); + }); + + it('should have presenceOf', function() { + var presenceOfInvalid = EC.presenceOf($('#INVALID')); + var presenceOfHideable = EC.presenceOf($('#shower')); + + expect(presenceOfInvalid.call()).toBe(false); + expect(presenceOfHideable.call()).toBe(true); + element(by.model('show')).click(); + expect(presenceOfHideable.call()).toBe(true); // Should be able to reuse. + }); + + it('should have stalenessOf', function() { + var stalenessOfInvalid = EC.stalenessOf($('#INVALID')); + var stalenessOfHideable = EC.stalenessOf($('#shower')); + + expect(stalenessOfInvalid.call()).toBe(true); + expect(stalenessOfHideable.call()).toBe(false); + element(by.model('show')).click(); + expect(stalenessOfHideable.call()).toBe(false); + }); + + it('should have visibilityOf', function() { + var visibilityOfInvalid = EC.visibilityOf($('#INVALID')); + var visibilityOfHideable = EC.visibilityOf($('#shower')); + + expect(visibilityOfInvalid.call()).toBe(false); + expect(visibilityOfHideable.call()).toBe(true); + element(by.model('show')).click(); + expect(visibilityOfHideable.call()).toBe(false); + }); + + it('should have invisibilityOf', function() { + var invisibilityOfInvalid = EC.invisibilityOf($('#INVALID')); + var invisibilityOfHideable = EC.invisibilityOf($('#shower')); + + expect(invisibilityOfInvalid.call()).toBe(true); + expect(invisibilityOfHideable.call()).toBe(false); + element(by.model('show')).click(); + expect(invisibilityOfHideable.call()).toBe(true); + }); + + it('should have titleContains', function() { + expect(EC.titleContains('My Angular').call()).toBe(true); + expect(EC.titleContains('My AngularJS App').call()).toBe(true); + }); + + it('should have titleIs', function() { + expect(EC.titleIs('My Angular').call()).toBe(false); + expect(EC.titleIs('My AngularJS App').call()).toBe(true); + }); + + it('should have elementToBeClickable', function() { + var invalidIsClickable = EC.elementToBeClickable($('#INVALID')); + var buttonIsClickable = EC.elementToBeClickable($('#disabledButton')); + + expect(invalidIsClickable.call()).toBe(false); + expect(buttonIsClickable.call()).toBe(true); + element(by.model('disabled')).click(); + expect(buttonIsClickable.call()).toBe(false); + }); + + it('should have textToBePresentInElement', function() { + var invalidHasText = EC.textToBePresentInElement($('#INVALID'), 'shouldnt throw'); + var hideableHasText = EC.textToBePresentInElement($('#shower'), 'Shown'); + + expect(invalidHasText.call()).toBe(false); + expect(hideableHasText.call()).toBe(true); + element(by.model('show')).click(); + expect(hideableHasText.call()).toBe(false); + }); + + it('should have textToBePresentInElementValue', function() { + var invalid = $('#INVALID'); + var about = element(by.model('aboutbox')); + + expect(EC.textToBePresentInElementValue(invalid, 'shouldnt throw').call()).toBe(false); + expect(EC.textToBePresentInElementValue(about, 'text box').call()).toBe(true); + }); + + it('should have elementToBeSelected', function() { + var checkbox = element(by.model('show')); + + expect(EC.elementToBeSelected(checkbox).call()).toBe(true); + checkbox.click(); + expect(EC.elementToBeSelected(checkbox).call()).toBe(false); + }); + + it('should have not', function() { + var presenceOfValidElement = EC.presenceOf($('#shower')); + expect(presenceOfValidElement.call()).toBe(true); + expect(EC.not(presenceOfValidElement).call()).toBe(false); + }); + + it('should have and', function() { + var presenceOfValidElement = EC.presenceOf($('#shower')); + var presenceOfInvalidElement = EC.presenceOf($('#INVALID')); + var validityOfTitle = EC.titleIs('My AngularJS App'); + + expect(EC.and(presenceOfValidElement, validityOfTitle).call()).toBe(true); + // support multiple conditions + expect(EC.and(presenceOfValidElement, + validityOfTitle, presenceOfInvalidElement).call()).toBe(false); + }); + + it('and should shortcircuit', function() { + var invalidElem = $('#INVALID'); + + var presenceOfInvalidElement = EC.presenceOf(invalidElem); + var isDisplayed = invalidElem.isDisplayed.bind(invalidElem); + + // check isDisplayed on invalid element + var condition = EC.and(presenceOfInvalidElement, isDisplayed); + + // Should short circuit after the first condition is false, and not throw error + expect(condition.call()).toBe(false); + }); + + it('should have or', function() { + var presenceOfValidElement = EC.presenceOf($('#shower')); + var presenceOfInvalidElement = EC.presenceOf($('#INVALID')); + var presenceOfInvalidElement2 = EC.presenceOf($('#INVALID2')); + + expect(EC.or(presenceOfInvalidElement, presenceOfInvalidElement2).call()).toBe(false); + // support multiple conditions + expect(EC.or(presenceOfInvalidElement, + presenceOfInvalidElement2, presenceOfValidElement).call()).toBe(true); + }); + + it('or should shortcircuit', function() { + var validElem = $('#shower'); + var invalidElem = $('#INVALID'); + + var presenceOfValidElement = EC.presenceOf(validElem); + var isDisplayed = invalidElem.isDisplayed.bind(invalidElem); + + // check isDisplayed on invalid element + var condition = EC.or(presenceOfValidElement, isDisplayed); + + // Should short circuit after the first condition is true, and not throw error + expect(condition.call()).toBe(true); + }); + + it('should be able to mix conditions', function() { + var valid = EC.presenceOf($('#shower')); + var invalid = EC.presenceOf($('#INVALID')); + + expect(EC.or(valid, EC.and(valid, invalid)).call()).toBe(true); + expect(EC.or(EC.not(valid), EC.and(valid, invalid)).call()).toBe(false); + }); +}); diff --git a/testapp/form/form.html b/testapp/form/form.html index 6e2d2382e..376c797a9 100644 --- a/testapp/form/form.html +++ b/testapp/form/form.html @@ -45,6 +45,10 @@

Selects

Checkboxes

Show? Shown!! + + Disable? + + W X