2
votes

Scenario

I have built search form with 2 states: First state triggers the second one (onclick), and sets some state parameters with $state.go() function. Second state has resolve function which gets state parameter, and resolves search results.

This approach seems much cleaner way to do it, oposing to classic approach: inject the search service in state one, get results, save them in search service, change state and load results from injected service.

Question

How to make unit test for this ?

Initial idea is that I should make a test which "should trigger a locate with searchTerm and switch to results page". It fails because the states do not resolve properly with jasmine. Sometimes it's 'locked' in first state, in other scenarios state is always ''.

When I removed resolve block from configuration it works as expected, and tests pass, so the problem is obviously in resolve block. It's weird that I do not get 'unknown provider' errors though.

Any thoughts or ideas how to fix this?

Closest question on StackOverflow was this : Angular ui router unit testing (states to urls) But there user invokes state change manually, while in my scenario it's invoked by another function.

Unit test code:

describe('SearchController tests', function(){
    var ctrl, $state, $rootScope, $httpBackend;

    // instantiate the app to provide all the dependencies
    beforeEach(module('MyApp'));

    beforeEach(inject(function($controller, _$state_, _$rootScope_, _$httpBackend_) {
        ctrl = $controller('SearchController', {});
        $state = _$state_;
        $httpBackend = _$httpBackend_;
        $rootScope = _$rootScope_;

        // mocking the views 
        // (instead of including stateMock script from the post, just to simplify)
        $httpBackend.expectGET("search.html").respond("<input ng-model='vm.searchTerm'><button ng-click='vm.search()'>Search</button>");
        $httpBackend.expectGET("search-results.html").respond("<div ng-repeat='res in vm.results'> {{ res }}</div>");

        $state.go('search');
        $rootScope.$digest();
    }));

    it('should trigger a search with searchTerm "rabbit" and change state to search results page', function(){
        ctrl.searchTerm = 'rabbit';
        $state.go('search');
        ctrl.search();
        $rootScope.$digest();
        expect($state.current.name).toBe('search-results');
    })
});

Plunker

http://plnkr.co/edit/6qiYCvCu1LftLzQRyTJy?p=preview

1

1 Answers

0
votes

This answer is another variation of my note in the question:

This approach seems much cleaner way to do it, oposing to classic approach: inject the search service in state one, get results, save them in search service, change state and load results from injected service.

Which means, when state changes to 'search-results', injected 'search_results' will be a promise which will still have to be resolved in the second state, meaning the SearchApi is yet to be invoked and resolved.

As I stated, I prefer resolving promises in between states, because it seems cleaner, and config block has all the info you need, for sake of code readability.

Unit test problem solution:

I resolved my main issue, the unit test. The problem was quite common, I did not include the module which contains parent state definition. In the plunker demo I've put all the dependencies in one module, for purposes of the demo, so all I had to do in the unit test was to inlude entire module:

beforeEach(module('MyApp'));

The real app is much bigger, and separated in multiple modules, components and states, following AngularAtom—Component-based organization.

One of the good sides of this approach is, quote:

Each state may contain one or more child states that, unlike sub-components, each define their own module. This is because each state, be it parent or child, is looked at as an independent citizen that can be added or removed at any time of the application lifecycle.

Downside of this particular make every state a module approach is that it makes unit test a bit more complicated with nested states.

Let's say we have triple nested state (which is not so un-common scenario):

  • state1
    • app.js
    • config.js
    • state1.first
      • app.js
      • config.js ...
      • state1.first.list
        • app.js
        • config.js
        • list-controller.js
        • list-view.html ...
      • state1.first.detail ...
    • state1.second ...

You want to make unit test for list-controller.js which is located under state1.first.list state. To create unit test from my first post (test a function in state1.first.list, which changes state to state1.first.detail state), your test must have imports for: state1, state1.first and state1.first.detail.

beforeEach(module('state1'));
beforeEach(module('state1.first'));
beforeEach(module('state1.first.detail'));

Reason for that is: every module contains it's own state configuration in config.js. If you try to execute this test, only importing state1.first.list and state1.first.detail, it will fail with no error message. The reason is it's missing parent(s) state(s) configuration, which is defined in different modules. Jasmine in combination with PhantomJS does not log these kind of errors by default, so this kind of error is a bit harder to spot, but that's a different issue.

These imports make unit tests more tightly coupled together, which seems to be against this pattern, but that too is a different issue.