From f8bc79d9e8451a9a3e13d100edce0d49cbedee37 Mon Sep 17 00:00:00 2001 From: marc Date: Mon, 12 Jan 2015 09:42:20 +0000 Subject: [PATCH 1/2] fix(ngScenario): Allow ngScenario to handle lazy-loaded and manually bootstrapped applications I know protractor is preferred, and ngScenario is only in maintenance mode. But, we are limited to ngScenario based on the devices/browsers we are targeting (no web-driver available). So, we need to address the bug where ngScenario does not work with manual bootstrap and also has issues if angular.resumeBootstrap is not yet defined (race condition when lazy-loading). --- src/Angular.js | 6 +- src/ngScenario/Application.js | 40 +++++++++--- test/AngularSpec.js | 20 ++++++ test/ngScenario/ApplicationSpec.js | 97 ++++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+), 11 deletions(-) diff --git a/src/Angular.js b/src/Angular.js index 52e74cbf396c..f04d61ef29b8 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -1408,8 +1408,12 @@ function bootstrap(element, modules, config) { forEach(extraModules, function(module) { modules.push(module); }); - doBootstrap(); + return doBootstrap(); }; + + if(isFunction(angular.resumeDeferredBootstrap)){ + angular.resumeDeferredBootstrap(); + } } /** diff --git a/src/ngScenario/Application.js b/src/ngScenario/Application.js index 80dd15742f3b..d434431971b5 100644 --- a/src/ngScenario/Application.js +++ b/src/ngScenario/Application.js @@ -68,19 +68,31 @@ angular.scenario.Application.prototype.navigateTo = function(url, loadFn, errorF try { var $window = self.getWindow_(); - if ($window.angular) { - // Disable animations - $window.angular.resumeBootstrap([['$provide', function($provide) { - return ['$animate', function($animate) { - $animate.enabled(false); - }]; - }]]); + if (!$window.angular) { + self.executeAction(loadFn); + return; + } + + if(!$window.angular.resumeBootstrap){ + $window.angular.resumeDeferredBootstrap = resumeDeferredBootstrap; + } else { + resumeDeferredBootstrap(); } - self.executeAction(loadFn); } catch (e) { errorFn(e); } + + function resumeDeferredBootstrap() { + // Disable animations + var $injector = $window.angular.resumeBootstrap([['$provide', function ($provide) { + return ['$animate', function ($animate) { + $animate.enabled(false); + }]; + }]]); + self.rootElement = $injector.get('$rootElement')[0]; + self.executeAction(loadFn); + } }).attr('src', url); // for IE compatibility set the name *after* setting the frame url @@ -105,7 +117,15 @@ angular.scenario.Application.prototype.executeAction = function(action) { if (!$window.angular) { return action.call(this, $window, _jQuery($window.document)); } - angularInit($window.document, function(element) { + + if(!!this.rootElement){ + executeWithElement(this.rootElement); + } + else { + angularInit($window.document, bind(this, executeWithElement)); + } + + function executeWithElement(element){ var $injector = $window.angular.element(element).injector(); var $element = _jQuery(element); @@ -118,5 +138,5 @@ angular.scenario.Application.prototype.executeAction = function(action) { action.call(self, $window, $element); }); }); - }); + } }; diff --git a/test/AngularSpec.js b/test/AngularSpec.js index e63789100947..a45ee2c258c9 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -1081,6 +1081,26 @@ describe('angular', function() { window.name = originalName; }); + it('should provide injector for deferred bootstrap', function() { + var injector; + window.name = 'NG_DEFER_BOOTSTRAP!'; + + injector = angular.bootstrap(element); + expect(injector).toBeUndefined(); + + injector = angular.resumeBootstrap(); + expect(injector).toBeDefined(); + }); + + it('should resume deferred bootstrap, if defined', function() { + var injector; + window.name = 'NG_DEFER_BOOTSTRAP!'; + + angular.resumeDeferredBootstrap = noop; + var spy = spyOn(angular, "resumeDeferredBootstrap"); + injector = angular.bootstrap(element); + expect(spy).toHaveBeenCalled(); + }); it('should wait for extra modules', function() { window.name = 'NG_DEFER_BOOTSTRAP!'; diff --git a/test/ngScenario/ApplicationSpec.js b/test/ngScenario/ApplicationSpec.js index b86a4e5eccdc..791c53a944ee 100644 --- a/test/ngScenario/ApplicationSpec.js +++ b/test/ngScenario/ApplicationSpec.js @@ -118,6 +118,75 @@ describe('angular.scenario.Application', function() { expect(called).toBeTruthy(); }); + it('should set rootElement when navigateTo instigates bootstrap', inject(function($injector, $browser) { + var called; + var testWindow = { + document: jqLite('
')[0], + angular: { + element: jqLite, + service: {}, + resumeBootstrap: noop + } + }; + jqLite(testWindow.document).data('$injector', $injector); + var resumeBootstrapSpy = spyOn(testWindow.angular, 'resumeBootstrap').andReturn($injector); + + var injectorGet = $injector.get; + spyOn($injector, 'get').andCallFake(function(name){ + switch(name){ + case "$rootElement": return jqLite(testWindow.document); + default: return injectorGet(name); + } + }); + + app.getWindow_ = function() { + return testWindow; + }; + app.navigateTo('http://localhost/', noop); + callLoadHandlers(app); + expect(app.rootElement).toBe(testWindow.document); + expect(resumeBootstrapSpy).toHaveBeenCalled(); + dealoc(testWindow.document); + })); + + it('should set setup resumeDeferredBootstrap if resumeBootstrap is not yet defined', inject(function($injector, $browser) { + var called; + var testWindow = { + document: jqLite('
')[0], + angular: { + element: jqLite, + service: {}, + resumeBootstrap: null + } + }; + jqLite(testWindow.document).data('$injector', $injector); + + var injectorGet = $injector.get; + var injectorSpy = spyOn($injector, 'get').andCallFake(function(name){ + switch(name){ + case "$rootElement": return jqLite(testWindow.document); + default: return injectorGet(name); + } + }); + + app.getWindow_ = function() { + return testWindow; + }; + app.navigateTo('http://localhost/', noop); + expect(testWindow.angular.resumeDeferredBootstrap).toBeUndefined(); + callLoadHandlers(app); + expect(testWindow.angular.resumeDeferredBootstrap).toBeDefined(); + expect(app.rootElement).toBeUndefined; + expect(injectorSpy).not.toHaveBeenCalled(); + + var resumeBootstrapSpy = spyOn(testWindow.angular, 'resumeBootstrap').andReturn($injector); + testWindow.angular.resumeDeferredBootstrap(); + expect(app.rootElement).toBe(testWindow.document); + expect(resumeBootstrapSpy).toHaveBeenCalled(); + expect(injectorSpy).toHaveBeenCalledWith("$rootElement"); + dealoc(testWindow.document); + })); + it('should wait for pending requests in executeAction', inject(function($injector, $browser) { var called, polled; var handlers = []; @@ -144,4 +213,32 @@ describe('angular.scenario.Application', function() { handlers[0](); dealoc(testWindow.document); })); + + it('should allow explicit rootElement', inject(function($injector, $browser) { + var called, polled; + var handlers = []; + var testWindow = { + document: jqLite('
')[0], + angular: { + element: jqLite, + service: {} + } + }; + $browser.notifyWhenNoOutstandingRequests = function(fn) { + handlers.push(fn); + }; + app.rootElement = testWindow.document; + jqLite(testWindow.document).data('$injector', $injector); + app.getWindow_ = function() { + return testWindow; + }; + app.executeAction(function($window, $document) { + expect($window).toEqual(testWindow); + expect($document).toBeDefined(); + expect($document[0].className).toEqual('test-foo'); + }); + expect(handlers.length).toEqual(1); + handlers[0](); + dealoc(testWindow.document); + })); }); From c064f03299982a4eeee11b62db40ca25b30c20b4 Mon Sep 17 00:00:00 2001 From: marc Date: Wed, 28 Jan 2015 15:12:14 +0000 Subject: [PATCH 2/2] Fix jshint and jscs errors. --- src/Angular.js | 2 +- src/ngScenario/Application.js | 12 ++++++------ test/ngScenario/ApplicationSpec.js | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Angular.js b/src/Angular.js index f04d61ef29b8..e745e55498e9 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -1411,7 +1411,7 @@ function bootstrap(element, modules, config) { return doBootstrap(); }; - if(isFunction(angular.resumeDeferredBootstrap)){ + if (isFunction(angular.resumeDeferredBootstrap)) { angular.resumeDeferredBootstrap(); } } diff --git a/src/ngScenario/Application.js b/src/ngScenario/Application.js index d434431971b5..e7d5bb86c956 100644 --- a/src/ngScenario/Application.js +++ b/src/ngScenario/Application.js @@ -73,7 +73,7 @@ angular.scenario.Application.prototype.navigateTo = function(url, loadFn, errorF return; } - if(!$window.angular.resumeBootstrap){ + if (!$window.angular.resumeBootstrap) { $window.angular.resumeDeferredBootstrap = resumeDeferredBootstrap; } else { resumeDeferredBootstrap(); @@ -85,8 +85,8 @@ angular.scenario.Application.prototype.navigateTo = function(url, loadFn, errorF function resumeDeferredBootstrap() { // Disable animations - var $injector = $window.angular.resumeBootstrap([['$provide', function ($provide) { - return ['$animate', function ($animate) { + var $injector = $window.angular.resumeBootstrap([['$provide', function($provide) { + return ['$animate', function($animate) { $animate.enabled(false); }]; }]]); @@ -118,14 +118,14 @@ angular.scenario.Application.prototype.executeAction = function(action) { return action.call(this, $window, _jQuery($window.document)); } - if(!!this.rootElement){ + if (!!this.rootElement) { executeWithElement(this.rootElement); } else { - angularInit($window.document, bind(this, executeWithElement)); + angularInit($window.document, angular.bind(this, executeWithElement)); } - function executeWithElement(element){ + function executeWithElement(element) { var $injector = $window.angular.element(element).injector(); var $element = _jQuery(element); diff --git a/test/ngScenario/ApplicationSpec.js b/test/ngScenario/ApplicationSpec.js index 791c53a944ee..d120f21a12ce 100644 --- a/test/ngScenario/ApplicationSpec.js +++ b/test/ngScenario/ApplicationSpec.js @@ -132,8 +132,8 @@ describe('angular.scenario.Application', function() { var resumeBootstrapSpy = spyOn(testWindow.angular, 'resumeBootstrap').andReturn($injector); var injectorGet = $injector.get; - spyOn($injector, 'get').andCallFake(function(name){ - switch(name){ + spyOn($injector, 'get').andCallFake(function(name) { + switch (name) { case "$rootElement": return jqLite(testWindow.document); default: return injectorGet(name); } @@ -162,8 +162,8 @@ describe('angular.scenario.Application', function() { jqLite(testWindow.document).data('$injector', $injector); var injectorGet = $injector.get; - var injectorSpy = spyOn($injector, 'get').andCallFake(function(name){ - switch(name){ + var injectorSpy = spyOn($injector, 'get').andCallFake(function(name) { + switch (name) { case "$rootElement": return jqLite(testWindow.document); default: return injectorGet(name); }