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) ?
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.
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:
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:
child
directive (no dependencies)parent
directive (no dependencies)child
and parent
modules, which is the only module you'll ever need to add as a dependency in your codeThat's pretty much it. I hope it helps anyone else that runs into such problems.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With