1

This is my test:

it('add.user() should POST to /users/, failure', function() {
    mockBackend.expectPOST("/users/", {username:'u', password: 'p', email: 'e', location: 'loc'}).respond(400, {msg: "bad request"});

    BaseService.add.user({username:'u', password: 'p', email: 'e', location: 'loc'});

    mockBackend.flush();
});

afterEach(function() {
    mockBackend.verifyNoOutstandingExpectation();
    mockBackend.verifyNoOutstandingRequest();
});

And when I run this test, I get this error:

Chromium 53.0.2785 (Ubuntu 0.0.0) Factory: BaseService add.user() should POST to /users/, failure FAILED
    [object Object] thrown
    Error: [$rootScope:inprog] $digest already in progress
    http://errors.angularjs.org/1.3.15/$rootScope/inprog?p0=%24digest
        at /home/user/Documents/ebdjango/ebdjangoapp/static/js/angular.js:63:12
        at beginPhase (/home/user/Documents/ebdjango/ebdjangoapp/static/js/angular.js:14820:15)
        at Scope.$digest (/home/user/Documents/ebdjango/ebdjangoapp/static/js/angular.js:14262:9)
        at Function.$httpBackend.verifyNoOutstandingExpectation (node_modules/angular-mocks/angular-mocks.js:1557:38)
        at Object.<anonymous> (tests/test_base.js:61:21)

This is BaseService.add.user():

self.add = {
    user: function(user) {
    return $http.post("/users/", user)
        .then(function successHandler(response) {
            return $http.post("/custom-api-auth/login", user)
         }).then(function successHandler(response) {
             $window.location.href = "/";

    // if there are errors, rewrite the error messages
    }).catch(function rejectHandler(errorResponse) {
                     for (prop in errorResponse.data) {
                             if (prop == "email") {
                                 errorResponse.data[prop] = "Please enter a valid email address.";
                             } else if (prop == "username") {
                                 errorResponse.data[prop] = "Username can only contain alphanumeric characters and '.'";
                             } else if (prop == "password") {
                                 errorResponse.data[prop] = "Please enter a valid password";
                             }
                     }
         throw errorResponse;
};

How do I prevent a $digest already in progress error from occurring?

Edit: If I remove throw errorResponse;, the test works But I need throw errorResponse; there because I need to display the errors on the front end (which another controller takes care of.. BaseService.add.user().catch() basically rewrites the errors which should be displayed on the front end).

Edit 2: When the error message says at Object.<anonymous> (tests/test_base.js:61:21) it points to the line: mockBackend.verifyNoOutstandingExpectation();

SilentDev
  • 20,997
  • 28
  • 111
  • 214
  • Error message is truncated. Please, always post them entirely. There's nothing in posted code that would cause that. The test is most likely not isolated enough from other units. If there's a router in the app, it should be stubbed. Btw, $window.location itself is bad, it will screw up the next test where a response doesn't return an error. – Estus Flask Apr 01 '17 at 22:52
  • @estus I edited the post to show the full error message. Also, what should I use then if not `$window.location` to set the URL? (when testing, I want to set the URL to any specific URL and verify that the URL changes after a successful POST. In the actual app - when Not testing - I use `$window.location` to redirect the user to a different URL) – SilentDev Apr 02 '17 at 02:55
  • can you share your `mockBackend`? – tanmay Apr 02 '17 at 09:40

1 Answers1

1

The problem with the code above is that it throws inside catch block. As opposed to other promise implementations, throwing and rejecting in $q is not the same thing.

Considering that $q promise chains are executed on digest ($httpBackend.flush() here) synchronously, throwing inside catch block will result in uncaught error, not in rejected promise. It likely prevents a digest from being completed and results in $digest already in progress error on next digest.

So generally $q.reject should be used for expected errors in promises, while throw should be used only for critical errors. It should be

return $q.reject(errorResponse);

It is recommended to use Jasmine promise matchers to test it:

expect(BaseService.add.user({ ... }).toBeRejectedWith({ ... });

Otherwise it has to be tested by more complicated promise chain:

var noError = new Error;

BaseService.add.user({ ... })
.then(() => $q.reject(noError))
.catch((err) => {
  expect(err).not.toBe(noError);
  expect(err).toEqual(...);
});

$rootScope.$digest();

Location changes should be stubbed in tests, because changing real location in tests is the last thing the one needs:

module({ $window: {
  location: jasmine.spyObj(['href'])
} })

And it is preferable to use $location service in Angular applications instead of accessing location directly, unless proven otherwise, $location.path('/...') instead of location.href = '/...'.

It is already safe for using in tests, although $location.path can be additionally spied for testing.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • Just an FYI, I remembered why i was using `$window.location.href` instead of `$location.path`. It's because I wanted to completely refresh the webpage and using `$location.path` doesn't refresh (it only changes the URL). – SilentDev Apr 04 '17 at 23:06
  • Yes, $location.path doesn't do that. Usually refreshing a page is considered a hack in SPAs that indicates a problem with design. Any way, testing this case is doable by mocking $window, as described in the post (testability is the reason why $window.location is better than location global). – Estus Flask Apr 04 '17 at 23:19
  • Okay thanks (the reason I need a full page refresh in this application is because I ran into this issue: http://stackoverflow.com/questions/43218285/using-django-template-inheritance-with-ngroute-where-does-div-ng-view-go-i - just in case you are familiar with Template inheritance and have a solution to it). – SilentDev Apr 04 '17 at 23:33
  • I see. I'm not familiar with Django templating, but yes, if it needs refreshing, it went wrong. What's the purpose for server-side templates there? Is it SEO? Considering that Angular is suited for SPAs, preferably shouldn't be touched by server-side templates at all, page contents should be generated solely by Angular. – Estus Flask Apr 05 '17 at 01:44