')($rootScope);
expect(element.text()).toEqual('template content');
}));
});
describe('templateUrl', function() {
beforeEach(module(
function() {
directive('hello', valueFn({
restrict: 'CAM', templateUrl: 'hello.html', transclude: true
}));
directive('cau', valueFn({
restrict: 'CAM', templateUrl: 'cau.html'
}));
directive('crossDomainTemplate', valueFn({
restrict: 'CAM', templateUrl: 'http://example.com/should-not-load.html'
}));
directive('trustedTemplate', function($sce) { return {
restrict: 'CAM',
templateUrl: function() {
return $sce.trustAsResourceUrl('http://example.com/trusted-template.html');
}};
});
directive('cError', valueFn({
restrict: 'CAM',
templateUrl:'error.html',
compile: function() {
throw Error('cError');
}
}));
directive('lError', valueFn({
restrict: 'CAM',
templateUrl: 'error.html',
compile: function() {
throw Error('lError');
}
}));
directive('iHello', valueFn({
restrict: 'CAM',
replace: true,
templateUrl: 'hello.html'
}));
directive('iCau', valueFn({
restrict: 'CAM',
replace: true,
templateUrl:'cau.html'
}));
directive('iCError', valueFn({
restrict: 'CAM',
replace: true,
templateUrl:'error.html',
compile: function() {
throw Error('cError');
}
}));
directive('iLError', valueFn({
restrict: 'CAM',
replace: true,
templateUrl: 'error.html',
compile: function() {
throw Error('lError');
}
}));
directive('replace', valueFn({
replace: true,
template: '
Hello, {{name}}! '
}));
}
));
it('should not load cross domain templates by default', inject(
function($compile, $rootScope, $templateCache, $sce) {
expect(function() {
$templateCache.put('http://example.com/should-not-load.html', 'Should not load even if in cache.');
$compile('
')($rootScope);
}).toThrowMinErr('$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy. URL: http://example.com/should-not-load.html');
}));
it('should load cross domain templates when trusted', inject(
function($compile, $httpBackend, $rootScope, $sce) {
$httpBackend.expect('GET', 'http://example.com/trusted-template.html').respond('
example.com/trusted_template_contents ');
element = $compile('
')($rootScope);
expect(sortedHtml(element)).
toEqual('
');
$httpBackend.flush();
expect(sortedHtml(element)).
toEqual('
example.com/trusted_template_contents
');
}));
it('should append template via $http and cache it in $templateCache', inject(
function($compile, $httpBackend, $templateCache, $rootScope, $browser) {
$httpBackend.expect('GET', 'hello.html').respond('
Hello! World!');
$templateCache.put('cau.html', '
Cau! ');
element = $compile('
ignore ignore
')($rootScope);
expect(sortedHtml(element)).
toEqual('
');
$rootScope.$digest();
expect(sortedHtml(element)).
toEqual('
Cau!
');
$httpBackend.flush();
expect(sortedHtml(element)).toEqual(
'
' +
'Hello! World! ' +
'Cau! ' +
'
');
}
));
it('should inline template via $http and cache it in $templateCache', inject(
function($compile, $httpBackend, $templateCache, $rootScope) {
$httpBackend.expect('GET', 'hello.html').respond('
Hello! ');
$templateCache.put('cau.html', '
Cau! ');
element = $compile('
ignore ignore
')($rootScope);
expect(sortedHtml(element)).
toEqual('
');
$rootScope.$digest();
expect(sortedHtml(element)).toBeOneOf(
'
Cau!
',
'
Cau!
' //ie8
);
$httpBackend.flush();
expect(sortedHtml(element)).toBeOneOf(
'
Hello! Cau!
',
'
Hello! Cau!
' //ie8
);
}
));
it('should compile, link and flush the template append', inject(
function($compile, $templateCache, $rootScope, $browser) {
$templateCache.put('hello.html', '
Hello, {{name}}! ');
$rootScope.name = 'Elvis';
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(sortedHtml(element)).
toEqual('
Hello, Elvis!
');
}
));
it('should compile, link and flush the template inline', inject(
function($compile, $templateCache, $rootScope) {
$templateCache.put('hello.html', '
Hello, {{name}}! ');
$rootScope.name = 'Elvis';
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(sortedHtml(element)).toBeOneOf(
'
Hello, Elvis!
',
'
Hello, Elvis!
' //ie8
);
}
));
it('should compile, flush and link the template append', inject(
function($compile, $templateCache, $rootScope) {
$templateCache.put('hello.html', '
Hello, {{name}}! ');
$rootScope.name = 'Elvis';
var template = $compile('
');
element = template($rootScope);
$rootScope.$digest();
expect(sortedHtml(element)).
toEqual('
Hello, Elvis!
');
}
));
it('should compile, flush and link the template inline', inject(
function($compile, $templateCache, $rootScope) {
$templateCache.put('hello.html', '
Hello, {{name}}! ');
$rootScope.name = 'Elvis';
var template = $compile('
');
element = template($rootScope);
$rootScope.$digest();
expect(sortedHtml(element)).toBeOneOf(
'
Hello, Elvis!
',
'
Hello, Elvis!
' //ie8
);
}
));
it('should compile template when replacing element in another template',
inject(function($compile, $templateCache, $rootScope) {
$templateCache.put('hello.html', '
');
$rootScope.name = 'Elvis';
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(sortedHtml(element)).
toEqual('
Hello, Elvis!
');
}));
it('should compile template when replacing root element',
inject(function($compile, $templateCache, $rootScope) {
$rootScope.name = 'Elvis';
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(sortedHtml(element)).
toEqual('
Hello, Elvis! ');
}));
it('should resolve widgets after cloning in append mode', function() {
module(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
});
inject(function($compile, $templateCache, $rootScope, $httpBackend, $browser,
$exceptionHandler) {
$httpBackend.expect('GET', 'hello.html').respond('
{{greeting}} ');
$httpBackend.expect('GET', 'error.html').respond('
');
$templateCache.put('cau.html', '
{{name}} ');
$rootScope.greeting = 'Hello';
$rootScope.name = 'Elvis';
var template = $compile(
'
' +
' ' +
' ' +
' ' +
' ' +
'
');
var e1;
var e2;
e1 = template($rootScope.$new(), noop); // clone
expect(e1.text()).toEqual('');
$httpBackend.flush();
e2 = template($rootScope.$new(), noop); // clone
$rootScope.$digest();
expect(e1.text()).toEqual('Hello Elvis');
expect(e2.text()).toEqual('Hello Elvis');
expect($exceptionHandler.errors.length).toEqual(2);
expect($exceptionHandler.errors[0][0].message).toEqual('cError');
expect($exceptionHandler.errors[1][0].message).toEqual('lError');
dealoc(e1);
dealoc(e2);
});
});
it('should resolve widgets after cloning in append mode without $templateCache', function() {
module(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
});
inject(function($compile, $templateCache, $rootScope, $httpBackend, $browser,
$exceptionHandler) {
$httpBackend.expect('GET', 'cau.html').respond('
{{name}} ');
$rootScope.name = 'Elvis';
var template = $compile('
');
var e1;
var e2;
e1 = template($rootScope.$new(), noop); // clone
expect(e1.text()).toEqual('');
$httpBackend.flush();
e2 = template($rootScope.$new(), noop); // clone
$rootScope.$digest();
expect(e1.text()).toEqual('Elvis');
expect(e2.text()).toEqual('Elvis');
dealoc(e1);
dealoc(e2);
});
});
it('should resolve widgets after cloning in inline mode', function() {
module(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
});
inject(function($compile, $templateCache, $rootScope, $httpBackend, $browser,
$exceptionHandler) {
$httpBackend.expect('GET', 'hello.html').respond('
{{greeting}} ');
$httpBackend.expect('GET', 'error.html').respond('
');
$templateCache.put('cau.html', '
{{name}} ');
$rootScope.greeting = 'Hello';
$rootScope.name = 'Elvis';
var template = $compile(
'
' +
' ' +
' ' +
' ' +
' ' +
'
');
var e1;
var e2;
e1 = template($rootScope.$new(), noop); // clone
expect(e1.text()).toEqual('');
$httpBackend.flush();
e2 = template($rootScope.$new(), noop); // clone
$rootScope.$digest();
expect(e1.text()).toEqual('Hello Elvis');
expect(e2.text()).toEqual('Hello Elvis');
expect($exceptionHandler.errors.length).toEqual(2);
expect($exceptionHandler.errors[0][0].message).toEqual('cError');
expect($exceptionHandler.errors[1][0].message).toEqual('lError');
dealoc(e1);
dealoc(e2);
});
});
it('should resolve widgets after cloning in inline mode without $templateCache', function() {
module(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
});
inject(function($compile, $templateCache, $rootScope, $httpBackend, $browser,
$exceptionHandler) {
$httpBackend.expect('GET', 'cau.html').respond('
{{name}} ');
$rootScope.name = 'Elvis';
var template = $compile('
');
var e1;
var e2;
e1 = template($rootScope.$new(), noop); // clone
expect(e1.text()).toEqual('');
$httpBackend.flush();
e2 = template($rootScope.$new(), noop); // clone
$rootScope.$digest();
expect(e1.text()).toEqual('Elvis');
expect(e2.text()).toEqual('Elvis');
dealoc(e1);
dealoc(e2);
});
});
it('should be implicitly terminal and not compile placeholder content in append', inject(
function($compile, $templateCache, $rootScope, log) {
// we can't compile the contents because that would result in a memory leak
$templateCache.put('hello.html', 'Hello!');
element = $compile('
')($rootScope);
expect(log).toEqual('');
}
));
it('should be implicitly terminal and not compile placeholder content in inline', inject(
function($compile, $templateCache, $rootScope, log) {
// we can't compile the contents because that would result in a memory leak
$templateCache.put('hello.html', 'Hello!');
element = $compile('
')($rootScope);
expect(log).toEqual('');
}
));
it('should throw an error and clear element content if the template fails to load', inject(
function($compile, $httpBackend, $rootScope) {
$httpBackend.expect('GET', 'hello.html').respond(404, 'Not Found!');
element = $compile('
content
')($rootScope);
expect(function() {
$httpBackend.flush();
}).toThrowMinErr('$compile', 'tpload', 'Failed to load template: hello.html');
expect(sortedHtml(element)).toBe('
');
}
));
it('should prevent multiple templates per element', function() {
module(function() {
directive('sync', valueFn({
restrict: 'C',
template: '
'
}));
directive('async', valueFn({
restrict: 'C',
templateUrl: 'template.html'
}));
});
inject(function($compile, $httpBackend){
$httpBackend.whenGET('template.html').respond('
template.html
');
expect(function() {
$compile('
');
$httpBackend.flush();
}).toThrowMinErr('$compile', 'multidir', 'Multiple directives [async, sync] asking for template on: '+
'
');
});
});
describe('delay compile / linking functions until after template is resolved', function(){
var template;
beforeEach(module(function() {
function logDirective (name, priority, options) {
directive(name, function(log) {
return (extend({
priority: priority,
compile: function() {
log(name + '-C');
return {
pre: function() { log(name + '-PreL'); },
post: function() { log(name + '-PostL'); }
}
}
}, options || {}));
});
}
logDirective('first', 10);
logDirective('second', 5, { templateUrl: 'second.html' });
logDirective('third', 3);
logDirective('last', 0);
logDirective('iFirst', 10, {replace: true});
logDirective('iSecond', 5, {replace: true, templateUrl: 'second.html' });
logDirective('iThird', 3, {replace: true});
logDirective('iLast', 0, {replace: true});
}));
it('should flush after link append', inject(
function($compile, $rootScope, $httpBackend, log) {
$httpBackend.expect('GET', 'second.html').respond('
{{1+2}}
');
template = $compile('
');
element = template($rootScope);
expect(log).toEqual('first-C');
log('FLUSH');
$httpBackend.flush();
$rootScope.$digest();
expect(log).toEqual(
'first-C; FLUSH; second-C; last-C; third-C; ' +
'first-PreL; second-PreL; last-PreL; third-PreL; ' +
'third-PostL; last-PostL; second-PostL; first-PostL');
var span = element.find('span');
expect(span.attr('first')).toEqual('');
expect(span.attr('second')).toEqual('');
expect(span.find('div').attr('third')).toEqual('');
expect(span.attr('last')).toEqual('');
expect(span.text()).toEqual('3');
}));
it('should flush after link inline', inject(
function($compile, $rootScope, $httpBackend, log) {
$httpBackend.expect('GET', 'second.html').respond('
{{1+2}}
');
template = $compile('
');
element = template($rootScope);
expect(log).toEqual('iFirst-C');
log('FLUSH');
$httpBackend.flush();
$rootScope.$digest();
expect(log).toEqual(
'iFirst-C; FLUSH; iSecond-C; iThird-C; iLast-C; ' +
'iFirst-PreL; iSecond-PreL; iThird-PreL; iLast-PreL; ' +
'iLast-PostL; iThird-PostL; iSecond-PostL; iFirst-PostL');
var div = element.find('div');
expect(div.attr('i-first')).toEqual('');
expect(div.attr('i-second')).toEqual('');
expect(div.attr('i-third')).toEqual('');
expect(div.attr('i-last')).toEqual('');
expect(div.text()).toEqual('3');
}));
it('should flush before link append', inject(
function($compile, $rootScope, $httpBackend, log) {
$httpBackend.expect('GET', 'second.html').respond('
{{1+2}}
');
template = $compile('
');
expect(log).toEqual('first-C');
log('FLUSH');
$httpBackend.flush();
expect(log).toEqual('first-C; FLUSH; second-C; last-C; third-C');
element = template($rootScope);
$rootScope.$digest();
expect(log).toEqual(
'first-C; FLUSH; second-C; last-C; third-C; ' +
'first-PreL; second-PreL; last-PreL; third-PreL; ' +
'third-PostL; last-PostL; second-PostL; first-PostL');
var span = element.find('span');
expect(span.attr('first')).toEqual('');
expect(span.attr('second')).toEqual('');
expect(span.find('div').attr('third')).toEqual('');
expect(span.attr('last')).toEqual('');
expect(span.text()).toEqual('3');
}));
it('should flush before link inline', inject(
function($compile, $rootScope, $httpBackend, log) {
$httpBackend.expect('GET', 'second.html').respond('
{{1+2}}
');
template = $compile('
');
expect(log).toEqual('iFirst-C');
log('FLUSH');
$httpBackend.flush();
expect(log).toEqual('iFirst-C; FLUSH; iSecond-C; iThird-C; iLast-C');
element = template($rootScope);
$rootScope.$digest();
expect(log).toEqual(
'iFirst-C; FLUSH; iSecond-C; iThird-C; iLast-C; ' +
'iFirst-PreL; iSecond-PreL; iThird-PreL; iLast-PreL; ' +
'iLast-PostL; iThird-PostL; iSecond-PostL; iFirst-PostL');
var div = element.find('div');
expect(div.attr('i-first')).toEqual('');
expect(div.attr('i-second')).toEqual('');
expect(div.attr('i-third')).toEqual('');
expect(div.attr('i-last')).toEqual('');
expect(div.text()).toEqual('3');
}));
});
it('should allow multiple elements in template', inject(function($compile, $httpBackend) {
$httpBackend.expect('GET', 'hello.html').respond('before
mid after');
element = jqLite('
');
$compile(element);
$httpBackend.flush();
expect(element.text()).toEqual('before mid after');
}));
it('should work when directive is on the root element', inject(
function($compile, $httpBackend, $rootScope) {
$httpBackend.expect('GET', 'hello.html').
respond('
3== ');
element = jqLite('
{{1+2}} ');
$compile(element)($rootScope);
$httpBackend.flush();
expect(element.text()).toEqual('3==3');
}
));
it('should work when directive is in a repeater', inject(
function($compile, $httpBackend, $rootScope) {
$httpBackend.expect('GET', 'hello.html').
respond('
i= ; ');
element = jqLite('
{{i}}
');
$compile(element)($rootScope);
$httpBackend.flush();
expect(element.text()).toEqual('i=1;i=2;');
}
));
it("should fail if replacing and template doesn't have a single root element", function() {
module(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
directive('template', function() {
return {
replace: true,
templateUrl: 'template.html'
}
});
});
inject(function($compile, $templateCache, $rootScope, $exceptionHandler) {
// no root element
$templateCache.put('template.html', 'dada');
$compile('
');
$rootScope.$digest();
expect($exceptionHandler.errors.pop().message).
toMatch(/\[\$compile:tplrt\] Template for directive 'template' must have exactly one root element\. template\.html/);
// multi root
$templateCache.put('template.html', '
');
$compile('
');
$rootScope.$digest();
expect($exceptionHandler.errors.pop().message).
toMatch(/\[\$compile:tplrt\] Template for directive 'template' must have exactly one root element\. template\.html/);
// ws is ok
$templateCache.put('template.html', '
\n');
$compile('
');
$rootScope.$apply();
expect($exceptionHandler.errors).toEqual([]);
});
});
it('should resume delayed compilation without duplicates when in a repeater', function() {
// this is a test for a regression
// scope creation, isolate watcher setup, controller instantiation, etc should happen
// only once even if we are dealing with delayed compilation of a node due to templateUrl
// and the template node is in a repeater
var controllerSpy = jasmine.createSpy('controller');
module(function($compileProvider) {
$compileProvider.directive('delayed', valueFn({
controller: controllerSpy,
templateUrl: 'delayed.html',
scope: {
title: '@'
}
}));
});
inject(function($templateCache, $compile, $rootScope) {
$rootScope.coolTitle = 'boom!';
$templateCache.put('delayed.html', '
{{title}}
');
element = $compile(
'
'
)($rootScope);
$rootScope.$apply();
expect(controllerSpy.callCount).toBe(2);
expect(element.text()).toBe('boom!1|boom!2|');
});
});
it('should support templateUrl with replace', function() {
// a regression https://github.com/angular/angular.js/issues/3792
module(function($compileProvider) {
$compileProvider.directive('simple', function() {
return {
templateUrl: '/some.html',
replace: true
};
});
});
inject(function($templateCache, $rootScope, $compile) {
$templateCache.put('/some.html',
'
' +
'
i = 1
' +
'
I dont know what `i` is.
' +
'
');
element = $compile('
')($rootScope);
$rootScope.$apply(function() {
$rootScope.i = 1;
});
expect(element.html()).toContain('i = 1');
});
});
});
describe('templateUrl as function', function() {
beforeEach(module(function() {
directive('myDirective', valueFn({
replace: true,
templateUrl: function($element, $attrs) {
expect($element.text()).toBe('original content');
expect($attrs.myDirective).toBe('some value');
return 'my-directive.html';
},
compile: function($element, $attrs) {
expect($element.text()).toBe('template content');
expect($attrs.id).toBe('templateContent');
}
}));
}));
it('should evaluate `templateUrl` when defined as fn and use returned value as url', inject(
function($compile, $rootScope, $templateCache) {
$templateCache.put('my-directive.html', '
template content');
element = $compile('
original content
')($rootScope);
expect(element.text()).toEqual('');
$rootScope.$digest();
expect(element.text()).toEqual('template content');
}));
});
describe('scope', function() {
var iscope;
beforeEach(module(function() {
forEach(['', 'a', 'b'], function(name) {
directive('scope' + uppercase(name), function(log) {
return {
scope: true,
restrict: 'CA',
compile: function() {
return {pre: function (scope, element) {
log(scope.$id);
expect(element.data('$scope')).toBe(scope);
}};
}
};
});
directive('iscope' + uppercase(name), function(log) {
return {
scope: {},
restrict: 'CA',
compile: function() {
return function (scope, element) {
iscope = scope;
log(scope.$id);
expect(element.data('$isolateScopeNoTemplate')).toBe(scope);
};
}
};
});
directive('tscope' + uppercase(name), function(log) {
return {
scope: true,
restrict: 'CA',
templateUrl: 'tscope.html',
compile: function() {
return function (scope, element) {
log(scope.$id);
expect(element.data('$scope')).toBe(scope);
};
}
};
});
directive('trscope' + uppercase(name), function(log) {
return {
scope: true,
replace: true,
restrict: 'CA',
templateUrl: 'trscope.html',
compile: function() {
return function (scope, element) {
log(scope.$id);
expect(element.data('$scope')).toBe(scope);
};
}
};
});
directive('tiscope' + uppercase(name), function(log) {
return {
scope: {},
restrict: 'CA',
templateUrl: 'tiscope.html',
compile: function() {
return function (scope, element) {
iscope = scope;
log(scope.$id);
expect(element.data('$isolateScope')).toBe(scope);
};
}
};
});
});
directive('log', function(log) {
return {
restrict: 'CA',
link: {pre: function(scope) {
log('log-' + scope.$id + '-' + (scope.$parent && scope.$parent.$id || 'no-parent'));
}}
};
});
}));
it('should allow creation of new scopes', inject(function($rootScope, $compile, log) {
element = $compile('
')($rootScope);
expect(log).toEqual('002; log-002-001; LOG');
expect(element.find('span').hasClass('ng-scope')).toBe(true);
}));
it('should allow creation of new isolated scopes for directives', inject(
function($rootScope, $compile, log) {
element = $compile('
')($rootScope);
expect(log).toEqual('log-001-no-parent; LOG; 002');
$rootScope.name = 'abc';
expect(iscope.$parent).toBe($rootScope);
expect(iscope.name).toBeUndefined();
}));
it('should allow creation of new scopes for directives with templates', inject(
function($rootScope, $compile, log, $httpBackend) {
$httpBackend.expect('GET', 'tscope.html').respond('
{{name}}; scopeId: {{$id}} ');
element = $compile('
')($rootScope);
$httpBackend.flush();
expect(log).toEqual('log-002-001; LOG; 002');
$rootScope.name = 'Jozo';
$rootScope.$apply();
expect(element.text()).toBe('Jozo; scopeId: 002');
expect(element.find('span').scope().$id).toBe('002');
}));
it('should allow creation of new scopes for replace directives with templates', inject(
function($rootScope, $compile, log, $httpBackend) {
$httpBackend.expect('GET', 'trscope.html').
respond('
{{name}}; scopeId: {{$id}}
');
element = $compile('
')($rootScope);
$httpBackend.flush();
expect(log).toEqual('log-002-001; LOG; 002');
$rootScope.name = 'Jozo';
$rootScope.$apply();
expect(element.text()).toBe('Jozo; scopeId: 002');
expect(element.find('a').scope().$id).toBe('002');
}));
it('should allow creation of new scopes for replace directives with templates in a repeater',
inject(function($rootScope, $compile, log, $httpBackend) {
$httpBackend.expect('GET', 'trscope.html').
respond('
{{name}}; scopeId: {{$id}} |
');
element = $compile('
')($rootScope);
$httpBackend.flush();
expect(log).toEqual('log-003-002; LOG; 003; log-005-004; LOG; 005; log-007-006; LOG; 007');
$rootScope.name = 'Jozo';
$rootScope.$apply();
expect(element.text()).toBe('Jozo; scopeId: 003 |Jozo; scopeId: 005 |Jozo; scopeId: 007 |');
expect(element.find('p').scope().$id).toBe('003');
expect(element.find('a').scope().$id).toBe('003');
}));
it('should allow creation of new isolated scopes for directives with templates', inject(
function($rootScope, $compile, log, $httpBackend) {
$httpBackend.expect('GET', 'tiscope.html').respond('
');
element = $compile('
')($rootScope);
$httpBackend.flush();
expect(log).toEqual('log-002-001; LOG; 002');
$rootScope.name = 'abc';
expect(iscope.$parent).toBe($rootScope);
expect(iscope.name).toBeUndefined();
}));
it('should correctly create the scope hierachy', inject(
function($rootScope, $compile, log) {
element = $compile(
'
' + //1
'' + //2
' ' + //3
' ' +
' ' +
'' + //4
' ' +
' ' +
'
'
)($rootScope);
expect(log).toEqual('002; 003; log-003-002; LOG; log-002-001; LOG; 004; log-004-001; LOG');
})
);
it('should allow more than one new scope directives per element, but directives should share' +
'the scope', inject(
function($rootScope, $compile, log) {
element = $compile('
')($rootScope);
expect(log).toEqual('002; 002');
})
);
it('should not allow more then one isolate scope creation per element', inject(
function($rootScope, $compile) {
expect(function(){
$compile('
');
}).toThrowMinErr('$compile', 'multidir', 'Multiple directives [iscopeA, scopeB] asking for new/isolated scope on: ' +
'
');
})
);
it('should create new scope even at the root of the template', inject(
function($rootScope, $compile, log) {
element = $compile('
')($rootScope);
expect(log).toEqual('002');
})
);
it('should create isolate scope even at the root of the template', inject(
function($rootScope, $compile, log) {
element = $compile('
')($rootScope);
expect(log).toEqual('002');
})
);
describe('scope()/isolate() scope getters', function() {
describe('with no directives', function() {
it('should return the scope of the parent node', inject(
function($rootScope, $compile) {
element = $compile('
')($rootScope);
expect(element.scope()).toBe($rootScope);
})
);
});
describe('with new scope directives', function() {
it('should return the new scope at the directive element', inject(
function($rootScope, $compile) {
element = $compile('
')($rootScope);
expect(element.scope().$parent).toBe($rootScope);
})
);
it('should return the new scope for children in the original template', inject(
function($rootScope, $compile) {
element = $compile('
')($rootScope);
expect(element.find('a').scope().$parent).toBe($rootScope);
})
);
it('should return the new scope for children in the directive template', inject(
function($rootScope, $compile, $httpBackend) {
$httpBackend.expect('GET', 'tscope.html').respond('
');
element = $compile('
')($rootScope);
$httpBackend.flush();
expect(element.find('a').scope().$parent).toBe($rootScope);
})
);
});
describe('with isolate scope directives', function() {
it('should return the root scope for directives at the root element', inject(
function($rootScope, $compile) {
element = $compile('
')($rootScope);
expect(element.scope()).toBe($rootScope);
})
);
it('should return the non-isolate scope at the directive element', inject(
function($rootScope, $compile) {
var directiveElement;
element = $compile('
')($rootScope);
directiveElement = element.children();
expect(directiveElement.scope()).toBe($rootScope);
expect(directiveElement.isolateScope().$parent).toBe($rootScope);
})
);
it('should return the isolate scope for children in the original template', inject(
function($rootScope, $compile) {
element = $compile('
')($rootScope);
expect(element.find('a').scope()).toBe($rootScope); //xx
})
);
it('should return the isolate scope for children in directive template', inject(
function($rootScope, $compile, $httpBackend) {
$httpBackend.expect('GET', 'tiscope.html').respond('
');
element = $compile('
')($rootScope);
expect(element.isolateScope()).toBeUndefined(); // this is the current behavior, not desired feature
$httpBackend.flush();
expect(element.find('a').scope()).toBe(element.isolateScope());
expect(element.isolateScope()).not.toBe($rootScope);
})
);
});
describe('with isolate scope directives and directives that manually create a new scope', function() {
it('should return the new scope at the directive element', inject(
function($rootScope, $compile) {
var directiveElement;
element = $compile('
')($rootScope);
$rootScope.$apply();
directiveElement = element.find('a');
expect(directiveElement.scope().$parent).toBe($rootScope);
expect(directiveElement.scope()).not.toBe(directiveElement.isolateScope());
})
);
it('should return the isolate scope for child elements', inject(
function($rootScope, $compile, $httpBackend) {
var directiveElement, child;
$httpBackend.expect('GET', 'tiscope.html').respond('
');
element = $compile('
')($rootScope);
$rootScope.$apply();
$httpBackend.flush();
directiveElement = element.find('a');
child = directiveElement.find('span');
expect(child.scope()).toBe(directiveElement.isolateScope());
})
);
});
});
});
});
});
describe('interpolation', function() {
var observeSpy, directiveAttrs;
beforeEach(module(function() {
directive('observer', function() {
return function(scope, elm, attr) {
directiveAttrs = attr;
observeSpy = jasmine.createSpy('$observe attr');
expect(attr.$observe('someAttr', observeSpy)).toBe(observeSpy);
};
});
directive('replaceSomeAttr', valueFn({
compile: function(element, attr) {
attr.$set('someAttr', 'bar-{{1+1}}');
expect(element).toBe(attr.$$element);
}
}));
}));
it('should compile and link both attribute and text bindings', inject(
function($rootScope, $compile) {
$rootScope.name = 'angular';
element = $compile('
text: {{name}}
')($rootScope);
$rootScope.$digest();
expect(element.text()).toEqual('text: angular');
expect(element.attr('name')).toEqual('attr: angular');
})
);
it('should process attribute interpolation in pre-linking phase at priority 100', function() {
module(function() {
directive('attrLog', function(log) {
return {
compile: function($element, $attrs) {
log('compile=' + $attrs.myName);
return {
pre: function($scope, $element, $attrs) {
log('preLinkP0=' + $attrs.myName);
},
post: function($scope, $element, $attrs) {
log('postLink=' + $attrs.myName);
}
}
}
}
});
});
module(function() {
directive('attrLogHighPriority', function(log) {
return {
priority: 101,
compile: function() {
return {
pre: function($scope, $element, $attrs) {
log('preLinkP101=' + $attrs.myName);
}
};
}
}
});
});
inject(function($rootScope, $compile, log) {
element = $compile('
')($rootScope);
$rootScope.name = 'angular';
$rootScope.$apply();
log('digest=' + element.attr('my-name'));
expect(log).toEqual('compile={{name}}; preLinkP101={{name}}; preLinkP0=; postLink=; digest=angular');
});
});
describe('SCE values', function() {
it('should resolve compile and link both attribute and text bindings', inject(
function($rootScope, $compile, $sce) {
$rootScope.name = $sce.trustAsHtml('angular');
element = $compile('
text: {{name}}
')($rootScope);
$rootScope.$digest();
expect(element.text()).toEqual('text: angular');
expect(element.attr('name')).toEqual('attr: angular');
}));
});
it('should decorate the binding with ng-binding and interpolation function', inject(
function($compile, $rootScope) {
element = $compile('
{{1+2}}
')($rootScope);
expect(element.hasClass('ng-binding')).toBe(true);
expect(element.data('$binding')[0].exp).toEqual('{{1+2}}');
}));
it('should observe interpolated attrs', inject(function($rootScope, $compile) {
$compile('
')($rootScope);
// should be async
expect(observeSpy).not.toHaveBeenCalled();
$rootScope.$apply(function() {
$rootScope.value = 'bound-value';
});
expect(observeSpy).toHaveBeenCalledOnceWith('bound-value');
}));
it('should set interpolated attrs to initial interpolation value', inject(function($rootScope, $compile) {
$rootScope.whatever = 'test value';
$compile('
')($rootScope);
expect(directiveAttrs.someAttr).toBe($rootScope.whatever);
}));
it('should allow directive to replace interpolated attributes before attr interpolation compilation', inject(
function($compile, $rootScope) {
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(element.attr('some-attr')).toEqual('bar-2');
}));
it('should call observer of non-interpolated attr through $evalAsync',
inject(function($rootScope, $compile) {
$compile('
')($rootScope);
expect(directiveAttrs.someAttr).toBe('nonBound');
expect(observeSpy).not.toHaveBeenCalled();
$rootScope.$digest();
expect(observeSpy).toHaveBeenCalled();
})
);
it('should delegate exceptions to $exceptionHandler', function() {
observeSpy = jasmine.createSpy('$observe attr').andThrow('ERROR');
module(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
directive('error', function() {
return function(scope, elm, attr) {
attr.$observe('someAttr', observeSpy);
attr.$observe('someAttr', observeSpy);
};
});
});
inject(function($compile, $rootScope, $exceptionHandler) {
$compile('
')($rootScope);
$rootScope.$digest();
expect(observeSpy).toHaveBeenCalled();
expect(observeSpy.callCount).toBe(2);
expect($exceptionHandler.errors).toEqual(['ERROR', 'ERROR']);
});
});
it('should translate {{}} in terminal nodes', inject(function($rootScope, $compile) {
element = $compile('
Greet {{name}}! ')($rootScope)
$rootScope.$digest();
expect(sortedHtml(element).replace(' selected="true"', '')).
toEqual('
' +
'Greet ! ' +
' ');
$rootScope.name = 'Misko';
$rootScope.$digest();
expect(sortedHtml(element).replace(' selected="true"', '')).
toEqual('
' +
'Greet Misko! ' +
' ');
}));
it('should support custom start/end interpolation symbols in template and directive template',
function() {
module(function($interpolateProvider, $compileProvider) {
$interpolateProvider.startSymbol('##').endSymbol(']]');
$compileProvider.directive('myDirective', function() {
return {
template: '
{{hello}}|{{hello|uppercase}} '
};
});
});
inject(function($compile, $rootScope) {
element = $compile('
')($rootScope);
$rootScope.hello = 'ahoj';
$rootScope.$digest();
expect(element.text()).toBe('AHOJ|ahoj|AHOJ');
});
});
it('should support custom start/end interpolation symbols in async directive template',
function() {
module(function($interpolateProvider, $compileProvider) {
$interpolateProvider.startSymbol('##').endSymbol(']]');
$compileProvider.directive('myDirective', function() {
return {
templateUrl: 'myDirective.html'
};
});
});
inject(function($compile, $rootScope, $templateCache) {
$templateCache.put('myDirective.html', '
{{hello}}|{{hello|uppercase}} ');
element = $compile('
')($rootScope);
$rootScope.hello = 'ahoj';
$rootScope.$digest();
expect(element.text()).toBe('AHOJ|ahoj|AHOJ');
});
});
it('should make attributes observable for terminal directives', function() {
module(function() {
directive('myAttr', function(log) {
return {
terminal: true,
link: function(scope, element, attrs) {
attrs.$observe('myAttr', function(val) {
log(val);
});
}
}
});
});
inject(function($compile, $rootScope, log) {
element = $compile('
')($rootScope);
expect(log).toEqual([]);
$rootScope.myVal = 'carrot';
$rootScope.$digest();
expect(log).toEqual(['carrot']);
});
})
});
describe('link phase', function() {
beforeEach(module(function() {
forEach(['a', 'b', 'c'], function(name) {
directive(name, function(log) {
return {
restrict: 'ECA',
compile: function() {
log('t' + uppercase(name))
return {
pre: function() {
log('pre' + uppercase(name));
},
post: function linkFn() {
log('post' + uppercase(name));
}
};
}
};
});
});
}));
it('should not store linkingFns for noop branches', inject(function ($rootScope, $compile) {
element = jqLite('
ignore
');
var linkingFn = $compile(element);
// Now prune the branches with no directives
element.find('span').remove();
expect(element.find('span').length).toBe(0);
// and we should still be able to compile without errors
linkingFn($rootScope);
}));
it('should compile from top to bottom but link from bottom up', inject(
function($compile, $rootScope, log) {
element = $compile('
')($rootScope);
expect(log).toEqual('tA; tB; tC; preA; preB; preC; postC; postB; postA');
}
));
it('should support link function on directive object', function() {
module(function() {
directive('abc', valueFn({
link: function(scope, element, attrs) {
element.text(attrs.abc);
}
}));
});
inject(function($compile, $rootScope) {
element = $compile('
FAIL
')($rootScope);
expect(element.text()).toEqual('WORKS');
});
});
it('should support $observe inside link function on directive object', function() {
module(function() {
directive('testLink', valueFn({
templateUrl: 'test-link.html',
link: function(scope, element, attrs) {
attrs.$observe( 'testLink', function ( val ) {
scope.testAttr = val;
});
}
}));
});
inject(function($compile, $rootScope, $templateCache) {
$templateCache.put('test-link.html', '{{testAttr}}' );
element = $compile('
')($rootScope);
$rootScope.$apply();
expect(element.text()).toBe('3');
});
});
});
describe('attrs', function() {
it('should allow setting of attributes', function() {
module(function() {
directive({
setter: valueFn(function(scope, element, attr) {
attr.$set('name', 'abc');
attr.$set('disabled', true);
expect(attr.name).toBe('abc');
expect(attr.disabled).toBe(true);
})
});
});
inject(function($rootScope, $compile) {
element = $compile('
')($rootScope);
expect(element.attr('name')).toEqual('abc');
expect(element.attr('disabled')).toEqual('disabled');
});
});
it('should read boolean attributes as boolean only on control elements', function() {
var value;
module(function() {
directive({
input: valueFn({
restrict: 'ECA',
link:function(scope, element, attr) {
value = attr.required;
}
})
});
});
inject(function($rootScope, $compile) {
element = $compile('
')($rootScope);
expect(value).toEqual(true);
});
});
it('should read boolean attributes as text on non-controll elements', function() {
var value;
module(function() {
directive({
div: valueFn({
restrict: 'ECA',
link:function(scope, element, attr) {
value = attr.required;
}
})
});
});
inject(function($rootScope, $compile) {
element = $compile('
')($rootScope);
expect(value).toEqual('some text');
});
});
it('should create new instance of attr for each template stamping', function() {
module(function($provide) {
var state = { first: [], second: [] };
$provide.value('state', state);
directive({
first: valueFn({
priority: 1,
compile: function(templateElement, templateAttr) {
return function(scope, element, attr) {
state.first.push({
template: {element: templateElement, attr:templateAttr},
link: {element: element, attr: attr}
});
}
}
}),
second: valueFn({
priority: 2,
compile: function(templateElement, templateAttr) {
return function(scope, element, attr) {
state.second.push({
template: {element: templateElement, attr:templateAttr},
link: {element: element, attr: attr}
});
}
}
})
});
});
inject(function($rootScope, $compile, state) {
var template = $compile('
');
dealoc(template($rootScope.$new(), noop));
dealoc(template($rootScope.$new(), noop));
// instance between directives should be shared
expect(state.first[0].template.element).toBe(state.second[0].template.element);
expect(state.first[0].template.attr).toBe(state.second[0].template.attr);
// the template and the link can not be the same instance
expect(state.first[0].template.element).not.toBe(state.first[0].link.element);
expect(state.first[0].template.attr).not.toBe(state.first[0].link.attr);
// each new template needs to be new instance
expect(state.first[0].link.element).not.toBe(state.first[1].link.element);
expect(state.first[0].link.attr).not.toBe(state.first[1].link.attr);
expect(state.second[0].link.element).not.toBe(state.second[1].link.element);
expect(state.second[0].link.attr).not.toBe(state.second[1].link.attr);
});
});
it('should properly $observe inside ng-repeat', function() {
var spies = [];
module(function() {
directive('observer', function() {
return function(scope, elm, attr) {
spies.push(jasmine.createSpy('observer ' + spies.length));
attr.$observe('some', spies[spies.length - 1]);
};
});
});
inject(function($compile, $rootScope) {
element = $compile('
')($rootScope);
$rootScope.$apply(function() {
$rootScope.items = [{id: 1}, {id: 2}];
});
expect(spies[0]).toHaveBeenCalledOnceWith('id_1');
expect(spies[1]).toHaveBeenCalledOnceWith('id_2');
spies[0].reset();
spies[1].reset();
$rootScope.$apply(function() {
$rootScope.items[0].id = 5;
});
expect(spies[0]).toHaveBeenCalledOnceWith('id_5');
});
});
describe('$set', function() {
var attr;
beforeEach(function(){
module(function() {
directive('input', valueFn({
restrict: 'ECA',
link: function(scope, element, attr) {
scope.attr = attr;
}
}));
});
inject(function($compile, $rootScope) {
element = $compile('
')($rootScope);
attr = $rootScope.attr;
expect(attr).toBeDefined();
});
});
it('should set attributes', function() {
attr.$set('ngMyAttr', 'value');
expect(element.attr('ng-my-attr')).toEqual('value');
expect(attr.ngMyAttr).toEqual('value');
});
it('should allow overriding of attribute name and remember the name', function() {
attr.$set('ngOther', '123', true, 'other');
expect(element.attr('other')).toEqual('123');
expect(attr.ngOther).toEqual('123');
attr.$set('ngOther', '246');
expect(element.attr('other')).toEqual('246');
expect(attr.ngOther).toEqual('246');
});
it('should remove attribute', function() {
attr.$set('ngMyAttr', 'value');
expect(element.attr('ng-my-attr')).toEqual('value');
attr.$set('ngMyAttr', undefined);
expect(element.attr('ng-my-attr')).toBe(undefined);
attr.$set('ngMyAttr', 'value');
attr.$set('ngMyAttr', null);
expect(element.attr('ng-my-attr')).toBe(undefined);
});
it('should not set DOM element attr if writeAttr false', function() {
attr.$set('test', 'value', false);
expect(element.attr('test')).toBeUndefined();
expect(attr.test).toBe('value');
});
});
});
describe('isolated locals', function() {
var componentScope, regularScope;
beforeEach(module(function() {
directive('myComponent', function() {
return {
scope: {
attr: '@',
attrAlias: '@attr',
ref: '=',
refAlias: '= ref',
reference: '=',
optref: '=?',
optrefAlias: '=? optref',
optreference: '=?',
expr: '&',
exprAlias: '&expr'
},
link: function(scope) {
componentScope = scope;
}
};
});
directive('badDeclaration', function() {
return {
scope: { attr: 'xxx' }
};
});
directive('storeScope', function() {
return {
link: function(scope) {
regularScope = scope;
}
}
});
}));
it('should give other directives the parent scope', inject(function($rootScope) {
compile('
');
$rootScope.$apply(function() {
$rootScope.value = 'from-parent';
});
expect(element.find('input').val()).toBe('from-parent');
expect(componentScope).not.toBe(regularScope);
expect(componentScope.$parent).toBe(regularScope)
}));
it('should not give the isolate scope to other directive template', function() {
module(function() {
directive('otherTplDir', function() {
return {
template: 'value: {{value}}'
};
});
});
inject(function($rootScope) {
compile('
');
$rootScope.$apply(function() {
$rootScope.value = 'from-parent';
});
expect(element.html()).toBe('value: from-parent');
});
});
it('should not give the isolate scope to other directive template (with templateUrl)', function() {
module(function() {
directive('otherTplDir', function() {
return {
templateUrl: 'other.html'
};
});
});
inject(function($rootScope, $templateCache) {
$templateCache.put('other.html', 'value: {{value}}')
compile('
');
$rootScope.$apply(function() {
$rootScope.value = 'from-parent';
});
expect(element.html()).toBe('value: from-parent');
});
});
it('should not give the isolate scope to regular child elements', function() {
inject(function($rootScope) {
compile('
value: {{value}}
');
$rootScope.$apply(function() {
$rootScope.value = 'from-parent';
});
expect(element.html()).toBe('value: from-parent');
});
});
describe('attribute', function() {
it('should copy simple attribute', inject(function() {
compile('
');
expect(componentScope.attr).toEqual('some text');
expect(componentScope.attrAlias).toEqual('some text');
expect(componentScope.attrAlias).toEqual(componentScope.attr);
}));
it('should set up the interpolation before it reaches the link function', inject(function() {
$rootScope.name = 'misko';
compile('');
expect(componentScope.attr).toEqual('hello misko');
expect(componentScope.attrAlias).toEqual('hello misko');
}));
it('should update when interpolated attribute updates', inject(function() {
compile('');
$rootScope.name = 'igor';
$rootScope.$apply();
expect(componentScope.attr).toEqual('hello igor');
expect(componentScope.attrAlias).toEqual('hello igor');
}));
});
describe('object reference', function() {
it('should update local when origin changes', inject(function() {
compile('');
expect(componentScope.ref).toBe(undefined);
expect(componentScope.refAlias).toBe(componentScope.ref);
$rootScope.name = 'misko';
$rootScope.$apply();
expect($rootScope.name).toBe('misko');
expect(componentScope.ref).toBe('misko');
expect(componentScope.refAlias).toBe('misko');
$rootScope.name = {};
$rootScope.$apply();
expect(componentScope.ref).toBe($rootScope.name);
expect(componentScope.refAlias).toBe($rootScope.name);
}));
it('should update local when both change', inject(function() {
compile('');
$rootScope.name = {mark:123};
componentScope.ref = 'misko';
$rootScope.$apply();
expect($rootScope.name).toEqual({mark:123})
expect(componentScope.ref).toBe($rootScope.name);
expect(componentScope.refAlias).toBe($rootScope.name);
$rootScope.name = 'igor';
componentScope.ref = {};
$rootScope.$apply();
expect($rootScope.name).toEqual('igor')
expect(componentScope.ref).toBe($rootScope.name);
expect(componentScope.refAlias).toBe($rootScope.name);
}));
it('should not break if local and origin both change to the same value', inject(function() {
$rootScope.name = 'aaa';
compile('');
//change both sides to the same item withing the same digest cycle
componentScope.ref = 'same';
$rootScope.name = 'same';
$rootScope.$apply();
//change origin back to it's previous value
$rootScope.name = 'aaa';
$rootScope.$apply();
expect($rootScope.name).toBe('aaa');
expect(componentScope.ref).toBe('aaa');
}));
it('should complain on non assignable changes', inject(function() {
compile('');
$rootScope.name = 'world';
$rootScope.$apply();
expect(componentScope.ref).toBe('hello world');
componentScope.ref = 'ignore me';
expect($rootScope.$apply).
toThrowMinErr("$compile", "nonassign", "Expression ''hello ' + name' used with directive 'myComponent' is non-assignable!");
expect(componentScope.ref).toBe('hello world');
// reset since the exception was rethrown which prevented phase clearing
$rootScope.$$phase = null;
$rootScope.name = 'misko';
$rootScope.$apply();
expect(componentScope.ref).toBe('hello misko');
}));
// regression
it('should stabilize model', inject(function() {
compile('');
var lastRefValueInParent;
$rootScope.$watch('name', function(ref) {
lastRefValueInParent = ref;
});
$rootScope.name = 'aaa';
$rootScope.$apply();
componentScope.reference = 'new';
$rootScope.$apply();
expect(lastRefValueInParent).toBe('new');
}));
describe('literal objects', function() {
it('should copy parent changes', inject(function() {
compile('');
$rootScope.name = 'a';
$rootScope.$apply();
expect(componentScope.reference).toEqual({name: 'a'});
$rootScope.name = 'b';
$rootScope.$apply();
expect(componentScope.reference).toEqual({name: 'b'});
}));
it('should not change the component when parent does not change', inject(function() {
compile('');
$rootScope.name = 'a';
$rootScope.$apply();
var lastComponentValue = componentScope.reference;
$rootScope.$apply();
expect(componentScope.reference).toBe(lastComponentValue);
}));
it('should complain when the component changes', inject(function() {
compile('');
$rootScope.name = 'a';
$rootScope.$apply();
componentScope.reference = {name: 'b'};
expect(function() {
$rootScope.$apply();
}).toThrowMinErr("$compile", "nonassign", "Expression '{name: name}' used with directive 'myComponent' is non-assignable!");
}));
it('should work for primitive literals', inject(function() {
test('1', 1);
test('null', null);
test('undefined', undefined);
test("'someString'", 'someString');
function test(literalString, literalValue) {
compile('');
$rootScope.$apply();
expect(componentScope.reference).toBe(literalValue);
dealoc(element);
}
}));
});
});
describe('optional object reference', function() {
it('should update local when origin changes', inject(function() {
compile('');
expect(componentScope.optRef).toBe(undefined);
expect(componentScope.optRefAlias).toBe(componentScope.optRef);
$rootScope.name = 'misko';
$rootScope.$apply();
expect(componentScope.optref).toBe($rootScope.name);
expect(componentScope.optrefAlias).toBe($rootScope.name);
$rootScope.name = {};
$rootScope.$apply();
expect(componentScope.optref).toBe($rootScope.name);
expect(componentScope.optrefAlias).toBe($rootScope.name);
}));
it('should not throw exception when reference does not exist', inject(function() {
compile('');
expect(componentScope.optref).toBe(undefined);
expect(componentScope.optrefAlias).toBe(undefined);
expect(componentScope.optreference).toBe(undefined);
}));
});
describe('executable expression', function() {
it('should allow expression execution with locals', inject(function() {
compile('');
$rootScope.count = 2;
expect(typeof componentScope.expr).toBe('function');
expect(typeof componentScope.exprAlias).toBe('function');
expect(componentScope.expr({offset: 1})).toEqual(3);
expect($rootScope.count).toEqual(3);
expect(componentScope.exprAlias({offset: 10})).toEqual(13);
expect($rootScope.count).toEqual(13);
}));
});
it('should throw on unknown definition', inject(function() {
expect(function() {
compile('');
}).toThrowMinErr("$compile", "iscp", "Invalid isolate scope definition for directive 'badDeclaration'. Definition: {... attr: 'xxx' ...}");
}));
it('should expose a $$isolateBindings property onto the scope', inject(function() {
compile('');
expect(typeof componentScope.$$isolateBindings).toBe('object');
expect(componentScope.$$isolateBindings.attr).toBe('@attr');
expect(componentScope.$$isolateBindings.attrAlias).toBe('@attr');
expect(componentScope.$$isolateBindings.ref).toBe('=ref');
expect(componentScope.$$isolateBindings.refAlias).toBe('=ref');
expect(componentScope.$$isolateBindings.reference).toBe('=reference');
expect(componentScope.$$isolateBindings.expr).toBe('&expr');
expect(componentScope.$$isolateBindings.exprAlias).toBe('&expr');
}));
});
describe('controller', function() {
it('should get required controller', function() {
module(function() {
directive('main', function(log) {
return {
priority: 2,
controller: function() {
this.name = 'main';
},
link: function(scope, element, attrs, controller) {
log(controller.name);
}
};
});
directive('dep', function(log) {
return {
priority: 1,
require: 'main',
link: function(scope, element, attrs, controller) {
log('dep:' + controller.name);
}
};
});
directive('other', function(log) {
return {
link: function(scope, element, attrs, controller) {
log(!!controller); // should be false
}
};
});
});
inject(function(log, $compile, $rootScope) {
element = $compile('
')($rootScope);
expect(log).toEqual('false; dep:main; main');
});
});
it('should get required controller via linkingFn (template)', function() {
module(function() {
directive('dirA', function() {
return {
controller: function() {
this.name = 'dirA';
}
};
});
directive('dirB', function(log) {
return {
require: 'dirA',
template: 'dirB
',
link: function(scope, element, attrs, dirAController) {
log('dirAController.name: ' + dirAController.name);
}
};
});
});
inject(function(log, $compile, $rootScope) {
element = $compile('
')($rootScope);
expect(log).toEqual('dirAController.name: dirA');
});
});
it('should get required controller via linkingFn (templateUrl)', function() {
module(function() {
directive('dirA', function() {
return {
controller: function() {
this.name = 'dirA';
}
};
});
directive('dirB', function(log) {
return {
require: 'dirA',
templateUrl: 'dirB.html',
link: function(scope, element, attrs, dirAController) {
log('dirAController.name: ' + dirAController.name);
}
};
});
});
inject(function(log, $compile, $rootScope, $templateCache) {
$templateCache.put('dirB.html', 'dirB
');
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(log).toEqual('dirAController.name: dirA');
});
});
it('should require controller of an isolate directive from a non-isolate directive on the ' +
'same element', function() {
var IsolateController = function() {};
var isolateDirControllerInNonIsolateDirective;
module(function() {
directive('isolate', function() {
return {
scope: {},
controller: IsolateController
};
});
directive('nonIsolate', function() {
return {
require: 'isolate',
link: function(_, __, ___, isolateDirController) {
isolateDirControllerInNonIsolateDirective = isolateDirController;
}
};
});
});
inject(function($compile, $rootScope) {
element = $compile('
')($rootScope);
expect(isolateDirControllerInNonIsolateDirective).toBeDefined();
expect(isolateDirControllerInNonIsolateDirective instanceof IsolateController).toBe(true);
});
});
it('should give the isolate scope to the controller of another replaced directives in the template', function() {
module(function() {
directive('testDirective', function() {
return {
replace: true,
restrict: 'E',
scope: {},
template: ' '
};
});
});
inject(function($rootScope) {
compile('
');
element = element.children().eq(0);
expect(element[0].checked).toBe(false);
element.isolateScope().model = true;
$rootScope.$digest();
expect(element[0].checked).toBe(true);
});
});
it('should share isolate scope with replaced directives (template)', function() {
var normalScope;
var isolateScope;
module(function() {
directive('isolate', function() {
return {
replace: true,
scope: {},
template: '{{name}} ',
link: function(s) {
isolateScope = s;
}
};
});
directive('nonIsolate', function() {
return {
link: function(s) {
normalScope = s;
}
};
});
});
inject(function($compile, $rootScope) {
element = $compile('
')($rootScope);
expect(normalScope).toBe($rootScope);
expect(normalScope.name).toEqual(undefined);
expect(isolateScope.name).toEqual('WORKS');
$rootScope.$digest();
expect(element.text()).toEqual('WORKS');
});
});
it('should share isolate scope with replaced directives (templateUrl)', function() {
var normalScope;
var isolateScope;
module(function() {
directive('isolate', function() {
return {
replace: true,
scope: {},
templateUrl: 'main.html',
link: function(s) {
isolateScope = s;
}
};
});
directive('nonIsolate', function() {
return {
link: function(s) {
normalScope = s;
}
};
});
});
inject(function($compile, $rootScope, $templateCache) {
$templateCache.put('main.html', '{{name}} ');
element = $compile('
')($rootScope);
$rootScope.$apply();
expect(normalScope).toBe($rootScope);
expect(normalScope.name).toEqual(undefined);
expect(isolateScope.name).toEqual('WORKS');
expect(element.text()).toEqual('WORKS');
});
});
it('should not get confused about where to use isolate scope when a replaced directive is used multiple times',
function() {
module(function() {
directive('isolate', function() {
return {
replace: true,
scope: {},
template: ' '
};
});
directive('scopeTester', function(log) {
return {
link: function($scope, $element) {
log($element.attr('scope-tester') + '=' + ($scope.$root === $scope ? 'non-isolate' : 'isolate'));
}
}
});
});
inject(function($compile, $rootScope, log) {
element = $compile('')($rootScope);
$rootScope.$digest();
expect(log).toEqual('inside=isolate; ' +
'outside replaced=non-isolate; ' + // outside
'outside replaced=isolate; ' + // replaced
'sibling=non-isolate')
});
});
it('should require controller of a non-isolate directive from an isolate directive on the ' +
'same element', function() {
var NonIsolateController = function() {};
var nonIsolateDirControllerInIsolateDirective;
module(function() {
directive('isolate', function() {
return {
scope: {},
require: 'nonIsolate',
link: function(_, __, ___, nonIsolateDirController) {
nonIsolateDirControllerInIsolateDirective = nonIsolateDirController;
}
};
});
directive('nonIsolate', function() {
return {
controller: NonIsolateController
};
});
});
inject(function($compile, $rootScope) {
element = $compile('
')($rootScope);
expect(nonIsolateDirControllerInIsolateDirective).toBeDefined();
expect(nonIsolateDirControllerInIsolateDirective instanceof NonIsolateController).toBe(true);
});
});
it('should support controllerAs', function() {
module(function() {
directive('main', function() {
return {
templateUrl: 'main.html',
transclude: true,
scope: {},
controller: function() {
this.name = 'lucas';
},
controllerAs: 'mainCtrl'
};
});
});
inject(function($templateCache, $compile, $rootScope) {
$templateCache.put('main.html', 'template:{{mainCtrl.name}}
');
element = $compile('transclude:{{mainCtrl.name}}
')($rootScope);
$rootScope.$apply();
expect(element.text()).toBe('template:lucas transclude:');
});
});
it('should support controller alias', function() {
module(function($controllerProvider) {
$controllerProvider.register('MainCtrl', function() {
this.name = 'lucas';
});
directive('main', function() {
return {
templateUrl: 'main.html',
scope: {},
controller: 'MainCtrl as mainCtrl'
};
});
});
inject(function($templateCache, $compile, $rootScope) {
$templateCache.put('main.html', '{{mainCtrl.name}} ');
element = $compile('
')($rootScope);
$rootScope.$apply();
expect(element.text()).toBe('lucas');
});
});
it('should require controller on parent element',function() {
module(function() {
directive('main', function(log) {
return {
controller: function() {
this.name = 'main';
}
};
});
directive('dep', function(log) {
return {
require: '^main',
link: function(scope, element, attrs, controller) {
log('dep:' + controller.name);
}
};
});
});
inject(function(log, $compile, $rootScope) {
element = $compile('')($rootScope);
expect(log).toEqual('dep:main');
});
});
it("should throw an error if required controller can't be found",function() {
module(function() {
directive('dep', function(log) {
return {
require: '^main',
link: function(scope, element, attrs, controller) {
log('dep:' + controller.name);
}
};
});
});
inject(function(log, $compile, $rootScope) {
expect(function() {
$compile('')($rootScope);
}).toThrowMinErr("$compile", "ctreq", "Controller 'main', required by directive 'dep', can't be found!");
});
});
it('should have optional controller on current element', function() {
module(function() {
directive('dep', function(log) {
return {
require: '?main',
link: function(scope, element, attrs, controller) {
log('dep:' + !!controller);
}
};
});
});
inject(function(log, $compile, $rootScope) {
element = $compile('')($rootScope);
expect(log).toEqual('dep:false');
});
});
it('should support multiple controllers', function() {
module(function() {
directive('c1', valueFn({
controller: function() { this.name = 'c1'; }
}));
directive('c2', valueFn({
controller: function() { this.name = 'c2'; }
}));
directive('dep', function(log) {
return {
require: ['^c1', '^c2'],
link: function(scope, element, attrs, controller) {
log('dep:' + controller[0].name + '-' + controller[1].name);
}
};
});
});
inject(function(log, $compile, $rootScope) {
element = $compile('')($rootScope);
expect(log).toEqual('dep:c1-c2');
});
});
it('should instantiate the controller just once when template/templateUrl', function() {
var syncCtrlSpy = jasmine.createSpy('sync controller'),
asyncCtrlSpy = jasmine.createSpy('async controller');
module(function() {
directive('myDirectiveSync', valueFn({
template: 'Hello!
',
controller: syncCtrlSpy
}));
directive('myDirectiveAsync', valueFn({
templateUrl: 'myDirectiveAsync.html',
controller: asyncCtrlSpy,
compile: function() {
return function() {
}
}
}));
});
inject(function($templateCache, $compile, $rootScope) {
expect(syncCtrlSpy).not.toHaveBeenCalled();
expect(asyncCtrlSpy).not.toHaveBeenCalled();
$templateCache.put('myDirectiveAsync.html', 'Hello!
');
element = $compile(''+
' ' +
' ' +
'
')($rootScope);
expect(syncCtrlSpy).not.toHaveBeenCalled();
expect(asyncCtrlSpy).not.toHaveBeenCalled();
$rootScope.$apply();
//expect(syncCtrlSpy).toHaveBeenCalledOnce();
expect(asyncCtrlSpy).toHaveBeenCalledOnce();
});
});
it('should instantiate controllers in the parent->child order when transluction, templateUrl and replacement ' +
'are in the mix', function() {
// When a child controller is in the transclusion that replaces the parent element that has a directive with
// a controller, we should ensure that we first instantiate the parent and only then stuff that comes from the
// transclusion.
//
// The transclusion moves the child controller onto the same element as parent controller so both controllers are
// on the same level.
module(function() {
directive('parentDirective', function() {
return {
transclude: true,
replace: true,
templateUrl: 'parentDirective.html',
controller: function (log) { log('parentController'); }
};
});
directive('childDirective', function() {
return {
require: '^parentDirective',
templateUrl: 'childDirective.html',
controller : function(log) { log('childController'); }
};
});
});
inject(function($templateCache, log, $compile, $rootScope) {
$templateCache.put('parentDirective.html', 'parentTemplateText;
');
$templateCache.put('childDirective.html', 'childTemplateText; ');
element = $compile('')($rootScope);
$rootScope.$apply();
expect(log).toEqual('parentController; childController');
expect(element.text()).toBe('childTemplateText;childContentText;')
});
});
it('should instantiate the controller after the isolate scope bindings are initialized (with template)', function () {
module(function () {
var Ctrl = function ($scope, log) {
log('myFoo=' + $scope.myFoo);
};
directive('myDirective', function () {
return {
scope: {
myFoo: "="
},
template: 'Hello
',
controller: Ctrl
};
});
});
inject(function ($templateCache, $compile, $rootScope, log) {
$rootScope.foo = "bar";
element = $compile('
')($rootScope);
$rootScope.$apply();
expect(log).toEqual('myFoo=bar');
});
});
it('should instantiate the controller after the isolate scope bindings are initialized (with templateUrl)', function () {
module(function () {
var Ctrl = function ($scope, log) {
log('myFoo=' + $scope.myFoo);
};
directive('myDirective', function () {
return {
scope: {
myFoo: "="
},
templateUrl: 'hello.html',
controller: Ctrl
};
});
});
inject(function ($templateCache, $compile, $rootScope, log) {
$templateCache.put('hello.html', 'Hello
');
$rootScope.foo = "bar";
element = $compile('
')($rootScope);
$rootScope.$apply();
expect(log).toEqual('myFoo=bar');
});
});
it('should instantiate controllers in the parent->child->baby order when nested transluction, templateUrl and ' +
'replacement are in the mix', function() {
// similar to the test above, except that we have one more layer of nesting and nested transclusion
module(function() {
directive('parentDirective', function() {
return {
transclude: true,
replace: true,
templateUrl: 'parentDirective.html',
controller: function (log) { log('parentController'); }
};
});
directive('childDirective', function() {
return {
require: '^parentDirective',
transclude: true,
replace: true,
templateUrl: 'childDirective.html',
controller : function(log) { log('childController'); }
};
});
directive('babyDirective', function() {
return {
require: '^childDirective',
templateUrl: 'babyDirective.html',
controller : function(log) { log('babyController'); }
};
});
});
inject(function($templateCache, log, $compile, $rootScope) {
$templateCache.put('parentDirective.html', 'parentTemplateText;
');
$templateCache.put('childDirective.html', 'childTemplateText; ');
$templateCache.put('babyDirective.html', 'babyTemplateText; ');
element = $compile('' +
'
' +
'childContentText;' +
'
babyContent;
' +
'
' +
'
')($rootScope);
$rootScope.$apply();
expect(log).toEqual('parentController; childController; babyController');
expect(element.text()).toBe('childContentText;babyTemplateText;')
});
});
it('should allow controller usage in pre-link directive functions with templateUrl', function () {
module(function () {
var Ctrl = function (log) {
log('instance');
};
directive('myDirective', function () {
return {
scope: true,
templateUrl: 'hello.html',
controller: Ctrl,
compile: function () {
return {
pre: function (scope, template, attr, ctrl) {},
post: function () {}
};
}
};
});
});
inject(function ($templateCache, $compile, $rootScope, log) {
$templateCache.put('hello.html', 'Hello
');
element = $compile('
')($rootScope);
$rootScope.$apply();
expect(log).toEqual('instance');
expect(element.text()).toBe('Hello');
});
});
it('should allow controller usage in pre-link directive functions with a template', function () {
module(function () {
var Ctrl = function (log) {
log('instance');
};
directive('myDirective', function () {
return {
scope: true,
template: 'Hello
',
controller: Ctrl,
compile: function () {
return {
pre: function (scope, template, attr, ctrl) {},
post: function () {}
};
}
};
});
});
inject(function ($templateCache, $compile, $rootScope, log) {
element = $compile('
')($rootScope);
$rootScope.$apply();
expect(log).toEqual('instance');
expect(element.text()).toBe('Hello');
});
});
});
describe('transclude', function() {
describe('content transclusion', function() {
it('should support transclude directive', function() {
module(function() {
directive('trans', function() {
return {
transclude: 'content',
replace: true,
scope: true,
template: 'W:{{$parent.$id}}-{{$id}}; '
}
});
});
inject(function(log, $rootScope, $compile) {
element = $compile('T:{{$parent.$id}}-{{$id}};
')
($rootScope);
$rootScope.$apply();
expect(element.text()).toEqual('W:001-002;T:001-003;');
expect(jqLite(element.find('span')[0]).text()).toEqual('T:001-003');
expect(jqLite(element.find('span')[1]).text()).toEqual(';');
});
});
it('should transclude transcluded content', function() {
module(function() {
directive('book', valueFn({
transclude: 'content',
template: ''
}));
directive('chapter', valueFn({
transclude: 'content',
templateUrl: 'chapter.html'
}));
directive('section', valueFn({
transclude: 'content',
template: ' '
}));
return function($httpBackend) {
$httpBackend.
expect('GET', 'chapter.html').
respond('');
}
});
inject(function(log, $rootScope, $compile, $httpBackend) {
element = $compile('')($rootScope);
$rootScope.$apply();
expect(element.text()).toEqual('book-');
$httpBackend.flush();
$rootScope.$apply();
expect(element.text()).toEqual('book-chapter-section-![(paragraph)]!');
});
});
it('should only allow one content transclusion per element', function() {
module(function() {
directive('first', valueFn({
transclude: true
}));
directive('second', valueFn({
transclude: true
}));
});
inject(function($compile) {
expect(function() {
$compile('
');
}).toThrowMinErr('$compile', 'multidir', /Multiple directives \[first, second\] asking for transclusion on: ',
link: function(scope, element) {
scope.$watch(
'show',
function(show) {
if (!show) {
element.find('div').find('div').remove();
}
}
);
}
}));
});
inject(function($compile, $rootScope) {
$rootScope.username = 'Misko';
$rootScope.select = true;
element = $compile(
'')
($rootScope);
$rootScope.$apply();
expect(element.text()).toEqual('Hello: Misko!user: Misko');
var widgetScope = $rootScope.$$childHead;
var transcludeScope = widgetScope.$$nextSibling;
expect(widgetScope.name).toEqual('Misko');
expect(widgetScope.$parent).toEqual($rootScope);
expect(transcludeScope.$parent).toEqual($rootScope);
$rootScope.select = false;
$rootScope.$apply();
expect(element.text()).toEqual('Hello: Misko!');
expect(widgetScope.$$nextSibling).toEqual(null);
});
});
it('should add a $$transcluded property onto the transcluded scope', function() {
module(function() {
directive('trans', function() {
return {
transclude: true,
replace: true,
scope: true,
template: ''
};
});
});
inject(function(log, $rootScope, $compile) {
element = $compile('')
($rootScope);
$rootScope.$apply();
expect(jqLite(element.find('span')[0]).text()).toEqual('I:');
expect(jqLite(element.find('span')[1]).text()).toEqual('T:true');
});
});
it('should clear contents of the ng-translude element before appending transcluded content', function() {
module(function() {
directive('trans', function() {
return {
transclude: true,
template: 'old stuff!
'
};
});
});
inject(function(log, $rootScope, $compile) {
element = $compile('unicorn!
')($rootScope);
$rootScope.$apply();
expect(sortedHtml(element.html())).toEqual('unicorn!
');
});
});
it('should throw on an ng-translude element inside no transclusion directive', function() {
inject(function ($rootScope, $compile) {
// we need to do this because different browsers print empty attributres differently
try {
$compile('')($rootScope);
} catch(e) {
expect(e.message).toMatch(new RegExp(
'^\\\[ngTransclude:orphan\\\] ' +
'Illegal use of ngTransclude directive in the template! ' +
'No parent directive that requires a transclusion found\. ' +
'Element:
',
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 ')($rootScope);
var children = element.children(), i;
expect(transcludeCtrl).toBeDefined();
expect(element.data('$transcludeController')).toBe(transcludeCtrl);
for (i=0; i ')($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();
});
});
});
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('001-002;001-003;');
});
});
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');
$compile('
');
expect(function() {
$httpBackend.flush();
}).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 trasclusion', 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
')($rootScope);
var children = element.children(), i;
for (i=0; i
')($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();
});
});
});
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('img[src] sanitization', function() {
it('should NOT require trusted values for img src', inject(function($rootScope, $compile, $sce) {
element = $compile(' ')($rootScope);
$rootScope.testUrl = 'http://example.com/image.png';
$rootScope.$digest();
expect(element.attr('src')).toEqual('http://example.com/image.png');
// But it should accept trusted values anyway.
$rootScope.testUrl = $sce.trustAsUrl('http://example.com/image2.png');
$rootScope.$digest();
expect(element.attr('src')).toEqual('http://example.com/image2.png');
}));
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 $$sanitizeUriProvider for reconfiguration of the src whitelist', function() {
module(function($compileProvider, $$sanitizeUriProvider) {
var newRe = /javascript:/,
returnVal;
expect($compileProvider.imgSrcSanitizationWhitelist()).toBe($$sanitizeUriProvider.imgSrcSanitizationWhitelist());
returnVal = $compileProvider.imgSrcSanitizationWhitelist(newRe);
expect(returnVal).toBe($compileProvider);
expect($$sanitizeUriProvider.imgSrcSanitizationWhitelist()).toBe(newRe);
expect($compileProvider.imgSrcSanitizationWhitelist()).toBe(newRe);
});
inject(function() {
// needed to the module definition above is run...
});
});
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.andReturn('someSanitizedUrl');
$rootScope.$apply();
expect(element.attr('src')).toBe('someSanitizedUrl');
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, true);
});
});
});
describe('a[href] sanitization', function() {
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', inject(function($compile, $rootScope) {
element = $compile(' ')($rootScope);
$rootScope.testUrl = "javascript:doEvilStuff()";
$rootScope.$apply();
expect(element.attr('title')).toBe('javascript:doEvilStuff()');
}));
it('should use $$sanitizeUriProvider for reconfiguration of the href whitelist', function() {
module(function($compileProvider, $$sanitizeUriProvider) {
var newRe = /javascript:/,
returnVal;
expect($compileProvider.aHrefSanitizationWhitelist()).toBe($$sanitizeUriProvider.aHrefSanitizationWhitelist());
returnVal = $compileProvider.aHrefSanitizationWhitelist(newRe);
expect(returnVal).toBe($compileProvider);
expect($$sanitizeUriProvider.aHrefSanitizationWhitelist()).toBe(newRe);
expect($compileProvider.aHrefSanitizationWhitelist()).toBe(newRe);
});
inject(function() {
// needed to the module definition above is run...
});
});
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.andReturn('someSanitizedUrl');
$rootScope.$apply();
expect(element.attr('href')).toBe('someSanitizedUrl');
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, false);
});
});
});
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('')($rootScope);
}).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('')($rootScope);
}).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('')($rootScope);
}).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('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()');
}));
});
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(angular.lowercase(element.attr('srcdoc'))).toEqual('hello
');
}));
});
}
describe('ngAttr* attribute binding', function() {
it('should bind after digest but not before', inject(function($compile, $rootScope) {
$rootScope.name = "Misko";
element = $compile(' ')($rootScope);
expect(element.attr('test')).toBeUndefined();
$rootScope.$digest();
expect(element.attr('test')).toBe('Misko');
}));
it('should work with different prefixes', inject(function($compile, $rootScope) {
$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 if they are prefixed with x- or data-', inject(function($compile, $rootScope) {
$rootScope.name = "Misko";
element = $compile(' ')($rootScope);
expect(element.attr('test2')).toBeUndefined();
expect(element.attr('test3')).toBeUndefined();
expect(element.attr('test4')).toBeUndefined();
$rootScope.$digest();
expect(element.attr('test2')).toBe('Misko');
expect(element.attr('test3')).toBe('Misko');
expect(element.attr('test4')).toBe('Misko');
}));
describe('when an attribute has a dash-separated name', function () {
it('should work with different prefixes', inject(function($compile, $rootScope) {
$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($compile, $rootScope) {
$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');
}));
});
});
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 of same directive', 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', 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 throw error if unterminated', function () {
module(function($compileProvider) {
$compileProvider.directive('foo', function() {
return {
};
});
});
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 function (scope, element) {
element.data('x', 'abc');
};
});
$compileProvider.directive('rangeDirective', function() {
return {
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 {
};
});
});
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('mock.animate'));
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.flushNext('addClass');
expect(data.params[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.flushNext('removeClass');
expect(data.params[1]).toBe('rice');
data = $animate.flushNext('addClass');
expect(data.params[1]).toBe('dice');
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.flushNext('removeClass');
expect(data.params[1]).toBe('ice dice');
expect(element.hasClass('ice')).toBe(false);
expect(element.hasClass('dice')).toBe(false);
expect(element.hasClass('fire')).toBe(true);
}));
});
});