Alex 75
Alex 75

Reputation: 3236

How to capture $compile or $digest error? (AngularJS directive with templateUrl)

I'm writing a unit test of an AngularJS 1.x directive.
If I use "template" it works.
If I use "templateUrl" it does not work (the directive element remains the same original HTML instead of being "compiled").

This is how I create the directive element to test in Jasmine:

function createDirectiveElement() {
    scope = $rootScope.$new();
    var elementHtml = '<my-directive>my directive</my-directive>';
    var element = $compile(elementHtml)(scope);
    scope.$digest();
    if (element[0].tagName == "my-directive".toUpperCase()) throw Error("Directive is not compiled");

    return element;
};

(this does not actually work, see Update for real code)

I'm using this workaround to use the $httpBackend from ngMockE2E (instead of the one in ngMock). In the browser developer "network" tab I don't see any request to the template file. It seems to work because I solved the error "Object # has no method 'passThrough'".

I know that the call to the template is done asynchronously using the $httpBackend (this means $compile exit before the template is really applied).

My question is: obviously $compile is not doing what I expect. How can I trap this error? If I use a wrong address in the templateUrl I don't receive any error. How can I found the problem happened when I called $compile(directive) or scope.$digest() ?

Thanks,
Alex


[Solution]

As suggested by @Corvusoft I inject $exceptionHandler and I check for errors after every test.
In the end this is the only code I have added:

afterEach(inject(function ($exceptionHandler) {
    if ($exceptionHandler.errors.length > 0)             
        throw $exceptionHandler.errors;
}));

Now I can clearly see the errors occurred in the Jasmine test result (instead of search for them in the console), example: Error: Unexpected request: GET /api/category/list No more request expected,Error: Unexpected request: GET /api/category/list No more request expected thrown

And, most important, my tests does not pass in case there are some errors.


[Update to show real example case]

Actually the real code to make templateUrl work use asynchronous beforeEach ("done") and a timeout to wait the end of compile/digest.
My directive use some prividers/services and the template contains other directives which in turn use their templateUrl and make calls to some APIs in the link function().

This is the current (working) test code:

// workaround to have .passThrough() in $httpBackend
beforeEach(angular.mock.http.init);  // set $httpBackend to use the ngMockE2E to have the .passThrough()
afterEach(angular.mock.http.reset);  // restore the $httpBackend to use ngMock

beforeEach(inject(function (_$compile_, _$rootScope_, _$http_, $httpBackend, $templateCache, $injector) {
    $compile = _$compile_;
    $rootScope = _$rootScope_;
    $http = _$http_;

    $httpBackend.whenGET(/\/Scripts of my app\/Angular\/*/).passThrough();
    $httpBackend.whenGET(/\/api\/*/).passThrough(); // comment out this to see the errors in Jasmine
}));

afterEach(inject(function ($exceptionHandler) {
    if ($exceptionHandler.errors.length > 0)             
        throw $exceptionHandler.errors;
}));

beforeEach(function(done) { 
    createDirectiveElementAsync(function (_element_) {
        element = _element_;
        scope = element.isolateScope();
        done();
    });
});

function createDirectiveElementAsync(callback) {
    var scope = $rootScope.$new();
    var elementHtml = '<my-directive>directive</my-directive>';
    var element = $compile(elementHtml)(scope);        
    scope.$digest();

    // I haven't found an "event" to know when the compile/digest end
    setTimeout(function () {
        if (element.tagName == "my-directive".toUpperCase()) throw Error("Directive is not compiled");
        callback(element);            
    }, 0.05*1000); // HACK: change it accordingly to your system/code 
};

it("is compiled", function () {
    expect(element).toBeDefined();
    expect(element.tagName).not.toEqual("my-directive".toUpperCase());
});

I hope this example helps someone else.

Upvotes: 4

Views: 1338

Answers (1)

Ben Crowhurst
Ben Crowhurst

Reputation: 8491

$exceptionHandler

Any uncaught exception in AngularJS expressions is delegated to this service. The default implementation simply delegates to $log.error which logs it into the browser console.

In unit tests, if angular-mocks.js is loaded, this service is overridden by mock $exceptionHandler which aids in testing.

angular.
  module('exceptionOverwrite', []).
  factory('$exceptionHandler', ['$log', 'logErrorsToBackend', function($log, logErrorsToBackend) {
    return function myExceptionHandler(exception, cause) {
      logErrorsToBackend(exception, cause);
      $log.warn(exception, cause);
    };
  }]);

Upvotes: 4

Related Questions