UNPKG

protractor

Version:

Webdriver E2E test wrapper for Angular.

585 lines (554 loc) 18.6 kB
/** * All scripts to be run on the client via executeAsyncScript or * executeScript should be put here. These scripts are transmitted over * the wire using their toString representation, and cannot reference * external variables. They can, however use the array passed in to * arguments. * * Some implementations seem to have issues with // comments, so use star-style * inside scripts. */ // jshint browser: true // jshint shadow: true /* global angular */ var clientSideScripts = exports; /** * Wait until Angular has finished rendering and has * no outstanding $http calls before continuing. * * Asynchronous. * * @param {string} selector The selector housing an ng-app * @param {function} callback callback */ clientSideScripts.waitForAngular = function(selector, callback) { var el = document.querySelector(selector); try { angular.element(el).injector().get('$browser'). notifyWhenNoOutstandingRequests(callback); } catch (e) { callback(e); } }; /** * 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. * * @return {Array.<Element>} The elements containing the binding. */ clientSideScripts.findBindings = function(binding, exactMatch, using) { using = using || document; 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('([^a-zA-Z\\d]|$)' + binding + '([^a-zA-Z\\d]|^)'); 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 {number} index The row index. * @param {Element} using The scope of the search. Defaults to 'document'. * * @return {Array.<Element>} The row of the repeater, or an array of elements * in the first row in the case of ng-repeat-start. */ clientSideScripts.findRepeaterRows = function(repeater, 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 (repeatElems[i].getAttribute(attr).indexOf(repeater) != -1) { 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 (repeatElems[i].getAttribute(attr).indexOf(repeater) != -1) { var elem = repeatElems[i]; var row = []; while (elem.nodeType != 8 || elem.nodeValue.indexOf(repeater) == -1) { if (elem.nodeType == 1) { row.push(elem); } elem = elem.nextSibling; } multiRows.push(row); } } } return [rows[index]].concat(multiRows[index]); }; /** * Find all rows of an ng-repeat. * * @param {string} repeater The text of the repeater, e.g. 'cat in cats'. * @param {Element} using The scope of the search. Defaults to 'document'. * * @return {Array.<Element>} All rows of the repeater. */ clientSideScripts.findAllRepeaterRows = function(repeater, 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 (repeatElems[i].getAttribute(attr).indexOf(repeater) != -1) { 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 (repeatElems[i].getAttribute(attr).indexOf(repeater) != -1) { var elem = repeatElems[i]; while (elem.nodeType != 8 || elem.nodeValue.indexOf(repeater) == -1) { rows.push(elem); elem = elem.nextSibling; } } } } return rows; }; /** * 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 {number} index The row index. * @param {string} binding The column binding, e.g. '{{cat.name}}'. * @param {Element} using The scope of the search. Defaults to 'document'. * * @return {Array.<Element>} The element in an array. */ clientSideScripts.findRepeaterElement = function(repeater, index, binding, using) { 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 (repeatElems[i].getAttribute(attr).indexOf(repeater) != -1) { 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 (repeatElems[i].getAttribute(attr).indexOf(repeater) != -1) { var elem = repeatElems[i]; var row = []; while (elem.nodeType != 8 || (elem.nodeValue && elem.nodeValue.indexOf(repeater) == -1)) { 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 (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 (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; }; /** * Find the elements in a column of an ng-repeat. * * @param {string} repeater The text of the repeater, e.g. 'cat in cats'. * @param {string} binding The column binding, e.g. '{{cat.name}}'. * @param {Element} using The scope of the search. Defaults to 'document'. * * @return {Array.<Element>} The elements in the column. */ clientSideScripts.findRepeaterColumn = function(repeater, binding, using) { 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 (repeatElems[i].getAttribute(attr).indexOf(repeater) != -1) { 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 (repeatElems[i].getAttribute(attr).indexOf(repeater) != -1) { var elem = repeatElems[i]; var row = []; while (elem.nodeType != 8 || (elem.nodeValue && elem.nodeValue.indexOf(repeater) == -1)) { if (elem.nodeType == 1) { row.push(elem); } elem = elem.nextSibling; } multiRows.push(row); } } } var bindings = []; for (var i = 0; i < rows.length; ++i) { 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) { 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; }; /** * Find an input elements by model name. * DEPRECATED - use findByModel * * @param {string} model The model name. * @param {Element} using The scope of the search. Defaults to 'document'. * * @return {Array.<Element>} The matching input elements. */ clientSideScripts.findInputs = function(model, using) { using = using || document; var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:']; for (var p = 0; p < prefixes.length; ++p) { var selector = 'input[' + prefixes[p] + 'model="' + model + '"]'; var inputs = using.querySelectorAll(selector); if (inputs.length) { return inputs; } } }; /** * Find elements by model name. * * @param {string} model The model name. * @param {Element} using The scope of the search. Defaults to 'document'. * * @return {Array.<Element>} The matching elements. */ clientSideScripts.findByModel = function(model, 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] + 'model="' + model + '"]'; 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. Defaults to 'document'. * * @return {Array.<Element>} The matching elements. */ clientSideScripts.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.innerText || element.textContent; } else { elementText = element.value; } if (elementText === 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. Defaults to 'document'. * * @return {Array.<Element>} The matching elements. */ clientSideScripts.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.innerText || element.textContent; } else { elementText = element.value; } if (elementText.indexOf(searchText) > -1) { matches.push(element); } } return matches; }; /** * Find multiple select elements by model name. * * @param {string} model The model name. * @param {Element} using The scope of the search. Defaults to 'document'. * * @return {Array.<Element>} The matching select elements. */ clientSideScripts.findSelects = function(model, using) { using = using || document; var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:']; for (var p = 0; p < prefixes.length; ++p) { var selector = 'select[' + prefixes[p] + 'model="' + model + '"]'; var inputs = using.querySelectorAll(selector); if (inputs.length) { return inputs; } } }; /** * Find selected option elements by model name. * * @param {string} model The model name. * @param {Element} using The scope of the search. Defaults to 'document'. * * @return {Array.<Element>} The matching select elements. */ clientSideScripts.findSelectedOptions = function(model, using) { using = using || document; var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:']; for (var p = 0; p < prefixes.length; ++p) { var selector = 'select[' + prefixes[p] + 'model="' + model + '"] option:checked'; var inputs = using.querySelectorAll(selector); if (inputs.length) { return inputs; } } }; /** * Find textarea elements by model name. * * @param {String} model The model name. * @param {Element} using The scope of the search. Defaults to 'document'. * * @return {Array.<Element>} An array of matching textarea elements. */ clientSideScripts.findTextareas = function(model, using) { using = using || document; var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:']; for (var p = 0; p < prefixes.length; ++p) { var selector = 'textarea[' + prefixes[p] + 'model="' + model + '"]'; var textareas = using.querySelectorAll(selector); if (textareas.length) { return textareas; } } }; /** * Find elements by css selector and textual content. * * @param {string} cssSelector The css selector to match. * @param {string} searchText The exact text to match. * @param {Element} using The scope of the search. Defaults to 'document'. * * @return {Array.<Element>} An array of matching elements. */ clientSideScripts.findByCssContainingText = function(cssSelector, searchText, using) { var using = using || document; var elements = using.querySelectorAll(cssSelector); var matches = []; for (var i = 0; i < elements.length; ++i) { var element = elements[i]; var elementText = element.innerText || element.textContent; if (elementText.indexOf(searchText) > -1) { 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 {function} asyncCallback callback */ clientSideScripts.testForAngular = function(attempts, asyncCallback) { var callback = function(args) { setTimeout(function() { asyncCallback(args); }, 0); }; var check = function(n) { try { if (window.angular && window.angular.resumeBootstrap) { callback([true, null]); } else if (n < 1) { if (window.angular) { callback([false, 'angular never provided resumeBootstrap']); } else { callback([false, 'retries looking for angular exceeded']); } } else { window.setTimeout(function() {check(n - 1);}, 1000); } } catch (e) { callback([false, 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. */ clientSideScripts.evaluate = function(element, expression) { return angular.element(element).scope().$eval(expression); }; /** * Return the current url using $location.absUrl(). * * @param {string} selector The selector housing an ng-app */ clientSideScripts.getLocationAbsUrl = function(selector) { var el = document.querySelector(selector); return angular.element(el).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 */ clientSideScripts.setLocation = function(selector, url) { var el = document.querySelector(selector); var $injector = angular.element(el).injector(); var $location = $injector.get('$location'); var $rootScope = $injector.get('$rootScope'); if (url !== $location.url()) { $location.url(url); $rootScope.$digest(); } };