');
});
});
it('should not pass transclusion into a templateUrl directive', function() {
module(function($compileProvider) {
$compileProvider.directive('transFoo', valueFn({
template: '
',
transclude: true
}));
$compileProvider.directive('noTransBar', valueFn({
templateUrl: 'noTransBar.html',
transclude: false
}));
});
inject(function($compile, $rootScope, $templateCache) {
$templateCache.put('noTransBar.html',
'
')($rootScope);
$rootScope.$digest();
}).toThrowMinErr('ngTransclude', 'orphan',
'Illegal use of ngTransclude directive in the template! ' +
'No parent directive that requires a transclusion found. ' +
'Element:
');
});
});
it('should expose transcludeFn in compile fn even for templateUrl', function() {
module(function() {
directive('transInCompile', valueFn({
transclude: true,
// template: '
whatever
',
templateUrl: 'foo.html',
compile: function(_, __, transclude) {
return function(scope, element) {
transclude(scope, function(clone, scope) {
element.html('');
element.append(clone);
});
};
}
}));
});
inject(function($compile, $rootScope, $templateCache) {
$templateCache.put('foo.html', '
whatever
');
compile('
transcluded content
');
$rootScope.$apply();
expect(trim(element.text())).toBe('transcluded content');
});
});
it('should make the result of a transclusion available to the parent directive in post-linking phase' +
'(template)', function() {
module(function() {
directive('trans', function(log) {
return {
transclude: true,
template: '
',
link: {
pre: function($scope, $element) {
log('pre(' + $element.text() + ')');
},
post: function($scope, $element) {
log('post(' + $element.text() + ')');
}
}
};
});
});
inject(function(log, $rootScope, $compile) {
element = $compile('
unicorn!
')($rootScope);
$rootScope.$apply();
expect(log).toEqual('pre(); post(unicorn!)');
});
});
it('should make the result of a transclusion available to the parent directive in post-linking phase' +
'(templateUrl)', function() {
// when compiling an async directive the transclusion is always processed before the directive
// this is different compared to sync directive. delaying the transclusion makes little sense.
module(function() {
directive('trans', function(log) {
return {
transclude: true,
templateUrl: 'trans.html',
link: {
pre: function($scope, $element) {
log('pre(' + $element.text() + ')');
},
post: function($scope, $element) {
log('post(' + $element.text() + ')');
}
}
};
});
});
inject(function(log, $rootScope, $compile, $templateCache) {
$templateCache.put('trans.html', '
');
element = $compile('
unicorn!
')($rootScope);
$rootScope.$apply();
expect(log).toEqual('pre(); post(unicorn!)');
});
});
it('should make the result of a transclusion available to the parent *replace* directive in post-linking phase' +
'(template)', function() {
module(function() {
directive('replacedTrans', function(log) {
return {
transclude: true,
replace: true,
template: '
',
link: {
pre: function($scope, $element) {
log('pre(' + $element.text() + ')');
},
post: function($scope, $element) {
log('post(' + $element.text() + ')');
}
}
};
});
});
inject(function(log, $rootScope, $compile) {
element = $compile('
unicorn!
')($rootScope);
$rootScope.$apply();
expect(log).toEqual('pre(); post(unicorn!)');
});
});
it('should make the result of a transclusion available to the parent *replace* directive in post-linking phase' +
' (templateUrl)', function() {
module(function() {
directive('replacedTrans', function(log) {
return {
transclude: true,
replace: true,
templateUrl: 'trans.html',
link: {
pre: function($scope, $element) {
log('pre(' + $element.text() + ')');
},
post: function($scope, $element) {
log('post(' + $element.text() + ')');
}
}
};
});
});
inject(function(log, $rootScope, $compile, $templateCache) {
$templateCache.put('trans.html', '
');
element = $compile('
unicorn!
')($rootScope);
$rootScope.$apply();
expect(log).toEqual('pre(); post(unicorn!)');
});
});
it('should copy the directive controller to all clones', function() {
var transcludeCtrl, cloneCount = 2;
module(function() {
directive('transclude', valueFn({
transclude: 'content',
controller: function($transclude) {
transcludeCtrl = this;
},
link: function(scope, el, attr, ctrl, $transclude) {
var i;
for (i = 0; i < cloneCount; i++) {
$transclude(cloneAttach);
}
function cloneAttach(clone) {
el.append(clone);
}
}
}));
});
inject(function($compile) {
element = $compile('
')($rootScope);
var children = element.children(), i;
expect(transcludeCtrl).toBeDefined();
expect(element.data('$transcludeController')).toBe(transcludeCtrl);
for (i = 0; i < cloneCount; i++) {
expect(children.eq(i).data('$transcludeController')).toBeUndefined();
}
});
});
it('should provide the $transclude controller local as 5th argument to the pre and post-link function', function() {
var ctrlTransclude, preLinkTransclude, postLinkTransclude;
module(function() {
directive('transclude', valueFn({
transclude: 'content',
controller: function($transclude) {
ctrlTransclude = $transclude;
},
compile: function() {
return {
pre: function(scope, el, attr, ctrl, $transclude) {
preLinkTransclude = $transclude;
},
post: function(scope, el, attr, ctrl, $transclude) {
postLinkTransclude = $transclude;
}
};
}
}));
});
inject(function($compile) {
element = $compile('
')($rootScope);
expect(ctrlTransclude).toBeDefined();
expect(ctrlTransclude).toBe(preLinkTransclude);
expect(ctrlTransclude).toBe(postLinkTransclude);
});
});
it('should allow an optional scope argument in $transclude', function() {
var capturedChildCtrl;
module(function() {
directive('transclude', valueFn({
transclude: 'content',
link: function(scope, element, attr, ctrl, $transclude) {
$transclude(scope, function(clone) {
element.append(clone);
});
}
}));
});
inject(function($compile) {
element = $compile('
{{$id}}
')($rootScope);
$rootScope.$apply();
expect(element.text()).toBe('' + $rootScope.$id);
});
});
it('should expose the directive controller to transcluded children', function() {
var capturedChildCtrl;
module(function() {
directive('transclude', valueFn({
transclude: 'content',
controller: function() {
},
link: function(scope, element, attr, ctrl, $transclude) {
$transclude(function(clone) {
element.append(clone);
});
}
}));
directive('child', valueFn({
require: '^transclude',
link: function(scope, element, attr, ctrl) {
capturedChildCtrl = ctrl;
}
}));
});
inject(function($compile) {
element = $compile('
')($rootScope);
expect(capturedChildCtrl).toBeTruthy();
});
});
// See issue https://github.com/angular/angular.js/issues/14924
it('should not process top-level transcluded text nodes merged into their sibling',
function() {
module(function() {
directive('transclude', valueFn({
template: '
',
transclude: true,
scope: {}
}));
});
inject(function($compile) {
element = jqLite('
');
element[0].appendChild(document.createTextNode('1{{ value }}'));
element[0].appendChild(document.createTextNode('2{{ value }}'));
element[0].appendChild(document.createTextNode('3{{ value }}'));
var initialWatcherCount = $rootScope.$countWatchers();
$compile(element)($rootScope);
$rootScope.$apply('value = 0');
var newWatcherCount = $rootScope.$countWatchers() - initialWatcherCount;
expect(element.text()).toBe('102030');
expect(newWatcherCount).toBe(3);
});
}
);
// see issue https://github.com/angular/angular.js/issues/9413
describe('passing a parent bound transclude function to the link ' +
'function returned from `$compile`', function() {
beforeEach(module(function() {
directive('lazyCompile', function($compile) {
return {
compile: function(tElement, tAttrs) {
var content = tElement.contents();
tElement.empty();
return function(scope, element, attrs, ctrls, transcludeFn) {
element.append(content);
$compile(content)(scope, undefined, {
parentBoundTranscludeFn: transcludeFn
});
};
}
};
});
directive('toggle', valueFn({
scope: {t: '=toggle'},
transclude: true,
template: '
'
}));
}));
it('should preserve the bound scope', function() {
inject(function($compile, $rootScope) {
element = $compile(
'
' +
'
' +
'
' +
'Success Error ' +
'
' +
'
')($rootScope);
$rootScope.$apply('t = false');
expect($rootScope.$countChildScopes()).toBe(1);
expect(element.text()).toBe('');
$rootScope.$apply('t = true');
expect($rootScope.$countChildScopes()).toBe(4);
expect(element.text()).toBe('Success');
$rootScope.$apply('t = false');
expect($rootScope.$countChildScopes()).toBe(1);
expect(element.text()).toBe('');
$rootScope.$apply('t = true');
expect($rootScope.$countChildScopes()).toBe(4);
expect(element.text()).toBe('Success');
});
});
it('should preserve the bound scope when using recursive transclusion', function() {
directive('recursiveTransclude', valueFn({
transclude: true,
template: '
'
}));
inject(function($compile, $rootScope) {
element = $compile(
'
' +
'
' +
'
' +
'
' +
'Success Error ' +
'
' +
'
' +
'
')($rootScope);
$rootScope.$apply('t = false');
expect($rootScope.$countChildScopes()).toBe(1);
expect(element.text()).toBe('');
$rootScope.$apply('t = true');
expect($rootScope.$countChildScopes()).toBe(4);
expect(element.text()).toBe('Success');
$rootScope.$apply('t = false');
expect($rootScope.$countChildScopes()).toBe(1);
expect(element.text()).toBe('');
$rootScope.$apply('t = true');
expect($rootScope.$countChildScopes()).toBe(4);
expect(element.text()).toBe('Success');
});
});
});
// see issue https://github.com/angular/angular.js/issues/9095
describe('removing a transcluded element', function() {
beforeEach(module(function() {
directive('toggle', function() {
return {
transclude: true,
template: '
'
};
});
}));
it('should not leak the transclude scope when the transcluded content is an element transclusion directive',
inject(function($compile, $rootScope) {
element = $compile(
'
'
)($rootScope);
$rootScope.$apply('t = true');
expect(element.text()).toContain('msg-1');
// Expected scopes: $rootScope, ngIf, transclusion, ngRepeat
expect($rootScope.$countChildScopes()).toBe(3);
$rootScope.$apply('t = false');
expect(element.text()).not.toContain('msg-1');
// Expected scopes: $rootScope
expect($rootScope.$countChildScopes()).toBe(0);
$rootScope.$apply('t = true');
expect(element.text()).toContain('msg-1');
// Expected scopes: $rootScope, ngIf, transclusion, ngRepeat
expect($rootScope.$countChildScopes()).toBe(3);
$rootScope.$apply('t = false');
expect(element.text()).not.toContain('msg-1');
// Expected scopes: $rootScope
expect($rootScope.$countChildScopes()).toBe(0);
}));
it('should not leak the transclude scope when the transcluded content is an multi-element transclusion directive',
inject(function($compile, $rootScope) {
element = $compile(
'
' +
'
{{ msg }}
' +
'
{{ msg }}
' +
'
'
)($rootScope);
$rootScope.$apply('t = true');
expect(element.text()).toContain('msg-1msg-1');
// Expected scopes: $rootScope, ngIf, transclusion, ngRepeat
expect($rootScope.$countChildScopes()).toBe(3);
$rootScope.$apply('t = false');
expect(element.text()).not.toContain('msg-1msg-1');
// Expected scopes: $rootScope
expect($rootScope.$countChildScopes()).toBe(0);
$rootScope.$apply('t = true');
expect(element.text()).toContain('msg-1msg-1');
// Expected scopes: $rootScope, ngIf, transclusion, ngRepeat
expect($rootScope.$countChildScopes()).toBe(3);
$rootScope.$apply('t = false');
expect(element.text()).not.toContain('msg-1msg-1');
// Expected scopes: $rootScope
expect($rootScope.$countChildScopes()).toBe(0);
}));
it('should not leak the transclude scope if the transcluded contains only comments',
inject(function($compile, $rootScope) {
element = $compile(
'
' +
'' +
'
'
)($rootScope);
$rootScope.$apply('t = true');
expect(element.html()).toContain('some comment');
// Expected scopes: $rootScope, ngIf, transclusion
expect($rootScope.$countChildScopes()).toBe(2);
$rootScope.$apply('t = false');
expect(element.html()).not.toContain('some comment');
// Expected scopes: $rootScope
expect($rootScope.$countChildScopes()).toBe(0);
$rootScope.$apply('t = true');
expect(element.html()).toContain('some comment');
// Expected scopes: $rootScope, ngIf, transclusion
expect($rootScope.$countChildScopes()).toBe(2);
$rootScope.$apply('t = false');
expect(element.html()).not.toContain('some comment');
// Expected scopes: $rootScope
expect($rootScope.$countChildScopes()).toBe(0);
}));
it('should not leak the transclude scope if the transcluded contains only text nodes',
inject(function($compile, $rootScope) {
element = $compile(
'
' +
'some text' +
'
'
)($rootScope);
$rootScope.$apply('t = true');
expect(element.html()).toContain('some text');
// Expected scopes: $rootScope, ngIf, transclusion
expect($rootScope.$countChildScopes()).toBe(2);
$rootScope.$apply('t = false');
expect(element.html()).not.toContain('some text');
// Expected scopes: $rootScope
expect($rootScope.$countChildScopes()).toBe(0);
$rootScope.$apply('t = true');
expect(element.html()).toContain('some text');
// Expected scopes: $rootScope, ngIf, transclusion
expect($rootScope.$countChildScopes()).toBe(2);
$rootScope.$apply('t = false');
expect(element.html()).not.toContain('some text');
// Expected scopes: $rootScope
expect($rootScope.$countChildScopes()).toBe(0);
}));
it('should mark as destroyed all sub scopes of the scope being destroyed',
inject(function($compile, $rootScope) {
element = $compile(
'
'
)($rootScope);
$rootScope.$apply('t = true');
var childScopes = getChildScopes($rootScope);
$rootScope.$apply('t = false');
for (var i = 0; i < childScopes.length; ++i) {
expect(childScopes[i].$$destroyed).toBe(true);
}
}));
});
describe('nested transcludes', function() {
beforeEach(module(function($compileProvider) {
$compileProvider.directive('noop', valueFn({}));
$compileProvider.directive('sync', valueFn({
template: '
',
transclude: true
}));
$compileProvider.directive('async', valueFn({
templateUrl: 'async',
transclude: true
}));
$compileProvider.directive('syncSync', valueFn({
template: '
',
transclude: true
}));
$compileProvider.directive('syncAsync', valueFn({
template: '
',
transclude: true
}));
$compileProvider.directive('asyncSync', valueFn({
templateUrl: 'asyncSync',
transclude: true
}));
$compileProvider.directive('asyncAsync', valueFn({
templateUrl: 'asyncAsync',
transclude: true
}));
}));
beforeEach(inject(function($templateCache) {
$templateCache.put('async', '
');
$templateCache.put('asyncSync', '
');
$templateCache.put('asyncAsync', '
');
}));
it('should allow nested transclude directives with sync template containing sync template', inject(function($compile, $rootScope) {
element = $compile('
transcluded content
')($rootScope);
$rootScope.$digest();
expect(element.text()).toEqual('transcluded content');
}));
it('should allow nested transclude directives with sync template containing async template', inject(function($compile, $rootScope) {
element = $compile('
transcluded content
')($rootScope);
$rootScope.$digest();
expect(element.text()).toEqual('transcluded content');
}));
it('should allow nested transclude directives with async template containing sync template', inject(function($compile, $rootScope) {
element = $compile('
transcluded content
')($rootScope);
$rootScope.$digest();
expect(element.text()).toEqual('transcluded content');
}));
it('should allow nested transclude directives with async template containing asynch template', inject(function($compile, $rootScope) {
element = $compile('
transcluded content
')($rootScope);
$rootScope.$digest();
expect(element.text()).toEqual('transcluded content');
}));
it('should not leak memory with nested transclusion', function() {
inject(function($compile, $rootScope) {
var size, initialSize = jqLiteCacheSize();
element = jqLite('
');
$compile(element)($rootScope.$new());
$rootScope.nums = [0,1,2];
$rootScope.$apply();
size = jqLiteCacheSize();
$rootScope.nums = [3,4,5];
$rootScope.$apply();
expect(jqLiteCacheSize()).toEqual(size);
element.remove();
expect(jqLiteCacheSize()).toEqual(initialSize);
});
});
});
describe('nested isolated scope transcludes', function() {
beforeEach(module(function($compileProvider) {
$compileProvider.directive('trans', valueFn({
restrict: 'E',
template: '
',
transclude: true
}));
$compileProvider.directive('transAsync', valueFn({
restrict: 'E',
templateUrl: 'transAsync',
transclude: true
}));
$compileProvider.directive('iso', valueFn({
restrict: 'E',
transclude: true,
template: '
',
scope: {}
}));
$compileProvider.directive('isoAsync1', valueFn({
restrict: 'E',
transclude: true,
template: '
',
scope: {}
}));
$compileProvider.directive('isoAsync2', valueFn({
restrict: 'E',
transclude: true,
templateUrl: 'isoAsync',
scope: {}
}));
}));
beforeEach(inject(function($templateCache) {
$templateCache.put('transAsync', '
');
$templateCache.put('isoAsync', '
');
}));
it('should pass the outer scope to the transclude on the isolated template sync-sync', inject(function($compile, $rootScope) {
$rootScope.val = 'transcluded content';
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(element.text()).toEqual('transcluded content');
}));
it('should pass the outer scope to the transclude on the isolated template async-sync', inject(function($compile, $rootScope) {
$rootScope.val = 'transcluded content';
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(element.text()).toEqual('transcluded content');
}));
it('should pass the outer scope to the transclude on the isolated template async-async', inject(function($compile, $rootScope) {
$rootScope.val = 'transcluded content';
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(element.text()).toEqual('transcluded content');
}));
});
describe('multiple siblings receiving transclusion', function() {
it('should only receive transclude from parent', function() {
module(function($compileProvider) {
$compileProvider.directive('myExample', valueFn({
scope: {},
link: function link(scope, element, attrs) {
var foo = element[0].querySelector('.foo');
scope.children = angular.element(foo).children().length;
},
template: '
' +
'
myExample {{children}}!
' +
'
has children
' +
'
' +
'
',
transclude: true
}));
});
inject(function($compile, $rootScope) {
var element = $compile('
')($rootScope);
$rootScope.$digest();
expect(element.text()).toEqual('myExample 0!');
dealoc(element);
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(element.text()).toEqual('myExample 1!has children');
dealoc(element);
});
});
});
});
describe('element transclusion', function() {
it('should support basic element transclusion', function() {
module(function() {
directive('trans', function(log) {
return {
transclude: 'element',
priority: 2,
controller: function($transclude) { this.$transclude = $transclude; },
compile: function(element, attrs, template) {
log('compile: ' + angular.mock.dump(element));
return function(scope, element, attrs, ctrl) {
log('link');
var cursor = element;
template(scope.$new(), function(clone) {cursor.after(cursor = clone);});
ctrl.$transclude(function(clone) {cursor.after(clone);});
};
}
};
});
});
inject(function(log, $rootScope, $compile) {
element = $compile('
')($rootScope);
$rootScope.$apply();
expect(log).toEqual('compile: ; link; LOG; LOG; HIGH');
expect(element.text()).toEqual('1-2;1-3;');
});
});
it('should only allow one element transclusion per element', function() {
module(function() {
directive('first', valueFn({
transclude: 'element'
}));
directive('second', valueFn({
transclude: 'element'
}));
});
inject(function($compile) {
expect(function() {
$compile('
');
}).toThrowMinErr('$compile', 'multidir', 'Multiple directives [first, second] asking for transclusion on: ' +
'');
});
});
it('should only allow one element transclusion per element when directives have different priorities', function() {
// we restart compilation in this case and we need to remember the duplicates during the second compile
// regression #3893
module(function() {
directive('first', valueFn({
transclude: 'element',
priority: 100
}));
directive('second', valueFn({
transclude: 'element'
}));
});
inject(function($compile) {
expect(function() {
$compile('
');
}).toThrowMinErr('$compile', 'multidir', /Multiple directives \[first, second] asking for transclusion on:
template.html');
expect(function() {
$compile('
');
$httpBackend.flush();
}).toThrowMinErr('$compile', 'multidir',
'Multiple directives [first, second] asking for transclusion on:
',
replace: true
}));
directive('first', valueFn({
transclude: 'element',
priority: 100
}));
directive('second', valueFn({
transclude: 'element'
}));
});
inject(function($compile) {
expect(function() {
$compile('
');
}).toThrowMinErr('$compile', 'multidir', /Multiple directives \[first, second] asking for transclusion on:
before
after
').contents();
expect(element.length).toEqual(3);
expect(nodeName_(element[1])).toBe('div');
$compile(element)($rootScope);
expect(nodeName_(element[1])).toBe('#comment');
expect(nodeName_(comment)).toBe('#comment');
});
});
it('should terminate compilation only for element transclusion', function() {
module(function() {
directive('elementTrans', function(log) {
return {
transclude: 'element',
priority: 50,
compile: log.fn('compile:elementTrans')
};
});
directive('regularTrans', function(log) {
return {
transclude: true,
priority: 50,
compile: log.fn('compile:regularTrans')
};
});
});
inject(function(log, $compile, $rootScope) {
$compile('
')($rootScope);
expect(log).toEqual('compile:elementTrans; compile:regularTrans; regular');
});
});
it('should instantiate high priority controllers only once, but low priority ones each time we transclude',
function() {
module(function() {
directive('elementTrans', function(log) {
return {
transclude: 'element',
priority: 50,
controller: function($transclude, $element) {
log('controller:elementTrans');
$transclude(function(clone) {
$element.after(clone);
});
$transclude(function(clone) {
$element.after(clone);
});
$transclude(function(clone) {
$element.after(clone);
});
}
};
});
directive('normalDir', function(log) {
return {
controller: function() {
log('controller:normalDir');
}
};
});
});
inject(function($compile, $rootScope, log) {
element = $compile('
')($rootScope);
expect(log).toEqual([
'controller:elementTrans',
'controller:normalDir',
'controller:normalDir',
'controller:normalDir'
]);
});
});
it('should allow to access $transclude in the same directive', function() {
var _$transclude;
module(function() {
directive('transclude', valueFn({
transclude: 'element',
controller: function($transclude) {
_$transclude = $transclude;
}
}));
});
inject(function($compile) {
element = $compile('
')($rootScope);
expect(_$transclude).toBeDefined();
});
});
it('should copy the directive controller to all clones', function() {
var transcludeCtrl, cloneCount = 2;
module(function() {
directive('transclude', valueFn({
transclude: 'element',
controller: function() {
transcludeCtrl = this;
},
link: function(scope, el, attr, ctrl, $transclude) {
var i;
for (i = 0; i < cloneCount; i++) {
$transclude(cloneAttach);
}
function cloneAttach(clone) {
el.after(clone);
}
}
}));
});
inject(function($compile) {
element = $compile('
')($rootScope);
var children = element.children(), i;
for (i = 0; i < cloneCount; i++) {
expect(children.eq(i).data('$transcludeController')).toBe(transcludeCtrl);
}
});
});
it('should expose the directive controller to transcluded children', function() {
var capturedTranscludeCtrl;
module(function() {
directive('transclude', valueFn({
transclude: 'element',
controller: function() {
},
link: function(scope, element, attr, ctrl, $transclude) {
$transclude(scope, function(clone) {
element.after(clone);
});
}
}));
directive('child', valueFn({
require: '^transclude',
link: function(scope, element, attr, ctrl) {
capturedTranscludeCtrl = ctrl;
}
}));
});
inject(function($compile) {
// We need to wrap the transclude directive's element in a parent element so that the
// cloned element gets deallocated/cleaned up correctly
element = $compile('
')($rootScope);
expect(capturedTranscludeCtrl).toBeTruthy();
});
});
it('should allow access to $transclude in a templateUrl directive', function() {
var transclude;
module(function() {
directive('template', valueFn({
templateUrl: 'template.html',
replace: true
}));
directive('transclude', valueFn({
transclude: 'content',
controller: function($transclude) {
transclude = $transclude;
}
}));
});
inject(function($compile, $httpBackend) {
$httpBackend.expectGET('template.html').respond('
');
element = $compile('
')($rootScope);
$httpBackend.flush();
expect(transclude).toBeDefined();
});
});
// issue #6006
it('should link directive with $element as a comment node', function() {
module(function($provide) {
directive('innerAgain', function(log) {
return {
transclude: 'element',
link: function(scope, element, attr, controllers, transclude) {
log('innerAgain:' + lowercase(nodeName_(element)) + ':' + trim(element[0].data));
transclude(scope, function(clone) {
element.parent().append(clone);
});
}
};
});
directive('inner', function(log) {
return {
replace: true,
templateUrl: 'inner.html',
link: function(scope, element) {
log('inner:' + lowercase(nodeName_(element)) + ':' + trim(element[0].data));
}
};
});
directive('outer', function(log) {
return {
transclude: 'element',
link: function(scope, element, attrs, controllers, transclude) {
log('outer:' + lowercase(nodeName_(element)) + ':' + trim(element[0].data));
transclude(scope, function(clone) {
element.parent().append(clone);
});
}
};
});
});
inject(function(log, $compile, $rootScope, $templateCache) {
$templateCache.put('inner.html', '
');
element = $compile('
')($rootScope);
$rootScope.$digest();
var child = element.children();
expect(log.toArray()).toEqual([
'outer:#comment:outer:',
'innerAgain:#comment:innerAgain:',
'inner:#comment:innerAgain:'
]);
expect(child.length).toBe(1);
expect(child.contents().length).toBe(2);
expect(lowercase(nodeName_(child.contents().eq(0)))).toBe('#comment');
expect(lowercase(nodeName_(child.contents().eq(1)))).toBe('div');
});
});
});
it('should be possible to change the scope of a directive using $provide', function() {
module(function($provide) {
directive('foo', function() {
return {
scope: {},
template: '
'
};
});
$provide.decorator('fooDirective', function($delegate) {
var directive = $delegate[0];
directive.scope.something = '=';
directive.template = '
{{something}} ';
return $delegate;
});
});
inject(function($compile, $rootScope) {
element = $compile('
')($rootScope);
$rootScope.bar = 'bar';
$rootScope.$digest();
expect(element.text()).toBe('bar');
});
});
it('should distinguish different bindings with the same binding name', function() {
module(function() {
directive('foo', function() {
return {
scope: {
foo: '=',
bar: '='
},
template: '
'
};
});
});
inject(function($compile, $rootScope) {
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(element.text()).toBe('foobar');
});
});
it('should safely create transclude comment node and not break with "-->"',
inject(function($rootScope) {
// see: https://github.com/angular/angular.js/issues/1740
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(element.text()).toBe('-->|x|');
}));
describe('lazy compilation', function() {
// See https://github.com/angular/angular.js/issues/7183
it('should pass transclusion through to template of a \'replace\' directive', function() {
module(function() {
directive('transSync', function() {
return {
transclude: true,
link: function(scope, element, attr, ctrl, transclude) {
expect(transclude).toEqual(jasmine.any(Function));
transclude(function(child) { element.append(child); });
}
};
});
directive('trans', function($timeout) {
return {
transclude: true,
link: function(scope, element, attrs, ctrl, transclude) {
// We use timeout here to simulate how ng-if works
$timeout(function() {
transclude(function(child) { element.append(child); });
});
}
};
});
directive('replaceWithTemplate', function() {
return {
templateUrl: 'template.html',
replace: true
};
});
});
inject(function($compile, $rootScope, $templateCache, $timeout) {
$templateCache.put('template.html', '
Content To Be Transcluded
');
expect(function() {
element = $compile('
')($rootScope);
$timeout.flush();
}).not.toThrow();
expect(element.text()).toEqual('Content To Be Transcluded');
});
});
it('should lazily compile the contents of directives that are transcluded', function() {
var innerCompilationCount = 0, transclude;
module(function() {
directive('trans', valueFn({
transclude: true,
controller: function($transclude) {
transclude = $transclude;
}
}));
directive('inner', valueFn({
template: '
FooBar ',
compile: function() {
innerCompilationCount += 1;
}
}));
});
inject(function($compile, $rootScope) {
element = $compile('
')($rootScope);
expect(innerCompilationCount).toBe(0);
transclude(function(child) { element.append(child); });
expect(innerCompilationCount).toBe(1);
expect(element.text()).toBe('FooBar');
});
});
it('should lazily compile the contents of directives that are transcluded with a template', function() {
var innerCompilationCount = 0, transclude;
module(function() {
directive('trans', valueFn({
transclude: true,
template: '
Baz
',
controller: function($transclude) {
transclude = $transclude;
}
}));
directive('inner', valueFn({
template: '
FooBar ',
compile: function() {
innerCompilationCount += 1;
}
}));
});
inject(function($compile, $rootScope) {
element = $compile('
')($rootScope);
expect(innerCompilationCount).toBe(0);
transclude(function(child) { element.append(child); });
expect(innerCompilationCount).toBe(1);
expect(element.text()).toBe('BazFooBar');
});
});
it('should lazily compile the contents of directives that are transcluded with a templateUrl', function() {
var innerCompilationCount = 0, transclude;
module(function() {
directive('trans', valueFn({
transclude: true,
templateUrl: 'baz.html',
controller: function($transclude) {
transclude = $transclude;
}
}));
directive('inner', valueFn({
template: '
FooBar ',
compile: function() {
innerCompilationCount += 1;
}
}));
});
inject(function($compile, $rootScope, $httpBackend) {
$httpBackend.expectGET('baz.html').respond('
Baz
');
element = $compile('
')($rootScope);
$httpBackend.flush();
expect(innerCompilationCount).toBe(0);
transclude(function(child) { element.append(child); });
expect(innerCompilationCount).toBe(1);
expect(element.text()).toBe('BazFooBar');
});
});
it('should lazily compile the contents of directives that are transclude element', function() {
var innerCompilationCount = 0, transclude;
module(function() {
directive('trans', valueFn({
transclude: 'element',
controller: function($transclude) {
transclude = $transclude;
}
}));
directive('inner', valueFn({
template: '
FooBar ',
compile: function() {
innerCompilationCount += 1;
}
}));
});
inject(function($compile, $rootScope) {
element = $compile('
')($rootScope);
expect(innerCompilationCount).toBe(0);
transclude(function(child) { element.append(child); });
expect(innerCompilationCount).toBe(1);
expect(element.text()).toBe('FooBar');
});
});
it('should lazily compile transcluded directives with ngIf on them', function() {
var innerCompilationCount = 0, outerCompilationCount = 0, transclude;
module(function() {
directive('outer', valueFn({
transclude: true,
compile: function() {
outerCompilationCount += 1;
},
controller: function($transclude) {
transclude = $transclude;
}
}));
directive('inner', valueFn({
template: '
FooBar ',
compile: function() {
innerCompilationCount += 1;
}
}));
});
inject(function($compile, $rootScope) {
$rootScope.shouldCompile = false;
element = $compile('
')($rootScope);
expect(outerCompilationCount).toBe(0);
expect(innerCompilationCount).toBe(0);
expect(transclude).toBeUndefined();
$rootScope.$apply('shouldCompile=true');
expect(outerCompilationCount).toBe(1);
expect(innerCompilationCount).toBe(0);
expect(transclude).toBeDefined();
transclude(function(child) { element.append(child); });
expect(outerCompilationCount).toBe(1);
expect(innerCompilationCount).toBe(1);
expect(element.text()).toBe('FooBar');
});
});
it('should eagerly compile multiple directives with transclusion and templateUrl/replace', function() {
var innerCompilationCount = 0;
module(function() {
directive('outer', valueFn({
transclude: true
}));
directive('outer', valueFn({
templateUrl: 'inner.html',
replace: true
}));
directive('inner', valueFn({
compile: function() {
innerCompilationCount += 1;
}
}));
});
inject(function($compile, $rootScope, $httpBackend) {
$httpBackend.expectGET('inner.html').respond('
');
element = $compile('
')($rootScope);
$httpBackend.flush();
expect(innerCompilationCount).toBe(1);
});
});
});
});
describe('multi-slot transclude', function() {
it('should only include elements without a matching transclusion element in default transclusion slot', function() {
module(function() {
directive('minionComponent', function() {
return {
restrict: 'E',
scope: {},
transclude: {
bossSlot: 'boss'
},
template:
'
'
};
});
});
inject(function($rootScope, $compile) {
element = $compile(
'
' +
'stuart ' +
'bob ' +
'gru ' +
'kevin ' +
' ')($rootScope);
$rootScope.$apply();
expect(element.text()).toEqual('stuartbobkevin');
});
});
it('should use the default transclusion slot if the ng-transclude attribute has the same value as its key', function() {
module(function() {
directive('minionComponent', function() {
return {
restrict: 'E',
scope: {},
transclude: {},
template:
'
' +
'
' +
'
'
};
});
});
inject(function($rootScope, $compile) {
element = $compile(
'
' +
'stuart ' +
'bob ' +
'kevin ' +
' ')($rootScope);
$rootScope.$apply();
var a = element.children().eq(0);
var b = element.children().eq(1);
var c = element.children().eq(2);
expect(a).toHaveClass('a');
expect(b).toHaveClass('b');
expect(c).toHaveClass('c');
expect(a.text()).toEqual('stuartbobkevin');
expect(b.text()).toEqual('stuartbobkevin');
expect(c.text()).toEqual('stuartbobkevin');
});
});
it('should include non-element nodes in the default transclusion', function() {
module(function() {
directive('minionComponent', function() {
return {
restrict: 'E',
scope: {},
transclude: {
bossSlot: 'boss'
},
template:
'
'
};
});
});
inject(function($rootScope, $compile) {
element = $compile(
'
' +
'text1' +
'stuart ' +
'bob ' +
'gru ' +
'text2' +
'kevin ' +
' ')($rootScope);
$rootScope.$apply();
expect(element.text()).toEqual('text1stuartbobtext2kevin');
});
});
it('should transclude elements to an `ng-transclude` with a matching transclusion slot name', function() {
module(function() {
directive('minionComponent', function() {
return {
restrict: 'E',
scope: {},
transclude: {
minionSlot: 'minion',
bossSlot: 'boss'
},
template:
'
' +
'
' +
'
'
};
});
});
inject(function($rootScope, $compile) {
element = $compile(
'
' +
'stuart ' +
'dorothy ' +
'gru ' +
'kevin ' +
' ')($rootScope);
$rootScope.$apply();
expect(element.children().eq(0).text()).toEqual('gru');
expect(element.children().eq(1).text()).toEqual('stuartkevin');
expect(element.children().eq(2).text()).toEqual('dorothy');
});
});
it('should use the `ng-transclude-slot` attribute if ng-transclude is used as an element', function() {
module(function() {
directive('minionComponent', function() {
return {
restrict: 'E',
scope: {},
transclude: {
minionSlot: 'minion',
bossSlot: 'boss'
},
template:
'
' +
'
' +
'
'
};
});
});
inject(function($rootScope, $compile) {
element = $compile(
'
' +
'stuart ' +
'dorothy ' +
'gru ' +
'kevin ' +
' ')($rootScope);
$rootScope.$apply();
expect(element.children().eq(0).text()).toEqual('gru');
expect(element.children().eq(1).text()).toEqual('stuartkevin');
expect(element.children().eq(2).text()).toEqual('dorothy');
});
});
it('should error if a required transclude slot is not filled', function() {
module(function() {
directive('minionComponent', function() {
return {
restrict: 'E',
scope: {},
transclude: {
minionSlot: 'minion',
bossSlot: 'boss'
},
template:
'
' +
'
' +
'
'
};
});
});
inject(function($rootScope, $compile) {
expect(function() {
element = $compile(
'
' +
'stuart ' +
'dorothy ' +
' ')($rootScope);
}).toThrowMinErr('$compile', 'reqslot', 'Required transclusion slot `bossSlot` was not filled.');
});
});
it('should not error if an optional transclude slot is not filled', function() {
module(function() {
directive('minionComponent', function() {
return {
restrict: 'E',
scope: {},
transclude: {
minionSlot: 'minion',
bossSlot: '?boss'
},
template:
'
' +
'
' +
'
'
};
});
});
inject(function($rootScope, $compile) {
element = $compile(
'
' +
'stuart ' +
'dorothy ' +
' ')($rootScope);
$rootScope.$apply();
expect(element.children().eq(1).text()).toEqual('stuart');
expect(element.children().eq(2).text()).toEqual('dorothy');
});
});
it('should error if we try to transclude a slot that was not declared by the directive', function() {
module(function() {
directive('minionComponent', function() {
return {
restrict: 'E',
scope: {},
transclude: {
minionSlot: 'minion'
},
template:
'
' +
'
' +
'
'
};
});
});
inject(function($rootScope, $compile) {
expect(function() {
element = $compile(
'
' +
'stuart ' +
'dorothy ' +
' ')($rootScope);
}).toThrowMinErr('$compile', 'noslot',
'No parent directive that requires a transclusion with slot name "bossSlot". ' +
'Element:
');
});
});
it('should allow the slot name to equal the element name', function() {
module(function() {
directive('foo', function() {
return {
restrict: 'E',
scope: {},
transclude: {
bar: 'bar'
},
template:
'
'
};
});
});
inject(function($rootScope, $compile) {
element = $compile(
'
' +
'baz ' +
' ')($rootScope);
$rootScope.$apply();
expect(element.text()).toEqual('baz');
});
});
it('should match the normalized form of the element name', function() {
module(function() {
directive('foo', function() {
return {
restrict: 'E',
scope: {},
transclude: {
fooBarSlot: 'fooBar',
mooKarSlot: 'mooKar'
},
template:
'
' +
'
'
};
});
});
inject(function($rootScope, $compile) {
element = $compile(
'
' +
'bar1 ' +
'bar2 ' +
'baz1 ' +
'baz2 ' +
' ')($rootScope);
$rootScope.$apply();
expect(element.children().eq(0).text()).toEqual('bar1bar2');
expect(element.children().eq(1).text()).toEqual('baz1baz2');
});
});
it('should return true from `isSlotFilled(slotName) for slots that have content in the transclusion', function() {
var capturedTranscludeFn;
module(function() {
directive('minionComponent', function() {
return {
restrict: 'E',
scope: {},
transclude: {
minionSlot: 'minion',
bossSlot: '?boss'
},
template:
'
' +
'
' +
'
',
link: function(s, e, a, c, transcludeFn) {
capturedTranscludeFn = transcludeFn;
}
};
});
});
inject(function($rootScope, $compile, log) {
element = $compile(
'
' +
' stuart ' +
' bob ' +
' dorothy ' +
' ')($rootScope);
$rootScope.$apply();
var hasMinions = capturedTranscludeFn.isSlotFilled('minionSlot');
var hasBosses = capturedTranscludeFn.isSlotFilled('bossSlot');
expect(hasMinions).toBe(true);
expect(hasBosses).toBe(false);
});
});
it('should not overwrite the contents of an `ng-transclude` element, if the matching optional slot is not filled', function() {
module(function() {
directive('minionComponent', function() {
return {
restrict: 'E',
scope: {},
transclude: {
minionSlot: 'minion',
bossSlot: '?boss'
},
template:
'
default boss content
' +
'
default minion content
' +
'
default content
'
};
});
});
inject(function($rootScope, $compile) {
element = $compile(
'
' +
'stuart ' +
'dorothy ' +
'kevin ' +
' ')($rootScope);
$rootScope.$apply();
expect(element.children().eq(0).text()).toEqual('default boss content');
expect(element.children().eq(1).text()).toEqual('stuartkevin');
expect(element.children().eq(2).text()).toEqual('dorothy');
});
});
// See issue https://github.com/angular/angular.js/issues/14924
it('should not process top-level transcluded text nodes merged into their sibling',
function() {
module(function() {
directive('transclude', valueFn({
template: '
',
transclude: {},
scope: {}
}));
});
inject(function($compile) {
element = jqLite('
');
element[0].appendChild(document.createTextNode('1{{ value }}'));
element[0].appendChild(document.createTextNode('2{{ value }}'));
element[0].appendChild(document.createTextNode('3{{ value }}'));
var initialWatcherCount = $rootScope.$countWatchers();
$compile(element)($rootScope);
$rootScope.$apply('value = 0');
var newWatcherCount = $rootScope.$countWatchers() - initialWatcherCount;
expect(element.text()).toBe('102030');
expect(newWatcherCount).toBe(3);
// Support: IE 11 only
// See #11781 and #14924
if (msie === 11) {
expect(element.find('ng-transclude').contents().length).toBe(1);
}
});
}
);
});
['img', 'audio', 'video'].forEach(function(tag) {
// Support: IE 9 only
// IE9 rejects the `video` / `audio` tags with "Error: Not implemented"
if (msie !== 9 || tag === 'img') {
describe(tag + '[src] context requirement', function() {
it('should NOT require trusted values for whitelisted URIs', inject(function($rootScope, $compile) {
element = $compile('<' + tag + ' src="{{testUrl}}">' + tag + '>')($rootScope);
$rootScope.testUrl = 'http://example.com/image.mp4'; // `http` is whitelisted
$rootScope.$digest();
expect(element.attr('src')).toEqual('http://example.com/image.mp4');
}));
it('should accept trusted values', inject(function($rootScope, $compile, $sce) {
// As a MEDIA_URL URL
element = $compile('<' + tag + ' src="{{testUrl}}">' + tag + '>')($rootScope);
// Some browsers complain if you try to write `javascript:` into an `img[src]`
// So for the test use something different
$rootScope.testUrl = $sce.trustAsMediaUrl('untrusted:foo()');
$rootScope.$digest();
expect(element.attr('src')).toEqual('untrusted:foo()');
// As a URL
element = $compile('<' + tag + ' src="{{testUrl}}">' + tag + '>')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('untrusted:foo()');
$rootScope.$digest();
expect(element.attr('src')).toEqual('untrusted:foo()');
// As a RESOURCE URL
element = $compile('<' + tag + ' src="{{testUrl}}">' + tag + '>')($rootScope);
$rootScope.testUrl = $sce.trustAsResourceUrl('untrusted:foo()');
$rootScope.$digest();
expect(element.attr('src')).toEqual('untrusted:foo()');
}));
});
}
});
// Support: IE 9 only
// IE 9 rejects the `source` / `track` tags with
// "Unable to get value of the property 'childNodes': object is null or undefined"
if (msie !== 9) {
['source', 'track'].forEach(function(tag) {
describe(tag + '[src]', function() {
it('should NOT require trusted values for whitelisted URIs', inject(function($rootScope, $compile) {
element = $compile('
<' + tag + ' src="{{testUrl}}">' + tag + '> ')($rootScope);
$rootScope.testUrl = 'http://example.com/image.mp4'; // `http` is whitelisted
$rootScope.$digest();
expect(element.find(tag).attr('src')).toEqual('http://example.com/image.mp4');
}));
it('should accept trusted values', inject(function($rootScope, $compile, $sce) {
// As a MEDIA_URL URL
element = $compile('
<' + tag + ' src="{{testUrl}}">' + tag + '> ')($rootScope);
$rootScope.testUrl = $sce.trustAsMediaUrl('javascript:foo()');
$rootScope.$digest();
expect(element.find(tag).attr('src')).toEqual('javascript:foo()');
// As a URL
element = $compile('
<' + tag + ' src="{{testUrl}}">' + tag + '> ')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('javascript:foo()');
$rootScope.$digest();
expect(element.find(tag).attr('src')).toEqual('javascript:foo()');
// As a RESOURCE URL
element = $compile('
<' + tag + ' src="{{testUrl}}">' + tag + '> ')($rootScope);
$rootScope.testUrl = $sce.trustAsResourceUrl('javascript:foo()');
$rootScope.$digest();
expect(element.find(tag).attr('src')).toEqual('javascript:foo()');
}));
});
});
}
describe('img[src] sanitization', function() {
it('should accept trusted values', inject(function($rootScope, $compile, $sce) {
element = $compile('
')($rootScope);
// Some browsers complain if you try to write `javascript:` into an `img[src]`
// So for the test use something different
$rootScope.testUrl = $sce.trustAsMediaUrl('someUntrustedThing:foo();');
$rootScope.$digest();
expect(element.attr('src')).toEqual('someUntrustedThing:foo();');
}));
it('should sanitize concatenated values even if they are trusted', inject(function($rootScope, $compile, $sce) {
element = $compile('
')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('untrusted:foo();');
$rootScope.$digest();
expect(element.attr('src')).toEqual('unsafe:untrusted:foo();ponies');
element = $compile('
')($rootScope);
$rootScope.testUrl2 = $sce.trustAsUrl('xyz;');
$rootScope.$digest();
expect(element.attr('src')).toEqual('http://xyz;');
element = $compile('
')($rootScope);
$rootScope.testUrl3 = $sce.trustAsUrl('untrusted:foo();');
$rootScope.$digest();
expect(element.attr('src')).toEqual('unsafe:untrusted:foo();untrusted:foo();');
}));
it('should not sanitize attributes other than src', inject(function($compile, $rootScope) {
element = $compile('
')($rootScope);
$rootScope.testUrl = 'javascript:doEvilStuff()';
$rootScope.$apply();
expect(element.attr('title')).toBe('javascript:doEvilStuff()');
}));
it('should use $$sanitizeUri', function() {
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri');
module(function($provide) {
$provide.value('$$sanitizeUri', $$sanitizeUri);
});
inject(function($compile, $rootScope) {
element = $compile('
')($rootScope);
$rootScope.testUrl = 'someUrl';
$$sanitizeUri.and.returnValue('someSanitizedUrl');
$rootScope.$apply();
expect(element.attr('src')).toBe('someSanitizedUrl');
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, true);
});
});
it('should use $$sanitizeUri on concatenated trusted values', function() {
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('someSanitizedUrl');
module(function($provide) {
$provide.value('$$sanitizeUri', $$sanitizeUri);
});
inject(function($compile, $rootScope, $sce) {
element = $compile('
')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('javascript:foo();');
$rootScope.$digest();
expect(element.attr('src')).toEqual('someSanitizedUrl');
element = $compile('
')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('xyz');
$rootScope.$digest();
expect(element.attr('src')).toEqual('someSanitizedUrl');
});
});
it('should not use $$sanitizeUri with trusted values', function() {
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.throwError('Should not have been called');
module(function($provide) {
$provide.value('$$sanitizeUri', $$sanitizeUri);
});
inject(function($compile, $rootScope, $sce) {
element = $compile('
')($rootScope);
// Assigning javascript:foo to src makes at least IE9-11 complain, so use another
// protocol name.
$rootScope.testUrl = $sce.trustAsMediaUrl('untrusted:foo();');
$rootScope.$apply();
expect(element.attr('src')).toEqual('untrusted:foo();');
});
});
});
describe('img[srcset] sanitization', function() {
it('should not error if srcset is undefined', function() {
var linked = false;
module(function() {
directive('setter', valueFn(function(scope, elem, attrs) {
// Set srcset to a value
attrs.$set('srcset', 'http://example.com/');
expect(attrs.srcset).toBe('http://example.com/');
// Now set it to undefined
attrs.$set('srcset', undefined);
expect(attrs.srcset).toBeUndefined();
linked = true;
}));
});
inject(function($compile, $rootScope) {
element = $compile('
')($rootScope);
expect(linked).toBe(true);
expect(element.attr('srcset')).toBeUndefined();
});
});
it('should NOT require trusted values for whitelisted values', inject(function($rootScope, $compile, $sce) {
element = $compile('
')($rootScope);
$rootScope.testUrl = 'http://example.com/image.png'; // `http` is whitelisted
$rootScope.$digest();
expect(element.attr('srcset')).toEqual('http://example.com/image.png');
}));
it('should accept trusted values, if they are also whitelisted', inject(function($rootScope, $compile, $sce) {
element = $compile('
')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('http://example.com');
$rootScope.$digest();
expect(element.attr('srcset')).toEqual('http://example.com');
}));
it('does not work with trusted values', inject(function($rootScope, $compile, $sce) {
// A limitation of the approach used for srcset is that you cannot use `trustAsUrl`.
// Use trustAsHtml and ng-bind-html to work around this.
element = $compile('
')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('javascript:something');
$rootScope.$digest();
expect(element.attr('srcset')).toEqual('unsafe:javascript:something');
element = $compile('
')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('javascript:something');
$rootScope.$digest();
expect(element.attr('srcset')).toEqual(
'unsafe:javascript:something ,unsafe:javascript:something');
}));
it('should use $$sanitizeUri', function() {
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('someSanitizedUrl');
module(function($provide) {
$provide.value('$$sanitizeUri', $$sanitizeUri);
});
inject(function($compile, $rootScope) {
element = $compile('
')($rootScope);
$rootScope.testUrl = 'someUrl';
$rootScope.$apply();
expect(element.attr('srcset')).toBe('someSanitizedUrl');
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, true);
element = $compile('
')($rootScope);
$rootScope.testUrl = 'javascript:yay';
$rootScope.$apply();
expect(element.attr('srcset')).toEqual('someSanitizedUrl ,someSanitizedUrl');
element = $compile('
')($rootScope);
$rootScope.testUrl = 'script:yay, javascript:nay';
$rootScope.$apply();
expect(element.attr('srcset')).toEqual('someSanitizedUrl ,someSanitizedUrl');
});
});
it('should sanitize all uris in srcset', inject(function($rootScope, $compile) {
element = $compile('
')($rootScope);
var testSet = {
'http://example.com/image.png':'http://example.com/image.png',
' http://example.com/image.png':'http://example.com/image.png',
'http://example.com/image.png ':'http://example.com/image.png',
'http://example.com/image.png 128w':'http://example.com/image.png 128w',
'http://example.com/image.png 2x':'http://example.com/image.png 2x',
'http://example.com/image.png 1.5x':'http://example.com/image.png 1.5x',
'http://example.com/image1.png 1x,http://example.com/image2.png 2x':'http://example.com/image1.png 1x,http://example.com/image2.png 2x',
'http://example.com/image1.png 1x ,http://example.com/image2.png 2x':'http://example.com/image1.png 1x ,http://example.com/image2.png 2x',
'http://example.com/image1.png 1x, http://example.com/image2.png 2x':'http://example.com/image1.png 1x,http://example.com/image2.png 2x',
'http://example.com/image1.png 1x , http://example.com/image2.png 2x':'http://example.com/image1.png 1x ,http://example.com/image2.png 2x',
'http://example.com/image1.png 48w,http://example.com/image2.png 64w':'http://example.com/image1.png 48w,http://example.com/image2.png 64w',
//Test regex to make sure doesn't mistake parts of url for width descriptors
'http://example.com/image1.png?w=48w,http://example.com/image2.png 64w':'http://example.com/image1.png?w=48w,http://example.com/image2.png 64w',
'http://example.com/image1.png 1x,http://example.com/image2.png 64w':'http://example.com/image1.png 1x,http://example.com/image2.png 64w',
'http://example.com/image1.png,http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
'http://example.com/image1.png ,http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
'http://example.com/image1.png, http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
'http://example.com/image1.png , http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
'http://example.com/image1.png 1x, http://example.com/image2.png 2x, http://example.com/image3.png 3x':
'http://example.com/image1.png 1x,http://example.com/image2.png 2x,http://example.com/image3.png 3x',
'javascript:doEvilStuff() 2x': 'unsafe:javascript:doEvilStuff() 2x',
'http://example.com/image1.png 1x,javascript:doEvilStuff() 2x':'http://example.com/image1.png 1x,unsafe:javascript:doEvilStuff() 2x',
'http://example.com/image1.jpg?x=a,b 1x,http://example.com/ima,ge2.jpg 2x':'http://example.com/image1.jpg?x=a,b 1x,http://example.com/ima,ge2.jpg 2x',
//Test regex to make sure doesn't mistake parts of url for pixel density descriptors
'http://example.com/image1.jpg?x=a2x,b 1x,http://example.com/ima,ge2.jpg 2x':'http://example.com/image1.jpg?x=a2x,b 1x,http://example.com/ima,ge2.jpg 2x'
};
forEach(testSet, function(ref, url) {
$rootScope.testUrl = url;
$rootScope.$digest();
expect(element.attr('srcset')).toEqual(ref);
});
}));
});
describe('a[href] sanitization', function() {
it('should NOT require trusted values for whitelisted values', inject(function($rootScope, $compile) {
$rootScope.testUrl = 'http://example.com/image.png'; // `http` is whitelisted
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(element.attr('href')).toEqual('http://example.com/image.png');
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(element.attr('ng-href')).toEqual('http://example.com/image.png');
}));
it('should accept trusted values for non-whitelisted values', inject(function($rootScope, $compile, $sce) {
$rootScope.testUrl = $sce.trustAsUrl('javascript:foo()'); // `javascript` is not whitelisted
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(element.attr('href')).toEqual('javascript:foo()');
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(element.attr('ng-href')).toEqual('javascript:foo()');
}));
it('should sanitize non-whitelisted values', inject(function($rootScope, $compile) {
$rootScope.testUrl = 'javascript:foo()'; // `javascript` is not whitelisted
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(element.attr('href')).toEqual('unsafe:javascript:foo()');
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(element.attr('href')).toEqual('unsafe:javascript:foo()');
}));
it('should not sanitize href on elements other than anchor', inject(function($compile, $rootScope) {
element = $compile('
')($rootScope);
$rootScope.testUrl = 'javascript:doEvilStuff()';
$rootScope.$apply();
expect(element.attr('href')).toBe('javascript:doEvilStuff()');
}));
it('should not sanitize attributes other than href/ng-href', inject(function($compile, $rootScope) {
element = $compile('
')($rootScope);
$rootScope.testUrl = 'javascript:doEvilStuff()';
$rootScope.$apply();
expect(element.attr('title')).toBe('javascript:doEvilStuff()');
}));
it('should use $$sanitizeUri', function() {
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('someSanitizedUrl');
module(function($provide) {
$provide.value('$$sanitizeUri', $$sanitizeUri);
});
inject(function($compile, $rootScope) {
element = $compile('
')($rootScope);
$rootScope.testUrl = 'someUrl';
$rootScope.$apply();
expect(element.attr('href')).toBe('someSanitizedUrl');
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, false);
$$sanitizeUri.calls.reset();
element = $compile('
')($rootScope);
$rootScope.$apply();
expect(element.attr('href')).toBe('someSanitizedUrl');
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, false);
});
});
it('should use $$sanitizeUri when working with svg and xlink:href', function() {
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('https://clean.example.org');
module(function($provide) {
$provide.value('$$sanitizeUri', $$sanitizeUri);
});
inject(function($compile, $rootScope) {
// This URL would fail the RESOURCE_URL whitelist, but that test shouldn't be run
// because these interpolations will be resolved against the URL context instead
$rootScope.testUrl = 'https://bad.example.org';
var elementA = $compile('
')($rootScope);
$rootScope.$apply();
expect(elementA.find('a').attr('xlink:href')).toBe('https://clean.example.org');
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl + 'aTag', false);
var elementImage = $compile('
')($rootScope);
$rootScope.$apply();
expect(elementImage.find('image').attr('xlink:href')).toBe('https://clean.example.org');
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl + 'imageTag', true);
});
});
it('should use $$sanitizeUri when working with svg and xlink:href through ng-href', function() {
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('https://clean.example.org');
module(function($provide) {
$provide.value('$$sanitizeUri', $$sanitizeUri);
});
inject(function($compile, $rootScope) {
// This URL would fail the RESOURCE_URL whitelist, but that test shouldn't be run
// because these interpolations will be resolved against the URL context instead
$rootScope.testUrl = 'https://bad.example.org';
element = $compile('
')($rootScope);
$rootScope.$apply();
expect(element.find('a').prop('href').baseVal).toBe('https://clean.example.org');
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, false);
});
});
it('should require a RESOURCE_URL context for xlink:href by if not on an anchor or image', function() {
inject(function($compile, $rootScope) {
element = $compile('
')($rootScope);
$rootScope.testUrl = 'https://bad.example.org';
expect(function() {
$rootScope.$apply();
}).toThrowMinErr('$interpolate', 'interr', 'Can\'t interpolate: {{ testUrl }}\n' +
'Error: [$sce:insecurl] Blocked loading resource from url not allowed by $sceDelegate policy. ' +
'URL: https://bad.example.org');
});
});
it('should not have endless digests when given arrays in concatenable context', inject(function($compile, $rootScope) {
element = $compile('
' +
'
')($rootScope);
$rootScope.testUrl = [1];
$rootScope.$digest();
$rootScope.testUrl = [];
$rootScope.$digest();
$rootScope.testUrl = {a:'b'};
$rootScope.$digest();
$rootScope.testUrl = {};
$rootScope.$digest();
}));
});
describe('interpolation on HTML DOM event handler attributes onclick, onXYZ, formaction', function() {
it('should disallow interpolation on onclick', inject(function($compile, $rootScope) {
// All interpolations are disallowed.
$rootScope.onClickJs = '';
expect(function() {
$compile('
');
}).toThrowMinErr(
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed. ' +
'Please use the ng- versions (such as ng-click instead of onclick) instead.');
expect(function() {
$compile('');
}).toThrowMinErr(
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed. ' +
'Please use the ng- versions (such as ng-click instead of onclick) instead.');
expect(function() {
$compile('');
}).toThrowMinErr(
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed. ' +
'Please use the ng- versions (such as ng-click instead of onclick) instead.');
}));
it('should pass through arbitrary values on onXYZ event attributes that contain a hyphen', inject(function($compile, $rootScope) {
element = $compile('')($rootScope);
$rootScope.onClickJs = 'javascript:doSomething()';
$rootScope.$apply();
expect(element.attr('on-click')).toEqual('javascript:doSomething()');
}));
it('should pass through arbitrary values on "on" and "data-on" attributes', inject(function($compile, $rootScope) {
element = $compile('')($rootScope);
$rootScope.dataOnVar = 'data-on text';
$rootScope.$apply();
expect(element.attr('data-on')).toEqual('data-on text');
element = $compile('')($rootScope);
$rootScope.onVar = 'on text';
$rootScope.$apply();
expect(element.attr('on')).toEqual('on text');
}));
});
describe('iframe[src]', function() {
it('should pass through src attributes for the same domain', inject(function($compile, $rootScope, $sce) {
element = $compile('')($rootScope);
$rootScope.testUrl = 'different_page';
$rootScope.$apply();
expect(element.attr('src')).toEqual('different_page');
}));
it('should clear out src attributes for a different domain', inject(function($compile, $rootScope, $sce) {
element = $compile('')($rootScope);
$rootScope.testUrl = 'http://a.different.domain.example.com';
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
'$interpolate', 'interr', 'Can\'t interpolate: {{testUrl}}\nError: [$sce:insecurl] Blocked ' +
'loading resource from url not allowed by $sceDelegate policy. URL: ' +
'http://a.different.domain.example.com');
}));
it('should clear out JS src attributes', inject(function($compile, $rootScope, $sce) {
element = $compile('')($rootScope);
$rootScope.testUrl = 'javascript:alert(1);';
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
'$interpolate', 'interr', 'Can\'t interpolate: {{testUrl}}\nError: [$sce:insecurl] Blocked ' +
'loading resource from url not allowed by $sceDelegate policy. URL: ' +
'javascript:alert(1);');
}));
it('should clear out non-resource_url src attributes', inject(function($compile, $rootScope, $sce) {
element = $compile('')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('javascript:doTrustedStuff()');
expect($rootScope.$apply).toThrowMinErr(
'$interpolate', 'interr', 'Can\'t interpolate: {{testUrl}}\nError: [$sce:insecurl] Blocked ' +
'loading resource from url not allowed by $sceDelegate policy. URL: javascript:doTrustedStuff()');
}));
it('should pass through $sce.trustAs() values in src attributes', inject(function($compile, $rootScope, $sce) {
element = $compile('')($rootScope);
$rootScope.testUrl = $sce.trustAsResourceUrl('javascript:doTrustedStuff()');
$rootScope.$apply();
expect(element.attr('src')).toEqual('javascript:doTrustedStuff()');
}));
});
describe('base[href]', function() {
it('should be a RESOURCE_URL context', inject(function($compile, $rootScope, $sce) {
element = $compile(' ')($rootScope);
$rootScope.testUrl = $sce.trustAsResourceUrl('https://example.com/');
$rootScope.$apply();
expect(element.attr('href')).toContain('https://example.com/');
$rootScope.testUrl = 'https://not.example.com/';
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
'$interpolate', 'interr', 'Can\'t interpolate: {{testUrl}}\nError: [$sce:insecurl] Blocked ' +
'loading resource from url not allowed by $sceDelegate policy. URL: ' +
'https://not.example.com/');
}));
});
describe('form[action]', function() {
it('should pass through action attribute for the same domain', inject(function($compile, $rootScope, $sce) {
element = $compile('')($rootScope);
$rootScope.testUrl = 'different_page';
$rootScope.$apply();
expect(element.attr('action')).toEqual('different_page');
}));
it('should clear out action attribute for a different domain', inject(function($compile, $rootScope, $sce) {
element = $compile('')($rootScope);
$rootScope.testUrl = 'http://a.different.domain.example.com';
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
'$interpolate', 'interr', 'Can\'t interpolate: {{testUrl}}\nError: [$sce:insecurl] Blocked ' +
'loading resource from url not allowed by $sceDelegate policy. URL: ' +
'http://a.different.domain.example.com');
}));
it('should clear out JS action attribute', inject(function($compile, $rootScope, $sce) {
element = $compile('')($rootScope);
$rootScope.testUrl = 'javascript:alert(1);';
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
'$interpolate', 'interr', 'Can\'t interpolate: {{testUrl}}\nError: [$sce:insecurl] Blocked ' +
'loading resource from url not allowed by $sceDelegate policy. URL: ' +
'javascript:alert(1);');
}));
it('should clear out non-resource_url action attribute', inject(function($compile, $rootScope, $sce) {
element = $compile('')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('javascript:doTrustedStuff()');
expect($rootScope.$apply).toThrowMinErr(
'$interpolate', 'interr', 'Can\'t interpolate: {{testUrl}}\nError: [$sce:insecurl] Blocked ' +
'loading resource from url not allowed by $sceDelegate policy. URL: javascript:doTrustedStuff()');
}));
it('should pass through $sce.trustAs() values in action attribute', inject(function($compile, $rootScope, $sce) {
element = $compile('')($rootScope);
$rootScope.testUrl = $sce.trustAsResourceUrl('javascript:doTrustedStuff()');
$rootScope.$apply();
expect(element.attr('action')).toEqual('javascript:doTrustedStuff()');
}));
});
describe('link[href]', function() {
it('should reject invalid RESOURCE_URLs', inject(function($compile, $rootScope) {
element = $compile(' ')($rootScope);
$rootScope.testUrl = 'https://evil.example.org/css.css';
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
'$interpolate', 'interr', 'Can\'t interpolate: {{testUrl}}\nError: [$sce:insecurl] Blocked ' +
'loading resource from url not allowed by $sceDelegate policy. URL: ' +
'https://evil.example.org/css.css');
}));
it('should accept valid RESOURCE_URLs', inject(function($compile, $rootScope, $sce) {
element = $compile(' ')($rootScope);
$rootScope.testUrl = './css1.css';
$rootScope.$apply();
expect(element.attr('href')).toContain('css1.css');
$rootScope.testUrl = $sce.trustAsResourceUrl('https://elsewhere.example.org/css2.css');
$rootScope.$apply();
expect(element.attr('href')).toContain('https://elsewhere.example.org/css2.css');
}));
it('should accept valid constants', inject(function($compile, $rootScope) {
element = $compile(' ')($rootScope);
$rootScope.$apply();
expect(element.attr('href')).toContain('https://elsewhere.example.org/css2.css');
}));
});
// Support: IE 9-10 only
// IEs <11 don't support srcdoc
if (!msie || msie === 11) {
describe('iframe[srcdoc]', function() {
it('should NOT set iframe contents for untrusted values', inject(function($compile, $rootScope, $sce) {
element = $compile('')($rootScope);
$rootScope.html = 'hello
';
expect(function() { $rootScope.$digest(); }).toThrowMinErr('$interpolate', 'interr', new RegExp(
/Can't interpolate: {{html}}\n/.source +
/[^[]*\[\$sce:unsafe] Attempting to use an unsafe value in a safe context./.source));
}));
it('should NOT set html for wrongly typed values', inject(function($rootScope, $compile, $sce) {
element = $compile('')($rootScope);
$rootScope.html = $sce.trustAsCss('hello
');
expect(function() { $rootScope.$digest(); }).toThrowMinErr('$interpolate', 'interr', new RegExp(
/Can't interpolate: \{\{html}}\n/.source +
/[^[]*\[\$sce:unsafe] Attempting to use an unsafe value in a safe context./.source));
}));
it('should set html for trusted values', inject(function($rootScope, $compile, $sce) {
element = $compile('')($rootScope);
$rootScope.html = $sce.trustAsHtml('hello
');
$rootScope.$digest();
expect(lowercase(element.attr('srcdoc'))).toEqual('hello
');
}));
});
}
describe('ngAttr* attribute binding', function() {
it('should bind after digest but not before', inject(function() {
$rootScope.name = 'Misko';
element = $compile(' ')($rootScope);
expect(element.attr('test')).toBeUndefined();
$rootScope.$digest();
expect(element.attr('test')).toBe('Misko');
}));
it('should bind after digest but not before when after overridden attribute', inject(function() {
$rootScope.name = 'Misko';
element = $compile(' ')($rootScope);
expect(element.attr('test')).toBe('123');
$rootScope.$digest();
expect(element.attr('test')).toBe('Misko');
}));
it('should bind after digest but not before when before overridden attribute', inject(function() {
$rootScope.name = 'Misko';
element = $compile(' ')($rootScope);
expect(element.attr('test')).toBe('123');
$rootScope.$digest();
expect(element.attr('test')).toBe('Misko');
}));
it('should set the attribute (after digest) even if there is no interpolation', inject(function() {
element = $compile(' ')($rootScope);
expect(element.attr('test')).toBeUndefined();
$rootScope.$digest();
expect(element.attr('test')).toBe('foo');
}));
it('should remove attribute if any bindings are undefined', inject(function() {
element = $compile(' ')($rootScope);
$rootScope.$digest();
expect(element.attr('test')).toBeUndefined();
$rootScope.name = 'caitp';
$rootScope.$digest();
expect(element.attr('test')).toBeUndefined();
$rootScope.emphasis = '!!!';
$rootScope.$digest();
expect(element.attr('test')).toBe('caitp!!!');
}));
describe('in directive', function() {
var log;
beforeEach(module(function() {
directive('syncTest', function(log) {
return {
link: {
pre: function(s, e, attr) { log(attr.test); },
post: function(s, e, attr) { log(attr.test); }
}
};
});
directive('asyncTest', function(log) {
return {
templateUrl: 'async.html',
link: {
pre: function(s, e, attr) { log(attr.test); },
post: function(s, e, attr) { log(attr.test); }
}
};
});
}));
beforeEach(inject(function($templateCache, _log_) {
log = _log_;
$templateCache.put('async.html', 'Test ');
}));
it('should provide post-digest value in synchronous directive link functions when after overridden attribute',
function() {
$rootScope.test = 'TEST';
element = $compile('
')($rootScope);
expect(element.attr('test')).toBe('123');
expect(log.toArray()).toEqual(['TEST', 'TEST']);
}
);
it('should provide post-digest value in synchronous directive link functions when before overridden attribute',
function() {
$rootScope.test = 'TEST';
element = $compile('
')($rootScope);
expect(element.attr('test')).toBe('123');
expect(log.toArray()).toEqual(['TEST', 'TEST']);
}
);
it('should provide post-digest value in asynchronous directive link functions when after overridden attribute',
function() {
$rootScope.test = 'TEST';
element = $compile('
')($rootScope);
expect(element.attr('test')).toBe('123');
$rootScope.$digest();
expect(log.toArray()).toEqual(['TEST', 'TEST']);
}
);
it('should provide post-digest value in asynchronous directive link functions when before overridden attribute',
function() {
$rootScope.test = 'TEST';
element = $compile('
')($rootScope);
expect(element.attr('test')).toBe('123');
$rootScope.$digest();
expect(log.toArray()).toEqual(['TEST', 'TEST']);
}
);
});
it('should work with different prefixes', inject(function() {
$rootScope.name = 'Misko';
element = $compile(' ')($rootScope);
expect(element.attr('test')).toBeUndefined();
expect(element.attr('test2')).toBeUndefined();
expect(element.attr('test3')).toBeUndefined();
$rootScope.$digest();
expect(element.attr('test')).toBe('Misko');
expect(element.attr('test2')).toBe('Misko');
expect(element.attr('test3')).toBe('Misko');
}));
it('should work with the "href" attribute', inject(function() {
$rootScope.value = 'test';
element = $compile(' ')($rootScope);
$rootScope.$digest();
expect(element.attr('href')).toBe('test/test');
}));
it('should work if they are prefixed with x- or data- and different prefixes', inject(function() {
$rootScope.name = 'Misko';
element = $compile(' ')($rootScope);
expect(element.attr('test2')).toBeUndefined();
expect(element.attr('test3')).toBeUndefined();
expect(element.attr('test4')).toBeUndefined();
expect(element.attr('test5')).toBeUndefined();
expect(element.attr('test6')).toBeUndefined();
$rootScope.$digest();
expect(element.attr('test2')).toBe('Misko');
expect(element.attr('test3')).toBe('Misko');
expect(element.attr('test4')).toBe('Misko');
expect(element.attr('test5')).toBe('Misko');
expect(element.attr('test6')).toBe('Misko');
}));
describe('when an attribute has a dash-separated name', function() {
it('should work with different prefixes', inject(function() {
$rootScope.name = 'JamieMason';
element = $compile(' ')($rootScope);
expect(element.attr('dash-test')).toBeUndefined();
expect(element.attr('dash-test2')).toBeUndefined();
expect(element.attr('dash-test3')).toBeUndefined();
$rootScope.$digest();
expect(element.attr('dash-test')).toBe('JamieMason');
expect(element.attr('dash-test2')).toBe('JamieMason');
expect(element.attr('dash-test3')).toBe('JamieMason');
}));
it('should work if they are prefixed with x- or data-', inject(function() {
$rootScope.name = 'JamieMason';
element = $compile(' ')($rootScope);
expect(element.attr('dash-test2')).toBeUndefined();
expect(element.attr('dash-test3')).toBeUndefined();
expect(element.attr('dash-test4')).toBeUndefined();
$rootScope.$digest();
expect(element.attr('dash-test2')).toBe('JamieMason');
expect(element.attr('dash-test3')).toBe('JamieMason');
expect(element.attr('dash-test4')).toBe('JamieMason');
}));
it('should keep attributes ending with -start single-element directives', function() {
module(function($compileProvider) {
$compileProvider.directive('dashStarter', function(log) {
return {
link: function(scope, element, attrs) {
log(attrs.onDashStart);
}
};
});
});
inject(function($compile, $rootScope, log) {
$compile(' ')($rootScope);
$rootScope.$digest();
expect(log).toEqual('starter');
});
});
it('should keep attributes ending with -end single-element directives', function() {
module(function($compileProvider) {
$compileProvider.directive('dashEnder', function(log) {
return {
link: function(scope, element, attrs) {
log(attrs.onDashEnd);
}
};
});
});
inject(function($compile, $rootScope, log) {
$compile(' ')($rootScope);
$rootScope.$digest();
expect(log).toEqual('ender');
});
});
});
});
describe('when an attribute has an underscore-separated name', function() {
it('should work with different prefixes', inject(function($compile, $rootScope) {
$rootScope.dimensions = '0 0 0 0';
element = $compile(' ')($rootScope);
expect(element.attr('viewBox')).toBeUndefined();
$rootScope.$digest();
expect(element.attr('viewBox')).toBe('0 0 0 0');
}));
it('should work if they are prefixed with x- or data-', inject(function($compile, $rootScope) {
$rootScope.dimensions = '0 0 0 0';
$rootScope.number = 0.42;
$rootScope.scale = 1;
element = $compile('' +
'' +
'' +
' ' +
'' +
' ')($rootScope);
expect(element.attr('viewBox')).toBeUndefined();
$rootScope.$digest();
expect(element.attr('viewBox')).toBe('0 0 0 0');
expect(element.find('filter').attr('filterUnits')).toBe('0.42');
expect(element.find('feDiffuseLighting').attr('surfaceScale')).toBe('1');
expect(element.find('feSpecularLighting').attr('surfaceScale')).toBe('1');
}));
});
describe('multi-element directive', function() {
it('should group on link function', inject(function($compile, $rootScope) {
$rootScope.show = false;
element = $compile(
'' +
' ' +
' ' +
'
')($rootScope);
$rootScope.$digest();
var spans = element.find('span');
expect(spans.eq(0)).toBeHidden();
expect(spans.eq(1)).toBeHidden();
}));
it('should group on compile function', inject(function($compile, $rootScope) {
$rootScope.show = false;
element = $compile(
'' +
'{{i}}A ' +
'{{i}}B; ' +
'
')($rootScope);
$rootScope.$digest();
expect(element.text()).toEqual('1A1B;2A2B;');
}));
it('should support grouping over text nodes', inject(function($compile, $rootScope) {
$rootScope.show = false;
element = $compile(
'' +
'{{i}}A ' +
':' + // Important: proves that we can iterate over non-elements
'{{i}}B; ' +
'
')($rootScope);
$rootScope.$digest();
expect(element.text()).toEqual('1A:1B;2A:2B;');
}));
it('should group on $root compile function', inject(function($compile, $rootScope) {
$rootScope.show = false;
element = $compile(
'
' +
'{{i}}A ' +
'{{i}}B; ' +
'
')($rootScope);
$rootScope.$digest();
element = jqLite(element[0].parentNode.childNodes); // reset because repeater is top level.
expect(element.text()).toEqual('1A1B;2A2B;');
}));
it('should group on nested groups', function() {
module(function($compileProvider) {
$compileProvider.directive('ngMultiBind', valueFn({
multiElement: true,
link: function(scope, element, attr) {
element.text(scope.$eval(attr.ngMultiBind));
}
}));
});
inject(function($compile, $rootScope) {
$rootScope.show = false;
element = $compile(
'
' +
'{{i}}A
' +
' ' +
' ' +
'{{i}}B;
' +
'
')($rootScope);
$rootScope.$digest();
element = jqLite(element[0].parentNode.childNodes); // reset because repeater is top level.
expect(element.text()).toEqual('1A..1B;2A..2B;');
});
});
it('should group on nested groups of same directive', inject(function($compile, $rootScope) {
$rootScope.show = false;
element = $compile(
'
' +
'{{i}}(
' +
'{{j}}- ' +
'{{j}} ' +
'){{i}};
' +
'
')($rootScope);
$rootScope.$digest();
element = jqLite(element[0].parentNode.childNodes); // reset because repeater is top level.
expect(element.text()).toEqual('1(2-23-3)1;2(2-23-3)2;');
}));
it('should set up and destroy the transclusion scopes correctly',
inject(function($compile, $rootScope) {
element = $compile(
''
)($rootScope);
$rootScope.$apply('val0 = true; val1 = true; val2 = true');
// At this point we should have something like:
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
var ngIfStartScope = element.find('div').eq(0).scope();
var ngIfEndScope = element.find('div').eq(1).scope();
expect(ngIfStartScope.$id).toEqual(ngIfEndScope.$id);
var ngIf1Scope = element.find('span').eq(0).scope();
var ngIf2Scope = element.find('span').eq(1).scope();
expect(ngIf1Scope.$id).not.toEqual(ngIf2Scope.$id);
expect(ngIf1Scope.$parent.$id).toEqual(ngIf2Scope.$parent.$id);
$rootScope.$apply('val1 = false');
// Now we should have something like:
//
//
//
//
//
//
//
//
//
//
//
//
//
expect(ngIfStartScope.$$destroyed).not.toEqual(true);
expect(ngIf1Scope.$$destroyed).toEqual(true);
expect(ngIf2Scope.$$destroyed).not.toEqual(true);
$rootScope.$apply('val0 = false');
// Now we should have something like:
//
//
//
//
expect(ngIfStartScope.$$destroyed).toEqual(true);
expect(ngIf1Scope.$$destroyed).toEqual(true);
expect(ngIf2Scope.$$destroyed).toEqual(true);
}));
it('should set up and destroy the transclusion scopes correctly',
inject(function($compile, $rootScope) {
element = $compile(
''
)($rootScope);
// To begin with there is (almost) nothing:
//
//
//
expect(element.scope().$id).toEqual($rootScope.$id);
// Now we create all the elements
$rootScope.$apply('val0 = [1]; val1 = true; val2 = true');
// At this point we have:
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
var ngIf1Scope = element.find('div').eq(0).scope();
var ngIf2Scope = element.find('div').eq(1).scope();
var ngRepeatScope = ngIf1Scope.$parent;
expect(ngIf1Scope.$id).not.toEqual(ngIf2Scope.$id);
expect(ngIf1Scope.$parent.$id).toEqual(ngRepeatScope.$id);
expect(ngIf2Scope.$parent.$id).toEqual(ngRepeatScope.$id);
// What is happening here??
// We seem to have a repeater scope which doesn't actually match to any element
expect(ngRepeatScope.$parent.$id).toEqual($rootScope.$id);
// Now remove the first ngIf element from the first item in the repeater
$rootScope.$apply('val1 = false');
// At this point we should have:
//
//
//
//
//
//
//
//
//
//
//
//
//
expect(ngRepeatScope.$$destroyed).toEqual(false);
expect(ngIf1Scope.$$destroyed).toEqual(true);
expect(ngIf2Scope.$$destroyed).toEqual(false);
// Now remove the second ngIf element from the first item in the repeater
$rootScope.$apply('val2 = false');
// We are mostly back to where we started
//
//
//
//
//
//
//
expect(ngRepeatScope.$$destroyed).toEqual(false);
expect(ngIf1Scope.$$destroyed).toEqual(true);
expect(ngIf2Scope.$$destroyed).toEqual(true);
// Finally remove the repeat items
$rootScope.$apply('val0 = []');
// Somehow this ngRepeat scope knows how to destroy itself...
expect(ngRepeatScope.$$destroyed).toEqual(true);
expect(ngIf1Scope.$$destroyed).toEqual(true);
expect(ngIf2Scope.$$destroyed).toEqual(true);
}));
it('should throw error if unterminated', function() {
module(function($compileProvider) {
$compileProvider.directive('foo', function() {
return {
multiElement: true
};
});
});
inject(function($compile, $rootScope) {
expect(function() {
element = $compile(
'' +
' ' +
'
');
}).toThrowMinErr('$compile', 'uterdir', 'Unterminated attribute, found \'foo-start\' but no matching \'foo-end\' found.');
});
});
it('should correctly collect ranges on multiple directives on a single element', function() {
module(function($compileProvider) {
$compileProvider.directive('emptyDirective', function() {
return {
multiElement: true,
link: function(scope, element) {
element.data('x', 'abc');
}
};
});
$compileProvider.directive('rangeDirective', function() {
return {
multiElement: true,
link: function(scope) {
scope.x = 'X';
scope.y = 'Y';
}
};
});
});
inject(function($compile, $rootScope) {
element = $compile(
'' +
'
{{x}}
' +
'
{{y}}
' +
'
'
)($rootScope);
$rootScope.$digest();
expect(element.text()).toBe('XY');
expect(angular.element(element[0].firstChild).data('x')).toBe('abc');
});
});
it('should throw error if unterminated (containing termination as a child)', function() {
module(function($compileProvider) {
$compileProvider.directive('foo', function() {
return {
multiElement: true
};
});
});
inject(function($compile) {
expect(function() {
element = $compile(
'' +
' ' +
'
');
}).toThrowMinErr('$compile', 'uterdir', 'Unterminated attribute, found \'foo-start\' but no matching \'foo-end\' found.');
});
});
it('should support data- and x- prefix', inject(function($compile, $rootScope) {
$rootScope.show = false;
element = $compile(
'' +
' ' +
' ' +
' ' +
' ' +
'
')($rootScope);
$rootScope.$digest();
var spans = element.find('span');
expect(spans.eq(0)).toBeHidden();
expect(spans.eq(1)).toBeHidden();
expect(spans.eq(2)).toBeHidden();
expect(spans.eq(3)).toBeHidden();
}));
});
describe('$animate animation hooks', function() {
beforeEach(module('ngAnimateMock'));
it('should automatically fire the addClass and removeClass animation hooks',
inject(function($compile, $animate, $rootScope) {
var data, element = jqLite('
');
$compile(element)($rootScope);
$rootScope.$digest();
expect(element.hasClass('fire')).toBe(true);
$rootScope.val1 = 'ice';
$rootScope.val2 = 'rice';
$rootScope.$digest();
data = $animate.queue.shift();
expect(data.event).toBe('addClass');
expect(data.args[1]).toBe('ice rice');
expect(element.hasClass('ice')).toBe(true);
expect(element.hasClass('rice')).toBe(true);
expect(element.hasClass('fire')).toBe(true);
$rootScope.val2 = 'dice';
$rootScope.$digest();
data = $animate.queue.shift();
expect(data.event).toBe('addClass');
expect(data.args[1]).toBe('dice');
data = $animate.queue.shift();
expect(data.event).toBe('removeClass');
expect(data.args[1]).toBe('rice');
expect(element.hasClass('ice')).toBe(true);
expect(element.hasClass('dice')).toBe(true);
expect(element.hasClass('fire')).toBe(true);
$rootScope.val1 = '';
$rootScope.val2 = '';
$rootScope.$digest();
data = $animate.queue.shift();
expect(data.event).toBe('removeClass');
expect(data.args[1]).toBe('ice dice');
expect(element.hasClass('ice')).toBe(false);
expect(element.hasClass('dice')).toBe(false);
expect(element.hasClass('fire')).toBe(true);
}));
});
describe('element replacement', function() {
it('should broadcast $destroy only on removed elements, not replaced', function() {
var linkCalls = [];
var destroyCalls = [];
module(function($compileProvider) {
$compileProvider.directive('replace', function() {
return {
multiElement: true,
replace: true,
templateUrl: 'template123'
};
});
$compileProvider.directive('foo', function() {
return {
priority: 1, // before the replace directive
link: function($scope, $element, $attrs) {
linkCalls.push($attrs.foo);
$element.on('$destroy', function() {
destroyCalls.push($attrs.foo);
});
}
};
});
});
inject(function($compile, $templateCache, $rootScope) {
$templateCache.put('template123', '
');
$compile(
'
' +
'
' +
'
'
)($rootScope);
expect(linkCalls).toEqual(['2', '3']);
expect(destroyCalls).toEqual([]);
$rootScope.$apply();
expect(linkCalls).toEqual(['2', '3', '1']);
expect(destroyCalls).toEqual(['2', '3']);
});
});
function getAll($root) {
// check for .querySelectorAll to support comment nodes
return [$root[0]].concat($root[0].querySelectorAll ? sliceArgs($root[0].querySelectorAll('*')) : []);
}
function testCompileLinkDataCleanup(template) {
inject(function($compile, $rootScope) {
var toCompile = jqLite(template);
var preCompiledChildren = getAll(toCompile);
forEach(preCompiledChildren, function(element, i) {
jqLite.data(element, 'foo', 'template#' + i);
});
var linkedElements = $compile(toCompile)($rootScope);
$rootScope.$apply();
linkedElements.remove();
forEach(preCompiledChildren, function(element, i) {
expect(jqLite.hasData(element)).toBe(false, 'template#' + i);
});
forEach(getAll(linkedElements), function(element, i) {
expect(jqLite.hasData(element)).toBe(false, 'linked#' + i);
});
});
}
it('should clean data of element-transcluded link-cloned elements', function() {
testCompileLinkDataCleanup('');
});
it('should clean data of element-transcluded elements', function() {
testCompileLinkDataCleanup('
');
});
function testReplaceElementCleanup(dirOptions) {
var template = '
';
module(function($compileProvider) {
$compileProvider.directive('theDir', function() {
return {
multiElement: true,
replace: dirOptions.replace,
transclude: dirOptions.transclude,
template: dirOptions.asyncTemplate ? undefined : template,
templateUrl: dirOptions.asyncTemplate ? 'the-dir-template-url' : undefined
};
});
});
inject(function($templateCache, $compile, $rootScope) {
$templateCache.put('the-dir-template-url', template);
testCompileLinkDataCleanup(
''
);
});
}
it('should clean data of elements removed for directive template', function() {
testReplaceElementCleanup({});
});
it('should clean data of elements removed for directive templateUrl', function() {
testReplaceElementCleanup({asyncTemplate: true});
});
it('should clean data of elements transcluded into directive template', function() {
testReplaceElementCleanup({transclude: true});
});
it('should clean data of elements transcluded into directive templateUrl', function() {
testReplaceElementCleanup({transclude: true, asyncTemplate: true});
});
it('should clean data of elements replaced with directive template', function() {
testReplaceElementCleanup({replace: true});
});
it('should clean data of elements replaced with directive templateUrl', function() {
testReplaceElementCleanup({replace: true, asyncTemplate: true});
});
});
describe('component helper', function() {
it('should return the module', function() {
var myModule = angular.module('my', []);
expect(myModule.component('myComponent', {})).toBe(myModule);
expect(myModule.component({})).toBe(myModule);
});
it('should register a directive', function() {
angular.module('my', []).component('myComponent', {
template: 'SUCCESS
',
controller: function(log) {
log('OK');
}
});
module('my');
inject(function($compile, $rootScope, log) {
element = $compile(' ')($rootScope);
expect(element.find('div').text()).toEqual('SUCCESS');
expect(log).toEqual('OK');
});
});
it('should register multiple directives when object passed as first parameter', function() {
var log = '';
angular.module('my', []).component({
fooComponent: {
template: 'FOO SUCCESS
',
controller: function() {
log += 'FOO:OK';
}
},
barComponent: {
template: 'BAR SUCCESS
',
controller: function() {
log += 'BAR:OK';
}
}
});
module('my');
inject(function($compile, $rootScope) {
var fooElement = $compile(' ')($rootScope);
var barElement = $compile(' ')($rootScope);
expect(fooElement.find('div').text()).toEqual('FOO SUCCESS');
expect(barElement.find('div').text()).toEqual('BAR SUCCESS');
expect(log).toEqual('FOO:OKBAR:OK');
});
});
it('should register a directive via $compileProvider.component()', function() {
module(function($compileProvider) {
$compileProvider.component('myComponent', {
template: 'SUCCESS
',
controller: function(log) {
log('OK');
}
});
});
inject(function($compile, $rootScope, log) {
element = $compile(' ')($rootScope);
expect(element.find('div').text()).toEqual('SUCCESS');
expect(log).toEqual('OK');
});
});
it('should add additional annotations to directive factory', function() {
var myModule = angular.module('my', []).component('myComponent', {
$canActivate: 'canActivate',
$routeConfig: 'routeConfig',
$customAnnotation: 'XXX'
});
expect(myModule._invokeQueue.pop().pop()[1]).toEqual(jasmine.objectContaining({
$canActivate: 'canActivate',
$routeConfig: 'routeConfig',
$customAnnotation: 'XXX'
}));
});
it('should expose additional annotations on the directive definition object', function() {
angular.module('my', []).component('myComponent', {
$canActivate: 'canActivate',
$routeConfig: 'routeConfig',
$customAnnotation: 'XXX'
});
module('my');
inject(function(myComponentDirective) {
expect(myComponentDirective[0]).toEqual(jasmine.objectContaining({
$canActivate: 'canActivate',
$routeConfig: 'routeConfig',
$customAnnotation: 'XXX'
}));
});
});
it('should support custom annotations if the controller is named', function() {
angular.module('my', []).component('myComponent', {
$customAnnotation: 'XXX',
controller: 'SomeNamedController'
});
module('my');
inject(function(myComponentDirective) {
expect(myComponentDirective[0]).toEqual(jasmine.objectContaining({
$customAnnotation: 'XXX'
}));
});
});
it('should provide a new empty controller if none is specified', function() {
angular.
module('my', []).
component('myComponent1', {$customAnnotation1: 'XXX'}).
component('myComponent2', {$customAnnotation2: 'YYY'});
module('my');
inject(function(myComponent1Directive, myComponent2Directive) {
var ctrl1 = myComponent1Directive[0].controller;
var ctrl2 = myComponent2Directive[0].controller;
expect(ctrl1).not.toBe(ctrl2);
expect(ctrl1.$customAnnotation1).toBe('XXX');
expect(ctrl1.$customAnnotation2).toBeUndefined();
expect(ctrl2.$customAnnotation1).toBeUndefined();
expect(ctrl2.$customAnnotation2).toBe('YYY');
});
});
it('should return ddo with reasonable defaults', function() {
angular.module('my', []).component('myComponent', {});
module('my');
inject(function(myComponentDirective) {
expect(myComponentDirective[0]).toEqual(jasmine.objectContaining({
controller: jasmine.any(Function),
controllerAs: '$ctrl',
template: '',
templateUrl: undefined,
transclude: undefined,
scope: {},
bindToController: {},
restrict: 'E'
}));
});
});
it('should return ddo with assigned options', function() {
function myCtrl() {}
angular.module('my', []).component('myComponent', {
controller: myCtrl,
controllerAs: 'ctrl',
template: 'abc',
templateUrl: 'def.html',
transclude: true,
bindings: {abc: '='}
});
module('my');
inject(function(myComponentDirective) {
expect(myComponentDirective[0]).toEqual(jasmine.objectContaining({
controller: myCtrl,
controllerAs: 'ctrl',
template: 'abc',
templateUrl: 'def.html',
transclude: true,
scope: {},
bindToController: {abc: '='},
restrict: 'E'
}));
});
});
it('should allow passing injectable functions as template/templateUrl', function() {
var log = '';
angular.module('my', []).component('myComponent', {
template: function($element, $attrs, myValue) {
log += 'template,' + $element + ',' + $attrs + ',' + myValue + '\n';
},
templateUrl: function($element, $attrs, myValue) {
log += 'templateUrl,' + $element + ',' + $attrs + ',' + myValue + '\n';
}
}).value('myValue', 'blah');
module('my');
inject(function(myComponentDirective) {
myComponentDirective[0].template('a', 'b');
myComponentDirective[0].templateUrl('c', 'd');
expect(log).toEqual('template,a,b,blah\ntemplateUrl,c,d,blah\n');
});
});
it('should allow passing injectable arrays as template/templateUrl', function() {
var log = '';
angular.module('my', []).component('myComponent', {
template: ['$element', '$attrs', 'myValue', function($element, $attrs, myValue) {
log += 'template,' + $element + ',' + $attrs + ',' + myValue + '\n';
}],
templateUrl: ['$element', '$attrs', 'myValue', function($element, $attrs, myValue) {
log += 'templateUrl,' + $element + ',' + $attrs + ',' + myValue + '\n';
}]
}).value('myValue', 'blah');
module('my');
inject(function(myComponentDirective) {
myComponentDirective[0].template('a', 'b');
myComponentDirective[0].templateUrl('c', 'd');
expect(log).toEqual('template,a,b,blah\ntemplateUrl,c,d,blah\n');
});
});
it('should allow passing transclude as object', function() {
angular.module('my', []).component('myComponent', {
transclude: {}
});
module('my');
inject(function(myComponentDirective) {
expect(myComponentDirective[0]).toEqual(jasmine.objectContaining({
transclude: {}
}));
});
});
it('should give ctrl as syntax priority over controllerAs', function() {
angular.module('my', []).component('myComponent', {
controller: 'MyCtrl as vm'
});
module('my');
inject(function(myComponentDirective) {
expect(myComponentDirective[0]).toEqual(jasmine.objectContaining({
controllerAs: 'vm'
}));
});
});
});
describe('$$createComment', function() {
it('should create empty comments if `debugInfoEnabled` is false', function() {
module(function($compileProvider) {
$compileProvider.debugInfoEnabled(false);
});
inject(function($compile) {
var comment = $compile.$$createComment('foo', 'bar');
expect(comment.data).toBe('');
});
});
it('should create descriptive comments if `debugInfoEnabled` is true', function() {
module(function($compileProvider) {
$compileProvider.debugInfoEnabled(true);
});
inject(function($compile) {
var comment = $compile.$$createComment('foo', 'bar');
expect(comment.data).toBe(' foo: bar ');
});
});
});
});