UNPKG

protractor

Version:

Webdriver E2E test wrapper for Angular.

1,200 lines (1,099 loc) 36.3 kB
var url = require('url'); var webdriver = require('selenium-webdriver'); var clientSideScripts = require('./clientsidescripts.js'); var ProtractorBy = require('./locators.js').ProtractorBy; 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', 'toWireValue']; var STACK_SUBSTRINGS_TO_FILTER = [ 'node_modules/minijasminenode/lib/', 'node_modules/selenium-webdriver', 'at Module.', 'at Object.Module.', 'at Function.Module', '(timers.js:', 'jasminewd/index.js', 'protractor/lib/' ]; /* * 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 * @param {Array.<webdriver.Locator>=} 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]); } return base; }; /** * 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. * * @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'); * * @param {webdriver.Locator} locator An element locator. * @return {ElementFinder} */ var element = function(locator) { var elementFinder = {}; var webElementFns = WEB_ELEMENT_FUNCTIONS.concat( ['findElements', 'isElementPresent', 'evaluate']); 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; }); }); }; }); // 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 * <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 {!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 * <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 {Protractor} ptor * @param {Array.<webdriver.Locator>=} 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 * <div class="parent"> * <div class="child"> * Child text * <div class="grandchild">{{person.phone}}</div> * </div> * </div> * * @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)); }; return elementFinder; }; /** * element.all is used for operations on an array of elements (as opposed * to a single element). * * @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'); * }); * * @param {webdriver.Locator} locator * @return {ElementArrayFinder} */ element.all = function(locator) { var elementArrayFinder = {}; /** * Count the number of elements found by the locator. * * @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.count = function() { return using().findElements(locator).then(function(arr) { return arr.length; }); }; /** * Get an element found by the locator by index. The index starts at 0. * * @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 {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)); }; /** * Get the first element found using the locator. * * @alias element.all(locator).first() * @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.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)); }; /** * Get the last matching element for the locator. * * @alias element.all(locator).last() * @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.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)); }; /** * 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 * <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.<webdriver.WebElement>)} 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); }; /** * Calls the input function on each WebElement found by the locator. * * @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(webdriver.WebElement)} fn Input function */ elementArrayFinder.each = function(fn) { using().findElements(locator).then(function(arr) { arr.forEach(function(webElem) { fn(webElem); }); }); }; /** * 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 * <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(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; }); }; return elementArrayFinder; }; return element; }; /** * 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)); }; }; /** * 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)); }; }; /** * @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. * @constructor */ var Protractor = function(webdriver, 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') { if (methodsToSync.indexOf(method) !== -1) { mixin(this, webdriver, method, this.waitForAngular.bind(this)); } else { mixin(this, webdriver, method); } } } /** * 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 = webdriver; /** * Helper function for finding elements. * * @type {function(webdriver.Locator): ElementFinder} */ this.element = buildElementHelper(this); /** * Helper function for finding elements by css. * * @type {function(string): ElementFinder} */ this.$ = buildCssHelper(this); /** * Helper function for finding arrays of elements by css. * * @type {function(string): ElementArrayFinder} */ this.$$ = buildMultiCssHelper(this); /** * 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; /** * An object that holds custom test parameters. * * @type {Object} */ this.params = {}; this.moduleNames_ = []; this.moduleScripts_ = []; this.moduleArgs_ = []; }; /** * 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.driver.executeAsyncScript( clientSideScripts.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; } }); }; // 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 * <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 * @see webdriver.WebElement.findElement * @return {!webdriver.WebElement} */ element.$ = function(selector) { var locator = thisPtor.By.css(selector); return this.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 * <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 * @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 = thisPtor.By.css(selector); return this.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); }; 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); }; /** * 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, 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; }); }; /** * 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, varArgs) { this.waitForAngular(); if (locatorOrElement.findElementsOverride) { return locatorOrElement.findElementsOverride(this.driver).then(function(arr) { return !!arr.length; }); } return this.driver.isElementPresent(locatorOrElement, varArgs); }; /** * 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. * * @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) { this.moduleNames_.push(name); this.moduleScripts_.push(script); var moduleArgs = Array.prototype.slice.call(arguments, 2); this.moduleArgs_.push(moduleArgs); }; /** * Clear the list of registered mock modules. */ Protractor.prototype.clearMockModules = function() { this.moduleNames_ = []; this.moduleScripts_ = []; this.moduleArgs_ = []; }; /** * Remove a registered mock module. * @param {!string} name The name of the module to remove. */ Protractor.prototype.removeMockModule = function(name) { var index = this.moduleNames_.indexOf(name); this.moduleNames_.splice(index, 1); this.moduleScripts_.splice(index, 1); this.moduleArgs_.splice(index, 1); }; /** * 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 seconds to wait for Angular to start. */ Protractor.prototype.get = function(destination, opt_timeout) { var timeout = opt_timeout || 10; var self = this; destination = url.resolve(this.baseUrl, destination); if (this.ignoreSynchronization) { return this.driver.get(destination); } this.driver.get('about:blank'); this.driver.executeScript( 'window.name = "' + DEFER_LABEL + '" + window.name;' + 'window.location.replace("' + destination + '");'); // At this point, we need to make sure the new url has loaded before // we try to execute any asynchronous scripts. this.driver.wait(function() { return self.driver.executeScript('return window.location.href;').then(function(url) { return url !== 'about:blank'; }); }, timeout * 1000, 'Timed out waiting for page to load'); var assertAngularOnPage = function(arr) { var hasAngular = arr[0]; if (!hasAngular) { var message = arr[1]; throw new Error('Angular could not be found on the page ' + destination + ' : ' + message); } }; // Make sure the page is an Angular page. self.driver.executeAsyncScript(clientSideScripts.testForAngular, timeout). then(assertAngularOnPage, function(err) { throw 'Error while running testForAngular: ' + err.message; }); // At this point, Angular will pause for us, until angular.resumeBootstrap // is called. for (var i = 0; i < this.moduleScripts_.length; ++i) { var name = this.moduleNames_[i]; var executeScriptArgs = [this.moduleScripts_[i]]. concat(this.moduleArgs_[i]); this.driver.executeScript.apply(this, executeScriptArgs). then(null, function(err) { throw 'Error wile running module script ' + name + ': ' + err.message; }); } return this.driver.executeScript( 'angular.resumeBootstrap(arguments[0]);', this.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.driver.executeScript('return window.location.href').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.driver.executeScript(clientSideScripts.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.driver.executeScript(clientSideScripts.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> * * 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 var clientSideScriptsList = []; for (var script in clientSideScripts) { clientSideScriptsList.push( script + ': ' + clientSideScripts[script].toString()); } this.driver.executeScript( 'window.clientSideScripts = {' + clientSideScriptsList.join(', ') + '}'); var flow = webdriver.promise.controlFlow(); flow.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'). */ Protractor.prototype.pause = function() { // Patch in a function to help us visualize what's going on in the control // flow. webdriver.promise.ControlFlow.prototype.getControlFlowText = function() { var descriptions = []; var getDescriptions = function(frameOrTask, descriptions) { if (frameOrTask.getDescription) { var getRelevantStack = function(stack) { return stack.filter(function(line) { var include = true; for (var i = 0; i < STACK_SUBSTRINGS_TO_FILTER.length; ++i) { if (line.toString().indexOf(STACK_SUBSTRINGS_TO_FILTER[i]) !== -1) { include = false; } } return include; }); }; descriptions.push({ description: frameOrTask.getDescription(), stack: getRelevantStack(frameOrTask.snapshot_.getStacktrace()) }); } else { for (var i = 0; i < frameOrTask.children_.length; ++i) { getDescriptions(frameOrTask.children_[i], descriptions); } } }; if (this.history_.length) { getDescriptions(this.history_[this.history_.length - 1], descriptions); } if (this.activeFrame_.getPendingTask()) { getDescriptions(this.activeFrame_.getPendingTask(), descriptions); } getDescriptions(this.activeFrame_.getRoot(), descriptions); var asString = '-- WebDriver control flow schedule \n'; for (var i = 0; i < descriptions.length; ++i) { asString += ' |- ' + descriptions[i].description; if (descriptions[i].stack.length) { asString += '\n |---' + descriptions[i].stack.join('\n |---'); } if (i != (descriptions.length - 1)) { asString += '\n'; } } return asString; }; // Call this private function instead of sending SIGUSR1 because Windows. process._debugProcess(process.pid); var flow = webdriver.promise.controlFlow(); flow.execute(function() { console.log('Starting WebDriver debugger in a child process. Pause is ' + 'still beta, please report issues at github.com/angular/protractor'); var nodedebug = require('child_process'). fork(__dirname + '/wddebugger.js', ['localhost:5858']); process.on('exit', function() { nodedebug.kill('SIGTERM'); }); }); 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. * * @param {webdriver.WebDriver} webdriver The configured webdriver instance. * @param {string=} opt_baseUrl A URL to prepend to relative gets. * @return {Protractor} */ exports.wrapDriver = function(webdriver, opt_baseUrl, opt_rootElement) { return new Protractor(webdriver, opt_baseUrl, opt_rootElement); }; /** * @type {Protractor} */ var instance; /** * Set a singleton instance of protractor. * @param {Protractor} ptor */ exports.setInstance = function(ptor) { instance = ptor; }; /** * Get the singleton instance. * @return {Protractor} */ exports.getInstance = function() { return instance; }; /** * Utility function that filters a stack trace to be more readable. It removes * Jasmine test frames and webdriver promise resolution. * @param {string} text Original stack trace. * @return {string} */ exports.filterStackTrace = function(text) { if (!text) { return text; } var lines = []; text.split(/\n/).forEach(function(line) { var include = true; for (var i = 0; i < STACK_SUBSTRINGS_TO_FILTER.length; ++i) { if (line.indexOf(STACK_SUBSTRINGS_TO_FILTER[i]) !== -1) { include = false; } } if (include) { lines.push(line); } }); return lines.join('\n'); };