Angular ui router unit testing (states to urls)

JavascriptUnit TestingAngularjsAngular Ui-Router

Javascript Problem Overview


I'm having some trouble unit testing the router in my application, which is built on the Angular ui router. What I want to test is whether state transitions change the URL appropriately (there will be more complicated tests later, but this is where I'm starting.)

Here is the relevant portion of my application code:

angular.module('scrapbooks')
 .config( function($stateProvider){
    $stateProvider.state('splash', {
       url: "/splash/",
       templateUrl: "/app/splash/splash.tpl.html",
       controller: "SplashCtrl"
    })
 })

And the testing code:

it("should change to the splash state", function(){
  inject(function($state, $rootScope){
     $rootScope.$apply(function(){
       $state.go("splash");
     });
     expect($state.current.name).to.equal("splash");
  })
})

Similar questions on Stackoverflow (and the official ui router test code) suggest wrapping the $state.go call in $apply should be enough. But I've done that and the state is still not updating. $state.current.name remains empty.

Javascript Solutions


Solution 1 - Javascript

Been having this issue as well, and finally figured out how to do it.

Here is a sample state:

angular.module('myApp', ['ui.router'])
.config(['$stateProvider', function($stateProvider) {
    $stateProvider.state('myState', {
        url: '/state/:id',
        templateUrl: 'template.html',
        controller: 'MyCtrl',
        resolve: {
            data: ['myService', function(service) {
                return service.findAll();
            }]
        }
    });
}]);

The unit test below will cover testing the URL w/ params, and executing the resolves which inject its own dependencies:

describe('myApp/myState', function() {

  var $rootScope, $state, $injector, myServiceMock, state = 'myState';

  beforeEach(function() {

    module('myApp', function($provide) {
      $provide.value('myService', myServiceMock = {});
    });

    inject(function(_$rootScope_, _$state_, _$injector_, $templateCache) {
      $rootScope = _$rootScope_;
      $state = _$state_;
      $injector = _$injector_;

      // We need add the template entry into the templateCache if we ever
      // specify a templateUrl
      $templateCache.put('template.html', '');
    })
  });

  it('should respond to URL', function() {
    expect($state.href(state, { id: 1 })).toEqual('#/state/1');
  });

  it('should resolve data', function() {
    myServiceMock.findAll = jasmine.createSpy('findAll').and.returnValue('findAll');
    // earlier than jasmine 2.0, replace "and.returnValue" with "andReturn"

    $state.go(state);
    $rootScope.$digest();
    expect($state.current.name).toBe(state);

    // Call invoke to inject dependencies and run function
    expect($injector.invoke($state.current.resolve.data)).toBe('findAll');
  });
});

Solution 2 - Javascript

If you want to check only the current state's name it's easier to use $state.transitionTo('splash')

it('should transition to splash', inject(function($state,$rootScope){
  $state.transitionTo('splash');
  $rootScope.$apply();
  expect($state.current.name).toBe('splash');
}));

Solution 3 - Javascript

I realize this is slightly off topic, but I came here from Google looking for a simple way to test a route's template, controller, and URL.

$state.get('stateName')

will give you

{
  url: '...',
  templateUrl: '...',
  controller: '...',
  name: 'stateName',
  resolve: {
    foo: function () {}
  }
}

in your tests.

So your tests could look something like this:

var state;
beforeEach(inject(function ($state) {
  state = $state.get('otherwise');
}));

it('matches a wild card', function () {
  expect(state.url).toEqual('/path/to/page');
});

it('renders the 404 page', function () {
  expect(state.templateUrl).toEqual('views/errors/404.html');
});

it('uses the right controller', function () {
  expect(state.controller).toEqual(...);
});

it('resolves the right thing', function () {
  expect(state.resolve.foo()).toEqual(...);
});

// etc

Solution 4 - Javascript

For a state that without resolve:

// TEST DESCRIPTION
describe('UI ROUTER', function () {
    // TEST SPECIFICATION
    it('should go to the state', function () {
        module('app');
        inject(function ($rootScope, $state, $templateCache) {
            // When you transition to the state with $state, UI-ROUTER
            // will look for the 'templateUrl' mentioned in the state's
            // configuration, so supply those templateUrls with templateCache
            $templateCache.put('app/templates/someTemplate.html');
            // Now GO to the state.
            $state.go('someState');
            // Run a digest cycle to update the $state object
            // you can also run it with $state.$digest();
            $state.$apply();

            // TEST EXPECTATION
            expect($state.current.name)
                .toBe('someState');
        });
    });
});

NOTE:-

For a nested state we may need to supply more than one template. For ex. if we have a nested state core.public.home and each state, i.e. core, core.public and core.public.home has a templateUrl defined, we will have to add $templateCache.put() for each state's templateUrl key:-

$templateCache.put('app/templates/template1.html'); $templateCache.put('app/templates/template2.html'); $templateCache.put('app/templates/template3.html');

Hope this helps. Good Luck.

Solution 5 - Javascript

You could use $state.$current.locals.globals to access all resolved values (see the code snippet).

// Given
$httpBackend

.expectGET('/api/users/123') .respond(200, { id: 1, email: '[email protected]');

// When                                                       
$state.go('users.show', { id: 123 });
$httpBackend.flush();                            
       
// Then
var user = $state.$current.locals.globals['user']
expact(user).to.have.property('id', 123);
expact(user).to.have.property('email', '[email protected]');

In ui-router 1.0.0 (currently beta) you could try to invoke $resolve.resolve(state, locals).then((resolved) => {}) in the specs. For instance https://github.com/lucassus/angular-webpack-seed/blob/9a5af271439fd447510c0e3e87332959cb0eda0f/src/app/contacts/one/one.state.spec.js#L29

Solution 6 - Javascript

If you're not interested in anything in the content of the template, you could just mock $templateCache:

beforeEach(inject(function($templateCache) {
        spyOn($templateCache,'get').and.returnValue('<div></div>');
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionTerrenceView Question on Stackoverflow
Solution 1 - JavascriptPhilip ChenView Answer on Stackoverflow
Solution 2 - Javascriptamatyas001View Answer on Stackoverflow
Solution 3 - JavascriptweltschmerzView Answer on Stackoverflow
Solution 4 - JavascriptAakashView Answer on Stackoverflow
Solution 5 - JavascriptluacassusView Answer on Stackoverflow
Solution 6 - JavascriptJelmer JellemaView Answer on Stackoverflow