diff --git a/lib/browser.ts b/lib/browser.ts index 51c4bf623..1742d180f 100644 --- a/lib/browser.ts +++ b/lib/browser.ts @@ -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 = {}; @@ -522,13 +522,10 @@ export class ProtractorBrowser extends AbstractExtendedWebDriver { let runWaitForAngularScript: () => wdpromise.Promise = () => { 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); } }; @@ -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 diff --git a/lib/clientsidescripts.js b/lib/clientsidescripts.js index 66a7bd001..dd99ebe20 100644 --- a/lib/clientsidescripts.js +++ b/lib/clientsidescripts.js @@ -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 * @@ -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 @@ -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 ' + @@ -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. * @@ -119,10 +194,9 @@ functions.waitForAllAngular2 = function(callback) { * @return {Array.} 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'); @@ -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. @@ -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 = []; @@ -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); @@ -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); @@ -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. @@ -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 = []; @@ -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]); @@ -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) { @@ -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. @@ -466,11 +534,10 @@ functions.findRepeaterColumn = wrapWithHelpers(findRepeaterColumn, repeaterMatch * @return {Array.} 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\\:']; @@ -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(); }; /** @@ -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'); @@ -715,12 +780,16 @@ functions.setLocation = function(selector, url) { * @return {!Array} 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.) diff --git a/lib/config.ts b/lib/config.ts index e1c82b510..0994a0bd7 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -348,8 +348,17 @@ export interface Config { baseUrl?: string; /** - * CSS Selector for the element housing the angular app - this defaults to - * 'body', but is necessary if ng-app is on a descendant of . + * A CSS Selector for a DOM element within your Angular application. + * Protractor will attempt to automatically find your application, but it is + * necessary to set rootElement in certain cases. + * + * In Angular 1, Protractor will use the element your app bootstrapped to by + * default. If that doesn't work, it will then search for hooks in `body` or + * `ng-app` elements (details here: https://git.io/v1b2r). + * + * In later versions of Angular, Protractor will try to hook into all angular + * apps on the page. Use rootElement to limit the scope of which apps + * Protractor waits for and searches within. */ rootElement?: string; @@ -611,7 +620,6 @@ export interface Config { v8Debug?: any; nodeDebug?: boolean; debuggerServerPort?: number; - useAllAngular2AppRoots?: boolean; frameworkPath?: string; elementExplorer?: any; debug?: boolean; diff --git a/lib/configParser.ts b/lib/configParser.ts index caf7d7e53..51715ba30 100644 --- a/lib/configParser.ts +++ b/lib/configParser.ts @@ -29,7 +29,7 @@ export class ConfigParser { specs: [], multiCapabilities: [], verboseMultiSessions: false, - rootElement: 'body', + rootElement: '', allScriptsTimeout: 11000, getPageTimeout: 10000, params: {}, diff --git a/lib/runner.ts b/lib/runner.ts index c720e0f69..b680c0598 100644 --- a/lib/runner.ts +++ b/lib/runner.ts @@ -208,9 +208,6 @@ export class Runner extends EventEmitter { if (config.debuggerServerPort) { browser_.debuggerServerPort = config.debuggerServerPort; } - if (config.useAllAngular2AppRoots) { - browser_.useAllAngular2AppRoots(); - } if (config.ng12Hybrid) { browser_.ng12Hybrid = config.ng12Hybrid; } diff --git a/scripts/test.js b/scripts/test.js index 47c91c8cf..13aa58646 100755 --- a/scripts/test.js +++ b/scripts/test.js @@ -8,6 +8,7 @@ var passingTests = [ 'node built/cli.js spec/basicConf.js --useBlockingProxy', 'node built/cli.js spec/multiConf.js', 'node built/cli.js spec/altRootConf.js', + 'node built/cli.js spec/inferRootConf.js', 'node built/cli.js spec/onCleanUpAsyncReturnValueConf.js', 'node built/cli.js spec/onCleanUpNoReturnValueConf.js', 'node built/cli.js spec/onCleanUpSyncReturnValueConf.js', diff --git a/spec/angular2Conf.js b/spec/angular2Conf.js index 9a7e44046..47b430b60 100644 --- a/spec/angular2Conf.js +++ b/spec/angular2Conf.js @@ -22,16 +22,6 @@ exports.config = { capabilities: env.capabilities, baseUrl: env.baseUrl, - - // Special option for Angular2, to test against all Angular2 applications - // on the page. This means that Protractor will wait for every app to be - // stable before each action, and search within all apps when finding - // elements. - useAllAngular2AppRoots: true, - - // Alternatively, you could specify one root element application, to test - // against only that one: - // rootElement: 'async-app' allScriptsTimeout: 120000, getPageTimeout: 120000, jasmineNodeOpts: { diff --git a/spec/angular2TimeoutConf.js b/spec/angular2TimeoutConf.js index 6e9bcf194..6f5bf3130 100644 --- a/spec/angular2TimeoutConf.js +++ b/spec/angular2TimeoutConf.js @@ -21,15 +21,5 @@ exports.config = { capabilities: env.capabilities, - baseUrl: env.baseUrl, - - // Special option for Angular2, to test against all Angular2 applications - // on the page. This means that Protractor will wait for every app to be - // stable before each action, and search within all apps when finding - // elements. - useAllAngular2AppRoots: true - - // Alternatively, you could specify one root element application, to test - // against only that one: - // rootElement: 'async-app' + baseUrl: env.baseUrl }; diff --git a/spec/driverProviderAttachSessionConf.js b/spec/driverProviderAttachSessionConf.js index 0775e11aa..e75bcb6c4 100644 --- a/spec/driverProviderAttachSessionConf.js +++ b/spec/driverProviderAttachSessionConf.js @@ -12,10 +12,4 @@ exports.config = { capabilities: env.capabilities, baseUrl: env.baseUrl, - - // Special option for Angular2, to test against all Angular2 applications - // on the page. This means that Protractor will wait for every app to be - // stable before each action, and search within all apps when finding - // elements. - useAllAngular2AppRoots: true }; diff --git a/spec/driverProviderLocalConf.js b/spec/driverProviderLocalConf.js index 99875d8af..9dfb2d827 100644 --- a/spec/driverProviderLocalConf.js +++ b/spec/driverProviderLocalConf.js @@ -11,10 +11,4 @@ exports.config = { capabilities: env.capabilities, baseUrl: env.baseUrl, - - // Special option for Angular2, to test against all Angular2 applications - // on the page. This means that Protractor will wait for every app to be - // stable before each action, and search within all apps when finding - // elements. - useAllAngular2AppRoots: true }; diff --git a/spec/hybrid/async_spec.js b/spec/hybrid/async_spec.js index ee465a295..be5a24f89 100644 --- a/spec/hybrid/async_spec.js +++ b/spec/hybrid/async_spec.js @@ -26,10 +26,8 @@ describe('async angular1/2 hybrid using ngUpgrade application', function() { expect($$('h4').first().getText()).toBe('Bindings'); browser.get('/upgrade'); expect($('h1').getText()).toBe('My App'); - browser.useAllAngular2AppRoots(); browser.get('/ng2'); expect($('h1').getText()).toBe('Test App for Angular 2'); - browser.rootEl = 'body'; browser.get('/upgrade'); expect($('h1').getText()).toBe('My App'); }); diff --git a/spec/hybridConf.js b/spec/hybridConf.js index 848702f63..114c4d1aa 100644 --- a/spec/hybridConf.js +++ b/spec/hybridConf.js @@ -12,7 +12,5 @@ exports.config = { capabilities: env.capabilities, - baseUrl: env.baseUrl, - - rootElement: 'body' + baseUrl: env.baseUrl }; diff --git a/spec/inferRootConf.js b/spec/inferRootConf.js new file mode 100644 index 000000000..939d07871 --- /dev/null +++ b/spec/inferRootConf.js @@ -0,0 +1,17 @@ +var env = require('./environment.js'); + +// Tests for an Angular app where ng-app is not on the body. +exports.config = { + seleniumAddress: env.seleniumAddress, + + framework: 'jasmine', + + // Spec patterns are relative to this config. + specs: [ + 'altRoot/*_spec.js' + ], + + capabilities: env.capabilities, + + baseUrl: env.baseUrl + '/ng1/', +}; diff --git a/spec/noGlobalsConf.js b/spec/noGlobalsConf.js index 65a1d667f..6b00ddac8 100644 --- a/spec/noGlobalsConf.js +++ b/spec/noGlobalsConf.js @@ -16,6 +16,4 @@ exports.config = { capabilities: env.capabilities, baseUrl: env.baseUrl, - - useAllAngular2AppRoots: true }; diff --git a/spec/unit/configParser_test.js b/spec/unit/configParser_test.js index bf22760e0..5e0c48b9f 100644 --- a/spec/unit/configParser_test.js +++ b/spec/unit/configParser_test.js @@ -63,7 +63,7 @@ describe('the config parser', function() { it('should have a default config', function() { var config = new ConfigParser().getConfig(); expect(config.specs).toEqual([]); - expect(config.rootElement).toEqual('body'); + expect(config.rootElement).toEqual(''); }); it('should merge in config from an object', function() {