Skip to content
This repository has been archived by the owner on Jul 29, 2024. It is now read-only.

Commit

Permalink
feat(rootEl): ***breaking change*** auto-detect the root element bett…
Browse files Browse the repository at this point in the history
…er (#3849)

This is a breaking change because it changes the default root element behavior
and removes the `config.useAllAngular2AppRoots` flag.  Modern angular apps now
default to using all app hooks, and ng1 apps now check several places, notably
the element the app bootstraps to.

Closes #1742
  • Loading branch information
sjelin authored Jan 3, 2017
1 parent c194af8 commit 9a73d41
Show file tree
Hide file tree
Showing 15 changed files with 174 additions and 121 deletions.
11 changes: 5 additions & 6 deletions lib/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ export class ProtractorBrowser extends AbstractExtendedWebDriver {
this.$ = build$(this.element, By);
this.$$ = build$$(this.element, By);
this.baseUrl = opt_baseUrl || '';
this.rootEl = opt_rootElement || 'body';
this.rootEl = opt_rootElement || '';
this.ignoreSynchronization = false;
this.getPageTimeout = DEFAULT_GET_PAGE_TIMEOUT;
this.params = {};
Expand Down Expand Up @@ -522,13 +522,10 @@ export class ProtractorBrowser extends AbstractExtendedWebDriver {
let runWaitForAngularScript: () => wdpromise.Promise<any> = () => {
if (this.plugins_.skipAngularStability() || this.bpClient) {
return wdpromise.fulfilled();
} else if (this.rootEl) {
} else {
return this.executeAsyncScript_(
clientSideScripts.waitForAngular, 'Protractor.waitForAngular()' + description,
this.rootEl);
} else {
return this.executeAsyncScript_(
clientSideScripts.waitForAllAngular2, 'Protractor.waitForAngular()' + description);
}
};

Expand Down Expand Up @@ -841,7 +838,9 @@ export class ProtractorBrowser extends AbstractExtendedWebDriver {
}

self.executeScriptWithDescription(
'angular.resumeBootstrap(arguments[0]);', msg('resume bootstrap'), moduleNames)
'window.__TESTABILITY__NG1_APP_ROOT_INJECTOR__ = ' +
'angular.resumeBootstrap(arguments[0]);',
msg('resume bootstrap'), moduleNames)
.then(null, deferred.reject);
} else {
// TODO: support mock modules in Angular2. For now, error if someone
Expand Down
203 changes: 136 additions & 67 deletions lib/clientsidescripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
/* 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
*
Expand All @@ -36,6 +43,84 @@ function wrapWithHelpers(fun) {
' 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
Expand All @@ -48,22 +133,38 @@ function wrapWithHelpers(fun) {
* be passed as a parameter.
*/
functions.waitForAngular = function(rootSelector, callback) {
var el = document.querySelector(rootSelector);

try {
if (window.angular && !(window.angular.version &&
window.angular.version.major > 1)) {
if (angular.getTestability) {
angular.getTestability(el).whenStable(callback);
} else if (angular.element(el).injector()) {
angular.element(el).injector().get('$browser').
window.angular.version.major > 1)) {
/* ng1 */
let hooks = getNg1Hooks(rootSelector);
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 if (window.getAngularTestability) {
} else if (rootSelector && window.getAngularTestability) {
var el = document.querySelector(rootSelector);
window.getAngularTestability(el).whenStable(callback);
} else if (window.getAllAngularTestabilities) {
var testabilities = window.getAllAngularTestabilities();
var count = testabilities.length;
var decrement = function() {
count--;
if (count === 0) {
callback();
}
};
testabilities.forEach(function(testability) {
testability.whenStable(decrement);
});
} else if (!window.angular) {
throw new Error('window.angular is undefined. This could be either ' +
'because this is a non-angular page or because your test involves ' +
Expand All @@ -75,39 +176,13 @@ functions.waitForAngular = function(rootSelector, callback) {
'obfuscation.');
} else {
throw new Error('Cannot get testability API for unknown angular ' +
'version "' + window.angular.version + '"');
'version "' + window.angular.version + '"');
}
} catch (err) {
callback(err.message);
}
};

/**
* Wait until all Angular2 applications on the page have become stable.
*
* Asynchronous.
*
* @param {function(string)} callback callback. If a failure occurs, it will
* be passed as a parameter.
*/
functions.waitForAllAngular2 = function(callback) {
try {
var testabilities = window.getAllAngularTestabilities();
var count = testabilities.length;
var decrement = function() {
count--;
if (count === 0) {
callback();
}
};
testabilities.forEach(function(testability) {
testability.whenStable(decrement);
});
} catch (err) {
callback(err.message);
}
};

/**
* Find a list of elements in the page by their angular binding.
*
Expand All @@ -119,10 +194,9 @@ functions.waitForAllAngular2 = function(callback) {
* @return {Array.<Element>} The elements containing the binding.
*/
functions.findBindings = function(binding, exactMatch, using, rootSelector) {
var root = document.querySelector(rootSelector || 'body');
using = using || document;
if (angular.getTestability) {
return angular.getTestability(root).
return getNg1Hooks(rootSelector).$$testability.
findBindings(using, binding, exactMatch);
}
var bindings = using.getElementsByClassName('ng-binding');
Expand Down Expand Up @@ -150,15 +224,6 @@ functions.findBindings = function(binding, exactMatch, using, rootSelector) {
return matches; /* Return the whole array for webdriver.findElements. */
};

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;
}
}

/**
* 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.
Expand Down Expand Up @@ -273,7 +338,6 @@ functions.findAllRepeaterRows = wrapWithHelpers(findAllRepeaterRows, repeaterMat
*/
function findRepeaterElement(repeater, exact, index, binding, using, rootSelector) {
var matches = [];
var root = document.querySelector(rootSelector || 'body');
using = using || document;

var rows = [];
Expand Down Expand Up @@ -317,7 +381,7 @@ function findRepeaterElement(repeater, exact, index, binding, using, rootSelecto
if (angular.getTestability) {
matches.push.apply(
matches,
angular.getTestability(root).findBindings(row, binding));
getNg1Hooks(rootSelector).$$testability.findBindings(row, binding));
} else {
if (row.className.indexOf('ng-binding') != -1) {
bindings.push(row);
Expand All @@ -334,7 +398,8 @@ function findRepeaterElement(repeater, exact, index, binding, using, rootSelecto
if (angular.getTestability) {
matches.push.apply(
matches,
angular.getTestability(root).findBindings(rowElem, binding));
getNg1Hooks(rootSelector).$$testability.findBindings(rowElem,
binding));
} else {
if (rowElem.className.indexOf('ng-binding') != -1) {
bindings.push(rowElem);
Expand All @@ -357,7 +422,8 @@ function findRepeaterElement(repeater, exact, index, binding, using, rootSelecto
}
return matches;
}
functions.findRepeaterElement = wrapWithHelpers(findRepeaterElement, repeaterMatch);
functions.findRepeaterElement =
wrapWithHelpers(findRepeaterElement, repeaterMatch, getNg1Hooks);

/**
* Find the elements in a column of an ng-repeat.
Expand All @@ -372,7 +438,6 @@ functions.findRepeaterElement = wrapWithHelpers(findRepeaterElement, repeaterMat
*/
function findRepeaterColumn(repeater, exact, binding, using, rootSelector) {
var matches = [];
var root = document.querySelector(rootSelector || 'body');
using = using || document;

var rows = [];
Expand Down Expand Up @@ -414,7 +479,8 @@ function findRepeaterColumn(repeater, exact, binding, using, rootSelector) {
if (angular.getTestability) {
matches.push.apply(
matches,
angular.getTestability(root).findBindings(rows[i], binding));
getNg1Hooks(rootSelector).$$testability.findBindings(rows[i],
binding));
} else {
if (rows[i].className.indexOf('ng-binding') != -1) {
bindings.push(rows[i]);
Expand All @@ -430,7 +496,8 @@ function findRepeaterColumn(repeater, exact, binding, using, rootSelector) {
if (angular.getTestability) {
matches.push.apply(
matches,
angular.getTestability(root).findBindings(multiRows[i][j], binding));
getNg1Hooks(rootSelector).$$testability.findBindings(
multiRows[i][j], binding));
} else {
var elem = multiRows[i][j];
if (elem.className.indexOf('ng-binding') != -1) {
Expand All @@ -454,7 +521,8 @@ function findRepeaterColumn(repeater, exact, binding, using, rootSelector) {
}
return matches;
}
functions.findRepeaterColumn = wrapWithHelpers(findRepeaterColumn, repeaterMatch);
functions.findRepeaterColumn =
wrapWithHelpers(findRepeaterColumn, repeaterMatch, getNg1Hooks);

/**
* Find elements by model name.
Expand All @@ -466,11 +534,10 @@ functions.findRepeaterColumn = wrapWithHelpers(findRepeaterColumn, repeaterMatch
* @return {Array.<Element>} The matching elements.
*/
functions.findByModel = function(model, using, rootSelector) {
var root = document.querySelector(rootSelector || 'body');
using = using || document;

if (angular.getTestability) {
return angular.getTestability(root).
return getNg1Hooks(rootSelector).$$testability.
findModels(using, model, true);
}
var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:'];
Expand Down Expand Up @@ -677,12 +744,11 @@ functions.allowAnimations = function(element, value) {
* @param {string} selector The selector housing an ng-app
*/
functions.getLocationAbsUrl = function(selector) {
var el = document.querySelector(selector);
var hooks = getNg1Hooks(selector);
if (angular.getTestability) {
return angular.getTestability(el).
getLocation();
return hooks.$$testability.getLocation();
}
return angular.element(el).injector().get('$location').absUrl();
return hooks.$injector.get('$location').absUrl();
};

/**
Expand All @@ -693,12 +759,11 @@ functions.getLocationAbsUrl = function(selector) {
* /path?search=a&b=c#hash
*/
functions.setLocation = function(selector, url) {
var el = document.querySelector(selector);
var hooks = getNg1Hooks(selector);
if (angular.getTestability) {
return angular.getTestability(el).
setLocation(url);
return hooks.$$testability.setLocation(url);
}
var $injector = angular.element(el).injector();
var $injector = hooks.$injector;
var $location = $injector.get('$location');
var $rootScope = $injector.get('$rootScope');

Expand All @@ -715,12 +780,16 @@ functions.setLocation = function(selector, url) {
* @return {!Array<!Object>} An array of pending http requests.
*/
functions.getPendingHttpRequests = function(selector) {
var el = document.querySelector(selector);
var $injector = angular.element(el).injector();
var $http = $injector.get('$http');
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.)
Expand Down
Loading

0 comments on commit 9a73d41

Please sign in to comment.