From 3c0e727136ab3d397c1a9a2bb02692d0aeb9be40 Mon Sep 17 00:00:00 2001 From: Hank Duan Date: Wed, 4 Jun 2014 11:52:44 -0700 Subject: [PATCH] refactor(protractor): reorganize internal structure of elementFinder/webelement - Allow chaining of actions (i.e. element(By.x).clear().sendKeys('abc)) - first(), last(), and get(index) are not executed immediately, allowing them to be placed in page objects - Rework the way that elementFinder and wrappedWebElement is represented - Breaking changes: - element.all is chained differently Before: element(By.x).element.all(By.y) Now: element(By.x).all(By.y) However, using element.all without chaining did not change, i.e. element.all(By.x) - Changed the way for retrieving underlying webElements Before: element(By.x).find(), element(By.x).findElement(By.y), and element(By.x).findElements(By.y) Now: element(By.x).getWebElement(), element(By.x).element(By.y).getWebElement(), and element(By.x).element(By.y).getWebElements(), respectively - browser.findElement returns a raw WebElement so $, $$, and evaluate will no longer be available --- lib/protractor.js | 1169 +++++++++++++++++------------------ spec/basic/elements_spec.js | 176 ++---- 2 files changed, 589 insertions(+), 756 deletions(-) diff --git a/lib/protractor.js b/lib/protractor.js index d45b99c1e..563b77654 100644 --- a/lib/protractor.js +++ b/lib/protractor.js @@ -1,9 +1,8 @@ var url = require('url'); +var util = require('util'); var webdriver = require('selenium-webdriver'); var clientSideScripts = require('./clientsidescripts.js'); - - var ProtractorBy = require('./locators.js').ProtractorBy; var DEFER_LABEL = 'NG_DEFER_BOOTSTRAP!'; @@ -60,24 +59,286 @@ var mixin = function(to, from, fnName, setupFn) { * * @private * @param {Protractor} ptor - * @param {Array.=} opt_usingChain * @return {function(webdriver.Locator): ElementFinder} */ -var buildElementHelper = function(ptor, opt_usingChain) { - var usingChain = opt_usingChain || []; - var using = function() { - var base = ptor; - for (var i = 0; i < usingChain.length; ++i) { - base = base.findElement(usingChain[i]); +var buildElementHelper = function(ptor) { + + /** + * ElementArrayFinder is used for operations on an array of elements (as opposed + * to a single element). + * + * @alias element.all(locator) + * @view + * + * + * @example + * element.all(by.css('.items li')).then(function(items) { + * expect(items.length).toBe(3); + * expect(items[0].getText()).toBe('First'); + * }); + * + * @constructor + * @param {webdriver.Locator} locator An element locator. + * @param {ElementFinder=} opt_parentElementFinder The element finder previous to + * this. (i.e. opt_parentElementFinder.all(locator) => this) + * @return {ElementArrayFinder} + */ + var ElementArrayFinder = function(locator, opt_parentElementFinder) { + if (!locator) { + throw new Error('Locator cannot be empty'); } - return base; + this.locator_ = locator; + this.parentElementFinder_ = opt_parentElementFinder || null; }; /** - * The element function returns an Element Finder. Element Finders do - * not actually attempt to find the element until a method is called on them, - * which means they can be set up in helper files before the page is - * available. + * Returns the array of WebElements represented by this ElementArrayFinder. + * + * @alias element.all(locator).getWebElements() + * @return {Array.} + */ + ElementArrayFinder.prototype.getWebElements = function() { + if (this.parentElementFinder_) { + var parentWebElement = this.parentElementFinder_.getWebElement(); + if (this.locator_.findElementsOverride) { + return this.locator_.findElementsOverride(ptor.driver, parentWebElement); + } else { + return parentWebElement.findElements(this.locator_); + } + } else { + ptor.waitForAngular(); + if (this.locator_.findElementsOverride) { + return this.locator_.findElementsOverride(ptor.driver); + } else { + return ptor.driver.findElements(this.locator_); + } + } + }; + + /** + * Get an element found by the locator by index. The index starts at 0. + * This does not actually retrieve the underlying element. + * + * @alias element.all(locator).get(index) + * @view + *
    + *
  • First
  • + *
  • Second
  • + *
  • Third
  • + *
+ * + * @example + * var list = element.all(by.css('.items li')); + * expect(list.get(0).getText()).toBe('First'); + * expect(list.get(1).getText()).toBe('Second'); + * + * @param {number} index Element index. + * @return {ElementFinder} finder representing element at the given index. + */ + ElementArrayFinder.prototype.get = function(index) { + return new ElementFinder(this.locator_, this.parentElementFinder_, null, index); + }; + + /** + * Get the first matching element for the locator. This does not actually + * retrieve the underlying element. + * + * @alias element.all(locator).first() + * @view + *
    + *
  • First
  • + *
  • Second
  • + *
  • Third
  • + *
+ * + * @example + * var first = element.all(by.css('.items li')).first(); + * expect(first.getText()).toBe('First'); + * + * @return {ElementFinder} finder representing the first matching element + */ + ElementArrayFinder.prototype.first = function() { + return this.get(0); + }; + + /** + * Get the last matching element for the locator. This does not actually + * retrieve the underlying element. + * + * @alias element.all(locator).last() + * @view + *
    + *
  • First
  • + *
  • Second
  • + *
  • Third
  • + *
+ * + * @example + * var last = element.all(by.css('.items li')).last(); + * expect(last.getText()).toBe('Third'); + * + * @return {ElementFinder} finder representing the last matching element + */ + ElementArrayFinder.prototype.last = function() { + return this.get(-1); + }; + + /** + * Count the number of elements found by the locator. + * + * @alias element.all(locator).count() + * @view + *
    + *
  • First
  • + *
  • Second
  • + *
  • Third
  • + *
+ * + * @example + * var list = element.all(by.css('.items li')); + * expect(list.count()).toBe(3); + * + * @return {!webdriver.promise.Promise} A promise which resolves to the + * number of elements matching the locator. + */ + ElementArrayFinder.prototype.count = function() { + return this.getWebElements().then(function(arr) { + return arr.length; + }); + }; + + /** + * Represents the ElementArrayFinder as an array of ElementFinders. + * + * @return {Array.} Return a promise, which resolves to a list + * of ElementFinders specified by the locator. + */ + ElementArrayFinder.prototype.asElementFinders_ = function() { + var self = this; + return this.getWebElements().then(function(arr) { + var list = []; + arr.forEach(function(webElem, index) { + list.push(new ElementFinder(self.locator_, self.parentElementFinder_, null, index)); + }); + return list; + }); + }; + + /** + * Find the elements specified by the locator. The input function is passed + * to the resulting promise, which resolves to an array of ElementFinders. + * + * @alias element.all(locator).then(thenFunction) + * @view + *
    + *
  • First
  • + *
  • Second
  • + *
  • Third
  • + *
+ * + * @example + * element.all(by.css('.items li')).then(function(arr) { + * expect(arr.length).toEqual(3); + * }); + * + * @param {function(Array.)} fn + * + * @type {webdriver.promise.Promise} a promise which will resolve to + * an array of ElementFinders matching the locator. + */ + ElementArrayFinder.prototype.then = function(fn) { + return this.asElementFinders_().then(fn); + }; + + /** + * Calls the input function on each ElementFinder found by the locator. + * + * @alias element.all(locator).each(eachFunction) + * @view + *
    + *
  • First
  • + *
  • Second
  • + *
  • Third
  • + *
+ * + * @example + * element.all(by.css('.items li')).each(function(element) { + * // Will print First, Second, Third. + * element.getText().then(console.log); + * }); + * + * @param {function(ElementFinder)} fn Input function + */ + ElementArrayFinder.prototype.each = function(fn) { + return this.asElementFinders_().then(function(arr) { + arr.forEach(function(elementFinder) { + fn(elementFinder); + }); + }); + }; + + /** + * Apply a map function to each element found using the locator. The + * callback receives the ElementFinder as the first argument and the index as + * a second arg. + * + * @alias element.all(locator).map(mapFunction) + * @view + *
    + *
  • First
  • + *
  • Second
  • + *
  • Third
  • + *
+ * + * @example + * var items = element.all(by.css('.items li')).map(function(elm, index) { + * return { + * index: index, + * text: elm.getText(), + * class: elm.getAttribute('class') + * }; + * }); + * expect(items).toEqual([ + * {index: 0, text: 'First', class: 'one'}, + * {index: 1, text: 'Second', class: 'two'}, + * {index: 2, text: 'Third', class: 'three'} + * ]); + * + * @param {function(ElementFinder, number)} mapFn Map function that + * will be applied to each element. + * @return {!webdriver.promise.Promise} A promise that resolves to an array + * of values returned by the map function. + */ + ElementArrayFinder.prototype.map = function(mapFn) { + return this.asElementFinders_().then(function(arr) { + var list = []; + arr.forEach(function(elementFinder, index) { + var mapResult = mapFn(elementFinder, index); + // All nested arrays and objects will also be fully resolved. + webdriver.promise.fullyResolved(mapResult).then(function(resolved) { + list.push(resolved); + }); + }); + return list; + }); + }; + + /** + * The ElementFinder can be treated as a WebElement for most purposes, in + * particular, you may perform actions (i.e. click, getText) on them as you + * would a WebElement. ElementFinders extend Promise, and once an action + * is performed on an ElementFinder, the latest result from the chain can be + * accessed using then. Unlike a WebElement, an ElementFinder will wait for + * angular to settle before performing finds or actions. + * + * ElementFinder can be used to build a chain of locators that is used to find + * an element. An ElementFinder does not actually attempt to find the element + * until an action is called, which means they can be set up in helper files + * before the page is available. * * @alias element(locator) * @view @@ -99,398 +360,290 @@ var buildElementHelper = function(ptor, opt_usingChain) { * input.sendKeys('123'); * expect(input.getAttribute('value')).toBe('Foo123'); * + * @constructor * @param {webdriver.Locator} locator An element locator. + * @param {ElementFinder=} opt_parentElementFinder The element finder previous + * to this. (i.e. opt_parentElementFinder.element(locator) => this) + * @param {webdriver.promise.Promise} opt_actionResult The promise which + * will be retrieved with then. Resolves to the latest action result, + * or null if no action has been called. + * @param {number=} opt_index The index of the element to retrieve. null means + * retrieve the only element, while -1 means retrieve the last element * @return {ElementFinder} */ - var element = function(locator) { - var elementFinder = {}; - - var webElementFns = WEB_ELEMENT_FUNCTIONS.concat( - ['findElements', 'isElementPresent', 'evaluate', 'allowAnimations']); - webElementFns.forEach(function(fnName) { - elementFinder[fnName] = function() { - var callerError = new Error(); - var args = arguments; - - return using().findElement(locator).then(function(element) { - return element[fnName].apply(element, args).then(null, function(e) { - e.stack = e.stack + '\n' + callerError.stack; - throw e; - }); - }); - }; + var ElementFinder = function(locator, opt_parentElementFinder, opt_actionResult, opt_index) { + if (!locator) { + throw new Error ('Locator cannot be empty'); + } + this.locator_ = locator; + this.parentElementFinder_ = opt_parentElementFinder || null; + this.actionResult_ = opt_actionResult || webdriver.promise.fulfilled(null); + this.opt_index_ = opt_index; + + var self = this; + WEB_ELEMENT_FUNCTIONS.forEach(function(fnName) { + if(!self[fnName]) { + self[fnName] = function() { + var webElem = self.getWebElement(); + var actionResult = webElem[fnName].apply(webElem, arguments); + return new ElementFinder( + locator, opt_parentElementFinder, + actionResult, opt_index); + }; + } }); + }; + util.inherits(ElementFinder, webdriver.promise.Promise); - // This is a special case since it doesn't return a promise, instead it - // returns a WebElement. - elementFinder.findElement = function(subLocator) { - return using().findElement(locator).findElement(subLocator); - }; - - /** - * Returns the specified WebElement. Throws the WebDriver error if the - * element doesn't exist. - * - * @alias element(locator).find() - * @return {webdriver.WebElement} - */ - elementFinder.find = function() { - return using().findElement(locator); - }; - - /** - * Determine whether an element is present on the page. - * - * @alias element(locator).isPresent() - * - * @view - * {{person.name}} - * - * @example - * // Element exists. - * expect(element(by.binding('person.name')).isPresent()).toBe(true); - * - * // Element not present. - * expect(element(by.binding('notPresent')).isPresent()).toBe(false); - * - * @return {!webdriver.promise.Promise} A promise which resolves to a - * boolean. - */ - elementFinder.isPresent = function() { - return using().isElementPresent(locator); - }; - - /** - * Returns the originally specified locator. - * - * @return {webdriver.Locator} The element locator. - */ - elementFinder.locator = function() { - return locator; - }; - - /** - * Calls to element may be chained to find elements within a parent. - * - * @alias element(locator).element(locator) - * @view - *
- *
- * Child text - *
{{person.phone}}
- *
- *
- * - * @example - * // Chain 2 element calls. - * var child = element(by.css('.parent')). - * element(by.css('.child')); - * expect(child.getText()).toBe('Child text\n555-123-4567'); - * - * // Chain 3 element calls. - * var triple = element(by.css('.parent')). - * element(by.css('.child')). - * element(by.binding('person.phone')); - * expect(triple.getText()).toBe('555-123-4567'); - * - * @param {Protractor} ptor - * @param {Array.=} opt_usingChain - * @return {function(webdriver.Locator): ElementFinder} - */ - elementFinder.element = - buildElementHelper(ptor, usingChain.concat(locator)); - - /** - * Shortcut for chaining css element finders. - * - * @alias element(locator).$(cssSelector) - * @view - *
- *
- * Child text - *
{{person.phone}}
- *
- *
- * - * @example - * // Chain 2 element calls. - * var child = element(by.css('.parent')).$('.child'); - * expect(child.getText()).toBe('Child text\n555-123-4567'); - * - * // Chain 3 element calls. - * var triple = $('.parent').$('.child').$('.grandchild'); - * expect(triple.getText()).toBe('555-123-4567'); - * - * @param {string} cssSelector A css selector. - * @return {ElementFinder} - */ - elementFinder.$ = function(cssSelector) { - return buildElementHelper(ptor, usingChain.concat(locator))( - webdriver.By.css(cssSelector)); - }; - - elementFinder.$$ = function(cssSelector) { - return buildElementHelper(ptor, usingChain).all( - webdriver.By.css(cssSelector)); - }; + /** + * Calls to element may be chained to find elements within a parent. + * + * @alias element(locator).element(locator) + * @view + *
+ *
+ * Child text + *
{{person.phone}}
+ *
+ *
+ * + * @example + * // Chain 2 element calls. + * var child = element(by.css('.parent')). + * element(by.css('.child')); + * expect(child.getText()).toBe('Child text\n555-123-4567'); + * + * // Chain 3 element calls. + * var triple = element(by.css('.parent')). + * element(by.css('.child')). + * element(by.binding('person.phone')); + * expect(triple.getText()).toBe('555-123-4567'); + * + * @param {webdriver.Locator} subLocator + * @return {ElementFinder} + */ + ElementFinder.prototype.element = function(subLocator) { + return new ElementFinder(subLocator, this); + }; - return elementFinder; + /** + * Calls to element may be chained to find an array of elements within a parent. + * + * @alias element(locator).all(locator) + * @view + *
+ *
    + *
  • First
  • + *
  • Second
  • + *
  • Third
  • + *
+ *
+ * + * @example + * var items = element(by.css('.parent')).all(by.tagName('li')) + * + * @param {webdriver.Locator} subLocator + * @return {ElementArrayFinder} + */ + ElementFinder.prototype.all = function(subLocator) { + return new ElementArrayFinder(subLocator, this); }; /** - * element.all is used for operations on an array of elements (as opposed - * to a single element). + * Shortcut for querying the document directly with css. * - * @alias element.all(locator) + * @alias $(cssSelector) * @view - *
    - *
  • First
  • - *
  • Second
  • - *
  • Third
  • - *
+ *
+ * First + * Second + *
* * @example - * element.all(by.css('.items li')).then(function(items) { - * expect(items.length).toBe(3); - * expect(items[0].getText()).toBe('First'); - * }); + * var item = $('.count .two'); + * expect(item.getText()).toBe('Second'); * - * @param {webdriver.Locator} locator - * @return {ElementArrayFinder} + * @param {string} selector A css selector + * @return {ElementFinder} which identifies the located + * {@link webdriver.WebElement} */ - element.all = function(locator) { - var elementArrayFinder = {}; - - /** - * Count the number of elements found by the locator. - * - * @alias element.all(locator).count() - * @view - *
    - *
  • First
  • - *
  • Second
  • - *
  • Third
  • - *
- * - * @example - * var list = element.all(by.css('.items li')); - * expect(list.count()).toBe(3); - * - * @return {!webdriver.promise.Promise} A promise which resolves to the - * number of elements matching the locator. - */ - elementArrayFinder.count = function() { - return using().findElements(locator).then(function(arr) { - return arr.length; - }); - }; + ElementFinder.prototype.$ = function(selector) { + return new ElementFinder(webdriver.By.css(selector), this); + }; - /** - * Get an element found by the locator by index. The index starts at 0. - * - * @alias element.all(locator).get(index) - * @view - *
    - *
  • First
  • - *
  • Second
  • - *
  • Third
  • - *
- * - * @example - * var list = element.all(by.css('.items li')); - * expect(list.get(0).getText()).toBe('First'); - * expect(list.get(1).getText()).toBe('Second'); - * - * @param {number} index Element index. - * @return {webdriver.WebElement} The element at the given index - */ - elementArrayFinder.get = function(index) { - var id = using().findElements(locator).then(function(arr) { - return arr[index]; - }); - return ptor.wrapWebElement(new webdriver.WebElement(ptor.driver, id)); - }; + /** + * Shortcut for querying the document directly with css. + * + * @alias $$(cssSelector) + * @view + *
+ * First + * Second + *
+ * + * @example + * // The following protractor expressions are equivalent. + * var list = element.all(by.css('.count span')); + * expect(list.count()).toBe(2); + * + * list = $$('.count span'); + * expect(list.count()).toBe(2); + * expect(list.get(0).getText()).toBe('First'); + * expect(list.get(1).getText()).toBe('Second'); + * + * @param {string} selector a css selector + * @return {ElementArrayFinder} which identifies the + * array of the located {@link webdriver.WebElement}s. + */ + ElementFinder.prototype.$$ = function(selector) { + return new ElementArrayFinder(webdriver.By.css(selector), this); + }; - /** - * Get the first element found using the locator. - * - * @alias element.all(locator).first() - * @view - *
    - *
  • First
  • - *
  • Second
  • - *
  • Third
  • - *
- * - * @example - * var list = element.all(by.css('.items li')); - * expect(list.first().getText()).toBe('First'); - * - * @return {webdriver.WebElement} The first matching element - */ - elementArrayFinder.first = function() { - var id = using().findElements(locator).then(function(arr) { - if (!arr.length) { - throw new Error('No element found using locator: ' + locator.message); - } - return arr[0]; - }); - return ptor.wrapWebElement(new webdriver.WebElement(ptor.driver, id)); - }; + /** + * Determine whether the element is present on the page. + * + * @alias element(locator).isPresent() + * + * @view + * {{person.name}} + * + * @example + * // Element exists. + * expect(element(by.binding('person.name')).isPresent()).toBe(true); + * + * // Element not present. + * expect(element(by.binding('notPresent')).isPresent()).toBe(false); + * + * @return {ElementFinder} which resolves to whether + * the element is present on the page. + */ + ElementFinder.prototype.isPresent = function() { + var isPresent = new ElementArrayFinder( + this.locator_, this.parentElementFinder_).count().then(function(count) { + return !!count; + }); + return new ElementFinder( + this.locator_, this.parentElementFinder_, isPresent, this.opt_index_); + }; - /** - * Get the last matching element for the locator. - * - * @alias element.all(locator).last() - * @view - *
    - *
  • First
  • - *
  • Second
  • - *
  • Third
  • - *
- * - * @example - * var list = element.all(by.css('.items li')); - * expect(list.last().getText()).toBe('Third'); - * - * @return {webdriver.WebElement} the last matching element - */ - elementArrayFinder.last = function() { - var id = using().findElements(locator).then(function(arr) { - return arr[arr.length - 1]; - }); - return ptor.wrapWebElement(new webdriver.WebElement(ptor.driver, id)); - }; + /** + * Override for WebElement.prototype.isElementPresent so that protractor waits + * for Angular to settle before making the check. + * + * @see ElementFinder.isPresent + * @return {ElementFinder} which resolves to whether + * the element is present on the page. + */ + ElementFinder.prototype.isElementPresent = function(subLocator) { + return this.element(subLocator).isPresent(); + }; - /** - * Find the elements specified by the locator. The input function is passed - * to the resulting promise, which resolves to an array of WebElements. - * - * @alias element.all(locator).then(thenFunction) - * @view - *
    - *
  • First
  • - *
  • Second
  • - *
  • Third
  • - *
- * - * @example - * element.all(by.css('.items li')).then(function(arr) { - * expect(arr.length).toEqual(3); - * }); - * - * @param {function(Array.)} fn - * - * @type {webdriver.promise.Promise} a promise which will resolve to - * an array of WebElements matching the locator. - */ - elementArrayFinder.then = function(fn) { - return using().findElements(locator).then(fn); - }; + /** + * @return {webdriver.Locator} + */ + ElementFinder.prototype.locator = function() { + return this.locator_; + }; - /** - * Calls the input function on each WebElement found by the locator. - * - * @alias element.all(locator).each(eachFunction) - * @view - *
    - *
  • First
  • - *
  • Second
  • - *
  • Third
  • - *
- * - * @example - * element.all(by.css('.items li')).each(function(element) { - * // Will print First, Second, Third. - * element.getText().then(console.log); - * }); - * - * @param {function(webdriver.WebElement)} fn Input function - */ - elementArrayFinder.each = function(fn) { - using().findElements(locator).then(function(arr) { - arr.forEach(function(webElem) { - fn(webElem); - }); - }); - }; + /** + * Returns the WebElement represented by this ElementFinder. + * Throws the WebDriver error if the element doesn't exist. + * If index is null, it makes sure that there is only one underlying + * WebElement described by the chain of locators and issues a warning + * otherwise. If index is not null, it retrieves the WebElement specified by + * the index. + * + * @example + * The following three expressions are equivalent. + * - element(by.css('.parent')).getWebElement(); + * - browser.waitForAngular(); browser.driver.findElement(by.css('.parent')); + * - browser.findElement(by.css('.parent')) + * + * @alias element(locator).getWebElement() + * @return {webdriver.WebElement} + */ + ElementFinder.prototype.getWebElement = function() { + var self = this; + var webElementsPromise = new ElementArrayFinder( + this.locator_, this.parentElementFinder_).getWebElements(); + + var id = webElementsPromise.then(function(arr) { + if (!arr.length) { + throw new Error('No element found using locator: ' + self.locator_.message); + } + var index = self.opt_index_; + if (index == null) { + // index null means we make sure there is only one element + if (arr.length > 1) { + console.log('warning: more than one element found for locator ' + + self.locator_.message + '- you may need to be more specific'); + } + index = 0; + } else if (index === -1) { + // -1 is special and means last + index = arr.length - 1; + } + return arr[index]; + }); + return new webdriver.WebElement(ptor.driver, id); + }; - /** - * Apply a map function to each element found using the locator. The - * callback receives the web element as the first argument and the index as - * a second arg. - * - * @alias element.all(locator).map(mapFunction) - * @view - *
    - *
  • First
  • - *
  • Second
  • - *
  • Third
  • - *
- * - * @example - * var items = element.all(by.css('.items li')).map(function(elm, index) { - * return { - * index: index, - * text: elm.getText(), - * class: elm.getAttribute('class') - * }; - * }); - * expect(items).toEqual([ - * {index: 0, text: 'First', class: 'one'}, - * {index: 1, text: 'Second', class: 'two'}, - * {index: 2, text: 'Third', class: 'three'} - * ]); - * - * @param {function(webdriver.WebElement, number)} mapFn Map function that - * will be applied to each element. - * @return {!webdriver.promise.Promise} A promise that resolves to an array - * of values returned by the map function. - */ - elementArrayFinder.map = function(mapFn) { - return using().findElements(locator).then(function(arr) { - var list = []; - arr.forEach(function(webElem, index) { - var mapResult = mapFn(webElem, index); - // All nested arrays and objects will also be fully resolved. - webdriver.promise.fullyResolved(mapResult).then(function(resolved) { - list.push(resolved); - }); - }); - return list; - }); - }; + /** + * Evaluates the input as if it were on the scope of the current element. + * @param {string} expression + * + * @return {ElementFinder} which resolves to the + * evaluated expression. The result will be resolved as in + * {@link webdriver.WebDriver.executeScript}. In summary - primitives will + * be resolved as is, functions will be converted to string, and elements + * will be returned as a WebElement. + */ + ElementFinder.prototype.evaluate = function(expression) { + var webElement = this.getWebElement(); + var evaluatedResult = webElement.getDriver().executeScript( + clientSideScripts.evaluate, webElement, expression); + return new ElementFinder(this.locator_, this.parentElementFinder_, + evaluatedResult, this.opt_index_); + }; - return elementArrayFinder; + /** + * Determine if animation is allowed on the current element. + * @param {string} value + * + * @return {ElementFinder} which resolves to whether animation is allowed. + */ + ElementFinder.prototype.allowAnimations = function(value) { + var webElement = this.getWebElement(); + var allowAnimationsResult = webElement.getDriver().executeScript( + clientSideScripts.allowAnimations, webElement, value); + return new ElementFinder(this.locator_, this.parentElementFinder_, + allowAnimationsResult, this.opt_index_); }; - return element; -}; + /** + * Access the underlying actionResult of ElementFinder. Implementation allows + * ElementFinder to be used as a webdriver.promise.Promise + * @param {function(webdriver.promise.Promise)} fn Function which takes + * the value of the underlying actionResult. + * + * @return {webdriver.promise.Promise} Promise which contains the results of + * evaluating fn. + */ + ElementFinder.prototype.then = function(fn) { + return this.actionResult_.then(function() { + return fn.apply(null, arguments); + }); + }; -/** - * Build the helper '$' function for a given instance of Protractor. - * - * @private - * @param {Protractor} ptor - * @return {function(string): ElementFinder} - */ -var buildCssHelper = function(ptor) { - return function(cssSelector) { - return buildElementHelper(ptor)(webdriver.By.css(cssSelector)); + var element = function(locator) { + return new ElementFinder(locator); }; -}; -/** - * Build the helper '$$' function for a given instance of Protractor. - * - * @private - * @param {Protractor} ptor - * @return {function(string): ElementArrayFinder} - */ -var buildMultiCssHelper = function(ptor) { - return function(cssSelector) { - return buildElementHelper(ptor).all(webdriver.By.css(cssSelector)); + element.all = function(locator) { + return new ElementArrayFinder(locator); }; + + return element; }; /** @@ -500,22 +653,23 @@ var buildMultiCssHelper = function(ptor) { * scope. * @constructor */ -var Protractor = function(webdriver, opt_baseUrl, opt_rootElement) { +var Protractor = function(webdriverInstance, opt_baseUrl, opt_rootElement) { // These functions should delegate to the webdriver instance, but should // wait for Angular to sync up before performing the action. This does not // include functions which are overridden by protractor below. var methodsToSync = ['getCurrentUrl', 'getPageSource', 'getTitle']; // Mix all other driver functionality into Protractor. - for (var method in webdriver) { - if(!this[method] && typeof webdriver[method] == 'function') { + for (var method in webdriverInstance) { + if(!this[method] && typeof webdriverInstance[method] == 'function') { if (methodsToSync.indexOf(method) !== -1) { - mixin(this, webdriver, method, this.waitForAngular.bind(this)); + mixin(this, webdriverInstance, method, this.waitForAngular.bind(this)); } else { - mixin(this, webdriver, method); + mixin(this, webdriverInstance, method); } } } + var self = this; /** * The wrapped webdriver instance. Use this to interact with pages that do @@ -523,7 +677,7 @@ var Protractor = function(webdriver, opt_baseUrl, opt_rootElement) { * * @type {webdriver.WebDriver} */ - this.driver = webdriver; + this.driver = webdriverInstance; /** * Helper function for finding elements. @@ -533,18 +687,22 @@ var Protractor = function(webdriver, opt_baseUrl, opt_rootElement) { this.element = buildElementHelper(this); /** - * Helper function for finding elements by css. + * Shorthand function for finding elements by css. * * @type {function(string): ElementFinder} */ - this.$ = buildCssHelper(this); + this.$ = function(selector) { + return self.element(webdriver.By.css(selector)); + }; /** - * Helper function for finding arrays of elements by css. + * Shorthand function for finding arrays of elements by css. * * @type {function(string): ElementArrayFinder} */ - this.$$ = buildMultiCssHelper(this); + this.$$ = function(selector) { + return self.all(webdriver.By.css(selector)); + }; /** * All get methods will be resolved against this base URL. Relative URLs are = @@ -627,184 +785,13 @@ Protractor.prototype.waitForAngular = function() { }); }; -// TODO: activeelement also returns a WebElement. - -/** - * Wrap a webdriver.WebElement with protractor specific functionality. - * - * @param {webdriver.WebElement} element - * @return {webdriver.WebElement} the wrapped web element. - */ -Protractor.prototype.wrapWebElement = function(element) { - // We want to be able to used varArgs in function signatures for clarity. - // jshint unused: false - var thisPtor = this; - // Before any of the WebElement functions, Protractor will wait to make sure - // Angular is synced up. - var originalFns = {}; - WEB_ELEMENT_FUNCTIONS.forEach(function(name) { - originalFns[name] = element[name]; - element[name] = function() { - thisPtor.waitForAngular(); - return originalFns[name].apply(element, arguments); - }; - }); - - var originalFindElement = element.findElement; - var originalFindElements = element.findElements; - var originalIsElementPresent = element.isElementPresent; - - /** - * Shortcut for querying the document directly with css. - * - * @alias $(cssSelector) - * @view - *
- * First - * Second - *
- * - * @example - * var item = $('.count .two'); - * expect(item.getText()).toBe('Second'); - * - * @param {string} selector A css selector - * @see webdriver.WebElement.findElement - * @return {!webdriver.WebElement} - */ - element.$ = function(selector) { - var locator = webdriver.By.css(selector); - return thisPtor.findElement(locator); - }; - - /** - * @see webdriver.WebElement.findElement - * @return {!webdriver.WebElement} - */ - element.findElement = function(locator, varArgs) { - thisPtor.waitForAngular(); - - var found; - if (locator.findElementsOverride) { - found = thisPtor.findElementsOverrideHelper_(element, locator); - } else { - found = originalFindElement.apply(element, arguments); - } - - return thisPtor.wrapWebElement(found); - }; - - /** - * Shortcut for querying the document directly with css. - * - * @alias $$(cssSelector) - * @view - *
- * First - * Second - *
- * - * @example - * // The following protractor expressions are equivalent. - * var list = element.all(by.css('.count span')); - * expect(list.count()).toBe(2); - * - * list = $$('.count span'); - * expect(list.count()).toBe(2); - * expect(list.get(0).getText()).toBe('First'); - * expect(list.get(1).getText()).toBe('Second'); - * - * @param {string} selector a css selector - * @see webdriver.WebElement.findElements - * @return {!webdriver.promise.Promise} A promise that will be resolved to an - * array of the located {@link webdriver.WebElement}s. - */ - element.$$ = function(selector) { - var locator = webdriver.By.css(selector); - return thisPtor.findElements(locator); - }; - - /** - * @see webdriver.WebElement.findElements - * @return {!webdriver.promise.Promise} A promise that will be resolved to an - * array of the located {@link webdriver.WebElement}s. - */ - element.findElements = function(locator, varArgs) { - thisPtor.waitForAngular(); - - var found; - if (locator.findElementsOverride) { - found = locator.findElementsOverride(element.getDriver(), element); - } else { - found = originalFindElements.apply(element, arguments); - } - - return found.then(function(elems) { - for (var i = 0; i < elems.length; ++i) { - thisPtor.wrapWebElement(elems[i]); - } - - return elems; - }); - }; - - /** - * @see webdriver.WebElement.isElementPresent - * @return {!webdriver.promise.Promise} A promise that will be resolved with - * whether an element could be located on the page. - */ - element.isElementPresent = function(locator, varArgs) { - thisPtor.waitForAngular(); - if (locator.findElementsOverride) { - return locator.findElementsOverride(element.getDriver(), element). - then(function (arr) { - return !!arr.length; - }); - } - return originalIsElementPresent.apply(element, arguments); - }; - - /** - * Evaluates the input as if it were on the scope of the current element. - * @param {string} expression - * - * @return {!webdriver.promise.Promise} A promise that will resolve to the - * evaluated expression. The result will be resolved as in - * {@link webdriver.WebDriver.executeScript}. In summary - primitives will - * be resolved as is, functions will be converted to string, and elements - * will be returned as a WebElement. - */ - element.evaluate = function(expression) { - thisPtor.waitForAngular(); - return element.getDriver().executeScript(clientSideScripts.evaluate, - element, expression); - }; - - element.allowAnimations = function(value) { - thisPtor.waitForAngular(); - return element.getDriver().executeScript(clientSideScripts.allowAnimations, - element, value); - }; - - return element; -}; - /** * Waits for Angular to finish rendering before searching for elements. * @see webdriver.WebDriver.findElement * @return {!webdriver.WebElement} */ -Protractor.prototype.findElement = function(locator, varArgs) { - var found; - this.waitForAngular(); - - if (locator.findElementsOverride) { - found = this.findElementsOverrideHelper_(null, locator); - } else { - found = this.driver.findElement(locator, varArgs); - } - - return this.wrapWebElement(found); +Protractor.prototype.findElement = function(locator) { + return this.element(locator).getWebElement(); }; /** @@ -813,23 +800,8 @@ Protractor.prototype.findElement = function(locator, varArgs) { * @return {!webdriver.promise.Promise} A promise that will be resolved to an * array of the located {@link webdriver.WebElement}s. */ -Protractor.prototype.findElements = function(locator, varArgs) { - var self = this, found; - this.waitForAngular(); - - if (locator.findElementsOverride) { - found = locator.findElementsOverride(this.driver); - } else { - found = this.driver.findElements(locator, varArgs); - } - - return found.then(function(elems) { - for (var i = 0; i < elems.length; ++i) { - self.wrapWebElement(elems[i]); - } - - return elems; - }); +Protractor.prototype.findElements = function(locator) { + return this.element.all(locator).getWebElements(); }; /** @@ -838,14 +810,10 @@ Protractor.prototype.findElements = function(locator, varArgs) { * @return {!webdriver.promise.Promise} A promise that will resolve to whether * the element is present on the page. */ -Protractor.prototype.isElementPresent = function(locatorOrElement, varArgs) { - this.waitForAngular(); - if (locatorOrElement.findElementsOverride) { - return locatorOrElement.findElementsOverride(this.driver).then(function(arr) { - return !!arr.length; - }); - } - return this.driver.isElementPresent(locatorOrElement, varArgs); +Protractor.prototype.isElementPresent = function(locatorOrElement) { + var element = (locatorOrElement instanceof webdriver.promise.Promise) ? + locatorOrElement : this.element(locatorOrElement); + return element.isPresent(); }; /** @@ -1122,35 +1090,6 @@ Protractor.prototype.pause = function() { flow.timeout(1000, 'waiting for debugger to attach'); }; -/** - * Builds a single web element from a locator with a findElementsOverride. - * Throws an error if an element is not found, and issues a warning - * if more than one element is described by the selector. - * - * @private - * @param {webdriver.WebElement} using A WebElement to scope the find, - * or null. - * @param {webdriver.Locator} locator - * @return {webdriver.WebElement} - */ -Protractor.prototype.findElementsOverrideHelper_ = function(using, locator) { - // We need to return a WebElement, so we construct one using a promise - // which will resolve to a WebElement. - return new webdriver.WebElement( - this.driver, - locator.findElementsOverride(this.driver, using).then(function(arr) { - if (!arr.length) { - throw new Error('No element found using locator: ' + locator.message); - } - if (arr.length > 1) { - console.log('warning: more than one element found for locator ' + - locator.message + - '- you may need to be more specific'); - } - return arr[0]; - })); -}; - /** * Create a new instance of Protractor by wrapping a webdriver instance. * diff --git a/spec/basic/elements_spec.js b/spec/basic/elements_spec.js index 9238f2da8..549ce803f 100644 --- a/spec/basic/elements_spec.js +++ b/spec/basic/elements_spec.js @@ -27,6 +27,18 @@ describe('ElementFinder', function() { expect(name.getText()).toEqual('Jane'); }); + it('should chain element actions', function() { + browser.get('index.html#/form'); + + var usernameInput = element(by.model('username')); + var name = element(by.binding('username')); + + expect(name.getText()).toEqual('Anon'); + + usernameInput.clear().sendKeys('Jane'); + expect(name.getText()).toEqual('Jane'); + }); + it('chained call should wait to grab the WebElement until a method is called', function() { // These should throw no error before a page is loaded. @@ -68,18 +80,14 @@ describe('ElementFinder', function() { expect(elems.length).toEqual(4); }); - element(by.id('baz')). - element.all(by.binding('item')). - then(function(elems) { - expect(elems.length).toEqual(2); - }); + element(by.id('baz')).all(by.binding('item')).then(function(elems) { + expect(elems.length).toEqual(2); + }); }); - it('should wait to grab multiple chained elements', - function() { + it('should wait to grab multiple chained elements', function() { // These should throw no error before a page is loaded. - var reused = element(by.id('baz')). - element.all(by.binding('item')); + var reused = element(by.id('baz')).all(by.binding('item')); browser.get('index.html#/conflict'); @@ -88,6 +96,21 @@ describe('ElementFinder', function() { expect(reused.last().getText()).toEqual('Inner other: innerbarbaz'); }); + it('should wait to grab elements chained by index', function() { + // These should throw no error before a page is loaded. + var reused = element(by.id('baz')).all(by.binding('item')); + var first = reused.first(); + var second = reused.get(1); + var last = reused.last(); + + browser.get('index.html#/conflict'); + + expect(reused.count()).toEqual(2); + expect(first.getText()).toEqual('Inner: inner'); + expect(second.getText()).toEqual('Inner other: innerbarbaz'); + expect(last.getText()).toEqual('Inner other: innerbarbaz'); + }); + it('should determine element presence properly with chaining', function() { browser.get('index.html#/conflict'); expect(element(by.id('baz')). @@ -228,6 +251,7 @@ describe('ElementFinder', function() { var byCss = by.css('body'); var byBinding = by.binding('greet'); + expect(element(byCss).locator()).toEqual(byCss); expect(element(byBinding).locator()).toEqual(byBinding); }); @@ -254,60 +278,10 @@ describe('shortcut css notation', function() { beforeEach(function() { browser.get('index.html#/bindings'); }); - - it('$ should be equivalent to by.css', function() { - var shortcut = $('.planet-info'); - var noShortcut = element(by.css('.planet-info')); - - expect(protractor.WebElement.equals(shortcut.find(), noShortcut.find())). - toBe(true); - }); - - it('$$ should be equivalent to by.css', function() { - var shortcut = element.all(by.css('option')); - var noShortcut = $$('option'); - shortcut.then(function(optionsFromShortcut) { - noShortcut.then(function(optionsFromLongForm) { - expect(optionsFromShortcut.length).toEqual(optionsFromLongForm.length); - - for (var i = 0; i < optionsFromLongForm.length; ++i) { - expect(protractor.WebElement.equals( - optionsFromLongForm[i], optionsFromShortcut[i])). - toBe(true); - } - }); - }); - }); - - it('$ chained should be equivalent to by.css', function() { - var select = element(by.css('select')); - var shortcut = select.$('option[value="4"]'); - var noShortcut = select.element(by.css('option[value="4"]')); - - expect(protractor.WebElement.equals(shortcut.find(), noShortcut.find())). - toBe(true); - }); - - it('$$ chained should be equivalent to by.css', function() { - var select = element(by.css('select')); - var shortcut = select.element.all(by.css('option')); - var noShortcut = select.$$('option'); - shortcut.then(function(optionsFromShortcut) { - noShortcut.then(function(optionsFromLongForm) { - expect(optionsFromShortcut.length).toEqual(optionsFromLongForm.length); - - for (var i = 0; i < optionsFromLongForm.length; ++i) { - expect(protractor.WebElement.equals( - optionsFromLongForm[i], optionsFromShortcut[i])). - toBe(true); - } - }); - }); - }); - + it('should chain $$ with $', function() { var withoutShortcutCount = - element(by.css('select')).element.all(by.css('option')).then(function(options) { + element(by.css('select')).all(by.css('option')).then(function(options) { return options.length; }); var withShortcutCount = $('select').$$('option').count(); @@ -315,83 +289,3 @@ describe('shortcut css notation', function() { expect(withoutShortcutCount).toEqual(withShortcutCount); }); }); - -describe('wrapping WebElements', function() { - var verifyMethodsAdded = function(result) { - expect(typeof result.evaluate).toBe('function'); - expect(typeof result.$).toBe('function'); - expect(typeof result.$$).toBe('function'); - }; - - beforeEach(function() { - browser.get('index.html#/bindings'); - }); - - describe('when found via #findElement', function() { - it('should wrap the result', function() { - browser.findElement(by.binding('planet.name')).then(verifyMethodsAdded); - - browser.findElement(by.css('option[value="4"]')).then(verifyMethodsAdded); - }); - - describe('when found with global element', function() { - it('should wrap the result', function() { - element(by.binding('planet.name')).find().then(verifyMethodsAdded); - element(by.css('option[value="4"]')).find().then(verifyMethodsAdded); - }); - }); - }); - - describe('when found via #findElements', function() { - it('should wrap the results', function() { - browser.findElements(by.binding('planet.name')).then(function(results) { - results.forEach(verifyMethodsAdded); - }); - browser.findElements(by.css('option[value="4"]')).then(function(results) { - results.forEach(verifyMethodsAdded); - }); - }); - - describe('when found with global element.all', function() { - it('should wrap the result', function() { - element.all(by.binding('planet.name')).then(function(results) { - results.forEach(verifyMethodsAdded); - }); - element.all(by.binding('planet.name')).get(0).then(verifyMethodsAdded); - element.all(by.binding('planet.name')).first().then(verifyMethodsAdded); - element.all(by.binding('planet.name')).last().then(verifyMethodsAdded); - element.all(by.css('option[value="4"]')).then(function(results) { - results.forEach(verifyMethodsAdded); - }); - }); - }); - }); - - describe('when chaining with another element', function() { - var info; - - beforeEach(function() { - info = browser.findElement(by.css('.planet-info')); - }); - - describe('when found via #findElement', function() { - it('should wrap the result', function() { - info.findElement(by.binding('planet.name')).then(verifyMethodsAdded); - - info.findElement(by.css('div:last-child')).then(verifyMethodsAdded); - }); - }); - - describe('when querying for many elements', function() { - it('should wrap the result', function() { - info.findElements(by.binding('planet.name')).then(function(results) { - results.forEach(verifyMethodsAdded); - }); - - info.findElements(by.css('div:last-child')).then(function(results) { - results.forEach(verifyMethodsAdded); - }); - }); - }); - }); -});