Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing the controller passed to an Angular Material Dialog instance

First off, I am trying to unit test the controller that is being passed to an Angular Material Dialog instance.

As a general question, does it make more sense to test such a controller separately, or by actually invoking$mdDialog.show()?

I am attempting the first method, but I'm running into some issues, mostly related to how Angular Material binds the "locals" to the controller.

Here is the code that I am using to invoke the dialog in my source code, which works as expected:

$mdDialog.show({
    controller: 'DeviceDetailController',
    controllerAs: 'vm',
    locals: {deviceId: "123"},
    bindToController: true,
    templateUrl: 'admin/views/deviceDetail.html',
    parent: angular.element(document.body),
    targetEvent: event
});

I don't believe the docs have been updated, but as of version 0.9.0 or so, the locals are available to the controller at the time the constructor function is called (see this issue on Github). Here is a stripped-down version of the controller constructor function under test, so you can see why I need the variable to be passed in and available when the controller is "instantiated":

function DeviceDetailController(devicesService) {
    var vm = this;

    vm.device = {};
//    vm.deviceId = null;           //this field is injected when the dialog is created, if there is one. For some reason I can't pre-assign it to null.

    activate();

    //////////
    function activate() {
        if (vm.deviceId != null) {
            loadDevice();
        }
    }

    function loadDevice() {
        devicesService.getDeviceById(vm.deviceId)
            .then(function(data) {
                vm.device = data.collection;
            };
    }
}

I am trying to test that the device is assigned to vm.device when a deviceId is passed in to the constructor function before it is invoked.

The test (jasmine and sinon, run by karma):

describe('DeviceDetailController', function() {
    var $controllerConstructor, scope, mockDevicesService;

    beforeEach(module("admin"));

    beforeEach(inject(function ($controller, $rootScope) {
        mockDevicesService = sinon.stub({
            getDeviceById: function () {}
        });
        $controllerConstructor = $controller;
        scope = $rootScope.$new();
    }));

    it('should get a device from devicesService if passed a deviceId', function() {
        var mockDeviceId = 3;
        var mockDevice = {onlyIWouldHaveThis: true};
        var mockDeviceResponse = {collection: [mockDevice]};
        var mockDevicePromise = {
            then: function (cb) {
                cb(mockDeviceResponse);
            }
        };

        var mockLocals = {deviceId: mockDeviceId, $scope: scope};

        mockDevicesService.getDeviceById.returns(mockDevicePromise);

        var ctrlConstructor = $controllerConstructor('DeviceDetailController as vm', mockLocals, true);
        angular.extend(ctrlConstructor.instance, mockLocals);
        ctrlConstructor();

        expect(scope.vm.deviceId).toBe(mockDeviceId);
        expect(scope.vm.device).toEqual(mockDevice);
    });
});

When I run this, the first assertion passes and the second one fails ("Expected Object({ }) to equal Object({ onlyIWouldHaveThis: true })."), which shows me that deviceId is being injected into the controller's scope, but apparently not in time for the if clause in the activate() method to see it.

You will notice that I am trying to mimic the basic procedure that Angular Material uses by calling $controller() with the third argument set to 'true', which causes $controller() to return the controller constructor function, as opposed to the resulting controller. I should then be able to extend the constructor with my local variables (just as Angular Material does in the code linked to above), and then invoke the constructor function to instantiate the controller.

I have tried a number of things, including passing an isolate scope to the controller by calling $rootScope.$new(true), to no effect (I actually can't say I fully understand isolate scope, but $mdDialog uses it by default).

Any help is appreciated!

like image 746
Sean Avatar asked Sep 11 '15 14:09

Sean


1 Answers

The first thing I would try would be to lose the 'as vm' from your call to $controller. You can just use the return value for your expect rather than testing scope.

Try this:

var ctrlConstructor = $controllerConstructor('DeviceDetailController', mockLocals, true);
    angular.extend(ctrlConstructor.instance, mockLocals);
    var vm = ctrlConstructor();

    expect(vm.deviceId).toBe(mockDeviceId);
    expect(vm.device).toEqual(mockDevice);
like image 75
Pete McKinney Avatar answered Sep 27 '22 18:09

Pete McKinney