UNPKG

protractor

Version:

Webdriver E2E test wrapper for Angular.

940 lines (876 loc) 31.5 kB
/** * All scripts to be run on the client via executeAsyncScript or * executeScript should be put here. * * NOTE: These scripts are transmitted over the wire as JavaScript text * constructed using their toString representation, and *cannot* * reference external variables. * * Some implementations seem to have issues with // comments, so use star-style * inside scripts. (TODO: add issue number / example implementations * that caused the switch to avoid the // comments.) */ // jshint browser: true // jshint shadow: true /* global angular */ var functions = {}; /////////////////////////////////////////////////////// //// //// //// HELPERS //// //// //// /////////////////////////////////////////////////////// /* Wraps a function up into a string with its helper functions so that it can * call those helper functions client side * * @param {function} fun The function to wrap up with its helpers * @param {...function} The helper functions. Each function must be named * * @return {string} The string which, when executed, will invoke fun in such a * way that it has access to its helper functions */ function wrapWithHelpers(fun) { var helpers = Array.prototype.slice.call(arguments, 1); if (!helpers.length) { return fun; } var FunClass = Function; // Get the linter to allow this eval return new FunClass( helpers.join(';') + String.fromCharCode(59) + ' return (' + fun.toString() + ').apply(this, arguments);'); } /* Tests if an ngRepeat matches a repeater * * @param {string} ngRepeat The ngRepeat to test * @param {string} repeater The repeater to test against * @param {boolean} exact If the ngRepeat expression needs to match the whole * repeater (not counting any `track by ...` modifier) or if it just needs to * match a substring * @return {boolean} If the ngRepeat matched the repeater */ function repeaterMatch(ngRepeat, repeater, exact) { if (exact) { return ngRepeat.split(' track by ')[0].split(' as ')[0].split('|')[0]. split('=')[0].trim() == repeater; } else { return ngRepeat.indexOf(repeater) != -1; } } /* Tries to find $$testability and possibly $injector for an ng1 app * * By default, doesn't care about $injector if it finds $$testability. However, * these priorities can be reversed. * * @param {string=} selector The selector for the element with the injector. If * falsy, tries a variety of methods to find an injector * @param {boolean=} injectorPlease Prioritize finding an injector * @return {$$testability?: Testability, $injector?: Injector} Returns whatever * ng1 app hooks it finds */ function getNg1Hooks(selector, injectorPlease) { function tryEl(el) { try { if (!injectorPlease && angular.getTestability) { var $$testability = angular.getTestability(el); if ($$testability) { return {$$testability: $$testability}; } } else { var $injector = angular.element(el).injector(); if ($injector) { return {$injector: $injector}; } } } catch(err) {} } function trySelector(selector) { var els = document.querySelectorAll(selector); for (var i = 0; i < els.length; i++) { var elHooks = tryEl(els[i]); if (elHooks) { return elHooks; } } } if (selector) { return trySelector(selector); } else if (window.__TESTABILITY__NG1_APP_ROOT_INJECTOR__) { var $injector = window.__TESTABILITY__NG1_APP_ROOT_INJECTOR__; var $$testability = null; try { $$testability = $injector.get('$$testability'); } catch (e) {} return {$injector: $injector, $$testability: $$testability}; } else { return tryEl(document.body) || trySelector('[ng-app]') || trySelector('[ng\\:app]') || trySelector('[ng-controller]') || trySelector('[ng\\:controller]'); } } /////////////////////////////////////////////////////// //// //// //// SCRIPTS //// //// //// /////////////////////////////////////////////////////// /** * Wait until Angular has finished rendering and has * no outstanding $http calls before continuing. The specific Angular app * is determined by the rootSelector. * * Asynchronous. * * @param {string} rootSelector The selector housing an ng-app * @param {function(string)} callback callback. If a failure occurs, it will * be passed as a parameter. */ functions.waitForAngular = function(rootSelector, callback) { try { // Wait for both angular1 testability and angular2 testability. var testCallback = callback; // Wait for angular1 testability first and run waitForAngular2 as a callback var waitForAngular1 = function(callback) { if (window.angular) { var hooks = getNg1Hooks(rootSelector); if (!hooks){ callback(); // not an angular1 app } else{ if (hooks.$$testability) { hooks.$$testability.whenStable(callback); } else if (hooks.$injector) { hooks.$injector.get('$browser') .notifyWhenNoOutstandingRequests(callback); } else if (!rootSelector) { throw new Error( 'Could not automatically find injector on page: "' + window.location.toString() + '". Consider using config.rootEl'); } else { throw new Error( 'root element (' + rootSelector + ') has no injector.' + ' this may mean it is not inside ng-app.'); } } } else {callback();} // not an angular1 app }; // Wait for Angular2 testability and then run test callback var waitForAngular2 = function() { if (window.getAngularTestability) { if (rootSelector) { var testability = null; var el = document.querySelector(rootSelector); try{ testability = window.getAngularTestability(el); } catch(e){} if (testability) { testability.whenStable(testCallback); return; } } // Didn't specify root element or testability could not be found // by rootSelector. This may happen in a hybrid app, which could have // more than one root. var testabilities = window.getAllAngularTestabilities(); var count = testabilities.length; // No angular2 testability, this happens when // going to a hybrid page and going back to a pure angular1 page if (count === 0) { testCallback(); return; } var decrement = function() { count--; if (count === 0) { testCallback(); } }; testabilities.forEach(function(testability) { testability.whenStable(decrement); }); } else {testCallback();} // not an angular2 app }; if (!(window.angular) && !(window.getAngularTestability)) { // no testability hook throw new Error( 'both angularJS testability and angular testability are undefined.' + ' This could be either ' + 'because this is a non-angular page or because your test involves ' + 'client-side navigation, which can interfere with Protractor\'s ' + 'bootstrapping. See http://git.io/v4gXM for details'); } else {waitForAngular1(waitForAngular2);} // Wait for angular1 and angular2 // Testability hooks sequentially } catch (err) { callback(err.message); } }; /** * Find a list of elements in the page by their angular binding. * * @param {string} binding The binding, e.g. {{cat.name}}. * @param {boolean} exactMatch Whether the binding needs to be matched exactly * @param {Element} using The scope of the search. * @param {string} rootSelector The selector to use for the root app element. * * @return {Array.<Element>} The elements containing the binding. */ functions.findBindings = function(binding, exactMatch, using, rootSelector) { using = using || document; if (angular.getTestability) { return getNg1Hooks(rootSelector).$$testability. findBindings(using, binding, exactMatch); } var bindings = using.getElementsByClassName('ng-binding'); var matches = []; for (var i = 0; i < bindings.length; ++i) { var dataBinding = angular.element(bindings[i]).data('$binding'); if (dataBinding) { var bindingName = dataBinding.exp || dataBinding[0].exp || dataBinding; if (exactMatch) { var matcher = new RegExp('({|\\s|^|\\|)' + /* See http://stackoverflow.com/q/3561711 */ binding.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&') + '(}|\\s|$|\\|)'); if (matcher.test(bindingName)) { matches.push(bindings[i]); } } else { if (bindingName.indexOf(binding) != -1) { matches.push(bindings[i]); } } } } return matches; /* Return the whole array for webdriver.findElements. */ }; /** * Find an array of elements matching a row within an ng-repeat. * Always returns an array of only one element for plain old ng-repeat. * Returns an array of all the elements in one segment for ng-repeat-start. * * @param {string} repeater The text of the repeater, e.g. 'cat in cats'. * @param {boolean} exact Whether the repeater needs to be matched exactly * @param {number} index The row index. * @param {Element} using The scope of the search. * * @return {Array.<Element>} The row of the repeater, or an array of elements * in the first row in the case of ng-repeat-start. */ function findRepeaterRows(repeater, exact, index, using) { using = using || document; var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:']; var rows = []; for (var p = 0; p < prefixes.length; ++p) { var attr = prefixes[p] + 'repeat'; var repeatElems = using.querySelectorAll('[' + attr + ']'); attr = attr.replace(/\\/g, ''); for (var i = 0; i < repeatElems.length; ++i) { if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) { rows.push(repeatElems[i]); } } } /* multiRows is an array of arrays, where each inner array contains one row of elements. */ var multiRows = []; for (var p = 0; p < prefixes.length; ++p) { var attr = prefixes[p] + 'repeat-start'; var repeatElems = using.querySelectorAll('[' + attr + ']'); attr = attr.replace(/\\/g, ''); for (var i = 0; i < repeatElems.length; ++i) { if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) { var elem = repeatElems[i]; var row = []; while (elem.nodeType != 8 || !repeaterMatch(elem.nodeValue, repeater)) { if (elem.nodeType == 1) { row.push(elem); } elem = elem.nextSibling; } multiRows.push(row); } } } var row = rows[index] || [], multiRow = multiRows[index] || []; return [].concat(row, multiRow); } functions.findRepeaterRows = wrapWithHelpers(findRepeaterRows, repeaterMatch); /** * Find all rows of an ng-repeat. * * @param {string} repeater The text of the repeater, e.g. 'cat in cats'. * @param {boolean} exact Whether the repeater needs to be matched exactly * @param {Element} using The scope of the search. * * @return {Array.<Element>} All rows of the repeater. */ function findAllRepeaterRows(repeater, exact, using) { using = using || document; var rows = []; var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:']; for (var p = 0; p < prefixes.length; ++p) { var attr = prefixes[p] + 'repeat'; var repeatElems = using.querySelectorAll('[' + attr + ']'); attr = attr.replace(/\\/g, ''); for (var i = 0; i < repeatElems.length; ++i) { if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) { rows.push(repeatElems[i]); } } } for (var p = 0; p < prefixes.length; ++p) { var attr = prefixes[p] + 'repeat-start'; var repeatElems = using.querySelectorAll('[' + attr + ']'); attr = attr.replace(/\\/g, ''); for (var i = 0; i < repeatElems.length; ++i) { if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) { var elem = repeatElems[i]; while (elem.nodeType != 8 || !repeaterMatch(elem.nodeValue, repeater)) { if (elem.nodeType == 1) { rows.push(elem); } elem = elem.nextSibling; } } } } return rows; } functions.findAllRepeaterRows = wrapWithHelpers(findAllRepeaterRows, repeaterMatch); /** * Find an element within an ng-repeat by its row and column. * * @param {string} repeater The text of the repeater, e.g. 'cat in cats'. * @param {boolean} exact Whether the repeater needs to be matched exactly * @param {number} index The row index. * @param {string} binding The column binding, e.g. '{{cat.name}}'. * @param {Element} using The scope of the search. * @param {string} rootSelector The selector to use for the root app element. * * @return {Array.<Element>} The element in an array. */ function findRepeaterElement(repeater, exact, index, binding, using, rootSelector) { var matches = []; using = using || document; var rows = []; var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:']; for (var p = 0; p < prefixes.length; ++p) { var attr = prefixes[p] + 'repeat'; var repeatElems = using.querySelectorAll('[' + attr + ']'); attr = attr.replace(/\\/g, ''); for (var i = 0; i < repeatElems.length; ++i) { if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) { rows.push(repeatElems[i]); } } } /* multiRows is an array of arrays, where each inner array contains one row of elements. */ var multiRows = []; for (var p = 0; p < prefixes.length; ++p) { var attr = prefixes[p] + 'repeat-start'; var repeatElems = using.querySelectorAll('[' + attr + ']'); attr = attr.replace(/\\/g, ''); for (var i = 0; i < repeatElems.length; ++i) { if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) { var elem = repeatElems[i]; var row = []; while (elem.nodeType != 8 || (elem.nodeValue && !repeaterMatch(elem.nodeValue, repeater))) { if (elem.nodeType == 1) { row.push(elem); } elem = elem.nextSibling; } multiRows.push(row); } } } var row = rows[index]; var multiRow = multiRows[index]; var bindings = []; if (row) { if (angular.getTestability) { matches.push.apply( matches, getNg1Hooks(rootSelector).$$testability.findBindings(row, binding)); } else { if (row.className.indexOf('ng-binding') != -1) { bindings.push(row); } var childBindings = row.getElementsByClassName('ng-binding'); for (var i = 0; i < childBindings.length; ++i) { bindings.push(childBindings[i]); } } } if (multiRow) { for (var i = 0; i < multiRow.length; ++i) { var rowElem = multiRow[i]; if (angular.getTestability) { matches.push.apply( matches, getNg1Hooks(rootSelector).$$testability.findBindings(rowElem, binding)); } else { if (rowElem.className.indexOf('ng-binding') != -1) { bindings.push(rowElem); } var childBindings = rowElem.getElementsByClassName('ng-binding'); for (var j = 0; j < childBindings.length; ++j) { bindings.push(childBindings[j]); } } } } for (var i = 0; i < bindings.length; ++i) { var dataBinding = angular.element(bindings[i]).data('$binding'); if (dataBinding) { var bindingName = dataBinding.exp || dataBinding[0].exp || dataBinding; if (bindingName.indexOf(binding) != -1) { matches.push(bindings[i]); } } } return matches; } functions.findRepeaterElement = wrapWithHelpers(findRepeaterElement, repeaterMatch, getNg1Hooks); /** * Find the elements in a column of an ng-repeat. * * @param {string} repeater The text of the repeater, e.g. 'cat in cats'. * @param {boolean} exact Whether the repeater needs to be matched exactly * @param {string} binding The column binding, e.g. '{{cat.name}}'. * @param {Element} using The scope of the search. * @param {string} rootSelector The selector to use for the root app element. * * @return {Array.<Element>} The elements in the column. */ function findRepeaterColumn(repeater, exact, binding, using, rootSelector) { var matches = []; using = using || document; var rows = []; var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:']; for (var p = 0; p < prefixes.length; ++p) { var attr = prefixes[p] + 'repeat'; var repeatElems = using.querySelectorAll('[' + attr + ']'); attr = attr.replace(/\\/g, ''); for (var i = 0; i < repeatElems.length; ++i) { if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) { rows.push(repeatElems[i]); } } } /* multiRows is an array of arrays, where each inner array contains one row of elements. */ var multiRows = []; for (var p = 0; p < prefixes.length; ++p) { var attr = prefixes[p] + 'repeat-start'; var repeatElems = using.querySelectorAll('[' + attr + ']'); attr = attr.replace(/\\/g, ''); for (var i = 0; i < repeatElems.length; ++i) { if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) { var elem = repeatElems[i]; var row = []; while (elem.nodeType != 8 || (elem.nodeValue && !repeaterMatch(elem.nodeValue, repeater))) { if (elem.nodeType == 1) { row.push(elem); } elem = elem.nextSibling; } multiRows.push(row); } } } var bindings = []; for (var i = 0; i < rows.length; ++i) { if (angular.getTestability) { matches.push.apply( matches, getNg1Hooks(rootSelector).$$testability.findBindings(rows[i], binding)); } else { if (rows[i].className.indexOf('ng-binding') != -1) { bindings.push(rows[i]); } var childBindings = rows[i].getElementsByClassName('ng-binding'); for (var k = 0; k < childBindings.length; ++k) { bindings.push(childBindings[k]); } } } for (var i = 0; i < multiRows.length; ++i) { for (var j = 0; j < multiRows[i].length; ++j) { if (angular.getTestability) { matches.push.apply( matches, getNg1Hooks(rootSelector).$$testability.findBindings( multiRows[i][j], binding)); } else { var elem = multiRows[i][j]; if (elem.className.indexOf('ng-binding') != -1) { bindings.push(elem); } var childBindings = elem.getElementsByClassName('ng-binding'); for (var k = 0; k < childBindings.length; ++k) { bindings.push(childBindings[k]); } } } } for (var j = 0; j < bindings.length; ++j) { var dataBinding = angular.element(bindings[j]).data('$binding'); if (dataBinding) { var bindingName = dataBinding.exp || dataBinding[0].exp || dataBinding; if (bindingName.indexOf(binding) != -1) { matches.push(bindings[j]); } } } return matches; } functions.findRepeaterColumn = wrapWithHelpers(findRepeaterColumn, repeaterMatch, getNg1Hooks); /** * Find elements by model name. * * @param {string} model The model name. * @param {Element} using The scope of the search. * @param {string} rootSelector The selector to use for the root app element. * * @return {Array.<Element>} The matching elements. */ functions.findByModel = function(model, using, rootSelector) { using = using || document; if (angular.getTestability) { return getNg1Hooks(rootSelector).$$testability. findModels(using, model, true); } var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:']; for (var p = 0; p < prefixes.length; ++p) { var selector = '[' + prefixes[p] + 'model="' + model + '"]'; var elements = using.querySelectorAll(selector); if (elements.length) { return elements; } } }; /** * Find elements by options. * * @param {string} optionsDescriptor The descriptor for the option * (i.e. fruit for fruit in fruits). * @param {Element} using The scope of the search. * * @return {Array.<Element>} The matching elements. */ functions.findByOptions = function(optionsDescriptor, using) { using = using || document; var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:']; for (var p = 0; p < prefixes.length; ++p) { var selector = '[' + prefixes[p] + 'options="' + optionsDescriptor + '"] option'; var elements = using.querySelectorAll(selector); if (elements.length) { return elements; } } }; /** * Find buttons by textual content. * * @param {string} searchText The exact text to match. * @param {Element} using The scope of the search. * * @return {Array.<Element>} The matching elements. */ functions.findByButtonText = function(searchText, using) { using = using || document; var elements = using.querySelectorAll('button, input[type="button"], input[type="submit"]'); var matches = []; for (var i = 0; i < elements.length; ++i) { var element = elements[i]; var elementText; if (element.tagName.toLowerCase() == 'button') { elementText = element.textContent || element.innerText || ''; } else { elementText = element.value; } if (elementText.trim() === searchText) { matches.push(element); } } return matches; }; /** * Find buttons by textual content. * * @param {string} searchText The exact text to match. * @param {Element} using The scope of the search. * * @return {Array.<Element>} The matching elements. */ functions.findByPartialButtonText = function(searchText, using) { using = using || document; var elements = using.querySelectorAll('button, input[type="button"], input[type="submit"]'); var matches = []; for (var i = 0; i < elements.length; ++i) { var element = elements[i]; var elementText; if (element.tagName.toLowerCase() == 'button') { elementText = element.textContent || element.innerText || ''; } else { elementText = element.value; } if (elementText.indexOf(searchText) > -1) { matches.push(element); } } return matches; }; /** * Find elements by css selector and textual content. * * @param {string} cssSelector The css selector to match. * @param {string} searchText The exact text to match or a serialized regex. * @param {Element} using The scope of the search. * * @return {Array.<Element>} An array of matching elements. */ functions.findByCssContainingText = function(cssSelector, searchText, using) { using = using || document; if (searchText.indexOf('__REGEXP__') === 0) { var match = searchText.split('__REGEXP__')[1].match(/\/(.*)\/(.*)?/); searchText = new RegExp(match[1], match[2] || ''); } var elements = using.querySelectorAll(cssSelector); var matches = []; for (var i = 0; i < elements.length; ++i) { var element = elements[i]; var elementText = element.textContent || element.innerText || ''; var elementMatches = searchText instanceof RegExp ? searchText.test(elementText) : elementText.indexOf(searchText) > -1; if (elementMatches) { matches.push(element); } } return matches; }; /** * Tests whether the angular global variable is present on a page. Retries * in case the page is just loading slowly. * * Asynchronous. * * @param {number} attempts Number of times to retry. * @param {boolean} ng12Hybrid Flag set if app is a hybrid of angular 1 and 2 * @param {function({version: ?number, message: ?string})} asyncCallback callback * */ functions.testForAngular = function(attempts, ng12Hybrid, asyncCallback) { var callback = function(args) { setTimeout(function() { asyncCallback(args); }, 0); }; var definitelyNg1 = !!ng12Hybrid; var definitelyNg2OrNewer = false; var check = function(n) { try { /* Figure out which version of angular we're waiting on */ if (!definitelyNg1 && !definitelyNg2OrNewer) { if (window.angular && !(window.angular.version && window.angular.version.major > 1)) { definitelyNg1 = true; } else if (window.getAllAngularTestabilities) { definitelyNg2OrNewer = true; } } /* See if our version of angular is ready */ if (definitelyNg1) { if (window.angular && window.angular.resumeBootstrap) { return callback({ver: 1}); } } else if (definitelyNg2OrNewer) { if (true /* ng2 has no resumeBootstrap() */) { return callback({ver: 2}); } } /* Try again (or fail) */ if (n < 1) { if (definitelyNg1 && window.angular) { callback({message: 'angular never provided resumeBootstrap'}); } else if (ng12Hybrid && !window.angular) { callback({message: 'angular 1 never loaded' + window.getAllAngularTestabilities ? ' (are you sure this app ' + 'uses ngUpgrade? Try un-setting ng12Hybrid)' : ''}); } else { callback({message: 'retries looking for angular exceeded'}); } } else { window.setTimeout(function() {check(n - 1);}, 1000); } } catch (e) { callback({message: e}); } }; check(attempts); }; /** * Evalute an Angular expression in the context of a given element. * * @param {Element} element The element in whose scope to evaluate. * @param {string} expression The expression to evaluate. * * @return {?Object} The result of the evaluation. */ functions.evaluate = function(element, expression) { return angular.element(element).scope().$eval(expression); }; functions.allowAnimations = function(element, value) { var ngElement = angular.element(element); if (ngElement.allowAnimations) { // AngularDart: $testability API. return ngElement.allowAnimations(value); } else { // AngularJS var enabledFn = ngElement.injector().get('$animate').enabled; return (value == null) ? enabledFn() : enabledFn(value); } }; /** * Return the current url using $location.absUrl(). * * @param {string} selector The selector housing an ng-app */ functions.getLocationAbsUrl = function(selector) { var hooks = getNg1Hooks(selector); if (angular.getTestability) { return hooks.$$testability.getLocation(); } return hooks.$injector.get('$location').absUrl(); }; /** * Browse to another page using in-page navigation. * * @param {string} selector The selector housing an ng-app * @param {string} url In page URL using the same syntax as $location.url(), * /path?search=a&b=c#hash */ functions.setLocation = function(selector, url) { var hooks = getNg1Hooks(selector); if (angular.getTestability) { return hooks.$$testability.setLocation(url); } var $injector = hooks.$injector; var $location = $injector.get('$location'); var $rootScope = $injector.get('$rootScope'); if (url !== $location.url()) { $location.url(url); $rootScope.$digest(); } }; /** * Retrieve the pending $http requests. * * @param {string} selector The selector housing an ng-app * @return {!Array<!Object>} An array of pending http requests. */ functions.getPendingHttpRequests = function(selector) { var hooks = getNg1Hooks(selector, true); var $http = hooks.$injector.get('$http'); return $http.pendingRequests; }; ['waitForAngular', 'findBindings', 'findByModel', 'getLocationAbsUrl', 'setLocation', 'getPendingHttpRequests'].forEach(function(funName) { functions[funName] = wrapWithHelpers(functions[funName], getNg1Hooks); }); /* Publish all the functions as strings to pass to WebDriver's * exec[Async]Script. In addition, also include a script that will * install all the functions on window (for debugging.) * * We also wrap any exceptions thrown by a clientSideScripts function * that is not an instance of the Error type into an Error type. If we * don't do so, then the resulting stack trace is completely unhelpful * and the exception message is just "unknown error." These types of * exceptions are the common case for dart2js code. This wrapping gives * us the Dart stack trace and exception message. */ var util = require('util'); var scriptsList = []; var scriptFmt = ( 'try { return (%s).apply(this, arguments); }\n' + 'catch(e) { throw (e instanceof Error) ? e : new Error(e); }'); for (var fnName in functions) { if (functions.hasOwnProperty(fnName)) { exports[fnName] = util.format(scriptFmt, functions[fnName]); scriptsList.push(util.format('%s: %s', fnName, functions[fnName])); } } exports.installInBrowser = (util.format( 'window.clientSideScripts = {%s};', scriptsList.join(', '))); /** * Automatically installed by Protractor when a page is loaded, this * default mock module decorates $timeout to keep track of any * outstanding timeouts. * * @param {boolean} trackOutstandingTimeouts */ exports.protractorBaseModuleFn = function(trackOutstandingTimeouts) { var ngMod = angular.module('protractorBaseModule_', []).config([ '$compileProvider', function($compileProvider) { if ($compileProvider.debugInfoEnabled) { $compileProvider.debugInfoEnabled(true); } } ]); if (trackOutstandingTimeouts) { ngMod.config([ '$provide', function ($provide) { $provide.decorator('$timeout', [ '$delegate', function ($delegate) { var $timeout = $delegate; var taskId = 0; if (!window['NG_PENDING_TIMEOUTS']) { window['NG_PENDING_TIMEOUTS'] = {}; } var extendedTimeout= function() { var args = Array.prototype.slice.call(arguments); if (typeof(args[0]) !== 'function') { return $timeout.apply(null, args); } taskId++; var fn = args[0]; window['NG_PENDING_TIMEOUTS'][taskId] = fn.toString(); var wrappedFn = (function(taskId_) { return function() { delete window['NG_PENDING_TIMEOUTS'][taskId_]; return fn.apply(null, arguments); }; })(taskId); args[0] = wrappedFn; var promise = $timeout.apply(null, args); promise.ptorTaskId_ = taskId; return promise; }; extendedTimeout.cancel = function() { var taskId_ = arguments[0] && arguments[0].ptorTaskId_; if (taskId_) { delete window['NG_PENDING_TIMEOUTS'][taskId_]; } return $timeout.cancel.apply($timeout, arguments); }; return extendedTimeout; } ]); } ]); } };