OdeToCode IC Logo

Bootstrap Dialogs and Promises In AngularJS Tests

Monday, June 17, 2013

The angular-mocks library we’ve been using in the previous posts make unit tests easier to write and includes some built-in mocks for Angular services like $browser, $log, and $httpBackend. However, mocks aren’t available for every service, and there are certainly other services you’ll want to fake to isolate code in a unit test.

For example, AngularStrap offers directives and services for integrating AngularJS with Bootstrap. One of the services is the $modal service that allows you to programmatically launch a modal dialog from inside a view model. Here is a controller that launches the dialog as soon as the controller itself is created.

(function (module) {
    "use strict";

    var severSelectDialog;
    var initServerSelectDialog = function ($modal, $scope) {
        return $modal(
            {
                template: '_serverSelect.html',
                persist: true, show: false, scope: $scope
            });
    };

    var showSelectServerDialog = function () {
        severSelectDialog.then(function (modal) { modal.modal("show"); });
    };

    var MainController = function ($scope, $modal) {
        severSelectDialog = initServerSelectDialog($modal, $scope);
        showSelectServerDialog();
        
        // ...
    };
    MainController.$inject = ["$scope", "$modal"];

    module.controller("MainController", MainController);

}(angular.module("mongoMagno")));

Using $modal to open a dialog is a two step process. First you call $modal and get a promise, then you call “show” on the modal object resolved by the promise (the promise exists because the markup template  for the modal might be asynchronously loaded from the server).

Using Jasmine, if you wanted to unit test the controller to ensure the controller interacts with the $modal appropriately, you could use a spy with some fakes like so:

describe('MainController', function() {

    var fakeModal = {
        command: null,
        modal: function(command) {
            this.command = command;
        }
    };

    var fakeModalPromise = {
        then: function(callback) {
            callback(fakeModal);
        }
    };

    var $modal, scope;
    beforeEach(angular.mock.inject(function($rootScope, $controller) {
        fakeModal.command = null;
        $modal = jasmine.createSpy("$modal").andReturn(fakeModalPromise);
        scope = $rootScope.$new();
        $controller('MainController', {
            $scope: scope,
            $modal: $modal
        });
    }));

    it("should show server connect dialog when launched", function() {
        expect(fakeModal.command).toBe("show");
    });
});

Another approach is not to use a fake promise but a real promise created by the Angular $q service.

describe('MainController', function() {

    var FakeModal = function() {

        this.modal = function(command) {
            this.command = command;
        };
    };

    var scope, fakeModal;
    beforeEach(angular.mock.inject(function($rootScope, $controller, $q) {
        var deferred = $q.defer();
        fakeModal = new FakeModal();
        deferred.resolve(fakeModal);

        $modal = jasmine.createSpy("$modal").andReturn(deferred.promise);
        scope = $rootScope.$new();
        $scope.$apply(function() {
            $controller('MainController', {
                $scope: scope,
                $modal: $modal
            });
        });
    }));

    it("should show connect dialog when launched", function() {
        expect(fakeModal.command).toBe("show");
    });
});

This approach requires a call to $scope.$apply (a real promise from the $q service doesn’t fully resolve until an $apply cycle executes).