From f24688030a63c9de4ce759ac9c6fab79ef773ed5 Mon Sep 17 00:00:00 2001 From: Julie Ralph Date: Tue, 6 Oct 2015 17:11:01 -0700 Subject: [PATCH 1/2] feat(lib): add support for waiting for angular2 Use Angular2's testability API, if present, when waiting for stability or loading a page. Closes #2396 --- lib/clientsidescripts.js | 18 ++++++++---- lib/protractor.js | 62 ++++++++++++++++++++++++---------------- spec/angular2Conf.js | 29 +++++++++++++++++++ spec/ng2/async_spec.js | 57 ++++++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 30 deletions(-) create mode 100644 spec/angular2Conf.js create mode 100644 spec/ng2/async_spec.js diff --git a/lib/clientsidescripts.js b/lib/clientsidescripts.js index f53059417..67d041b75 100644 --- a/lib/clientsidescripts.js +++ b/lib/clientsidescripts.js @@ -50,6 +50,10 @@ functions.waitForAngular = function(rootSelector, callback) { var el = document.querySelector(rootSelector); try { + if (window.getAngularTestability) { + window.getAngularTestability(el).whenStable(callback); + return; + } if (!window.angular) { throw new Error('angular could not be found on the window'); } @@ -555,6 +559,8 @@ functions.findByCssContainingText = function(cssSelector, searchText, using) { * * @param {number} attempts Number of times to retry. * @param {function} asyncCallback callback + * + * @return {{version: ?number, message: ?string}} */ functions.testForAngular = function(attempts, asyncCallback) { var callback = function(args) { @@ -564,19 +570,21 @@ functions.testForAngular = function(attempts, asyncCallback) { }; var check = function(n) { try { - if (window.angular && window.angular.resumeBootstrap) { - callback([true, null]); + if (window.getAllAngularTestabilities) { + callback({ver: 2}); + } else if (window.angular && window.angular.resumeBootstrap) { + callback({ver: 1}); } else if (n < 1) { if (window.angular) { - callback([false, 'angular never provided resumeBootstrap']); + callback({message: 'angular never provided resumeBootstrap'}); } else { - callback([false, 'retries looking for angular exceeded']); + callback({message: 'retries looking for angular exceeded'}); } } else { window.setTimeout(function() {check(n - 1);}, 1000); } } catch (e) { - callback([false, e]); + callback({message: e}); } }; check(attempts); diff --git a/lib/protractor.js b/lib/protractor.js index a942294c3..2ba67c8bb 100644 --- a/lib/protractor.js +++ b/lib/protractor.js @@ -621,40 +621,52 @@ Protractor.prototype.get = function(destination, opt_timeout) { msg('test for angular'), Math.floor(timeout / 1000)). then(function(angularTestResult) { - var hasAngular = angularTestResult[0]; - if (!hasAngular) { - var message = angularTestResult[1]; + var angularVersion = angularTestResult.ver; + if (!angularVersion) { + var message = angularTestResult.message; throw new Error('Angular could not be found on the page ' + destination + ' : ' + message); } + return angularVersion; }, function(err) { throw 'Error while running testForAngular: ' + err.message; }) - .then(null, deferred.reject); + .then(loadMocks, deferred.reject); + + function loadMocks(angularVersion) { + if (angularVersion === 1) { + // At this point, Angular will pause for us until angular.resumeBootstrap + // is called. + var moduleNames = []; + for (var i = 0; i < self.mockModules_.length; ++i) { + var mockModule = self.mockModules_[i]; + var name = mockModule.name; + moduleNames.push(name); + var executeScriptArgs = [mockModule.script, msg('add mock module ' + name)]. + concat(mockModule.args); + self.executeScript_.apply(self, executeScriptArgs). + then(null, function(err) { + throw 'Error while running module script ' + name + + ': ' + err.message; + }) + .then(null, deferred.reject); + } - // At this point, Angular will pause for us until angular.resumeBootstrap - // is called. - var moduleNames = []; - for (var i = 0; i < this.mockModules_.length; ++i) { - var mockModule = this.mockModules_[i]; - var name = mockModule.name; - moduleNames.push(name); - var executeScriptArgs = [mockModule.script, msg('add mock module ' + name)]. - concat(mockModule.args); - this.executeScript_.apply(this, executeScriptArgs). - then(null, function(err) { - throw 'Error while running module script ' + name + - ': ' + err.message; - }) - .then(null, deferred.reject); + self.executeScript_( + 'angular.resumeBootstrap(arguments[0]);', + msg('resume bootstrap'), + moduleNames) + .then(null, deferred.reject); + } else { + // TODO: support mock modules in Angular2. For now, error if someone + // has tried to use one. + if (self.mockModules_.length > 1) { + deferred.reject('Trying to load mock modules on an Angular2 app ' + + 'is not yet supported.'); + } + } } - this.executeScript_( - 'angular.resumeBootstrap(arguments[0]);', - msg('resume bootstrap'), - moduleNames) - .then(null, deferred.reject); - this.driver.controlFlow().execute(function() { return self.plugins_.onPageStable().then(function() { deferred.fulfill(); diff --git a/spec/angular2Conf.js b/spec/angular2Conf.js new file mode 100644 index 000000000..706b2031f --- /dev/null +++ b/spec/angular2Conf.js @@ -0,0 +1,29 @@ +var env = require('./environment.js'); + +// This is the configuration for a smoke test for an Angular2 application. +// +// *** NOTE *** +// As Angular2 is in rapid development, the test application that ships with +// the Protractor repository does not yet contain an Angular2 section. This +// configuration assumes that you are serving the examples from the +// angular/angular repository at localhost:8000. +// See https://github.com/angular/angular/blob/master/DEVELOPER.md for +// setup instructions. +// +// TODO: when Angular2 is beta, include a test application in the +// Protractor repository. +exports.config = { + seleniumAddress: env.seleniumAddress, + + framework: 'jasmine2', + + specs: [ + 'ng2/async_spec.js' + ], + + capabilities: env.capabilities, + + baseUrl: 'http://localhost:8000', + + rootElement: 'async-app' +}; diff --git a/spec/ng2/async_spec.js b/spec/ng2/async_spec.js new file mode 100644 index 000000000..6253dcb27 --- /dev/null +++ b/spec/ng2/async_spec.js @@ -0,0 +1,57 @@ +describe('async angular2 application', function() { + var URL = 'examples/src/async/index.html'; + + beforeEach(function() { + browser.get(URL); + }); + + it('should work with synchronous actions', function() { + var increment = $('#increment'); + increment.$('.action').click(); + + expect(increment.$('.val').getText()).toEqual('1'); + }); + + it('should wait for asynchronous actions', function() { + var timeout = $('#delayedIncrement'); + + // At this point, the async action is still pending, so the count should + // still be 0. + expect(timeout.$('.val').getText()).toEqual('0'); + + timeout.$('.action').click(); + + expect(timeout.$('.val').getText()).toEqual('1'); + }); + + it('should turn off when ignoreSynchronization is true', function() { + var timeout = $('#delayedIncrement'); + + // At this point, the async action is still pending, so the count should + // still be 0. + expect(timeout.$('.val').getText()).toEqual('0'); + + browser.ignoreSynchronization = true; + + timeout.$('.action').click(); + timeout.$('.cancel').click(); + + browser.ignoreSynchronization = false; + + // whenStable should be called since the async action is cancelled. The + // count should still be 0; + expect(timeout.$('.val').getText()).toEqual('0'); + }); + + it('should wait for a series of asynchronous actions', function() { + var timeout = $('#multiDelayedIncrements'); + + // At this point, the async action is still pending, so the count should + // still be 0. + expect(timeout.$('.val').getText()).toEqual('0'); + + timeout.$('.action').click(); + + expect(timeout.$('.val').getText()).toEqual('10'); + }); +}); From c5d37c2abebf9aa9dd3324df93ac447529eea53b Mon Sep 17 00:00:00 2001 From: Julie Ralph Date: Tue, 6 Oct 2015 18:10:48 -0700 Subject: [PATCH 2/2] feat(lib): add useAllAngularAppRoots option This allows waiting for all angular applications on the page, for angular2 apps only. --- lib/clientsidescripts.js | 32 +++++++++++++++++++++++++++++--- lib/protractor.js | 30 ++++++++++++++++++++++++++---- lib/runner.js | 3 +++ spec/angular2Conf.js | 12 ++++++++++-- 4 files changed, 68 insertions(+), 9 deletions(-) diff --git a/lib/clientsidescripts.js b/lib/clientsidescripts.js index 67d041b75..593a703d9 100644 --- a/lib/clientsidescripts.js +++ b/lib/clientsidescripts.js @@ -38,7 +38,8 @@ function wrapWithHelpers(fun) { /** * Wait until Angular has finished rendering and has - * no outstanding $http calls before continuing. + * no outstanding $http calls before continuing. The specific Angular app + * is determined by the rootSelector. * * Asynchronous. * @@ -72,6 +73,32 @@ functions.waitForAngular = function(rootSelector, callback) { } }; +/** + * 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. * @@ -558,9 +585,8 @@ functions.findByCssContainingText = function(cssSelector, searchText, using) { * Asynchronous. * * @param {number} attempts Number of times to retry. - * @param {function} asyncCallback callback + * @param {function({version: ?number, message: ?string})} asyncCallback callback * - * @return {{version: ?number, message: ?string}} */ functions.testForAngular = function(attempts, asyncCallback) { var callback = function(args) { diff --git a/lib/protractor.js b/lib/protractor.js index 2ba67c8bb..f8995068e 100644 --- a/lib/protractor.js +++ b/lib/protractor.js @@ -257,6 +257,18 @@ Protractor.prototype.getProcessedConfig = null; */ Protractor.prototype.forkNewDriverInstance = null; + +/** + * Instead of using a single root element, search through all angular apps + * available on the page when finding elements or waiting for stability. + * Only compatible with Angular2. + */ +Protractor.prototype.useAllAngular2AppRoots = function() { + // The empty string is an invalid css selector, so we use it to easily + // signal to scripts to not find a root element. + this.rootEl = ''; +}; + /** * The same as {@code webdriver.WebDriver.prototype.executeScript}, * but with a customized description for debugging. @@ -325,10 +337,20 @@ Protractor.prototype.waitForAngular = function(opt_description) { }, 'Ignore Synchronization Protractor.waitForAngular()'); } - return this.executeAsyncScript_( - clientSideScripts.waitForAngular, - 'Protractor.waitForAngular()' + description, - this.rootEl). + function runWaitForAngularScript() { + if (self.rootEl) { + return self.executeAsyncScript_( + clientSideScripts.waitForAngular, + 'Protractor.waitForAngular()' + description, + self.rootEl); + } else { + return self.executeAsyncScript_( + clientSideScripts.waitForAllAngular2, + 'Protractor.waitForAngular()' + description); + } + } + + return runWaitForAngularScript(). then(function(browserErr) { if (browserErr) { throw 'Error while waiting for Protractor to ' + diff --git a/lib/runner.js b/lib/runner.js index 5c2c35b9b..ece9bd56e 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -197,6 +197,9 @@ Runner.prototype.createBrowser = function(plugins) { if (config.debuggerServerPort) { browser_.debuggerServerPort_ = config.debuggerServerPort; } + if (config.useAllAngular2AppRoots) { + browser_.useAllAngular2AppRoots(); + } var self = this; diff --git a/spec/angular2Conf.js b/spec/angular2Conf.js index 706b2031f..f87aa3668 100644 --- a/spec/angular2Conf.js +++ b/spec/angular2Conf.js @@ -6,7 +6,7 @@ var env = require('./environment.js'); // As Angular2 is in rapid development, the test application that ships with // the Protractor repository does not yet contain an Angular2 section. This // configuration assumes that you are serving the examples from the -// angular/angular repository at localhost:8000. +// angular/angular repository at localhost:8000. // See https://github.com/angular/angular/blob/master/DEVELOPER.md for // setup instructions. // @@ -25,5 +25,13 @@ exports.config = { baseUrl: 'http://localhost:8000', - rootElement: 'async-app' + // 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' };