From 3e354dd13541393097890f41a2503038e7053dcc Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Tue, 15 Nov 2016 19:31:28 +0200 Subject: [PATCH] fix(ngMock/$controller): respect `$compileProvider.preAssignBindingsEnabled()` Fixes #15387 --- src/ngMock/angular-mocks.js | 66 +++++++++----- test/ngMock/angular-mocksSpec.js | 146 ++++++++++++++++++++++++------- 2 files changed, 158 insertions(+), 54 deletions(-) diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index d4cf61ed5647..52f9171e07a2 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -2184,6 +2184,10 @@ angular.mock.$RootElementProvider = function() { * A decorator for {@link ng.$controller} with additional `bindings` parameter, useful when testing * controllers of directives that use {@link $compile#-bindtocontroller- `bindToController`}. * + * Depending on the value of + * {@link ng.$compileProvider#preAssignBindingsEnabled `preAssignBindingsEnabled()`}, the properties + * will be bound before or after invoking the constructor. + * * * ## Example * @@ -2202,18 +2206,24 @@ angular.mock.$RootElementProvider = function() { * // Controller definition ... * * myMod.controller('MyDirectiveController', ['$log', function($log) { - * $log.info(this.name); + * this.log = function() { + * $log.info(this.name); + * }; * }]); * * * // In a test ... * * describe('myDirectiveController', function() { - * it('should write the bound name to the log', inject(function($controller, $log) { - * var ctrl = $controller('MyDirectiveController', { /* no locals */ }, { name: 'Clark Kent' }); - * expect(ctrl.name).toEqual('Clark Kent'); - * expect($log.info.logs).toEqual(['Clark Kent']); - * })); + * describe('log()', function() { + * it('should write the bound name to the log', inject(function($controller, $log) { + * var ctrl = $controller('MyDirectiveController', { /* no locals */ }, { name: 'Clark Kent' }); + * ctrl.log(); + * + * expect(ctrl.name).toEqual('Clark Kent'); + * expect($log.info.logs).toEqual(['Clark Kent']); + * })); + * }); * }); * * ``` @@ -2232,26 +2242,34 @@ angular.mock.$RootElementProvider = function() { * to work correctly. * * @param {Object} locals Injection locals for Controller. - * @param {Object=} bindings Properties to add to the controller before invoking the constructor. This is used - * to simulate the `bindToController` feature and simplify certain kinds of tests. + * @param {Object=} bindings Properties to add to the controller instance. This is used to simulate + * the `bindToController` feature and simplify certain kinds of tests. * @return {Object} Instance of given controller. */ -angular.mock.$ControllerDecorator = ['$delegate', function($delegate) { - return function(expression, locals, later, ident) { - if (later && typeof later === 'object') { - var instantiate = $delegate(expression, locals, true, ident); - angular.extend(instantiate.instance, later); - - var instance = instantiate(); - if (instance !== instantiate.instance) { - angular.extend(instance, later); +function createControllerDecorator(compileProvider) { + angular.mock.$ControllerDecorator = ['$delegate', function($delegate) { + return function(expression, locals, later, ident) { + if (later && typeof later === 'object') { + var preAssignBindingsEnabled = compileProvider.preAssignBindingsEnabled(); + + var instantiate = $delegate(expression, locals, true, ident); + if (preAssignBindingsEnabled) { + angular.extend(instantiate.instance, later); + } + + var instance = instantiate(); + if (!preAssignBindingsEnabled || instance !== instantiate.instance) { + angular.extend(instance, later); + } + + return instance; } + return $delegate(expression, locals, later, ident); + }; + }]; - return instance; - } - return $delegate(expression, locals, later, ident); - }; -}]; + return angular.mock.$ControllerDecorator; +} /** * @ngdoc service @@ -2360,11 +2378,11 @@ angular.module('ngMock', ['ng']).provider({ $httpBackend: angular.mock.$HttpBackendProvider, $rootElement: angular.mock.$RootElementProvider, $componentController: angular.mock.$ComponentControllerProvider -}).config(['$provide', function($provide) { +}).config(['$provide', '$compileProvider', function($provide, $compileProvider) { $provide.decorator('$timeout', angular.mock.$TimeoutDecorator); $provide.decorator('$$rAF', angular.mock.$RAFDecorator); $provide.decorator('$rootScope', angular.mock.$RootScopeDecorator); - $provide.decorator('$controller', angular.mock.$ControllerDecorator); + $provide.decorator('$controller', createControllerDecorator($compileProvider)); }]); /** diff --git a/test/ngMock/angular-mocksSpec.js b/test/ngMock/angular-mocksSpec.js index c5f138be2730..8e696f58072a 100644 --- a/test/ngMock/angular-mocksSpec.js +++ b/test/ngMock/angular-mocksSpec.js @@ -2055,27 +2055,15 @@ describe('ngMock', function() { describe('$controllerDecorator', function() { - it('should support creating controller with bindings', function() { - var called = false; - var data = [ - { name: 'derp1', id: 0 }, - { name: 'testname', id: 1 }, - { name: 'flurp', id: 2 } - ]; - module(function($controllerProvider) { - $controllerProvider.register('testCtrl', function() { - called = true; - expect(this.data).toBe(data); - }); - }); - inject(function($controller, $rootScope) { - $controller('testCtrl', { scope: $rootScope }, { data: data }); - expect(called).toBe(true); - }); - }); - it('should support assigning bindings when a value is returned from the constructor', - function() { + describe('with `preAssignBindingsEnabled(true)`', function() { + + beforeEach(module(function($compileProvider) { + $compileProvider.preAssignBindingsEnabled(true); + })); + + + it('should support creating controller with bindings', function() { var called = false; var data = [ { name: 'derp1', id: 0 }, @@ -2084,10 +2072,8 @@ describe('ngMock', function() { ]; module(function($controllerProvider) { $controllerProvider.register('testCtrl', function() { - called = true; expect(this.data).toBe(data); - - return {}; + called = true; }); }); inject(function($controller, $rootScope) { @@ -2095,11 +2081,64 @@ describe('ngMock', function() { expect(ctrl.data).toBe(data); expect(called).toBe(true); }); + }); + + + it('should support assigning bindings when a value is returned from the constructor', + function() { + var called = false; + var data = [ + { name: 'derp1', id: 0 }, + { name: 'testname', id: 1 }, + { name: 'flurp', id: 2 } + ]; + module(function($controllerProvider) { + $controllerProvider.register('testCtrl', function() { + expect(this.data).toBe(data); + called = true; + return {}; + }); + }); + inject(function($controller, $rootScope) { + var ctrl = $controller('testCtrl', { scope: $rootScope }, { data: data }); + expect(ctrl.data).toBe(data); + expect(called).toBe(true); + }); + } + ); + + + if (/chrome/.test(window.navigator.userAgent)) { + it('should support assigning bindings to class-based controller', function() { + var called = false; + var data = [ + { name: 'derp1', id: 0 }, + { name: 'testname', id: 1 }, + { name: 'flurp', id: 2 } + ]; + module(function($controllerProvider) { + // eslint-disable-next-line no-eval + var TestCtrl = eval('(class { constructor() { called = true; } })'); + $controllerProvider.register('testCtrl', TestCtrl); + }); + inject(function($controller, $rootScope) { + var ctrl = $controller('testCtrl', { scope: $rootScope }, { data: data }); + expect(ctrl.data).toBe(data); + expect(called).toBe(true); + }); + }); } - ); + }); + - if (/chrome/.test(window.navigator.userAgent)) { - it('should support assigning bindings to class-based controller', function() { + describe('with `preAssignBindingsEnabled(false)`', function() { + + beforeEach(module(function($compileProvider) { + $compileProvider.preAssignBindingsEnabled(false); + })); + + + it('should support creating controller with bindings', function() { var called = false; var data = [ { name: 'derp1', id: 0 }, @@ -2107,9 +2146,10 @@ describe('ngMock', function() { { name: 'flurp', id: 2 } ]; module(function($controllerProvider) { - // eslint-disable-next-line no-eval - var TestCtrl = eval('(class { constructor() { called = true; } })'); - $controllerProvider.register('testCtrl', TestCtrl); + $controllerProvider.register('testCtrl', function() { + expect(this.data).toBeUndefined(); + called = true; + }); }); inject(function($controller, $rootScope) { var ctrl = $controller('testCtrl', { scope: $rootScope }, { data: data }); @@ -2117,7 +2157,53 @@ describe('ngMock', function() { expect(called).toBe(true); }); }); - } + + + it('should support assigning bindings when a value is returned from the constructor', + function() { + var called = false; + var data = [ + { name: 'derp1', id: 0 }, + { name: 'testname', id: 1 }, + { name: 'flurp', id: 2 } + ]; + module(function($controllerProvider) { + $controllerProvider.register('testCtrl', function() { + expect(this.data).toBeUndefined(); + called = true; + return {}; + }); + }); + inject(function($controller, $rootScope) { + var ctrl = $controller('testCtrl', { scope: $rootScope }, { data: data }); + expect(ctrl.data).toBe(data); + expect(called).toBe(true); + }); + } + ); + + + if (/chrome/.test(window.navigator.userAgent)) { + it('should support assigning bindings to class-based controller', function() { + var called = false; + var data = [ + { name: 'derp1', id: 0 }, + { name: 'testname', id: 1 }, + { name: 'flurp', id: 2 } + ]; + module(function($controllerProvider) { + // eslint-disable-next-line no-eval + var TestCtrl = eval('(class { constructor() { called = true; } })'); + $controllerProvider.register('testCtrl', TestCtrl); + }); + inject(function($controller, $rootScope) { + var ctrl = $controller('testCtrl', { scope: $rootScope }, { data: data }); + expect(ctrl.data).toBe(data); + expect(called).toBe(true); + }); + }); + } + }); });