UNPKG

protractor

Version:

Webdriver E2E test wrapper for Angular.

1,508 lines (1,412 loc) 52.4 kB
var url = require('url'); var util = require('util'); var webdriver = require('selenium-webdriver'); var helper = require('./util'); var log = require('./logger.js'); var clientSideScripts = require('./clientsidescripts.js'); var ProtractorBy = require('./locators.js').ProtractorBy; /* global angular */ var DEFER_LABEL = 'NG_DEFER_BOOTSTRAP!'; var WEB_ELEMENT_FUNCTIONS = [ 'click', 'sendKeys', 'getTagName', 'getCssValue', 'getAttribute', 'getText', 'getSize', 'getLocation', 'isEnabled', 'isSelected', 'submit', 'clear', 'isDisplayed', 'getOuterHtml', 'getInnerHtml', 'getId']; var DEFAULT_RESET_URL = 'data:text/html,<html></html>'; var DEFAULT_GET_PAGE_TIMEOUT = 10000; /* * Mix in other webdriver functionality to be accessible via protractor. */ for (var foo in webdriver) { exports[foo] = webdriver[foo]; } /** * @type {ProtractorBy} */ exports.By = new ProtractorBy(); /** * Mix a function from one object onto another. The function will still be * called in the context of the original object. * * @private * @param {Object} to * @param {Object} from * @param {string} fnName * @param {function=} setupFn */ var mixin = function(to, from, fnName, setupFn) { to[fnName] = function() { if (setupFn) { setupFn(); } return from[fnName].apply(from, arguments); }; }; /** * Build the helper 'element' function for a given instance of Protractor. * * @private * @param {Protractor} ptor * @return {function(webdriver.Locator): ElementFinder} */ var buildElementHelper = function(ptor) { /** * ElementArrayFinder is used for operations on an array of elements (as opposed * to a single element). * * The ElementArrayFinder is used to set up a chain of conditions that identify * an array of elements. In particular, you can call all(locator) and * filter(filterFn) to return a new ElementArrayFinder modified by the * conditions, and you can call get(index) to return a single ElementFinder at * position 'index'. * * Similar to jquery, ElementArrayFinder will search all branches of the DOM * to find the elements that satisfy the conditions (i.e. all, filter, get). * However, an ElementArrayFinder will not actually retrieve the elements until * an action is called, which means it can be set up in helper files (i.e. * page objects) before the page is available, and reused as the page changes. * * You can treat an ElementArrayFinder as an array of WebElements for most * purposes, in particular, you may perform actions (i.e. click, getText) on * them as you would an array of WebElements. The action will apply to * every element identified by the ElementArrayFinder. ElementArrayFinder * extends Promise, and once an action is performed on an ElementArrayFinder, * the latest result can be accessed using then, and will be returned as an * array of the results; the array has length equal to the length of the * elements found by the ElementArrayFinder and each result represents the * result of performing the action on the element. Unlike a WebElement, an * ElementArrayFinder will wait for the angular app to settle before * performing finds or actions. * * @alias element.all(locator) * @view * <ul class="items"> * <li>First</li> * <li>Second</li> * <li>Third</li> * </ul> * * @example * element.all(by.css('.items li')).then(function(items) { * expect(items.length).toBe(3); * expect(items[0].getText()).toBe('First'); * }); * * @constructor * @param {function(): Array.<webdriver.WebElement>} getWebElements A function * that returns a list of the underlying Web Elements. * @param {webdriver.Locator} locator The most relevant locator. It is only * used for error reporting and ElementArrayFinder.locator. * @param {Array.<webdriver.promise.Promise>} opt_actionResults An array * of promises which will be retrieved with then. Resolves to the latest * action result, or null if no action has been called. * @return {ElementArrayFinder} */ var ElementArrayFinder = function(getWebElements, locator, opt_actionResults) { this.getWebElements = getWebElements || null; this.actionResults_ = opt_actionResults; this.locator_ = locator; var self = this; WEB_ELEMENT_FUNCTIONS.forEach(function(fnName) { self[fnName] = function() { var args = arguments; var actionFn = function(webElem) { return webElem[fnName].apply(webElem, args); }; return self.applyAction_(actionFn); }; }); }; util.inherits(ElementArrayFinder, webdriver.promise.Promise); /** * Create a shallow copy of ElementArrayFinder. * * @return {!ElementArrayFinder} A shallow copy of this. */ ElementArrayFinder.prototype.clone = function() { // A shallow copy is all we need since the underlying fields can never be // modified. (Locator can be modified by the user, but that should // rarely/never happen and it doesn't affect functionalities). return new ElementArrayFinder( this.getWebElements, this.locator_, this.actionResults_); }; /** * Calls to ElementArrayFinder may be chained to find an array of elements * using the current elements in this ElementArrayFinder as the starting point. * This function returns a new ElementArrayFinder which would contain the * children elements found (and could also be empty). * * @alias element.all(locator).all(locator) * @view * <div id='id1' class="parent"> * <ul> * <li class="foo">1a</li> * <li class="baz">1b</li> * </ul> * </div> * <div id='id2' class="parent"> * <ul> * <li class="foo">2a</li> * <li class="bar">2b</li> * </ul> * </div> * * @example * var foo = element.all(by.css('.parent')).all(by.css('.foo')) * expect(foo.getText()).toEqual(['1a', '2a']) * var baz = element.all(by.css('.parent')).all(by.css('.baz')) * expect(baz.getText()).toEqual(['1b']) * var nonexistent = element.all(by.css('.parent')).all(by.css('.NONEXISTENT')) * expect(nonexistent.getText()).toEqual(['']) * * @param {webdriver.Locator} subLocator * @return {ElementArrayFinder} */ ElementArrayFinder.prototype.all = function(locator) { var self = this; var getWebElements = function() { if (self.getWebElements === null) { // This is the first time we are looking for an element return ptor.waitForAngular().then(function() { if (locator.findElementsOverride) { return locator.findElementsOverride(ptor.driver, null, ptor.rootEl); } else { return ptor.driver.findElements(locator); } }); } else { return self.getWebElements().then(function(parentWebElements) { var childrenPromiseList = []; // For each parent web element, find their children and construct a // list of Promise<List<child_web_element>> parentWebElements.forEach(function(parentWebElement) { var childrenPromise = locator.findElementsOverride ? locator.findElementsOverride(ptor.driver, parentWebElement, ptor.rootEl) : parentWebElement.findElements(locator); childrenPromiseList.push(childrenPromise); }); // Resolve the list of Promise<List<child_web_elements>> and merge into // a single list return webdriver.promise.all(childrenPromiseList).then( function(resolved) { var childrenList = []; resolved.forEach(function(resolvedE) { childrenList = childrenList.concat(resolvedE); }); return childrenList; }); }); } }; return new ElementArrayFinder(getWebElements, locator); }; /** * Apply a filter function to each element within the ElementArrayFinder. Returns * a new ElementArrayFinder with all elements that pass the filter function. The * filter function receives the ElementFinder as the first argument * and the index as a second arg. * This does not actually retrieve the underlying list of elements, so it can * be used in page objects. * * @alias element.all(locator).filter(filterFn) * @view * <ul class="items"> * <li class="one">First</li> * <li class="two">Second</li> * <li class="three">Third</li> * </ul> * * @example * element.all(by.css('.items li')).filter(function(elem, index) { * return elem.getText().then(function(text) { * return text === 'Third'; * }); * }).then(function(filteredElements) { * filteredElements[0].click(); * }); * * @param {function(ElementFinder, number): webdriver.WebElement.Promise} filterFn * Filter function that will test if an element should be returned. * filterFn can either return a boolean or a promise that resolves to a boolean. * @return {!ElementArrayFinder} A ElementArrayFinder that represents an array * of element that satisfy the filter function. */ ElementArrayFinder.prototype.filter = function(filterFn) { var self = this; var getWebElements = function() { return self.getWebElements().then(function(parentWebElements) { var list = []; parentWebElements.forEach(function(parentWebElement, index) { var elementFinder = ElementFinder.fromWebElement_(parentWebElement, self.locator_); var filterResults = filterFn(elementFinder, index); if (filterResults instanceof webdriver.promise.Promise) { filterResults.then(function(satisfies) { if (satisfies) { list.push(parentWebElements[index]); } }); } else if (filterResults) { list.push(parentWebElements[index]); } }); return list; }); }; return new ElementArrayFinder(getWebElements, this.locator_); }; /** * Get an element within the ElementArrayFinder by index. The index starts at 0. * Negative indices are wrapped (i.e. -i means ith element from last) * This does not actually retrieve the underlying element. * * @alias element.all(locator).get(index) * @view * <ul class="items"> * <li>First</li> * <li>Second</li> * <li>Third</li> * </ul> * * @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) { var self = this; var getWebElements = function() { return self.getWebElements().then(function(parentWebElements) { var i = index; if (i < 0) { // wrap negative indices i = parentWebElements.length + i; } if (i < 0 || i >= parentWebElements.length) { throw new Error('Index out of bound. Trying to access element at ' + 'index:' + index + ', but there are only ' + parentWebElements.length + ' elements'); } return [parentWebElements[i]]; }); }; return new ElementArrayFinder(getWebElements, this.locator_).toElementFinder_(); }; /** * Get the first matching element for the ElementArrayFinder. This does not * actually retrieve the underlying element. * * @alias element.all(locator).first() * @view * <ul class="items"> * <li>First</li> * <li>Second</li> * <li>Third</li> * </ul> * * @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 ElementArrayFinder. This does not * actually retrieve the underlying element. * * @alias element.all(locator).last() * @view * <ul class="items"> * <li>First</li> * <li>Second</li> * <li>Third</li> * </ul> * * @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); }; /** * Shorthand function for finding arrays of elements by css. * * @type {function(string): ElementArrayFinder} */ ElementArrayFinder.prototype.$$ = function(selector) { return this.all(webdriver.By.css(selector)); }; /** * Returns an ElementFinder representation of ElementArrayFinder. It ensures * that the ElementArrayFinder resolves to one and only one underlying element. * * @return {ElementFinder} An ElementFinder representation * @private */ ElementArrayFinder.prototype.toElementFinder_ = function() { return new ElementFinder(this); }; /** * Count the number of elements represented by the ElementArrayFinder. * * @alias element.all(locator).count() * @view * <ul class="items"> * <li>First</li> * <li>Second</li> * <li>Third</li> * </ul> * * @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; }, function(err) { if (err.code == webdriver.error.ErrorCode.NO_SUCH_ELEMENT) { return 0; } else { throw err; } }); }; /** * Returns the most relevant locator. * * @example * $('#ID1').locator() // returns by.css('#ID1') * $('#ID1').$('#ID2').locator() // returns by.css('#ID2') * $$('#ID1').filter(filterFn).get(0).click().locator() // returns by.css('#ID1') * * @return {webdriver.Locator} */ ElementArrayFinder.prototype.locator = function() { return this.locator_; }; /** * Apply an action function to every element in the ElementArrayFinder, * and return a new ElementArrayFinder that contains the results of the actions. * * @param {function(ElementFinder)} actionFn * * @return {ElementArrayFinder} * @private */ ElementArrayFinder.prototype.applyAction_ = function(actionFn) { var callerError = new Error(); var actionResults = this.getWebElements().then(function(arr) { var list = []; arr.forEach(function(webElem) { list.push(actionFn(webElem)); }); return webdriver.promise.all(list); }).then(null, function(e) { e.stack = e.stack + '\n' + callerError.stack; throw e; }); return new ElementArrayFinder(this.getWebElements, this.locator_, actionResults); }; /** * Represents the ElementArrayFinder as an array of ElementFinders. * * @return {Array.<ElementFinder>} 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) { list.push(ElementFinder.fromWebElement_(webElem, self.locator_)); }); return list; }); }; /** * Retrieve the elements represented by the ElementArrayFinder. The input * function is passed to the resulting promise, which resolves to an * array of ElementFinders. * * @alias element.all(locator).then(thenFunction) * @view * <ul class="items"> * <li>First</li> * <li>Second</li> * <li>Third</li> * </ul> * * @example * element.all(by.css('.items li')).then(function(arr) { * expect(arr.length).toEqual(3); * }); * * @param {function(Array.<ElementFinder>)} fn * @param {function(Error)} errorFn * * @type {webdriver.promise.Promise} a promise which will resolve to * an array of ElementFinders represented by the ElementArrayFinder. */ ElementArrayFinder.prototype.then = function(fn, errorFn) { if (this.actionResults_) { return this.actionResults_.then(fn, errorFn); } else { return this.asElementFinders_().then(fn, errorFn); } }; /** * Calls the input function on each ElementFinder represented by the ElementArrayFinder. * * @alias element.all(locator).each(eachFunction) * @view * <ul class="items"> * <li>First</li> * <li>Second</li> * <li>Third</li> * </ul> * * @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, index) { fn(elementFinder, index); }); }); }; /** * Apply a map function to each element within the ElementArrayFinder. The * callback receives the ElementFinder as the first argument and the index as * a second arg. * * @alias element.all(locator).map(mapFunction) * @view * <ul class="items"> * <li class="one">First</li> * <li class="two">Second</li> * <li class="three">Third</li> * </ul> * * @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; }); }; /** * Apply a reduce function against an accumulator and every element found * using the locator (from left-to-right). The reduce function has to reduce * every element into a single value (the accumulator). Returns promise of * the accumulator. The reduce function receives the accumulator, current * ElementFinder, the index, and the entire array of ElementFinders, * respectively. * * @alias element.all(locator).reduce(reduceFn) * @view * <ul class="items"> * <li class="one">First</li> * <li class="two">Second</li> * <li class="three">Third</li> * </ul> * * @example * var value = element.all(by.css('.items li')).reduce(function(acc, elem) { * return elem.getText().then(function(text) { * return acc + text + ' '; * }); * }); * * expect(value).toEqual('First Second Third '); * * @param {function(number, ElementFinder, number, Array.<ElementFinder>)} * reduceFn Reduce function that reduces every element into a single value. * @param {*} initialValue Initial value of the accumulator. * @return {!webdriver.promise.Promise} A promise that resolves to the final * value of the accumulator. */ ElementArrayFinder.prototype.reduce = function(reduceFn, initialValue) { var valuePromise = webdriver.promise.fulfilled(initialValue); return this.asElementFinders_().then(function(arr) { arr.forEach(function(elementFinder, index) { valuePromise = valuePromise.then(function(value) { return reduceFn(value, elementFinder, index, arr); }); }); return valuePromise; }); }; /** * Evaluates the input as if it were on the scope of the current underlying * elements. * * @view * <span id="foo">{{variableInScope}}</span> * * @example * var value = element(by.id('foo')).evaluate('variableInScope'); * * @param {string} expression * * @return {ElementArrayFinder} which resolves to the * evaluated expression for each underlying element. * 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. */ ElementArrayFinder.prototype.evaluate = function(expression) { var evaluationFn = function(webElem) { return webElem.getDriver().executeScript( clientSideScripts.evaluate, webElem, expression); }; return this.applyAction_(evaluationFn); }; /** * Determine if animation is allowed on the current underlying elements. * @param {string} value * * @example * // Turns off ng-animate animations for all elements in the <body> * element(by.css('body')).allowAnimations(false); * * @return {ElementArrayFinder} which resolves to whether animation is allowed. */ ElementArrayFinder.prototype.allowAnimations = function(value) { var allowAnimationsTestFn = function(webElem) { return webElem.getDriver().executeScript( clientSideScripts.allowAnimations, webElem, value); }; return this.applyAction_(allowAnimationsTestFn); }; /** * The ElementFinder simply represents a single element of an * ElementArrayFinder (and is more like a convenience object). As a result, * anything that can be done with an ElementFinder, can also be done using * an ElementArrayFinder. * * 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 * <span>{{person.name}}</span> * <span ng-bind="person.email"></span> * <input type="text" ng-model="person.name"/> * * @example * // Find element with {{scopeVar}} syntax. * element(by.binding('person.name')).getText().then(function(name) { * expect(name).toBe('Foo'); * }); * * // Find element with ng-bind="scopeVar" syntax. * expect(element(by.binding('person.email')).getText()).toBe('foo@bar.com'); * * // Find by model. * var input = element(by.model('person.name')); * input.sendKeys('123'); * expect(input.getAttribute('value')).toBe('Foo123'); * * @constructor * @param {ElementArrayFinder} elementArrayFinder The ElementArrayFinder * that this is branched from. * @return {ElementFinder} */ var ElementFinder = function(elementArrayFinder) { if (!elementArrayFinder) { throw new Error('BUG: elementArrayFinder cannot be empty'); } this.parentElementArrayFinder = elementArrayFinder; // This filter verifies that there is only 1 element returned by the // elementArrayFinder. It will warn if there are more than 1 element and // throw an error if there are no elements. var getWebElements = function() { return elementArrayFinder.getWebElements().then(function(webElements) { if (webElements.length === 0) { throw new webdriver.error.Error( webdriver.error.ErrorCode.NO_SUCH_ELEMENT, 'No element found using locator: ' + elementArrayFinder.locator_.toString()); } else { if (webElements.length > 1) { log.warn('more than one element found for locator ' + elementArrayFinder.locator_.toString() + ' - the first result will be used'); } return [webElements[0]]; } }); }; // Store a copy of the underlying elementArrayFinder, but with the more // restrictive getWebElements (which checks that there is only 1 element). this.elementArrayFinder_ = new ElementArrayFinder( getWebElements, elementArrayFinder.locator_, elementArrayFinder.actionResults_); // Decorate ElementFinder with webdriver functions. Simply calls the // underlying elementArrayFinder to perform these functions. var self = this; WEB_ELEMENT_FUNCTIONS.forEach(function(fnName) { self[fnName] = function() { return self.elementArrayFinder_[fnName]. apply(self.elementArrayFinder_, arguments).toElementFinder_(); }; }); }; util.inherits(ElementFinder, webdriver.promise.Promise); ElementFinder.fromWebElement_ = function(webElem, locator) { var getWebElements = function() { return webdriver.promise.fulfilled([webElem]); }; return new ElementArrayFinder(getWebElements, locator).toElementFinder_(); }; /** * Create a shallow copy of ElementFinder. * * @return {!ElementFinder} A shallow copy of this. */ ElementFinder.prototype.clone = function() { // A shallow copy is all we need since the underlying fields can never be // modified return new ElementFinder(this.parentElementArrayFinder); }; /** * @see ElementArrayFinder.prototype.locator * * @return {webdriver.Locator} */ ElementFinder.prototype.locator = function() { return this.elementArrayFinder_.locator(); }; /** * Returns the WebElement represented by this ElementFinder. * Throws the WebDriver error if the element doesn't exist. * * @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 id = this.elementArrayFinder_.getWebElements().then( function(parentWebElements) { return parentWebElements[0]; }); return new webdriver.WebElementPromise(ptor.driver, id); }; /** * 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, errorFn) { return this.elementArrayFinder_.then(function(actionResults) { if (!fn) { return actionResults[0]; } return fn(actionResults[0]); }, errorFn); }; /** * Calls to element may be chained to find an array of elements within a parent. * * @alias element(locator).all(locator) * @view * <div class="parent"> * <ul> * <li class="one">First</li> * <li class="two">Second</li> * <li class="three">Third</li> * </ul> * </div> * * @example * var items = element(by.css('.parent')).all(by.tagName('li')) * * @param {webdriver.Locator} subLocator * @return {ElementArrayFinder} */ ElementFinder.prototype.all = function(subLocator) { return this.elementArrayFinder_.all(subLocator); }; /** * Calls to element may be chained to find elements within a parent. * * @alias element(locator).element(locator) * @view * <div class="parent"> * <div class="child"> * Child text * <div>{{person.phone}}</div> * </div> * </div> * * @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 this.all(subLocator).toElementFinder_(); }; /** * Shortcut for querying the document directly with css. * * @alias $$(cssSelector) * @view * <div class="count"> * <span class="one">First</span> * <span class="two">Second</span> * </div> * * @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 this.all(webdriver.By.css(selector)); }; /** * Shortcut for querying the document directly with css. * * @alias $(cssSelector) * @view * <div class="count"> * <span class="one">First</span> * <span class="two">Second</span> * </div> * * @example * var item = $('.count .two'); * expect(item.getText()).toBe('Second'); * * @param {string} selector A css selector * @return {ElementFinder} which identifies the located * {@link webdriver.WebElement} */ ElementFinder.prototype.$ = function(selector) { return this.element(webdriver.By.css(selector)); }; /** * Determine whether the element is present on the page. * * @view * <span>{{person.name}}</span> * * @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() { return this.parentElementArrayFinder.count().then(function(count) { return !!count; }); }; /** * Override for WebElement.prototype.isElementPresent so that protractor waits * for Angular to settle before making the check. * * @see ElementFinder.isPresent * * @param {webdriver.Locator} subLocator Locator for element to look for. * @return {ElementFinder} which resolves to whether * the element is present on the page. */ ElementFinder.prototype.isElementPresent = function(subLocator) { return this.element(subLocator).isPresent(); }; /** * Evaluates the input as if it were on the scope of the current element. * @see ElementArrayFinder.evaluate * * @param {string} expression * * @return {ElementFinder} which resolves to the evaluated expression. */ ElementFinder.prototype.evaluate = function(expression) { return this.elementArrayFinder_.evaluate(expression).toElementFinder_(); }; /** * @see ElementArrayFinder.prototype.allowAnimations. * @param {string} value * * @return {ElementFinder} which resolves to whether animation is allowed. */ ElementFinder.prototype.allowAnimations = function(value) { return this.elementArrayFinder_.allowAnimations(value).toElementFinder_(); }; /** * Webdriver relies on this function to be present on Promises, so adding * this dummy function as we inherited from webdriver.promise.Promise, but * this function is irrelevant to our usage * * @return {boolean} Always false as ElementFinder is never in pending state. */ ElementFinder.prototype.isPending = function() { return false; }; var element = function(locator) { return new ElementArrayFinder().all(locator).toElementFinder_(); }; element.all = function(locator) { return new ElementArrayFinder().all(locator); }; return element; }; /** * @alias browser * @constructor * @extends {webdriver.WebDriver} * @param {webdriver.WebDriver} webdriver * @param {string=} opt_baseUrl A base URL to run get requests against. * @param {string=} opt_rootElement Selector element that has an ng-app in * scope. */ 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 webdriverInstance) { if(!this[method] && typeof webdriverInstance[method] == 'function') { if (methodsToSync.indexOf(method) !== -1) { mixin(this, webdriverInstance, method, this.waitForAngular.bind(this)); } else { mixin(this, webdriverInstance, method); } } } var self = this; /** * The wrapped webdriver instance. Use this to interact with pages that do * not contain Angular (such as a log-in screen). * * @type {webdriver.WebDriver} */ this.driver = webdriverInstance; /** * Helper function for finding elements. * * @type {function(webdriver.Locator): ElementFinder} */ this.element = buildElementHelper(this); /** * Shorthand function for finding elements by css. * * @type {function(string): ElementFinder} */ this.$ = function(selector) { return self.element(webdriver.By.css(selector)); }; /** * Shorthand function for finding arrays of elements by css. * * @type {function(string): ElementArrayFinder} */ this.$$ = function(selector) { return self.element.all(webdriver.By.css(selector)); }; /** * All get methods will be resolved against this base URL. Relative URLs are = * resolved the way anchor tags resolve. * * @type {string} */ this.baseUrl = opt_baseUrl || ''; /** * The css selector for an element on which to find Angular. This is usually * 'body' but if your ng-app is on a subsection of the page it may be * a subelement. * * @type {string} */ this.rootEl = opt_rootElement || 'body'; /** * If true, Protractor will not attempt to synchronize with the page before * performing actions. This can be harmful because Protractor will not wait * until $timeouts and $http calls have been processed, which can cause * tests to become flaky. This should be used only when necessary, such as * when a page continuously polls an API using $timeout. * * @type {boolean} */ this.ignoreSynchronization = false; /** * Timeout in milliseconds to wait for pages to load when calling `get`. * * @type {number} */ this.getPageTimeout = DEFAULT_GET_PAGE_TIMEOUT; /** * An object that holds custom test parameters. * * @type {Object} */ this.params = {}; /** * The reset URL to use between page loads. * * @type {string} */ this.resetUrl = DEFAULT_RESET_URL; this.driver.getCapabilities().then(function(caps) { // Internet Explorer does not accept data URLs, which are the default // reset URL for Protractor. // Safari accepts data urls, but SafariDriver fails after one is used. var browserName = caps.get('browserName'); if (browserName === 'internet explorer' || browserName === 'safari') { self.resetUrl = 'about:blank'; } }); /** * Information about mock modules that will be installed during every * get(). * * @type {Array<{name: string, script: function|string, args: Array.<string>}>} */ this.mockModules_ = []; this.addBaseMockModules_(); }; /** * The same as {@code webdriver.WebDriver.prototype.executeScript}, * but with a customized description for debugging. * * @private * @param {!(string|Function)} script The script to execute. * @param {string} description A description of the command for debugging. * @param {...*} var_args The arguments to pass to the script. * @return {!webdriver.promise.Promise.<T>} A promise that will resolve to the * scripts return value. * @template T */ Protractor.prototype.executeScript_ = function(script, description) { if (typeof script === 'function') { script = 'return (' + script + ').apply(null, arguments);'; } return this.driver.schedule( new webdriver.Command(webdriver.CommandName.EXECUTE_SCRIPT). setParameter('script', script). setParameter('args', Array.prototype.slice.call(arguments, 2)), description); }; /** * The same as {@code webdriver.WebDriver.prototype.executeAsyncScript}, * but with a customized description for debugging. * * @private * @param {!(string|Function)} script The script to execute. * @param {string} description A description for debugging purposes. * @param {...*} var_args The arguments to pass to the script. * @return {!webdriver.promise.Promise.<T>} A promise that will resolve to the * scripts return value. * @template T */ Protractor.prototype.executeAsyncScript_ = function(script, description) { if (typeof script === 'function') { script = 'return (' + script + ').apply(null, arguments);'; } return this.driver.schedule( new webdriver.Command(webdriver.CommandName.EXECUTE_ASYNC_SCRIPT). setParameter('script', script). setParameter('args', Array.prototype.slice.call(arguments, 2)), description); }; /** * Instruct webdriver to wait until Angular has finished rendering and has * no outstanding $http calls before continuing. * * @return {!webdriver.promise.Promise} A promise that will resolve to the * scripts return value. */ Protractor.prototype.waitForAngular = function() { if (this.ignoreSynchronization) { return webdriver.promise.fulfilled(); } return this.executeAsyncScript_( clientSideScripts.waitForAngular, 'Protractor.waitForAngular()', this.rootEl). then(function(browserErr) { if (browserErr) { throw 'Error while waiting for Protractor to ' + 'sync with the page: ' + JSON.stringify(browserErr); } }).then(null, function(err) { var timeout; if (/asynchronous script timeout/.test(err.message)) { // Timeout on Chrome timeout = /-?[\d\.]*\ seconds/.exec(err.message); } else if (/Timed out waiting for async script/.test(err.message)) { // Timeout on Firefox timeout = /-?[\d\.]*ms/.exec(err.message); } else if (/Timed out waiting for an asynchronous script/.test(err.message)) { // Timeout on Safari timeout = /-?[\d\.]*\ ms/.exec(err.message); } if (timeout) { throw 'Timed out waiting for Protractor to synchronize with ' + 'the page after ' + timeout + '. Please see ' + 'https://github.com/angular/protractor/blob/master/docs/faq.md'; } else { throw err; } }); }; /** * Waits for Angular to finish rendering before searching for elements. * @see webdriver.WebDriver.findElement * @return {!webdriver.WebElement} */ Protractor.prototype.findElement = function(locator) { return this.element(locator).getWebElement(); }; /** * Waits for Angular to finish rendering before searching for elements. * @see webdriver.WebDriver.findElements * @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) { return this.element.all(locator).getWebElements(); }; /** * Tests if an element is present on the page. * @see webdriver.WebDriver.isElementPresent * @return {!webdriver.promise.Promise} A promise that will resolve to whether * the element is present on the page. */ Protractor.prototype.isElementPresent = function(locatorOrElement) { var element = (locatorOrElement instanceof webdriver.promise.Promise) ? locatorOrElement : this.element(locatorOrElement); return element.isPresent(); }; /** * Add a module to load before Angular whenever Protractor.get is called. * Modules will be registered after existing modules already on the page, * so any module registered here will override preexisting modules with the same * name. * * @example * browser.addMockModule('modName', function() { * angular.module('modName', []).value('foo', 'bar'); * }); * * @param {!string} name The name of the module to load or override. * @param {!string|Function} script The JavaScript to load the module. * @param {...*} varArgs Any additional arguments will be provided to * the script and may be referenced using the `arguments` object. */ Protractor.prototype.addMockModule = function(name, script) { var moduleArgs = Array.prototype.slice.call(arguments, 2); this.mockModules_.push({ name: name, script: script, args: moduleArgs }); }; /** * Clear the list of registered mock modules. */ Protractor.prototype.clearMockModules = function() { this.mockModules_ = []; this.addBaseMockModules_(); }; /** * Remove a registered mock module. * * @example * browser.removeMockModule('modName'); * * @param {!string} name The name of the module to remove. */ Protractor.prototype.removeMockModule = function(name) { for (var i = 0; i < this.mockModules_.length; ++i) { if (this.mockModules_[i].name == name) { this.mockModules_.splice(i, 1); } } }; /** * Add the base mock modules used for all Protractor tests. * * @private */ Protractor.prototype.addBaseMockModules_ = function() { this.addMockModule('protractorBaseModule_', function() { angular.module('protractorBaseModule_', []). config(['$compileProvider', function($compileProvider) { if ($compileProvider.debugInfoEnabled) { $compileProvider.debugInfoEnabled(true); } }]); }); }; /** * @see webdriver.WebDriver.get * * Navigate to the given destination and loads mock modules before * Angular. Assumes that the page being loaded uses Angular. * If you need to access a page which does not have Angular on load, use * the wrapped webdriver directly. * * @param {string} destination Destination URL. * @param {number=} opt_timeout Number of milliseconds to wait for Angular to * start. */ Protractor.prototype.get = function(destination, opt_timeout) { var timeout = opt_timeout ? opt_timeout : this.getPageTimeout; var self = this; destination = this.baseUrl.indexOf('file://') === 0 ? this.baseUrl + destination : url.resolve(this.baseUrl, destination); var msg = function(str) { return 'Protractor.get(' + destination + ') - ' + str; }; if (this.ignoreSynchronization) { return this.driver.get(destination); } this.driver.get(this.resetUrl); this.executeScript_( 'window.name = "' + DEFER_LABEL + '" + window.name;' + 'window.location.replace("' + destination + '");', msg('reset url')); // We need to make sure the new url has loaded before // we try to execute any asynchronous scripts. this.driver.wait(function() { return self.executeScript_('return window.location.href;', msg('get url')). then(function(url) { return url !== self.resetUrl; }, function(err) { if (err.code == 13) { // Ignore the error, and continue trying. This is because IE // driver sometimes (~1%) will throw an unknown error from this // execution. See https://github.com/angular/protractor/issues/841 // This shouldn't mask errors because it will fail with the timeout // anyway. return false; } else { throw err; } }); }, timeout, 'waiting for page to load for ' + timeout + 'ms'); // Make sure the page is an Angular page. self.executeAsyncScript_(clientSideScripts.testForAngular, msg('test for angular'), Math.floor(timeout / 1000)). then(function(angularTestResult) { var hasAngular = angularTestResult[0]; if (!hasAngular) { var message = angularTestResult[1]; throw new Error('Angular could not be found on the page ' + destination + ' : ' + message); } }, function(err) { throw 'Error while running testForAngular: ' + err.message; }); // At this point, Angular will pause for us until angular.resumeBootstrap // is called. var moduleNames = []; for (var i = 0; i < this.mockModules_.length; ++i) { var mockModule = this.mockModules_[i]; var name = mockModule.name; moduleNames.push(name); var executeScriptArgs = [mockModule.script, msg('add mock module ' + name)]. concat(mockModule.args); this.executeScript_.apply(this, executeScriptArgs). then(null, function(err) { throw 'Error while running module script ' + name + ': ' + err.message; }); } return this.executeScript_( 'angular.resumeBootstrap(arguments[0]);', msg('resume bootstrap'), moduleNames); }; /** * See webdriver.WebDriver.refresh * * Makes a full reload of the current page and loads mock modules before * Angular. Assumes that the page being loaded uses Angular. * If you need to access a page which does not have Angular on load, use * the wrapped webdriver directly. * * @param {number=} opt_timeout Number of seconds to wait for Angular to start. */ Protractor.prototype.refresh = function(opt_timeout) { var timeout = opt_timeout || 10; var self = this; if (self.ignoreSynchronization) { return self.driver.navigate().refresh(); } return self.executeScript_( 'return window.location.href', 'Protractor.refresh() - getUrl').then(function(href) { return self.get(href, timeout); }); }; /** * Mixin navigation methods back into the navigation object so that * they are invoked as before, i.e. driver.navigate().refresh() */ Protractor.prototype.navigate = function() { var nav = this.driver.navigate(); mixin(nav, this, 'refresh'); return nav; }; /** * Browse to another page using in-page navigation. * * @param {string} url In page URL using the same syntax as $location.url() * @returns {!webdriver.promise.Promise} A promise that will resolve once * page has been changed. */ Protractor.prototype.setLocation = function(url) { this.waitForAngular(); return this.executeScript_(clientSideScripts.setLocation, 'Protractor.setLocation()', this.rootEl, url).then(function(browserErr) { if (browserErr) { throw 'Error while navigating to \'' + url + '\' : ' + JSON.stringify(browserErr); } }); }; /** * Returns the current absolute url from AngularJS. */ Protractor.prototype.getLocationAbsUrl = function() { this.waitForAngular(); return this.executeScript_(clientSideScripts.getLocationAbsUrl, 'Protractor.getLocationAbsUrl()', this.rootEl); }; /** * Pauses the test and injects some helper functions into the browser, so that * debugging may be done in the browser console. * * This should be used under node in debug mode, i.e. with * protractor debug <configuration.js> * * @example * While in the debugger, commands can be scheduled through webdriver by * entering the repl: * debug> repl * Press Ctrl + C to leave rdebug repl * > ptor.findElement(protractor.By.input('user').sendKeys('Laura')); * > ptor.debugger(); * debug> c * * This will run the sendKeys command as the next task, then re-enter the * debugger. */ Protractor.prototype.debugger = function() { // jshint debug: true this.driver.executeScript(clientSideScripts.installInBrowser); webdriver.promise.controlFlow().execute(function() { debugger; }, 'add breakpoint to control flow'); }; /** * Beta (unstable) pause function for debugging webdriver tests. Use * browser.pause() in your test to enter the protractor debugger from that * point in the control flow. * Does not require changes to the command line (no need to add 'debug'). * * @example * element(by.id('foo')).click(); * browser.pause(); * // Execution will stop before the next click action. * element(by.id('bar')).click(); * * @param {number=} opt_debugPort Optional port to use for the debugging process */ Protractor.prototype.pause = function(opt_debugPort) { // Patch in a function to help us visualize what's going on in the control // flow. webdriver.promise.ControlF