UI-router interfers with $httpbackend unit test, angular js

This is a controller with a submit function:

$scope.submit = function(){   

 $http.post('/api/project', $scope.project)
      .success(function(data, status){
        $modalInstance.dismiss(true);
      })
      .error(function(data){
        console.log(data);
      })
  }
}

This is my test

it('should make a post to /api/project on submit and close the modal on success', function() {
    scope.submit();

    $httpBackend.expectPOST('/api/project').respond(200, 'test');

    $httpBackend.flush();

    expect(modalInstance.dismiss).toHaveBeenCalledWith(true);
  });

The error I get is:

Error: Unexpected request: GET views/appBar.html

views/appBar.html is my templateUrl:

 .state('project', {
    url: '/',
    templateUrl:'views/appBar.html',
    controller: 'ProjectsCtrl'
  })

So somehow ui-router is making my $httpBackend point to this instead of my submit function. I have the same issue in all my tests using $httpBackend.

Is there any solution to this?


Solution 1:

Take this gist https://gist.github.com/wilsonwc/8358542

angular.module('stateMock',[]);
angular.module('stateMock').service("$state", function($q){
    this.expectedTransitions = [];
    this.transitionTo = function(stateName){
        if(this.expectedTransitions.length > 0){
            var expectedState = this.expectedTransitions.shift();
            if(expectedState !== stateName){
                throw Error("Expected transition to state: " + expectedState + " but transitioned to " + stateName );
            }
        }else{
            throw Error("No more transitions were expected! Tried to transition to "+ stateName );
        }
        console.log("Mock transition to: " + stateName);
        var deferred = $q.defer();
        var promise = deferred.promise;
        deferred.resolve();
        return promise;
    }
    this.go = this.transitionTo;
    this.expectTransitionTo = function(stateName){
        this.expectedTransitions.push(stateName);
    }

    this.ensureAllTransitionsHappened = function(){
        if(this.expectedTransitions.length > 0){
            throw Error("Not all transitions happened!");
        }
    }
});

Add it to a file called stateMock in your test/mock folder, include that file in your karma config if it isn't already picked up.

The setup before your test should then look something like this:

beforeEach(module('stateMock'));

// Initialize the controller and a mock scope
beforeEach(inject(function ($state //other vars as needed) {
    state = $state;
    //initialize other stuff
}

Then in your test you should add

state.expectTransitionTo('project');

Solution 2:

This Github issue about Unit Testing UI Router explains more fully what's happening.

The problem is that $httpBackend.flush() triggers a broadcast which then triggers the otherwise case of the stateProvider.

A simple solution can be to do the following setup, as mentionned by @darinclark in Github thread mentionned above. This is valid if you do not need to test state transitions. Otherwise have a look to @rosswil's answer that is inspired by @Vratislav answer on Github.

beforeEach(module(function ($urlRouterProvider) {
    $urlRouterProvider.otherwise(function(){return false;});
}));

EDITED

Thanks to Chris T to report this in the comments, seems after v0.2.14? the best way to do this is to use

beforeEach(module(function($urlRouterProvider) {
  $urlRouterProvider.deferIntercept();
}));

Solution 3:

If you don't want to add gist files like it says in the correct solution you can add a "when" condition to your $httpBackend to ignore GET petitions of views like this:

$httpBackend.when("GET", function (url) {
    // This condition works for my needs, but maybe you need to improve it
    return url.indexOf(".tpl.html") !== -1;
}).passThrough();