angular / angular.js

AngularJS - HTML enhanced for web apps!
https://angularjs.org
MIT License
58.81k stars 27.49k forks source link

NgModelController can't be instantiated by $controller in mock tests #7720

Open pocesar opened 10 years ago

pocesar commented 10 years ago

When testing a service, it's impossible right now to instantiate a new ngModelController and create it on the fly, only through $compile and element.controller(), since ngModelController isn't a named controller, but merely a function name inside the angular closure.

the following all fail:

inject(function(NgModelController) { // of course, it isn't a service, there's no provider
});

inject(function($controller, $rootScope) { 
  // it's not a named controller, can't be instantiated, blind shooting with controller names
  // Error: Argument '...' is not a function, got undefined
  $controller('ngModelController', {$scope: $rootScope.$new() }); 
  $controller('NgModelController', {$scope: $rootScope.$new() });
  $controller('NgModel', {$scope: $rootScope.$new() }); 
  $controller('ngModel', {$scope: $rootScope.$new() });
});

Angular does it internal testing relying on the NgModelController function. The only way for it to work is doing

$compile('<input ng-model="dummy">')($rootScope.$new()).controller('ngModel');
Narretz commented 10 years ago

This is a problem that also happens if you put your directive controllers into the directive close, because you don't want them to live in the global scope or be available through a module.

The way this works in angular core is that the ngModelController is global before the project is build (where it adds a closure around the whole project) I don't really know how this could be improved, but it seems to be an inconvenience more tha anything.

rodyhaddad commented 10 years ago

As @pocesar hinted to, we can make it a named controller, as in registered with $controller, and referenced by name in the ngModelDirective definition. That sounds reasonable.

p0lar-bear commented 7 years ago

For now I've found that one can inject ngModelDirective, which results in a one-element array containing the $compile object for ngModel. From there you can just reference the controller property of the object to get the ngModelController. This is exactly how $componentController gets the controller of components (albeit using the $injector service).

See: https://stackoverflow.com/a/45601995/887925

describe('...', function () {
    var $controller,
        $rootScope,
        ngModelController;

    beforeEach(inject(function(_$controller_, _$rootScope_, _ngModelDirective_) {
        $controller = _$controller_;
        $rootScope = _$rootScope_;
        ngModelController = _ngModelDirective_[0].controller;
    }));
});

From there you can call $controller on your ngModelController. Just make sure to pass it a scope, jqLite object and an attrs object or else it'll throw a missing dependency error.

var scope = $rootScope.new();
scope.value = null;
$controller(ngModelController, null, {
    $scope: scope,
    $element: angular.element('<input>'),
    $attrs: { ngModel: 'value' }
});