Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

testing ng-transclude doesn't work

I'm writing two directives that wrap ui-bootstrap's tabset and tab directives.
In order for the content of my directives to be passed to the wrapped directives, I'm using transclusion in both of them.
This works quite well, the only problem is that I'm failing at writing a test that checks that. My test uses a replacement directive as a mock for the wrapped directive, which I replace using $compileProvider before each test.

The test code looks something like this:

beforeEach(module('myModule', function($compileProvider) {
    // Mock the internally used 'tab' which is a third party and should not be tested here
    $compileProvider.directive('tab', function() {
        // Provide a directive with a high priority and 'terminal' set to true, makes sure that
        // the mock directive will get executed, and that the real directive will not
        var mock = {
            priority: 100,
            terminal: true,
            restrict: 'EAC',
            replace: true,
            transclude: true,
            template: '<div class="mock" ng-transclude></div>'
        };

        return mock;
    });
}));

beforeEach(function() {
    inject(function(_$compile_, _$rootScope_) {
        $compile = _$compile_;
        $rootScope = _$rootScope_;
    });
});

beforeEach(function() {
    $scope = $rootScope.$new();
});

afterEach(function() {
    $scope.$destroy();
});

it('Places the enclosed html inside the tab body', function() {
    element = $compile("<div><my-tab>test paragraph</my-tab></div>")($scope);
    $scope.$digest();

    console.log("element.html() = ", element.html());

    expect(element.text().trim()).toEqual("test paragraph");
});

The template of my directive looks something like this:

<div><tab><div ng-transclude></div></tab></div>

The directive module looks something like this:

angular.module('myModule', ['ui.bootstrap'])

.directive('myTab', function() {
    return {
        restrict: 'E',
        replace: true,
        transclude: true,
        templateUrl: 'templates/my-tab.tpl.html',

        scope: {
        }
    };
});

The result of the print to the console is this:

LOG: 'element.html() = ', '<div class="ng-isolate-scope" id=""><div id="" heading="" class="mock"><ng-transclude></ng-transclude></div></div>'

Any ideas on why the transclusion doesn't take place (again, it works outside of the test just fine) ?

Update

I've since moved on to other things and directives, and ran into this issue again, but now it's more crucial, the reason being, that the directive I place inside the parent directive, requires the parent controller in its link function.

I've done more research into this, and it turns out that for some reason, compiling the mock directive doesn't create an instance of the transcluded content.
The reason I know that, is that I've placed a printout in every hook possible in both directives (both the mock and the transcluded one), i.e. compile, pre-link, post-link and controller constructor, and I see that the only printouts are from the mock directive.

Now, here's the really interesting part: I've tried using the transclude function in the mock directive's link function to "force" the compilation of the transcluded directive, which worked ! (another proof that it didn't take place implicitly).
Where's the catch you ask ? Well, it still doesn't work. This time, since the link function of the transcluded directive fails since it doesn't find the controller of the mock directive. What ?!

Here's the code:

Code

var mod = angular.module('MyModule', []);

mod.directive('parent', function() {
    return {
        restrict: 'E',
        replace: true,
        template: '<div class="parent">...</div>',

        controller: function() {
            this.foo = function() { ... };
        }
    };
});

mod.directive('child', function() {
    return {
        restrict: 'E',
        require: '^parent',        

        link: function(scope, element, attrs, parentCtrl) {
            parentCtrl.foo();
        }
    };
});

Test

describe('child directive', function() {
    beforeEach(module('MyModule', function($compileProvider) {
        $compileProvider.directive('parent', function() {
            return {
                priority: 100,
                terminal: true,
                restrict: 'E',
                replace: true,
                transclude: true,

                template: '<div class="mock"><ng-transclude></ng-transclude></div>',

                controller: function() {
                    this.foo = jasmine.createSpy();
                },

                link: function(scope, element, attrs, ctrls, transcludeFn) {
                    transcludeFn();
                }
            };
        });
    }));
});

This test fails with an error message such as:

Error: [$compile:ctreq] Controller 'parent', required by directive 'child', can't be found!

Any thoughts, ideas, suggestions would be highly appreciated.

like image 249
ethanfar Avatar asked Mar 05 '15 07:03

ethanfar


1 Answers

Ok, probably the shortest bounty ever in the history of SO ...

The problem was with the terminal: true and priority: 100 properties of the mock directive. I was under the impression (from an article I read online about how to mock directives), that these properties cause the compiler to stop compiling directives with the same name and prioritize the mock directive to be evaluated first.
I was obviously wrong. Looking at this and this, it becomes clear that:

  1. 'terminal' stops any other directives that were not processed yet from being processed
  2. 'priority' is used to make sure that the mock directive is processed before the directive it is mocking

The problem, is that this causes all other processing to stop, including the ng-transclude directive, which has the default priority of 0.

However, removing these properties causes all hell to break loose, since both directives have been registered, and so forth (I won't burden you with all the gory details). In order to be able to remove these properties, the two directives should reside in different modules, and there should be no dependency between them. In short, when testing the child directive, the only directive named parent that's evaluated should be the mock directive.
In order to support real life usage, I've introduced three modules to the system:

  • A module for the child directive (no dependencies)
  • A module for the parent directive (no dependencies)
  • A module that has no content, but has a dependency on both child and parent modules, which is the only module you'll ever need to add as a dependency in your code

That's pretty much it. I hope it helps anyone else that runs into such problems.

like image 131
ethanfar Avatar answered Nov 10 '22 08:11

ethanfar