Skip to content

Instantly share code, notes, and snippets.

@cognivator
Last active August 25, 2016 19:33
Show Gist options
  • Save cognivator/ad61c75e80985d0b44fa40e9cc8e4bb9 to your computer and use it in GitHub Desktop.
Save cognivator/ad61c75e80985d0b44fa40e9cc8e4bb9 to your computer and use it in GitHub Desktop.
ui-router: testing for controller instantiation

Scenario

You're using ui-router to manage Angular 1.x routing states. You have a parent state that should always navigate to a child state, but without relying on ui-router-extras like $deepStateRedirect. This is to avoid deep linking into the child state. Some logins need to operate this way, for instance, so the Terms of Service acknowledgement cannot be skipped.

Configuration

This is an example ui-router state hierarchy that defines a series of states used for login pages.

$stateProvider
  .state('login', {
      url: '/login',
      templateUrl: 'app/login/login.html',
      controller: 'LoginController',
      controllerAs: 'loginVm'
    })
  .state('login.tos', {
      templateUrl: 'app/login/tos.html',
      controller: 'TosController',
      controllerAs: 'tosVm'
    })
  .state('login.credentials', {
      templateUrl: 'app/login/credentials.html',
      controller: 'credentialsController',
      controllerAs: 'credVm'
    });

Note the lack of urls for the child states. This prevents directly navigating to any child state. Also note that $deepStateRedirect (part of ui-router-extras would allow a configuration like the following to automatically navigate from the parent login state to the child login.tos state,

$stateProvider
  .state('login', {
      url: '/login',
      templateUrl: 'app/login/login.html',
      controller: 'LoginController',
      controllerAs: 'loginVm',
      deepStateRedirect: {
        default: {
          state: 'login.tos'
        }
      }
    })
    ...

The problem with using $deepStateRedirect in this case is it also allows a browser refresh, for instance, to remain in any child state like login.credentials, which may not be desirable. (The same might be true of any sequential wizard, where navigating away and then back to the wizard should always start at the beginning.)

Without $deepStateRedirect, therefore, it is expected that the LoginController will be responsible for issuing $state.go('login.tos'); whenever the login state is activated.

Testing

In order to test such a controller-mitigated state transition, the LoginController must be instantiated by ui-router during testing. The following test harness involves compiling a simple HTML snippet that provides the <div ui-view /> tag required by ui-router. (Inspired by ui-router unit tests)

it('should chain from the parent state to the proper child state', function() {
  $httpBackend.when('GET', function() {return true;}).respond(200);
  $compile('<div> <div ui-view/> </div>')($rootScope);

  $state.go('login');
  $rootScope.$digest();

  expect($state.is('login.terms')).to.be.true;
};

Note the use of $httpBackend to mock responses to any request the LoginController may make. If your controller doesn't make such requests, this mock isn't necessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment