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

Commit bc58332

Browse files
authoredJan 4, 2017
feat(rootEl): ***breaking change*** auto-detect the root element better (#3928)
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 0fdd3fb commit bc58332

15 files changed

+174
-121
lines changed
 

‎lib/browser.ts

+5-6
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

+136-67
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.)

‎lib/config.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -348,8 +348,17 @@ export interface Config {
348348
baseUrl?: string;
349349

350350
/**
351-
* CSS Selector for the element housing the angular app - this defaults to
352-
* 'body', but is necessary if ng-app is on a descendant of <body>.
351+
* A CSS Selector for a DOM element within your Angular application.
352+
* Protractor will attempt to automatically find your application, but it is
353+
* necessary to set rootElement in certain cases.
354+
*
355+
* In Angular 1, Protractor will use the element your app bootstrapped to by
356+
* default. If that doesn't work, it will then search for hooks in `body` or
357+
* `ng-app` elements (details here: https://git.io/v1b2r).
358+
*
359+
* In later versions of Angular, Protractor will try to hook into all angular
360+
* apps on the page. Use rootElement to limit the scope of which apps
361+
* Protractor waits for and searches within.
353362
*/
354363
rootElement?: string;
355364

@@ -611,7 +620,6 @@ export interface Config {
611620
v8Debug?: any;
612621
nodeDebug?: boolean;
613622
debuggerServerPort?: number;
614-
useAllAngular2AppRoots?: boolean;
615623
frameworkPath?: string;
616624
elementExplorer?: any;
617625
debug?: boolean;

‎lib/configParser.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export class ConfigParser {
2929
specs: [],
3030
multiCapabilities: [],
3131
verboseMultiSessions: false,
32-
rootElement: 'body',
32+
rootElement: '',
3333
allScriptsTimeout: 11000,
3434
getPageTimeout: 10000,
3535
params: {},

‎lib/runner.ts

-3
Original file line numberDiff line numberDiff line change
@@ -208,9 +208,6 @@ export class Runner extends EventEmitter {
208208
if (config.debuggerServerPort) {
209209
browser_.debuggerServerPort = config.debuggerServerPort;
210210
}
211-
if (config.useAllAngular2AppRoots) {
212-
browser_.useAllAngular2AppRoots();
213-
}
214211
if (config.ng12Hybrid) {
215212
browser_.ng12Hybrid = config.ng12Hybrid;
216213
}

‎scripts/test.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ var passingTests = [
88
'node built/cli.js spec/basicConf.js --useBlockingProxy',
99
'node built/cli.js spec/multiConf.js',
1010
'node built/cli.js spec/altRootConf.js',
11+
'node built/cli.js spec/inferRootConf.js',
1112
'node built/cli.js spec/onCleanUpAsyncReturnValueConf.js',
1213
'node built/cli.js spec/onCleanUpNoReturnValueConf.js',
1314
'node built/cli.js spec/onCleanUpSyncReturnValueConf.js',

‎spec/angular2Conf.js

-10
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,6 @@ exports.config = {
2222
capabilities: env.capabilities,
2323

2424
baseUrl: env.baseUrl,
25-
26-
// Special option for Angular2, to test against all Angular2 applications
27-
// on the page. This means that Protractor will wait for every app to be
28-
// stable before each action, and search within all apps when finding
29-
// elements.
30-
useAllAngular2AppRoots: true,
31-
32-
// Alternatively, you could specify one root element application, to test
33-
// against only that one:
34-
// rootElement: 'async-app'
3525
allScriptsTimeout: 120000,
3626
getPageTimeout: 120000,
3727
jasmineNodeOpts: {

‎spec/angular2TimeoutConf.js

+1-11
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,5 @@ exports.config = {
2121

2222
capabilities: env.capabilities,
2323

24-
baseUrl: env.baseUrl,
25-
26-
// Special option for Angular2, to test against all Angular2 applications
27-
// on the page. This means that Protractor will wait for every app to be
28-
// stable before each action, and search within all apps when finding
29-
// elements.
30-
useAllAngular2AppRoots: true
31-
32-
// Alternatively, you could specify one root element application, to test
33-
// against only that one:
34-
// rootElement: 'async-app'
24+
baseUrl: env.baseUrl
3525
};

‎spec/driverProviderAttachSessionConf.js

-6
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,4 @@ exports.config = {
1212
capabilities: env.capabilities,
1313

1414
baseUrl: env.baseUrl,
15-
16-
// Special option for Angular2, to test against all Angular2 applications
17-
// on the page. This means that Protractor will wait for every app to be
18-
// stable before each action, and search within all apps when finding
19-
// elements.
20-
useAllAngular2AppRoots: true
2115
};

‎spec/driverProviderLocalConf.js

-6
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,4 @@ exports.config = {
1111
capabilities: env.capabilities,
1212

1313
baseUrl: env.baseUrl,
14-
15-
// Special option for Angular2, to test against all Angular2 applications
16-
// on the page. This means that Protractor will wait for every app to be
17-
// stable before each action, and search within all apps when finding
18-
// elements.
19-
useAllAngular2AppRoots: true
2014
};

‎spec/hybrid/async_spec.js

-2
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,8 @@ describe('async angular1/2 hybrid using ngUpgrade application', function() {
2626
expect($$('h4').first().getText()).toBe('Bindings');
2727
browser.get('/upgrade');
2828
expect($('h1').getText()).toBe('My App');
29-
browser.useAllAngular2AppRoots();
3029
browser.get('/ng2');
3130
expect($('h1').getText()).toBe('Test App for Angular 2');
32-
browser.rootEl = 'body';
3331
browser.get('/upgrade');
3432
expect($('h1').getText()).toBe('My App');
3533
});

‎spec/hybridConf.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,5 @@ exports.config = {
1212

1313
capabilities: env.capabilities,
1414

15-
baseUrl: env.baseUrl,
16-
17-
rootElement: 'body'
15+
baseUrl: env.baseUrl
1816
};

‎spec/inferRootConf.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
var env = require('./environment.js');
2+
3+
// Tests for an Angular app where ng-app is not on the body.
4+
exports.config = {
5+
seleniumAddress: env.seleniumAddress,
6+
7+
framework: 'jasmine',
8+
9+
// Spec patterns are relative to this config.
10+
specs: [
11+
'altRoot/*_spec.js'
12+
],
13+
14+
capabilities: env.capabilities,
15+
16+
baseUrl: env.baseUrl + '/ng1/',
17+
};

‎spec/noGlobalsConf.js

-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,4 @@ exports.config = {
1616
capabilities: env.capabilities,
1717

1818
baseUrl: env.baseUrl,
19-
20-
useAllAngular2AppRoots: true
2119
};

‎spec/unit/configParser_test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ describe('the config parser', function() {
6363
it('should have a default config', function() {
6464
var config = new ConfigParser().getConfig();
6565
expect(config.specs).toEqual([]);
66-
expect(config.rootElement).toEqual('body');
66+
expect(config.rootElement).toEqual('');
6767
});
6868

6969
it('should merge in config from an object', function() {

0 commit comments

Comments
 (0)
This repository has been archived.