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

Commit 9a73d41

Browse files
authored
feat(rootEl): ***breaking change*** auto-detect the root element better (#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
1 parent c194af8 commit 9a73d41

15 files changed

+174
-121
lines changed

lib/browser.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ export class ProtractorBrowser extends AbstractExtendedWebDriver {
338338
this.$ = build$(this.element, By);
339339
this.$$ = build$$(this.element, By);
340340
this.baseUrl = opt_baseUrl || '';
341-
this.rootEl = opt_rootElement || 'body';
341+
this.rootEl = opt_rootElement || '';
342342
this.ignoreSynchronization = false;
343343
this.getPageTimeout = DEFAULT_GET_PAGE_TIMEOUT;
344344
this.params = {};
@@ -522,13 +522,10 @@ export class ProtractorBrowser extends AbstractExtendedWebDriver {
522522
let runWaitForAngularScript: () => wdpromise.Promise<any> = () => {
523523
if (this.plugins_.skipAngularStability() || this.bpClient) {
524524
return wdpromise.fulfilled();
525-
} else if (this.rootEl) {
525+
} else {
526526
return this.executeAsyncScript_(
527527
clientSideScripts.waitForAngular, 'Protractor.waitForAngular()' + description,
528528
this.rootEl);
529-
} else {
530-
return this.executeAsyncScript_(
531-
clientSideScripts.waitForAllAngular2, 'Protractor.waitForAngular()' + description);
532529
}
533530
};
534531

@@ -841,7 +838,9 @@ export class ProtractorBrowser extends AbstractExtendedWebDriver {
841838
}
842839

843840
self.executeScriptWithDescription(
844-
'angular.resumeBootstrap(arguments[0]);', msg('resume bootstrap'), moduleNames)
841+
'window.__TESTABILITY__NG1_APP_ROOT_INJECTOR__ = ' +
842+
'angular.resumeBootstrap(arguments[0]);',
843+
msg('resume bootstrap'), moduleNames)
845844
.then(null, deferred.reject);
846845
} else {
847846
// TODO: support mock modules in Angular2. For now, error if someone

lib/clientsidescripts.js

Lines changed: 136 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@
1616
/* global angular */
1717
var functions = {};
1818

19+
///////////////////////////////////////////////////////
20+
//// ////
21+
//// HELPERS ////
22+
//// ////
23+
///////////////////////////////////////////////////////
24+
25+
1926
/* Wraps a function up into a string with its helper functions so that it can
2027
* call those helper functions client side
2128
*
@@ -36,6 +43,84 @@ function wrapWithHelpers(fun) {
3643
' return (' + fun.toString() + ').apply(this, arguments);');
3744
}
3845

46+
/* Tests if an ngRepeat matches a repeater
47+
*
48+
* @param {string} ngRepeat The ngRepeat to test
49+
* @param {string} repeater The repeater to test against
50+
* @param {boolean} exact If the ngRepeat expression needs to match the whole
51+
* repeater (not counting any `track by ...` modifier) or if it just needs to
52+
* match a substring
53+
* @return {boolean} If the ngRepeat matched the repeater
54+
*/
55+
function repeaterMatch(ngRepeat, repeater, exact) {
56+
if (exact) {
57+
return ngRepeat.split(' track by ')[0].split(' as ')[0].split('|')[0].
58+
split('=')[0].trim() == repeater;
59+
} else {
60+
return ngRepeat.indexOf(repeater) != -1;
61+
}
62+
}
63+
64+
/* Tries to find $$testability and possibly $injector for an ng1 app
65+
*
66+
* By default, doesn't care about $injector if it finds $$testability. However,
67+
* these priorities can be reversed.
68+
*
69+
* @param {string=} selector The selector for the element with the injector. If
70+
* falsy, tries a variety of methods to find an injector
71+
* @param {boolean=} injectorPlease Prioritize finding an injector
72+
* @return {$$testability?: Testability, $injector?: Injector} Returns whatever
73+
* ng1 app hooks it finds
74+
*/
75+
function getNg1Hooks(selector, injectorPlease) {
76+
function tryEl(el) {
77+
try {
78+
if (!injectorPlease && angular.getTestability) {
79+
var $$testability = angular.getTestability(el);
80+
if ($$testability) {
81+
return {$$testability: $$testability};
82+
}
83+
} else {
84+
var $injector = angular.element(el).injector();
85+
if ($injector) {
86+
return {$injector: $injector};
87+
}
88+
}
89+
} catch(err) {}
90+
}
91+
function trySelector(selector) {
92+
var els = document.querySelectorAll(selector);
93+
for (var i = 0; i < els.length; i++) {
94+
var elHooks = tryEl(els[i]);
95+
if (elHooks) {
96+
return elHooks;
97+
}
98+
}
99+
}
100+
101+
if (selector) {
102+
return trySelector(selector);
103+
} else if (window.__TESTABILITY__NG1_APP_ROOT_INJECTOR__) {
104+
var $injector = window.__TESTABILITY__NG1_APP_ROOT_INJECTOR__;
105+
var $$testability = null;
106+
try {
107+
$$testability = $injector.get('$$testability');
108+
} catch (e) {}
109+
return {$injector: $injector, $$testability: $$testability};
110+
} else {
111+
return tryEl(document.body) ||
112+
trySelector('[ng-app]') || trySelector('[ng:app]') ||
113+
trySelector('[ng-controller]') || trySelector('[ng:controller]');
114+
}
115+
}
116+
117+
///////////////////////////////////////////////////////
118+
//// ////
119+
//// SCRIPTS ////
120+
//// ////
121+
///////////////////////////////////////////////////////
122+
123+
39124
/**
40125
* Wait until Angular has finished rendering and has
41126
* no outstanding $http calls before continuing. The specific Angular app
@@ -48,22 +133,38 @@ function wrapWithHelpers(fun) {
48133
* be passed as a parameter.
49134
*/
50135
functions.waitForAngular = function(rootSelector, callback) {
51-
var el = document.querySelector(rootSelector);
52-
53136
try {
54137
if (window.angular && !(window.angular.version &&
55-
window.angular.version.major > 1)) {
56-
if (angular.getTestability) {
57-
angular.getTestability(el).whenStable(callback);
58-
} else if (angular.element(el).injector()) {
59-
angular.element(el).injector().get('$browser').
138+
window.angular.version.major > 1)) {
139+
/* ng1 */
140+
let hooks = getNg1Hooks(rootSelector);
141+
if (hooks.$$testability) {
142+
hooks.$$testability.whenStable(callback);
143+
} else if (hooks.$injector) {
144+
hooks.$injector.get('$browser').
60145
notifyWhenNoOutstandingRequests(callback);
146+
} else if (!!rootSelector) {
147+
throw new Error('Could not automatically find injector on page: "' +
148+
window.location.toString() + '". Consider using config.rootEl');
61149
} else {
62150
throw new Error('root element (' + rootSelector + ') has no injector.' +
63151
' this may mean it is not inside ng-app.');
64152
}
65-
} else if (window.getAngularTestability) {
153+
} else if (rootSelector && window.getAngularTestability) {
154+
var el = document.querySelector(rootSelector);
66155
window.getAngularTestability(el).whenStable(callback);
156+
} else if (window.getAllAngularTestabilities) {
157+
var testabilities = window.getAllAngularTestabilities();
158+
var count = testabilities.length;
159+
var decrement = function() {
160+
count--;
161+
if (count === 0) {
162+
callback();
163+
}
164+
};
165+
testabilities.forEach(function(testability) {
166+
testability.whenStable(decrement);
167+
});
67168
} else if (!window.angular) {
68169
throw new Error('window.angular is undefined. This could be either ' +
69170
'because this is a non-angular page or because your test involves ' +
@@ -75,39 +176,13 @@ functions.waitForAngular = function(rootSelector, callback) {
75176
'obfuscation.');
76177
} else {
77178
throw new Error('Cannot get testability API for unknown angular ' +
78-
'version "' + window.angular.version + '"');
179+
'version "' + window.angular.version + '"');
79180
}
80181
} catch (err) {
81182
callback(err.message);
82183
}
83184
};
84185

85-
/**
86-
* Wait until all Angular2 applications on the page have become stable.
87-
*
88-
* Asynchronous.
89-
*
90-
* @param {function(string)} callback callback. If a failure occurs, it will
91-
* be passed as a parameter.
92-
*/
93-
functions.waitForAllAngular2 = function(callback) {
94-
try {
95-
var testabilities = window.getAllAngularTestabilities();
96-
var count = testabilities.length;
97-
var decrement = function() {
98-
count--;
99-
if (count === 0) {
100-
callback();
101-
}
102-
};
103-
testabilities.forEach(function(testability) {
104-
testability.whenStable(decrement);
105-
});
106-
} catch (err) {
107-
callback(err.message);
108-
}
109-
};
110-
111186
/**
112187
* Find a list of elements in the page by their angular binding.
113188
*
@@ -119,10 +194,9 @@ functions.waitForAllAngular2 = function(callback) {
119194
* @return {Array.<Element>} The elements containing the binding.
120195
*/
121196
functions.findBindings = function(binding, exactMatch, using, rootSelector) {
122-
var root = document.querySelector(rootSelector || 'body');
123197
using = using || document;
124198
if (angular.getTestability) {
125-
return angular.getTestability(root).
199+
return getNg1Hooks(rootSelector).$$testability.
126200
findBindings(using, binding, exactMatch);
127201
}
128202
var bindings = using.getElementsByClassName('ng-binding');
@@ -150,15 +224,6 @@ functions.findBindings = function(binding, exactMatch, using, rootSelector) {
150224
return matches; /* Return the whole array for webdriver.findElements. */
151225
};
152226

153-
function repeaterMatch(ngRepeat, repeater, exact) {
154-
if (exact) {
155-
return ngRepeat.split(' track by ')[0].split(' as ')[0].split('|')[0].
156-
split('=')[0].trim() == repeater;
157-
} else {
158-
return ngRepeat.indexOf(repeater) != -1;
159-
}
160-
}
161-
162227
/**
163228
* Find an array of elements matching a row within an ng-repeat.
164229
* Always returns an array of only one element for plain old ng-repeat.
@@ -273,7 +338,6 @@ functions.findAllRepeaterRows = wrapWithHelpers(findAllRepeaterRows, repeaterMat
273338
*/
274339
function findRepeaterElement(repeater, exact, index, binding, using, rootSelector) {
275340
var matches = [];
276-
var root = document.querySelector(rootSelector || 'body');
277341
using = using || document;
278342

279343
var rows = [];
@@ -317,7 +381,7 @@ function findRepeaterElement(repeater, exact, index, binding, using, rootSelecto
317381
if (angular.getTestability) {
318382
matches.push.apply(
319383
matches,
320-
angular.getTestability(root).findBindings(row, binding));
384+
getNg1Hooks(rootSelector).$$testability.findBindings(row, binding));
321385
} else {
322386
if (row.className.indexOf('ng-binding') != -1) {
323387
bindings.push(row);
@@ -334,7 +398,8 @@ function findRepeaterElement(repeater, exact, index, binding, using, rootSelecto
334398
if (angular.getTestability) {
335399
matches.push.apply(
336400
matches,
337-
angular.getTestability(root).findBindings(rowElem, binding));
401+
getNg1Hooks(rootSelector).$$testability.findBindings(rowElem,
402+
binding));
338403
} else {
339404
if (rowElem.className.indexOf('ng-binding') != -1) {
340405
bindings.push(rowElem);
@@ -357,7 +422,8 @@ function findRepeaterElement(repeater, exact, index, binding, using, rootSelecto
357422
}
358423
return matches;
359424
}
360-
functions.findRepeaterElement = wrapWithHelpers(findRepeaterElement, repeaterMatch);
425+
functions.findRepeaterElement =
426+
wrapWithHelpers(findRepeaterElement, repeaterMatch, getNg1Hooks);
361427

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

378443
var rows = [];
@@ -414,7 +479,8 @@ function findRepeaterColumn(repeater, exact, binding, using, rootSelector) {
414479
if (angular.getTestability) {
415480
matches.push.apply(
416481
matches,
417-
angular.getTestability(root).findBindings(rows[i], binding));
482+
getNg1Hooks(rootSelector).$$testability.findBindings(rows[i],
483+
binding));
418484
} else {
419485
if (rows[i].className.indexOf('ng-binding') != -1) {
420486
bindings.push(rows[i]);
@@ -430,7 +496,8 @@ function findRepeaterColumn(repeater, exact, binding, using, rootSelector) {
430496
if (angular.getTestability) {
431497
matches.push.apply(
432498
matches,
433-
angular.getTestability(root).findBindings(multiRows[i][j], binding));
499+
getNg1Hooks(rootSelector).$$testability.findBindings(
500+
multiRows[i][j], binding));
434501
} else {
435502
var elem = multiRows[i][j];
436503
if (elem.className.indexOf('ng-binding') != -1) {
@@ -454,7 +521,8 @@ function findRepeaterColumn(repeater, exact, binding, using, rootSelector) {
454521
}
455522
return matches;
456523
}
457-
functions.findRepeaterColumn = wrapWithHelpers(findRepeaterColumn, repeaterMatch);
524+
functions.findRepeaterColumn =
525+
wrapWithHelpers(findRepeaterColumn, repeaterMatch, getNg1Hooks);
458526

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

472539
if (angular.getTestability) {
473-
return angular.getTestability(root).
540+
return getNg1Hooks(rootSelector).$$testability.
474541
findModels(using, model, true);
475542
}
476543
var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:'];
@@ -677,12 +744,11 @@ functions.allowAnimations = function(element, value) {
677744
* @param {string} selector The selector housing an ng-app
678745
*/
679746
functions.getLocationAbsUrl = function(selector) {
680-
var el = document.querySelector(selector);
747+
var hooks = getNg1Hooks(selector);
681748
if (angular.getTestability) {
682-
return angular.getTestability(el).
683-
getLocation();
749+
return hooks.$$testability.getLocation();
684750
}
685-
return angular.element(el).injector().get('$location').absUrl();
751+
return hooks.$injector.get('$location').absUrl();
686752
};
687753

688754
/**
@@ -693,12 +759,11 @@ functions.getLocationAbsUrl = function(selector) {
693759
* /path?search=a&b=c#hash
694760
*/
695761
functions.setLocation = function(selector, url) {
696-
var el = document.querySelector(selector);
762+
var hooks = getNg1Hooks(selector);
697763
if (angular.getTestability) {
698-
return angular.getTestability(el).
699-
setLocation(url);
764+
return hooks.$$testability.setLocation(url);
700765
}
701-
var $injector = angular.element(el).injector();
766+
var $injector = hooks.$injector;
702767
var $location = $injector.get('$location');
703768
var $rootScope = $injector.get('$rootScope');
704769

@@ -715,12 +780,16 @@ functions.setLocation = function(selector, url) {
715780
* @return {!Array<!Object>} An array of pending http requests.
716781
*/
717782
functions.getPendingHttpRequests = function(selector) {
718-
var el = document.querySelector(selector);
719-
var $injector = angular.element(el).injector();
720-
var $http = $injector.get('$http');
783+
var hooks = getNg1Hooks(selector, true);
784+
var $http = hooks.$injector.get('$http');
721785
return $http.pendingRequests;
722786
};
723787

788+
['waitForAngular', 'findBindings', 'findByModel', 'getLocationAbsUrl',
789+
'setLocation', 'getPendingHttpRequests'].forEach(function(funName) {
790+
functions[funName] = wrapWithHelpers(functions[funName], getNg1Hooks);
791+
});
792+
724793
/* Publish all the functions as strings to pass to WebDriver's
725794
* exec[Async]Script. In addition, also include a script that will
726795
* install all the functions on window (for debugging.)

0 commit comments

Comments
 (0)