'use strict'; /* eslint-disable no-script-url */ describe('$compile', function() { var document = window.document; function isUnknownElement(el) { return !!el.toString().match(/Unknown/); } function isSVGElement(el) { return !!el.toString().match(/SVG/); } function isHTMLElement(el) { return !!el.toString().match(/HTML/); } function supportsMathML() { var d = document.createElement('div'); d.innerHTML = ''; return !isUnknownElement(d.firstChild); } // IE9-11 do not support foreignObject in svg... function supportsForeignObject() { var d = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'); return !!d.toString().match(/SVGForeignObject/); } function getChildScopes(scope) { var children = []; if (!scope.$$childHead) { return children; } var childScope = scope.$$childHead; do { children.push(childScope); children = children.concat(getChildScopes(childScope)); } while ((childScope = childScope.$$nextSibling)); return children; } var element, directive, $compile, $rootScope; beforeEach(module(provideLog, function($provide, $compileProvider) { element = null; directive = $compileProvider.directive; directive('log', function(log) { return { restrict: 'CAM', priority:0, compile: valueFn(function(scope, element, attrs) { log(attrs.log || 'LOG'); }) }; }); directive('highLog', function(log) { return { restrict: 'CAM', priority:3, compile: valueFn(function(scope, element, attrs) { log(attrs.highLog || 'HIGH'); })}; }); directive('mediumLog', function(log) { return { restrict: 'CAM', priority:2, compile: valueFn(function(scope, element, attrs) { log(attrs.mediumLog || 'MEDIUM'); })}; }); directive('greet', function() { return { restrict: 'CAM', priority:10, compile: valueFn(function(scope, element, attrs) { element.text('Hello ' + attrs.greet); })}; }); directive('set', function() { return function(scope, element, attrs) { element.text(attrs.set); }; }); directive('mediumStop', valueFn({ priority: 2, terminal: true })); directive('stop', valueFn({ terminal: true })); directive('negativeStop', valueFn({ priority: -100, // even with negative priority we still should be able to stop descend terminal: true })); directive('svgContainer', function() { return { template: '', replace: true, transclude: true }; }); directive('svgCustomTranscludeContainer', function() { return { template: '', transclude: true, link: function(scope, element, attr, ctrls, $transclude) { var futureParent = element.children().eq(0); $transclude(function(clone) { futureParent.append(clone); }, futureParent); } }; }); directive('svgCircle', function() { return { template: '', templateNamespace: 'svg', replace: true }; }); directive('myForeignObject', function() { return { template: '', templateNamespace: 'svg', replace: true, transclude: true }; }); return function(_$compile_, _$rootScope_) { $rootScope = _$rootScope_; $compile = _$compile_; }; })); function compile(html) { element = angular.element(html); $compile(element)($rootScope); } afterEach(function() { dealoc(element); }); describe('configuration', function() { it('should use $$sanitizeUriProvider for reconfiguration of the `aHrefSanitizationWhitelist`', function() { module(function($compileProvider, $$sanitizeUriProvider) { var newRe = /safe:/, 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 $$sanitizeUriProvider for reconfiguration of the `imgSrcSanitizationWhitelist`', function() { module(function($compileProvider, $$sanitizeUriProvider) { var newRe = /safe:/, 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 allow debugInfoEnabled to be configured', function() { module(function($compileProvider) { expect($compileProvider.debugInfoEnabled()).toBe(true); // the default $compileProvider.debugInfoEnabled(false); expect($compileProvider.debugInfoEnabled()).toBe(false); }); inject(); }); it('should allow strictComponentBindingsEnabled to be configured', function() { module(function($compileProvider) { expect($compileProvider.strictComponentBindingsEnabled()).toBe(false); // the default $compileProvider.strictComponentBindingsEnabled(true); expect($compileProvider.strictComponentBindingsEnabled()).toBe(true); }); inject(); }); it('should allow onChangesTtl to be configured', function() { module(function($compileProvider) { expect($compileProvider.onChangesTtl()).toBe(10); // the default $compileProvider.onChangesTtl(2); expect($compileProvider.onChangesTtl()).toBe(2); }); inject(); }); it('should allow commentDirectivesEnabled to be configured', function() { module(function($compileProvider) { expect($compileProvider.commentDirectivesEnabled()).toBe(true); // the default $compileProvider.commentDirectivesEnabled(false); expect($compileProvider.commentDirectivesEnabled()).toBe(false); }); inject(); }); it('should allow cssClassDirectivesEnabled to be configured', function() { module(function($compileProvider) { expect($compileProvider.cssClassDirectivesEnabled()).toBe(true); // the default $compileProvider.cssClassDirectivesEnabled(false); expect($compileProvider.cssClassDirectivesEnabled()).toBe(false); }); inject(); }); it('should register a directive', function() { module(function() { directive('div', function(log) { return { restrict: 'ECA', link: function(scope, element) { log('OK'); element.text('SUCCESS'); } }; }); }); inject(function($compile, $rootScope, log) { element = $compile('
')($rootScope); expect(element.text()).toEqual('SUCCESS'); expect(log).toEqual('OK'); }); }); it('should allow registration of multiple directives with same name', function() { module(function() { directive('div', function(log) { return { restrict: 'ECA', link: { pre: log.fn('pre1'), post: log.fn('post1') } }; }); directive('div', function(log) { return { restrict: 'ECA', link: { pre: log.fn('pre2'), post: log.fn('post2') } }; }); }); inject(function($compile, $rootScope, log) { element = $compile('
')($rootScope); expect(log).toEqual('pre1; pre2; post2; post1'); }); }); it('should throw an exception if a directive is called "hasOwnProperty"', function() { module(function() { expect(function() { directive('hasOwnProperty', function() { }); }).toThrowMinErr('ng','badname', 'hasOwnProperty is not a valid directive name'); }); inject(function($compile) {}); }); it('should throw an exception if a directive name starts with a non-lowercase letter', function() { module(function() { expect(function() { directive('BadDirectiveName', function() { }); }).toThrowMinErr('$compile','baddir', 'Directive/Component name \'BadDirectiveName\' is invalid. The first character must be a lowercase letter'); }); inject(function($compile) {}); }); it('should throw an exception if a directive name has leading or trailing whitespace', function() { module(function() { function assertLeadingOrTrailingWhitespaceInDirectiveName(name) { expect(function() { directive(name, function() { }); }).toThrowMinErr( '$compile','baddir', 'Directive/Component name \'' + name + '\' is invalid. ' + 'The name should not contain leading or trailing whitespaces'); } assertLeadingOrTrailingWhitespaceInDirectiveName(' leadingWhitespaceDirectiveName'); assertLeadingOrTrailingWhitespaceInDirectiveName('trailingWhitespaceDirectiveName '); assertLeadingOrTrailingWhitespaceInDirectiveName(' leadingAndTrailingWhitespaceDirectiveName '); }); inject(function($compile) {}); }); it('should throw an exception if the directive name is not defined', function() { module(function() { expect(function() { directive(); }).toThrowMinErr('ng','areq'); }); inject(function($compile) {}); }); it('should ignore special chars before processing attribute directive name', function() { // a regression https://github.com/angular/angular.js/issues/16278 module(function() { directive('t', function(log) { return { restrict: 'A', link: { pre: log.fn('pre'), post: log.fn('post') } }; }); }); inject(function($compile, $rootScope, log) { $compile('
')($rootScope); $compile('
')($rootScope); $compile('
')($rootScope); expect(log).toEqual('pre; post; pre; post; pre; post'); }); }); it('should throw an exception if the directive factory is not defined', function() { module(function() { expect(function() { directive('myDir'); }).toThrowMinErr('ng','areq'); }); inject(function($compile) {}); }); it('should preserve context within declaration', function() { module(function() { directive('ff', function(log) { var declaration = { restrict: 'E', template: function() { log('ff template: ' + (this === declaration)); }, compile: function() { log('ff compile: ' + (this === declaration)); return function() { log('ff post: ' + (this === declaration)); }; } }; return declaration; }); directive('fff', function(log) { var declaration = { restrict: 'E', link: { pre: function() { log('fff pre: ' + (this === declaration)); }, post: function() { log('fff post: ' + (this === declaration)); } } }; return declaration; }); directive('ffff', function(log) { var declaration = { restrict: 'E', compile: function() { return { pre: function() { log('ffff pre: ' + (this === declaration)); }, post: function() { log('ffff post: ' + (this === declaration)); } }; } }; return declaration; }); directive('fffff', function(log) { var declaration = { restrict: 'E', templateUrl: function() { log('fffff templateUrl: ' + (this === declaration)); return 'fffff.html'; }, link: function() { log('fffff post: ' + (this === declaration)); } }; return declaration; }); }); inject(function($compile, $rootScope, $templateCache, log) { $templateCache.put('fffff.html', ''); $compile('')($rootScope); $compile('')($rootScope); $compile('')($rootScope); $compile('')($rootScope); $rootScope.$digest(); expect(log).toEqual( 'ff template: true; ' + 'ff compile: true; ' + 'ff post: true; ' + 'fff pre: true; ' + 'fff post: true; ' + 'ffff pre: true; ' + 'ffff post: true; ' + 'fffff templateUrl: true; ' + 'fffff post: true' ); }); }); }); describe('svg namespace transcludes', function() { var ua = window.navigator.userAgent; var isEdge = /Edge/.test(ua); // this method assumes some sort of sized SVG element is being inspected. function assertIsValidSvgCircle(elem) { expect(isUnknownElement(elem)).toBe(false); expect(isSVGElement(elem)).toBe(true); var box = elem.getBoundingClientRect(); expect(box.width === 0 && box.height === 0).toBe(false); } it('should handle transcluded svg elements', inject(function($compile) { element = jqLite('
' + '' + '
'); $compile(element.contents())($rootScope); document.body.appendChild(element[0]); var circle = element.find('circle'); assertIsValidSvgCircle(circle[0]); })); it('should handle custom svg elements inside svg tag', inject(function() { element = jqLite('
' + '' + '
'); $compile(element.contents())($rootScope); document.body.appendChild(element[0]); var circle = element.find('circle'); assertIsValidSvgCircle(circle[0]); })); it('should handle transcluded custom svg elements', inject(function() { element = jqLite('
' + '' + '
'); $compile(element.contents())($rootScope); document.body.appendChild(element[0]); var circle = element.find('circle'); assertIsValidSvgCircle(circle[0]); })); if (supportsForeignObject()) { // Supports: Chrome 53-57+ // Since Chrome 53-57+, the reported size of `` elements and their descendants // is affected by global display settings (e.g. font size) and browser settings (e.g. default // zoom level). In order to avoid false negatives, we compare against the size of the // equivalent, hand-written SVG instead of fixed widths/heights. var HAND_WRITTEN_SVG = '' + '' + '
test
' + '
' + '
'; it('should handle foreignObject', inject(function() { element = jqLite( '
' + // By hand (for reference) HAND_WRITTEN_SVG + // By directive '' + '' + '
test
' + '
' + '
' + '
'); $compile(element.contents())($rootScope); document.body.appendChild(element[0]); var referenceElem = element.find('div')[0]; var testElem = element.find('div')[1]; var referenceBounds = referenceElem.getBoundingClientRect(); var testBounds = testElem.getBoundingClientRect(); expect(isHTMLElement(testElem)).toBe(true); expect(referenceBounds.width).toBeGreaterThan(0); expect(referenceBounds.height).toBeGreaterThan(0); expect(testBounds.width).toBe(referenceBounds.width); expect(testBounds.height).toBe(referenceBounds.height); })); it('should handle custom svg containers that transclude to foreignObject that transclude html', inject(function() { element = jqLite( '
' + // By hand (for reference) HAND_WRITTEN_SVG + // By directive '' + '' + '
test
' + '
' + '
' + '
'); $compile(element.contents())($rootScope); document.body.appendChild(element[0]); var referenceElem = element.find('div')[0]; var testElem = element.find('div')[1]; var referenceBounds = referenceElem.getBoundingClientRect(); var testBounds = testElem.getBoundingClientRect(); expect(isHTMLElement(testElem)).toBe(true); expect(referenceBounds.width).toBeGreaterThan(0); expect(referenceBounds.height).toBeGreaterThan(0); expect(testBounds.width).toBe(referenceBounds.width); expect(testBounds.height).toBe(referenceBounds.height); })); // NOTE: This test may be redundant. // Support: Edge 14-15+ // An `` element inside a `` element on MS Edge has no // size, causing the included `` element to also have no size and thus fails an // assertion (relying on the element having a non-zero size). if (!isEdge) { it('should handle custom svg containers that transclude to foreignObject' + ' that transclude to custom svg containers that transclude to custom elements', inject(function() { element = jqLite('
' + '' + '
'); $compile(element.contents())($rootScope); document.body.appendChild(element[0]); var circle = element.find('circle'); assertIsValidSvgCircle(circle[0]); })); } } it('should handle directives with templates that manually add the transclude further down', inject(function() { element = jqLite('
' + '' + '
'); $compile(element.contents())($rootScope); document.body.appendChild(element[0]); var circle = element.find('circle'); assertIsValidSvgCircle(circle[0]); })); it('should support directives with SVG templates and a slow url ' + 'that are stamped out later by a transcluding directive', function() { module(function() { directive('svgCircleUrl', valueFn({ replace: true, templateUrl: 'template.html', templateNamespace: 'SVG' })); }); inject(function($compile, $rootScope, $httpBackend) { $httpBackend.expect('GET', 'template.html').respond(''); element = $compile('')($rootScope); // initially the template is not yet loaded $rootScope.$apply(function() { $rootScope.list = [1]; }); expect(element.find('svg-circle-url').length).toBe(1); expect(element.find('circle').length).toBe(0); // template is loaded and replaces the existing nodes $httpBackend.flush(); expect(element.find('svg-circle-url').length).toBe(0); expect(element.find('circle').length).toBe(1); // new entry should immediately use the loaded template $rootScope.$apply(function() { $rootScope.list.push(2); }); expect(element.find('svg-circle-url').length).toBe(0); expect(element.find('circle').length).toBe(2); }); }); }); describe('compile phase', function() { it('should attach scope to the document node when it is compiled explicitly', inject(function($document) { $compile($document)($rootScope); expect($document.scope()).toBe($rootScope); })); it('should not wrap root text nodes in spans', function() { element = jqLite( '
A
\n ' + '
B
C\t\n ' + '
'); $compile(element.contents())($rootScope); var spans = element.find('span'); expect(spans.length).toEqual(0); }); it('should be able to compile text nodes at the root', inject(function($rootScope) { element = jqLite('
Name: {{name}}
\nColor: {{color}}
'); $rootScope.name = 'Lucas'; $rootScope.color = 'blue'; $compile(element.contents())($rootScope); $rootScope.$digest(); expect(element.text()).toEqual('Name: Lucas\nColor: blue'); })); it('should not leak memory when there are top level empty text nodes', function() { // We compile the contents of element (i.e. not element itself) // Then delete these contents and check the cache has been reset to zero // First with only elements at the top level element = jqLite('
'); $compile(element.contents())($rootScope); element.empty(); expect(jqLiteCacheSize()).toEqual(0); // Next with non-empty text nodes at the top level // (in this case the compiler will wrap them in a ) element = jqLite('
xxx
'); $compile(element.contents())($rootScope); element.empty(); expect(jqLiteCacheSize()).toEqual(0); // Next with comment nodes at the top level element = jqLite('
'); $compile(element.contents())($rootScope); element.empty(); expect(jqLiteCacheSize()).toEqual(0); // Finally with empty text nodes at the top level element = jqLite('
\n
'); $compile(element.contents())($rootScope); element.empty(); expect(jqLiteCacheSize()).toEqual(0); }); it('should not blow up when elements with no childNodes property are compiled', inject( function($compile, $rootScope) { // it turns out that when a browser plugin is bound to a DOM element (typically ), // the plugin's context rather than the usual DOM apis are exposed on this element, so // childNodes might not exist. element = jqLite('
{{1+2}}
'); try { element[0].childNodes[1] = {nodeType: 3, nodeName: 'OBJECT', textContent: 'fake node'}; } catch (e) { /* empty */ } if (!element[0].childNodes[1]) return; // browser doesn't support this kind of mocking expect(element[0].childNodes[1].textContent).toBe('fake node'); $compile(element)($rootScope); $rootScope.$apply(); // object's children can't be compiled in this case, so we expect them to be raw expect(element.html()).toBe('3'); })); it('should detect anchor elements with the string "SVG" in the `href` attribute as an anchor', inject(function($compile, $rootScope) { element = jqLite(''); $compile(element.contents())($rootScope); $rootScope.$digest(); document.body.appendChild(element[0]); expect(element.find('span').text()).toContain('Should render'); })); describe('multiple directives per element', function() { it('should allow multiple directives per element', inject(function($compile, $rootScope, log) { element = $compile( '')($rootScope); expect(element.text()).toEqual('Hello angular'); expect(log).toEqual('L; M; H'); })); it('should recurse to children', inject(function($compile, $rootScope) { element = $compile('
01234
')($rootScope); expect(element.text()).toEqual('0hello2angular4'); })); it('should allow directives in classes', inject(function($compile, $rootScope, log) { element = $compile('
')($rootScope); expect(element.html()).toEqual('Hello angular'); expect(log).toEqual('123'); })); it('should allow directives in SVG element classes', inject(function($compile, $rootScope, log) { if (!window.SVGElement) return; element = $compile('')($rootScope); var text = element.children().eq(0); // In old Safari, SVG elements don't have innerHTML, so element.html() won't work // (https://bugs.webkit.org/show_bug.cgi?id=136903) expect(text.text()).toEqual('Hello angular'); expect(log).toEqual('123'); })); it('should ignore not set CSS classes on SVG elements', inject(function($compile, $rootScope, log) { if (!window.SVGElement) return; // According to spec SVG element className property is readonly, but only FF // implements it this way which causes compile exceptions. element = $compile('{{1}}')($rootScope); $rootScope.$digest(); expect(element.text()).toEqual('1'); })); it('should receive scope, element, and attributes', function() { var injector; module(function() { directive('log', function($injector, $rootScope) { injector = $injector; return { restrict: 'CA', compile: function(element, templateAttr) { expect(typeof templateAttr.$normalize).toBe('function'); expect(typeof templateAttr.$set).toBe('function'); expect(isElement(templateAttr.$$element)).toBeTruthy(); expect(element.text()).toEqual('unlinked'); expect(templateAttr.exp).toEqual('abc'); expect(templateAttr.aa).toEqual('A'); expect(templateAttr.bb).toEqual('B'); expect(templateAttr.cc).toEqual('C'); return function(scope, element, attr) { expect(element.text()).toEqual('unlinked'); expect(attr).toBe(templateAttr); expect(scope).toEqual($rootScope); element.text('worked'); }; } }; }); }); inject(function($rootScope, $compile, $injector) { element = $compile( '
unlinked
')($rootScope); expect(element.text()).toEqual('worked'); expect(injector).toBe($injector); // verify that directive is injectable }); }); }); describe('error handling', function() { it('should handle exceptions', function() { module(function($exceptionHandlerProvider) { $exceptionHandlerProvider.mode('log'); directive('factoryError', function() { throw 'FactoryError'; }); directive('templateError', valueFn({ compile: function() { throw 'TemplateError'; } })); directive('linkingError', valueFn(function() { throw 'LinkingError'; })); }); inject(function($rootScope, $compile, $exceptionHandler) { element = $compile('
')($rootScope); expect($exceptionHandler.errors[0]).toEqual('FactoryError'); expect($exceptionHandler.errors[1][0]).toEqual('TemplateError'); expect(sortTag($exceptionHandler.errors[1][1])). toEqual('
'); expect($exceptionHandler.errors[2][0]).toEqual('LinkingError'); expect(sortTag($exceptionHandler.errors[2][1])). toEqual('
'); // Support: IE 9-11 only, Edge 15+ // IE/Edge sort attributes in a different order. function sortTag(text) { var parts, elementName; parts = text .replace('<', '') .replace('>', '') .split(' '); elementName = parts.shift(); parts.sort(); parts.unshift(elementName); return '<' + parts.join(' ') + '>'; } }); }); it('should allow changing the template structure after the current node', function() { module(function() { directive('after', valueFn({ compile: function(element) { element.after('B'); } })); }); inject(function($compile, $rootScope, log) { element = jqLite('
A
'); $compile(element)($rootScope); expect(element.text()).toBe('AB'); expect(log).toEqual('LOG'); }); }); it('should allow changing the template structure after the current node inside ngRepeat', function() { module(function() { directive('after', valueFn({ compile: function(element) { element.after('B'); } })); }); inject(function($compile, $rootScope, log) { element = jqLite('
A
'); $compile(element)($rootScope); $rootScope.$digest(); expect(element.text()).toBe('ABAB'); expect(log).toEqual('LOG; LOG'); }); }); it('should allow modifying the DOM structure in post link fn', function() { module(function() { directive('removeNode', valueFn({ link: function($scope, $element) { $element.remove(); } })); }); inject(function($compile, $rootScope) { element = jqLite('
{{test}}
'); $rootScope.test = 'Hello'; $compile(element)($rootScope); $rootScope.$digest(); expect(element.children().length).toBe(1); expect(element.text()).toBe('Hello'); }); }); }); describe('compiler control', function() { describe('priority', function() { it('should honor priority', inject(function($compile, $rootScope, log) { element = $compile( '')($rootScope); expect(log).toEqual('L; M; H'); })); }); describe('terminal', function() { it('should prevent further directives from running', inject(function($rootScope, $compile) { element = $compile('')($rootScope); expect(element.text()).toEqual('OK'); } )); it('should prevent further directives from running, but finish current priority level', inject(function($rootScope, $compile, log) { // class is processed after attrs, so putting log in class will put it after // the stop in the current level. This proves that the log runs after stop element = $compile( '')($rootScope); expect(element.text()).toEqual('OK'); expect(log.toArray().sort()).toEqual(['HIGH', 'MEDIUM']); }) ); }); describe('restrict', function() { it('should allow restriction of availability', function() { module(function() { forEach({div: 'E', attr: 'A', clazz: 'C', comment: 'M', all: 'EACM'}, function(restrict, name) { directive(name, function(log) { return { restrict: restrict, compile: valueFn(function(scope, element, attr) { log(name); }) }; }); }); }); inject(function($rootScope, $compile, log) { dealoc($compile('')($rootScope)); expect(log).toEqual(''); log.reset(); dealoc($compile('
')($rootScope)); expect(log).toEqual('div'); log.reset(); dealoc($compile('')($rootScope)); expect(log).toEqual(''); log.reset(); dealoc($compile('')($rootScope)); expect(log).toEqual('attr'); log.reset(); dealoc($compile('')($rootScope)); expect(log).toEqual(''); log.reset(); dealoc($compile('')($rootScope)); expect(log).toEqual('clazz'); log.reset(); dealoc($compile('')($rootScope)); expect(log).toEqual('comment'); log.reset(); dealoc($compile('')($rootScope)); expect(log).toEqual('all; all; all; all'); }); }); it('should use EA rule as the default', function() { module(function() { directive('defaultDir', function(log) { return { compile: function() { log('defaultDir'); } }; }); }); inject(function($rootScope, $compile, log) { dealoc($compile('')($rootScope)); expect(log).toEqual('defaultDir'); log.reset(); dealoc($compile('')($rootScope)); expect(log).toEqual('defaultDir'); log.reset(); dealoc($compile('')($rootScope)); expect(log).toEqual(''); log.reset(); }); }); }); describe('template', function() { beforeEach(module(function() { directive('replace', valueFn({ restrict: 'CAM', replace: true, template: '
Replace!
', compile: function(element, attr) { attr.$set('compiled', 'COMPILED'); expect(element).toBe(attr.$$element); } })); directive('nomerge', valueFn({ restrict: 'CAM', replace: true, template: '
No Merge!
', compile: function(element, attr) { attr.$set('compiled', 'COMPILED'); expect(element).toBe(attr.$$element); } })); directive('append', valueFn({ restrict: 'CAM', template: '
Append!
', compile: function(element, attr) { attr.$set('compiled', 'COMPILED'); expect(element).toBe(attr.$$element); } })); directive('replaceWithInterpolatedClass', valueFn({ replace: true, template: '
Replace with interpolated class!
', compile: function(element, attr) { attr.$set('compiled', 'COMPILED'); expect(element).toBe(attr.$$element); } })); directive('replaceWithInterpolatedStyle', valueFn({ replace: true, template: '
Replace with interpolated style!
', compile: function(element, attr) { attr.$set('compiled', 'COMPILED'); expect(element).toBe(attr.$$element); } })); directive('replaceWithTr', valueFn({ replace: true, template: 'TR' })); directive('replaceWithTd', valueFn({ replace: true, template: 'TD' })); directive('replaceWithTh', valueFn({ replace: true, template: 'TH' })); directive('replaceWithThead', valueFn({ replace: true, template: 'TD' })); directive('replaceWithTbody', valueFn({ replace: true, template: 'TD' })); directive('replaceWithTfoot', valueFn({ replace: true, template: 'TD' })); directive('replaceWithOption', valueFn({ replace: true, template: '' })); directive('replaceWithOptgroup', valueFn({ replace: true, template: 'OPTGROUP' })); })); it('should replace element with template', inject(function($compile, $rootScope) { element = $compile('
ignore
')($rootScope); expect(element.text()).toEqual('Replace!'); expect(element.find('div').attr('compiled')).toEqual('COMPILED'); })); it('should append element with template', inject(function($compile, $rootScope) { element = $compile('
ignore
')($rootScope); expect(element.text()).toEqual('Append!'); expect(element.find('div').attr('compiled')).toEqual('COMPILED'); })); it('should compile template when replacing', inject(function($compile, $rootScope, log) { element = $compile('
ignore
')($rootScope); $rootScope.$digest(); expect(element.text()).toEqual('Replace!'); expect(log).toEqual('LOG; HIGH; MEDIUM'); })); it('should compile template when appending', inject(function($compile, $rootScope, log) { element = $compile('
ignore
')($rootScope); $rootScope.$digest(); expect(element.text()).toEqual('Append!'); expect(log).toEqual('LOG; HIGH; MEDIUM'); })); it('should merge attributes including style attr', inject(function($compile, $rootScope) { element = $compile( '
')($rootScope); var div = element.find('div'); expect(div.hasClass('medium-log')).toBe(true); expect(div.hasClass('log')).toBe(true); expect(div.css('width')).toBe('10px'); expect(div.css('height')).toBe('20px'); expect(div.attr('replace')).toEqual(''); expect(div.attr('high-log')).toEqual(''); })); it('should not merge attributes if they are the same', inject(function($compile, $rootScope) { element = $compile( '
')($rootScope); var div = element.find('div'); expect(div.hasClass('medium-log')).toBe(true); expect(div.hasClass('log')).toBe(true); expect(div.attr('id')).toEqual('myid'); })); it('should correctly merge attributes that contain special characters', inject(function($compile, $rootScope) { element = $compile( '
')($rootScope); var div = element.find('div'); expect(div.attr('(click)')).toEqual('doSomething()'); expect(div.attr('[value]')).toEqual('someExpression'); expect(div.attr('ω')).toEqual('omega'); })); it('should not add white-space when merging an attribute that is "" in the replaced element', inject(function($compile, $rootScope) { element = $compile( '
')($rootScope); var div = element.find('div'); expect(div.hasClass('log')).toBe(true); expect(div.attr('class')).toBe('log'); }) ); it('should not set merged attributes twice in $attrs', function() { var attrs; module(function() { directive('logAttrs', function() { return { link: function($scope, $element, $attrs) { attrs = $attrs; } }; }); }); inject(function($compile, $rootScope) { element = $compile( '
')($rootScope); var div = element.find('div'); expect(div.attr('class')).toBe('myLog log'); expect(attrs.class).toBe('myLog log'); }); }); it('should prevent multiple templates per element', inject(function($compile) { try { $compile('
'); this.fail(new Error('should have thrown Multiple directives error')); } catch (e) { expect(e.message).toMatch(/Multiple directives .* asking for template/); } })); it('should play nice with repeater when replacing', inject(function($compile, $rootScope) { element = $compile( '
' + '
' + '
')($rootScope); $rootScope.$digest(); expect(element.text()).toEqual('Replace!Replace!'); })); it('should play nice with repeater when appending', inject(function($compile, $rootScope) { element = $compile( '
' + '
' + '
')($rootScope); $rootScope.$digest(); expect(element.text()).toEqual('Append!Append!'); })); it('should handle interpolated css class from replacing directive', inject( function($compile, $rootScope) { element = $compile('
')($rootScope); $rootScope.$digest(); expect(element).toHaveClass('class_2'); })); // Support: IE 9-11 only if (!msie) { // style interpolation not working on IE (including IE11). it('should handle interpolated css style from replacing directive', inject( function($compile, $rootScope) { element = $compile('
')($rootScope); $rootScope.$digest(); expect(element.css('width')).toBe('2px'); } )); } it('should merge interpolated css class', inject(function($compile, $rootScope) { element = $compile('
')($rootScope); $rootScope.$apply(function() { $rootScope.cls = 'two'; }); expect(element).toHaveClass('one'); expect(element).toHaveClass('two'); // interpolated expect(element).toHaveClass('three'); expect(element).toHaveClass('log'); // merged from replace directive template })); it('should merge interpolated css class with ngRepeat', inject(function($compile, $rootScope) { element = $compile( '
' + '
' + '
')($rootScope); $rootScope.$apply(function() { $rootScope.cls = 'two'; }); var child = element.find('div').eq(0); expect(child).toHaveClass('one'); expect(child).toHaveClass('two'); // interpolated expect(child).toHaveClass('three'); expect(child).toHaveClass('log'); // merged from replace directive template })); it('should interpolate the values once per digest', inject(function($compile, $rootScope, log) { element = $compile('
{{log("A")}} foo {{::log("B")}}
')($rootScope); $rootScope.log = log; $rootScope.$digest(); expect(log).toEqual('A; B; A; B'); })); it('should update references to replaced jQuery context', function() { module(function($compileProvider) { $compileProvider.directive('foo', function() { return { replace: true, template: '
' }; }); }); inject(function($compile, $rootScope) { element = jqLite(document.createElement('span')).attr('foo', ''); expect(nodeName_(element)).toBe('span'); var preCompiledNode = element[0]; var linked = $compile(element)($rootScope); expect(linked).toBe(element); expect(nodeName_(element)).toBe('div'); if (element.context) { expect(element.context).toBe(element[0]); } }); }); describe('replace and not exactly one root element', function() { var templateVar; beforeEach(module(function() { directive('template', function() { return { replace: true, template: function() { return templateVar; } }; }); })); they('should throw if: $prop', { 'no root element': 'dada', 'multiple root elements': '
' }, function(directiveTemplate) { inject(function($compile) { templateVar = directiveTemplate; expect(function() { $compile('

'); }).toThrowMinErr('$compile', 'tplrt', 'Template for directive \'template\' must have exactly one root element.' ); }); }); they('should not throw if the root element is accompanied by: $prop', { 'whitespace': '
Hello World!
\n', 'comments': '
Hello World!
\n', 'comments + whitespace': '
Hello World!
\n' }, function(directiveTemplate) { inject(function($compile, $rootScope) { templateVar = directiveTemplate; var element; expect(function() { element = $compile('

')($rootScope); }).not.toThrow(); expect(element.length).toBe(1); expect(element.text()).toBe('Hello World!'); }); }); }); it('should support templates with root tags', inject(function($compile, $rootScope) { expect(function() { element = $compile('
')($rootScope); }).not.toThrow(); expect(nodeName_(element)).toMatch(/tr/i); })); it('should support templates with root tags', inject(function($compile, $rootScope) { expect(function() { element = $compile('
')($rootScope); }).not.toThrow(); expect(nodeName_(element)).toMatch(/td/i); })); it('should support templates with root tags', inject(function($compile, $rootScope) { expect(function() { element = $compile('
')($rootScope); }).not.toThrow(); expect(nodeName_(element)).toMatch(/th/i); })); it('should support templates with root tags', inject(function($compile, $rootScope) { expect(function() { element = $compile('
')($rootScope); }).not.toThrow(); expect(nodeName_(element)).toMatch(/thead/i); })); it('should support templates with root tags', inject(function($compile, $rootScope) { expect(function() { element = $compile('
')($rootScope); }).not.toThrow(); expect(nodeName_(element)).toMatch(/tbody/i); })); it('should support templates with root tags', inject(function($compile, $rootScope) { expect(function() { element = $compile('
')($rootScope); }).not.toThrow(); expect(nodeName_(element)).toMatch(/tfoot/i); })); it('should support templates with root tags', inject(function($compile, $rootScope) { expect(function() { element = $compile('
')($rootScope); }).not.toThrow(); expect(nodeName_(element)).toMatch(/optgroup/i); })); it('should support SVG templates using directive.templateNamespace=svg', function() { module(function() { directive('svgAnchor', valueFn({ replace: true, template: '{{text}}', templateNamespace: 'SVG', scope: { linkurl: '@svgAnchor', text: '@?' } })); }); inject(function($compile, $rootScope) { element = $compile('')($rootScope); var child = element.children().eq(0); $rootScope.$digest(); expect(nodeName_(child)).toMatch(/a/i); expect(isSVGElement(child[0])).toBe(true); expect(child[0].href.baseVal).toBe('/foo/bar'); }); }); if (supportsMathML()) { // MathML is only natively supported in Firefox at the time of this test's writing, // and even there, the browser does not export MathML element constructors globally. it('should support MathML templates using directive.templateNamespace=math', function() { module(function() { directive('pow', valueFn({ replace: true, transclude: true, template: '{{pow}}', templateNamespace: 'MATH', scope: { pow: '@pow' }, link: function(scope, elm, attr, ctrl, transclude) { transclude(function(node) { elm.prepend(node[0]); }); } })); }); inject(function($compile, $rootScope) { element = $compile('8')($rootScope); $rootScope.$digest(); var child = element.children().eq(0); expect(nodeName_(child)).toMatch(/msup/i); expect(isUnknownElement(child[0])).toBe(false); expect(isHTMLElement(child[0])).toBe(false); }); }); } it('should keep prototype properties on directive', function() { module(function() { function DirectiveClass() { this.restrict = 'E'; this.template = '

{{value}}

'; } DirectiveClass.prototype.compile = function() { return function(scope, element, attrs) { scope.value = 'Test Value'; }; }; directive('templateUrlWithPrototype', valueFn(new DirectiveClass())); }); inject(function($compile, $rootScope) { element = $compile('')($rootScope); $rootScope.$digest(); expect(element.find('p')[0].innerHTML).toEqual('Test Value'); }); }); }); describe('template as function', function() { beforeEach(module(function() { directive('myDirective', valueFn({ replace: true, template: function($element, $attrs) { expect($element.text()).toBe('original content'); expect($attrs.myDirective).toBe('some value'); return '
template content
'; }, compile: function($element, $attrs) { expect($element.text()).toBe('template content'); expect($attrs.id).toBe('templateContent'); } })); })); it('should evaluate `template` when defined as fn and use returned string as template', inject( function($compile, $rootScope) { element = $compile('
original content
')($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 new Error('cError'); } })); directive('lError', valueFn({ restrict: 'CAM', templateUrl: 'error.html', compile: function() { throw new 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 new Error('cError'); } })); directive('iLError', valueFn({ restrict: 'CAM', replace: true, templateUrl: 'error.html', compile: function() { throw new Error('lError'); } })); directive('replace', valueFn({ replace: true, template: 'Hello, {{name}}!' })); directive('replaceWithTr', valueFn({ replace: true, templateUrl: 'tr.html' })); directive('replaceWithTd', valueFn({ replace: true, templateUrl: 'td.html' })); directive('replaceWithTh', valueFn({ replace: true, templateUrl: 'th.html' })); directive('replaceWithThead', valueFn({ replace: true, templateUrl: 'thead.html' })); directive('replaceWithTbody', valueFn({ replace: true, templateUrl: 'tbody.html' })); directive('replaceWithTfoot', valueFn({ replace: true, templateUrl: 'tfoot.html' })); directive('replaceWithOption', valueFn({ replace: true, templateUrl: 'option.html' })); directive('replaceWithOptgroup', valueFn({ replace: true, templateUrl: 'optgroup.html' })); } )); it('should not load cross domain templates by default', inject( function($compile, $rootScope) { expect(function() { $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 trust what is already in the template cache', inject( function($compile, $httpBackend, $rootScope, $templateCache) { $httpBackend.expect('GET', 'http://example.com/should-not-load.html').respond('example.com/remote-version'); $templateCache.put('http://example.com/should-not-load.html', 'example.com/cached-version'); element = $compile('
')($rootScope); expect(sortedHtml(element)).toEqual('
'); $rootScope.$digest(); expect(sortedHtml(element)).toEqual('
example.com/cached-version
'); } )); 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('
ignoreignore
')($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('
ignoreignore
')($rootScope); expect(sortedHtml(element)). toEqual('
'); $rootScope.$digest(); expect(sortedHtml(element)).toBe('
Cau!
'); $httpBackend.flush(); expect(sortedHtml(element)).toBe('
Hello!Cau!
'); } )); 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)).toBe('
Hello, Elvis!
'); } )); 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)).toBe('
Hello, Elvis!
'); } )); 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('$templateRequest', '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: ' + '
'); }); }); it('should copy classes from pre-template node into linked element', function() { module(function() { directive('test', valueFn({ templateUrl: 'test.html', replace: true })); }); inject(function($compile, $templateCache, $rootScope) { var child; $templateCache.put('test.html', '

Hello

'); element = $compile('
')($rootScope, function(node) { node.addClass('clonefn-class'); }); $rootScope.$digest(); expect(element).toHaveClass('template-class'); expect(element).toHaveClass('clonefn-class'); }); }); 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'); } )); describe('when directive is in a repeater', function() { var is; beforeEach(function() { is = [1, 2]; }); function runTest() { 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=' + is.join(';i=') + ';'); }); } it('should work in jqLite and jQuery with jQuery.cleanData last patched by Angular', runTest); it('should work with another library patching jqLite/jQuery.cleanData after Angular', function() { var cleanedCount = 0; var currentCleanData = jqLite.cleanData; jqLite.cleanData = function(elems) { cleanedCount += elems.length; // Don't return the output and explicitly pass only the first parameter // so that we're sure we're not relying on either of them. jQuery UI patch // behaves in this way. currentCleanData(elems); }; runTest(); // The initial ng-repeat div is dumped after parsing hence we expect cleanData // count to be one larger than size of the iterated array. expect(cleanedCount).toBe(is.length + 1); // Restore the previous cleanData. jqLite.cleanData = currentCleanData; }); }); describe('replace and not exactly one root element', function() { beforeEach(module(function() { directive('template', function() { return { replace: true, templateUrl: 'template.html' }; }); })); they('should throw if: $prop', { 'no root element': 'dada', 'multiple root elements': '
' }, function(directiveTemplate) { inject(function($compile, $templateCache, $rootScope) { $templateCache.put('template.html', directiveTemplate); expect(function() { $compile('

')($rootScope); $rootScope.$digest(); }).toThrowMinErr('$compile', 'tplrt', 'Template for directive \'template\' must have exactly one root element. ' + 'template.html'); }); }); they('should not throw if the root element is accompanied by: $prop', { 'whitespace': '
Hello World!
\n', 'comments': '
Hello World!
\n', 'comments + whitespace': '
Hello World!
\n' }, function(directiveTemplate) { inject(function($compile, $templateCache, $rootScope) { $templateCache.put('template.html', directiveTemplate); element = $compile('

')($rootScope); expect(function() { $rootScope.$digest(); }).not.toThrow(); expect(element.length).toBe(1); expect(element.text()).toBe('Hello World!'); }); }); }); 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).toHaveBeenCalledTimes(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'); }); }); it('should support templates with root tags', inject(function($compile, $rootScope, $templateCache) { $templateCache.put('tr.html', 'TR'); expect(function() { element = $compile('
')($rootScope); }).not.toThrow(); $rootScope.$digest(); expect(nodeName_(element)).toMatch(/tr/i); })); it('should support templates with root tags', inject(function($compile, $rootScope, $templateCache) { $templateCache.put('td.html', 'TD'); expect(function() { element = $compile('
')($rootScope); }).not.toThrow(); $rootScope.$digest(); expect(nodeName_(element)).toMatch(/td/i); })); it('should support templates with root tags', inject(function($compile, $rootScope, $templateCache) { $templateCache.put('th.html', 'TH'); expect(function() { element = $compile('
')($rootScope); }).not.toThrow(); $rootScope.$digest(); expect(nodeName_(element)).toMatch(/th/i); })); it('should support templates with root tags', inject(function($compile, $rootScope, $templateCache) { $templateCache.put('thead.html', 'TD'); expect(function() { element = $compile('
')($rootScope); }).not.toThrow(); $rootScope.$digest(); expect(nodeName_(element)).toMatch(/thead/i); })); it('should support templates with root tags', inject(function($compile, $rootScope, $templateCache) { $templateCache.put('tbody.html', 'TD'); expect(function() { element = $compile('
')($rootScope); }).not.toThrow(); $rootScope.$digest(); expect(nodeName_(element)).toMatch(/tbody/i); })); it('should support templates with root tags', inject(function($compile, $rootScope, $templateCache) { $templateCache.put('tfoot.html', 'TD'); expect(function() { element = $compile('
')($rootScope); }).not.toThrow(); $rootScope.$digest(); expect(nodeName_(element)).toMatch(/tfoot/i); })); it('should support templates with root '); expect(function() { element = $compile('
')($rootScope); }).not.toThrow(); $rootScope.$digest(); expect(nodeName_(element)).toMatch(/option/i); })); it('should support templates with root tags', inject(function($compile, $rootScope, $templateCache) { $templateCache.put('optgroup.html', 'OPTGROUP'); expect(function() { element = $compile('
')($rootScope); }).not.toThrow(); $rootScope.$digest(); expect(nodeName_(element)).toMatch(/optgroup/i); })); it('should support SVG templates using directive.templateNamespace=svg', function() { module(function() { directive('svgAnchor', valueFn({ replace: true, templateUrl: 'template.html', templateNamespace: 'SVG', scope: { linkurl: '@svgAnchor', text: '@?' } })); }); inject(function($compile, $rootScope, $templateCache) { $templateCache.put('template.html', '{{text}}'); element = $compile('')($rootScope); $rootScope.$digest(); var child = element.children().eq(0); expect(nodeName_(child)).toMatch(/a/i); expect(isSVGElement(child[0])).toBe(true); expect(child[0].href.baseVal).toBe('/foo/bar'); }); }); if (supportsMathML()) { // MathML is only natively supported in Firefox at the time of this test's writing, // and even there, the browser does not export MathML element constructors globally. it('should support MathML templates using directive.templateNamespace=math', function() { module(function() { directive('pow', valueFn({ replace: true, transclude: true, templateUrl: 'template.html', templateNamespace: 'math', scope: { pow: '@pow' }, link: function(scope, elm, attr, ctrl, transclude) { transclude(function(node) { elm.prepend(node[0]); }); } })); }); inject(function($compile, $rootScope, $templateCache) { $templateCache.put('template.html', '{{pow}}'); element = $compile('8')($rootScope); $rootScope.$digest(); var child = element.children().eq(0); expect(nodeName_(child)).toMatch(/msup/i); expect(isUnknownElement(child[0])).toBe(false); expect(isHTMLElement(child[0])).toBe(false); }); }); } it('should keep prototype properties on sync version of async directive', function() { module(function() { function DirectiveClass() { this.restrict = 'E'; this.templateUrl = 'test.html'; } DirectiveClass.prototype.compile = function() { return function(scope, element, attrs) { scope.value = 'Test Value'; }; }; directive('templateUrlWithPrototype', valueFn(new DirectiveClass())); }); inject(function($compile, $rootScope, $httpBackend) { $httpBackend.whenGET('test.html'). respond('

{{value}}

'); element = $compile('')($rootScope); $httpBackend.flush(); $rootScope.$digest(); expect(element.find('p')[0].innerHTML).toEqual('Test Value'); }); }); }); 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('stscope' + uppercase(name), function(log) { return { scope: true, restrict: 'CA', template: '', 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('stiscope' + uppercase(name), function(log) { return { scope: {}, restrict: 'CA', template: '', 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')); }} }; }); directive('prototypeMethodNameAsScopeVarA', function() { return { scope: { 'constructor': '=?', 'valueOf': '=' }, restrict: 'AE', template: '' }; }); directive('prototypeMethodNameAsScopeVarB', function() { return { scope: { 'constructor': '@?', 'valueOf': '@' }, restrict: 'AE', template: '' }; }); directive('prototypeMethodNameAsScopeVarC', function() { return { scope: { 'constructor': '&?', 'valueOf': '&' }, restrict: 'AE', template: '' }; }); directive('prototypeMethodNameAsScopeVarD', function() { return { scope: { 'constructor': '' }; }); directive('watchAsScopeVar', function() { return { scope: { 'watch': '=' }, restrict: 'AE', template: '' }; }); })); it('should allow creation of new scopes', inject(function($rootScope, $compile, log) { element = $compile('
')($rootScope); expect(log).toEqual('2; log-2-1; 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-1-no-parent; LOG; 2'); $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-2-1; LOG; 2'); $rootScope.name = 'Jozo'; $rootScope.$apply(); expect(element.text()).toBe('Jozo; scopeId: 2'); expect(element.find('span').scope().$id).toBe(2); })); 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-2-1; LOG; 2'); $rootScope.name = 'Jozo'; $rootScope.$apply(); expect(element.text()).toBe('Jozo; scopeId: 2'); expect(element.find('a').scope().$id).toBe(2); })); 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-3-2; LOG; 3; log-5-4; LOG; 5; log-7-6; LOG; 7'); $rootScope.name = 'Jozo'; $rootScope.$apply(); expect(element.text()).toBe('Jozo; scopeId: 3 |Jozo; scopeId: 5 |Jozo; scopeId: 7 |'); expect(element.find('p').scope().$id).toBe(3); expect(element.find('a').scope().$id).toBe(3); })); 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-2-1; LOG; 2'); $rootScope.name = 'abc'; expect(iscope.$parent).toBe($rootScope); expect(iscope.name).toBeUndefined(); })); it('should correctly create the scope hierarchy', inject( function($rootScope, $compile, log) { element = $compile( '
' + //1 '' + //2 '' + //3 '' + '' + '' + //4 '' + '' + '
' )($rootScope); expect(log).toEqual('2; 3; log-3-2; LOG; log-2-1; LOG; 4; log-4-1; 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('2; 2'); }) ); it('should not allow more than 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 not allow more than one isolate/new scope creation per element regardless of `templateUrl`', inject(function($httpBackend) { $httpBackend.expect('GET', 'tiscope.html').respond('
Hello, world !
'); expect(function() { compile('
'); $httpBackend.flush(); }).toThrowMinErr('$compile', 'multidir', 'Multiple directives [scopeB, tiscopeA] asking for new/isolated scope on: ' + '
'); }) ); it('should not allow more than one isolate scope creation per element regardless of directive priority', function() { module(function($compileProvider) { $compileProvider.directive('highPriorityScope', function() { return { restrict: 'C', priority: 1, scope: true, link: function() {} }; }); }); inject(function($compile) { expect(function() { $compile('
'); }).toThrowMinErr('$compile', 'multidir', 'Multiple directives [highPriorityScope, iscopeA] 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('2'); }) ); it('should create isolate scope even at the root of the template', inject( function($rootScope, $compile, log) { element = $compile('
')($rootScope); expect(log).toEqual('2'); }) ); 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); }) ); it('should return the new scope for children in the directive sync template', inject( function($rootScope, $compile) { element = $compile('
')($rootScope); expect(element.find('span').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); }) ); it('should return the isolate scope for children in directive sync template', inject( function($rootScope, $compile) { element = $compile('
')($rootScope); expect(element.find('span').scope()).toBe(element.isolateScope()); expect(element.isolateScope()).not.toBe($rootScope); }) ); it('should handle "=" bindings with same method names in Object.prototype correctly when not present', inject( function($rootScope, $compile) { var func = function() { element = $compile( '
' )($rootScope); }; expect(func).not.toThrow(); var scope = element.isolateScope(); expect(element.find('span').scope()).toBe(scope); expect(scope).not.toBe($rootScope); // Not shadowed because optional expect(scope.constructor).toBe($rootScope.constructor); expect(scope.hasOwnProperty('constructor')).toBe(false); // Shadowed with undefined because not optional expect(scope.valueOf).toBeUndefined(); expect(scope.hasOwnProperty('valueOf')).toBe(true); }) ); it('should handle "=" bindings with same method names in Object.prototype correctly when present', inject( function($rootScope, $compile) { $rootScope.constructor = 'constructor'; $rootScope.valueOf = 'valueOf'; var func = function() { element = $compile( '
' )($rootScope); }; expect(func).not.toThrow(); var scope = element.isolateScope(); expect(element.find('span').scope()).toBe(scope); expect(scope).not.toBe($rootScope); expect(scope.constructor).toBe('constructor'); expect(scope.hasOwnProperty('constructor')).toBe(true); expect(scope.valueOf).toBe('valueOf'); expect(scope.hasOwnProperty('valueOf')).toBe(true); }) ); it('should throw an error for undefined non-optional "=" bindings when ' + 'strictComponentBindingsEnabled is true', function() { module(function($compileProvider) { $compileProvider.strictComponentBindingsEnabled(true); }); inject( function($rootScope, $compile) { var func = function() { element = $compile( '
' )($rootScope); }; expect(func).toThrowMinErr('$compile', 'missingattr', 'Attribute \'valueOf\' of \'prototypeMethodNameAs' + 'ScopeVarA\' is non-optional and must be set!'); }); }); it('should not throw an error for set non-optional "=" bindings when ' + 'strictComponentBindingsEnabled is true', function() { module(function($compileProvider) { $compileProvider.strictComponentBindingsEnabled(true); }); inject( function($rootScope, $compile) { var func = function() { element = $compile( '
' )($rootScope); }; expect(func).not.toThrow(); }); }); it('should not throw an error for undefined optional "=" bindings when ' + 'strictComponentBindingsEnabled is true', function() { module(function($compileProvider) { $compileProvider.strictComponentBindingsEnabled(true); }); inject( function($rootScope, $compile) { var func = function() { element = $compile( '
' )($rootScope); }; expect(func).not.toThrow(); }); }); it('should handle "@" bindings with same method names in Object.prototype correctly when not present', inject( function($rootScope, $compile) { var func = function() { element = $compile('
')($rootScope); }; expect(func).not.toThrow(); var scope = element.isolateScope(); expect(element.find('span').scope()).toBe(scope); expect(scope).not.toBe($rootScope); // Does not shadow value because optional expect(scope.constructor).toBe($rootScope.constructor); expect(scope.hasOwnProperty('constructor')).toBe(false); // Shadows value because not optional expect(scope.valueOf).toBeUndefined(); expect(scope.hasOwnProperty('valueOf')).toBe(true); }) ); it('should handle "@" bindings with same method names in Object.prototype correctly when present', inject( function($rootScope, $compile) { var func = function() { element = $compile( '
' )($rootScope); }; expect(func).not.toThrow(); expect(element.find('span').scope()).toBe(element.isolateScope()); expect(element.isolateScope()).not.toBe($rootScope); expect(element.isolateScope()['constructor']).toBe('constructor'); expect(element.isolateScope()['valueOf']).toBe('valueOf'); }) ); it('should throw an error for undefined non-optional "@" bindings when ' + 'strictComponentBindingsEnabled is true', function() { module(function($compileProvider) { $compileProvider.strictComponentBindingsEnabled(true); }); inject( function($rootScope, $compile) { var func = function() { element = $compile( '
' )($rootScope); }; expect(func).toThrowMinErr('$compile', 'missingattr', 'Attribute \'valueOf\' of \'prototypeMethodNameAs' + 'ScopeVarB\' is non-optional and must be set!'); }); }); it('should not throw an error for set non-optional "@" bindings when ' + 'strictComponentBindingsEnabled is true', function() { module(function($compileProvider) { $compileProvider.strictComponentBindingsEnabled(true); }); inject( function($rootScope, $compile) { var func = function() { element = $compile( '
' )($rootScope); }; expect(func).not.toThrow(); }); }); it('should not throw an error for undefined optional "@" bindings when ' + 'strictComponentBindingsEnabled is true', function() { module(function($compileProvider) { $compileProvider.strictComponentBindingsEnabled(true); }); inject( function($rootScope, $compile) { var func = function() { element = $compile( '
' )($rootScope); }; expect(func).not.toThrow(); }); }); it('should handle "&" bindings with same method names in Object.prototype correctly when not present', inject( function($rootScope, $compile) { var func = function() { element = $compile('
')($rootScope); }; expect(func).not.toThrow(); expect(element.find('span').scope()).toBe(element.isolateScope()); expect(element.isolateScope()).not.toBe($rootScope); expect(element.isolateScope()['constructor']).toBe($rootScope.constructor); expect(element.isolateScope()['valueOf']()).toBeUndefined(); }) ); it('should handle "&" bindings with same method names in Object.prototype correctly when present', inject( function($rootScope, $compile) { $rootScope.constructor = function() { return 'constructor'; }; $rootScope.valueOf = function() { return 'valueOf'; }; var func = function() { element = $compile( '
' )($rootScope); }; expect(func).not.toThrow(); expect(element.find('span').scope()).toBe(element.isolateScope()); expect(element.isolateScope()).not.toBe($rootScope); expect(element.isolateScope()['constructor']()).toBe('constructor'); expect(element.isolateScope()['valueOf']()).toBe('valueOf'); }) ); it('should throw an error for undefined non-optional "&" bindings when ' + 'strictComponentBindingsEnabled is true', function() { module(function($compileProvider) { $compileProvider.strictComponentBindingsEnabled(true); }); inject( function($rootScope, $compile) { var func = function() { element = $compile( '
' )($rootScope); }; expect(func).toThrowMinErr('$compile', 'missingattr', 'Attribute \'valueOf\' of \'prototypeMethodNameAs' + 'ScopeVarC\' is non-optional and must be set!'); }); }); it('should not throw an error for set non-optional "&" bindings when ' + 'strictComponentBindingsEnabled is true', function() { module(function($compileProvider) { $compileProvider.strictComponentBindingsEnabled(true); }); inject( function($rootScope, $compile) { var func = function() { element = $compile( '
' )($rootScope); }; expect(func).not.toThrow(); }); }); it('should not throw an error for undefined optional "&" bindings when ' + 'strictComponentBindingsEnabled is true', function() { module(function($compileProvider) { $compileProvider.strictComponentBindingsEnabled(true); }); inject( function($rootScope, $compile) { var func = function() { element = $compile( '
' )($rootScope); }; expect(func).not.toThrow(); }); }); it('should throw an error for undefined non-optional "<" bindings when ' + 'strictComponentBindingsEnabled is true', function() { module(function($compileProvider) { $compileProvider.strictComponentBindingsEnabled(true); }); inject( function($rootScope, $compile) { var func = function() { element = $compile( '
' )($rootScope); }; expect(func).toThrowMinErr('$compile', 'missingattr', 'Attribute \'valueOf\' of \'prototypeMethodNameAs' + 'ScopeVarD\' is non-optional and must be set!'); }); }); it('should not throw an error for set non-optional "<" bindings when ' + 'strictComponentBindingsEnabled is true', function() { module(function($compileProvider) { $compileProvider.strictComponentBindingsEnabled(true); }); inject( function($rootScope, $compile) { var func = function() { element = $compile( '
' )($rootScope); }; expect(func).not.toThrow(); }); }); it('should not throw an error for undefined optional "<" bindings when ' + 'strictComponentBindingsEnabled is true', function() { module(function($compileProvider) { $compileProvider.strictComponentBindingsEnabled(true); }); inject( function($rootScope, $compile) { var func = function() { element = $compile( '
' )($rootScope); }; expect(func).not.toThrow(); }); }); it('should not throw exception when using "watch" as binding in Firefox', inject( function($rootScope, $compile) { $rootScope.watch = 'watch'; var func = function() { element = $compile( '
' )($rootScope); }; expect(func).not.toThrow(); expect(element.find('span').scope()).toBe(element.isolateScope()); expect(element.isolateScope()).not.toBe($rootScope); expect(element.isolateScope()['watch']).toBe('watch'); }) ); it('should handle @ bindings on BOOLEAN attributes', function() { var checkedVal; module(function($compileProvider) { $compileProvider.directive('test', function() { return { scope: { checked: '@' }, link: function(scope, element, attrs) { checkedVal = scope.checked; } }; }); }); inject(function($compile, $rootScope) { $compile('')($rootScope); expect(checkedVal).toEqual(true); }); }); it('should handle updates to @ bindings on BOOLEAN attributes', function() { var componentScope; module(function($compileProvider) { $compileProvider.directive('test', function() { return { scope: {checked: '@'}, link: function(scope, element, attrs) { componentScope = scope; attrs.$set('checked', true); } }; }); }); inject(function($compile, $rootScope) { $compile('')($rootScope); expect(componentScope.checked).toBe(true); }); }); }); 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()); }) ); it('should return the isolate scope for child elements in directive sync template', inject( function($rootScope, $compile) { var directiveElement, child; element = $compile('
')($rootScope); $rootScope.$apply(); directiveElement = element.find('a'); child = directiveElement.find('span'); expect(child.scope()).toBe(directiveElement.isolateScope()); }) ); }); }); describe('multidir isolated scope error messages', function() { angular.module('fakeIsoledScopeModule', []) .directive('fakeScope', function(log) { return { scope: true, restrict: 'CA', compile: function() { return {pre: function(scope, element) { log(scope.$id); expect(element.data('$scope')).toBe(scope); }}; } }; }) .directive('fakeIScope', function(log) { return { scope: {}, restrict: 'CA', compile: function() { return function(scope, element) { iscope = scope; log(scope.$id); expect(element.data('$isolateScopeNoTemplate')).toBe(scope); }; } }; }); beforeEach(module('fakeIsoledScopeModule', function() { directive('anonymModuleScopeDirective', function(log) { return { scope: true, restrict: 'CA', compile: function() { return {pre: function(scope, element) { log(scope.$id); expect(element.data('$scope')).toBe(scope); }}; } }; }); })); it('should add module name to multidir isolated scope message if directive defined through module', inject( function($rootScope, $compile) { expect(function() { $compile('
'); }).toThrowMinErr('$compile', 'multidir', 'Multiple directives [fakeIScope (module: fakeIsoledScopeModule), fakeScope (module: fakeIsoledScopeModule)] ' + 'asking for new/isolated scope on:
'); }) ); it('shouldn\'t add module name to multidir isolated scope message if directive is defined directly with $compileProvider', inject( function($rootScope, $compile) { expect(function() { $compile('
'); }).toThrowMinErr('$compile', 'multidir', 'Multiple directives [anonymModuleScopeDirective, fakeIScope (module: fakeIsoledScopeModule)] ' + 'asking for new/isolated scope on:
'); }) ); }); }); }); }); describe('interpolation', function() { var observeSpy, directiveAttrs, deregisterObserver; beforeEach(module(function() { directive('observer', function() { return function(scope, elm, attr) { directiveAttrs = attr; observeSpy = jasmine.createSpy('$observe attr'); deregisterObserver = attr.$observe('someAttr', 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 one-time bind if the expression starts with two colons', inject( function($rootScope, $compile) { $rootScope.name = 'angular'; element = $compile('
text: {{::name}}
')($rootScope); expect($rootScope.$$watchers.length).toBe(2); $rootScope.$digest(); expect(element.text()).toEqual('text: angular'); expect(element.attr('name')).toEqual('attr: angular'); expect($rootScope.$$watchers.length).toBe(0); $rootScope.name = 'not-angular'; $rootScope.$digest(); expect(element.text()).toEqual('text: angular'); expect(element.attr('name')).toEqual('attr: angular'); }) ); it('should one-time bind if the expression starts with a space and two colons', inject( function($rootScope, $compile) { $rootScope.name = 'angular'; element = $compile('
text: {{ ::name }}
')($rootScope); expect($rootScope.$$watchers.length).toBe(2); $rootScope.$digest(); expect(element.text()).toEqual('text: angular'); expect(element.attr('name')).toEqual('attr: angular'); expect($rootScope.$$watchers.length).toBe(0); $rootScope.name = 'not-angular'; $rootScope.$digest(); expect(element.text()).toEqual('text: angular'); expect(element.attr('name')).toEqual('attr: angular'); }) ); it('should interpolate a multi-part expression for regular attributes', inject(function($compile, $rootScope) { element = $compile('
')($rootScope); $rootScope.$digest(); expect(element.attr('foo')).toBe('some/'); $rootScope.$apply(function() { $rootScope.id = 1; }); expect(element.attr('foo')).toEqual('some/1'); })); 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'); }); }); it('should allow the attribute to be removed before the attribute interpolation', function() { module(function() { directive('removeAttr', function() { return { restrict:'A', compile: function(tElement, tAttr) { tAttr.$set('removeAttr', null); } }; }); }); inject(function($rootScope, $compile) { expect(function() { element = $compile('
')($rootScope); }).not.toThrow(); expect(element.attr('remove-attr')).toBeUndefined(); }); }); 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'); })); }); describe('decorating with binding info', function() { it('should not occur if `debugInfoEnabled` is false', function() { module(function($compileProvider) { $compileProvider.debugInfoEnabled(false); }); inject(function($compile, $rootScope) { element = $compile('
{{1+2}}
')($rootScope); expect(element.hasClass('ng-binding')).toBe(false); expect(element.data('$binding')).toBeUndefined(); }); }); it('should occur if `debugInfoEnabled` is true', function() { module(function($compileProvider) { $compileProvider.debugInfoEnabled(true); }); inject(function($compile, $rootScope) { element = $compile('
{{1+2}}
')($rootScope); expect(element.hasClass('ng-binding')).toBe(true); expect(element.data('$binding')).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 return a deregistration function while observing an attribute', inject(function($rootScope, $compile) { $compile('
')($rootScope); $rootScope.$apply('value = "first-value"'); expect(observeSpy).toHaveBeenCalledWith('first-value'); deregisterObserver(); $rootScope.$apply('value = "new-value"'); expect(observeSpy).not.toHaveBeenCalledWith('new-value'); })); it('should set interpolated attrs to initial interpolation value', inject(function($rootScope, $compile) { // we need the interpolated attributes to be initialized so that linking fn in a component // can access the value during link $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 call observer only when the attribute value changes', function() { module(function() { directive('observingDirective', function() { return { restrict: 'E', scope: { someAttr: '@' } }; }); }); inject(function($rootScope, $compile) { $compile('')($rootScope); $rootScope.$digest(); expect(observeSpy).not.toHaveBeenCalledWith(undefined); }); }); it('should delegate exceptions to $exceptionHandler', function() { observeSpy = jasmine.createSpy('$observe attr').and.throwError('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).toHaveBeenCalledTimes(2); expect($exceptionHandler.errors).toEqual([new Error('ERROR'), new Error('ERROR')]); }); }); it('should translate {{}} in terminal nodes', inject(function($rootScope, $compile) { element = $compile('')($rootScope); $rootScope.$digest(); expect(sortedHtml(element).replace(' selected="selected"', '')). toEqual(''); $rootScope.name = 'Misko'; $rootScope.$digest(); expect(sortedHtml(element).replace(' selected="selected"', '')). toEqual(''); })); it('should handle consecutive text elements as a single text element', inject(function($rootScope, $compile) { // No point it running the test, if there is no MutationObserver if (!window.MutationObserver) return; // Create and register the MutationObserver var observer = new window.MutationObserver(noop); observer.observe(document.body, {childList: true, subtree: true}); // Run the actual test var base = jqLite('
— {{ "This doesn\'t." }}
'); element = $compile(base)($rootScope); $rootScope.$digest(); expect(element.text()).toBe('— This doesn\'t.'); // Unregister the MutationObserver (and hope it doesn't mess up with subsequent tests) observer.disconnect(); })); it('should not process text nodes merged into their sibling', inject(function($compile, $rootScope) { var div = document.createElement('div'); div.appendChild(document.createTextNode('1{{ value }}')); div.appendChild(document.createTextNode('2{{ value }}')); div.appendChild(document.createTextNode('3{{ value }}')); element = jqLite(div.childNodes); var initialWatcherCount = $rootScope.$countWatchers(); $compile(element)($rootScope); $rootScope.$apply('value = 0'); var newWatcherCount = $rootScope.$countWatchers() - initialWatcherCount; expect(element.text()).toBe('102030'); expect(newWatcherCount).toBe(3); dealoc(div); })); 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('
##hello|uppercase]]|
')($rootScope); $rootScope.hello = 'ahoj'; $rootScope.$digest(); expect(element.text()).toBe('AHOJ|ahoj|AHOJ'); }); }); it('should support custom start interpolation symbol, even when `endSymbol` doesn\'t change', function() { module(function($compileProvider, $interpolateProvider) { $interpolateProvider.startSymbol('[['); $compileProvider.directive('myDirective', function() { return { template: '{{ hello }}|{{ hello | uppercase }}' }; }); }); inject(function($compile, $rootScope) { var tmpl = '
[[ hello | uppercase }}|
'; element = $compile(tmpl)($rootScope); $rootScope.hello = 'ahoj'; $rootScope.$digest(); expect(element.text()).toBe('AHOJ|ahoj|AHOJ'); }); } ); it('should support custom end interpolation symbol, even when `startSymbol` doesn\'t change', function() { module(function($compileProvider, $interpolateProvider) { $interpolateProvider.endSymbol(']]'); $compileProvider.directive('myDirective', function() { return { template: '{{ hello }}|{{ hello | uppercase }}' }; }); }); inject(function($compile, $rootScope) { var tmpl = '
{{ hello | uppercase ]]|
'; element = $compile(tmpl)($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('
##hello|uppercase]]|
')($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('collector', function() { var collected; beforeEach(module(function($compileProvider) { collected = false; $compileProvider.directive('testCollect', function() { return { restrict: 'EACM', link: function() { collected = true; } }; }); })); it('should collect comment directives by default', inject(function() { var html = ''; element = $compile('
' + html + '
')($rootScope); expect(collected).toBe(true); })); it('should collect css class directives by default', inject(function() { element = $compile('
')($rootScope); expect(collected).toBe(true); })); forEach([ {commentEnabled: true, cssEnabled: true}, {commentEnabled: true, cssEnabled: false}, {commentEnabled: false, cssEnabled: true}, {commentEnabled: false, cssEnabled: false} ], function(config) { describe('commentDirectivesEnabled(' + config.commentEnabled + ') ' + 'cssClassDirectivesEnabled(' + config.cssEnabled + ')', function() { beforeEach(module(function($compileProvider) { $compileProvider.commentDirectivesEnabled(config.commentEnabled); $compileProvider.cssClassDirectivesEnabled(config.cssEnabled); })); var $compile, $rootScope; beforeEach(inject(function(_$compile_,_$rootScope_) { $compile = _$compile_; $rootScope = _$rootScope_; })); it('should handle comment directives appropriately', function() { var html = ''; element = $compile('
' + html + '
')($rootScope); expect(collected).toBe(config.commentEnabled); }); it('should handle css directives appropriately', function() { element = $compile('
')($rootScope); expect(collected).toBe(config.cssEnabled); }); it('should not prevent to compile entity directives', function() { element = $compile('')($rootScope); expect(collected).toBe(true); }); it('should not prevent to compile attribute directives', function() { element = $compile('')($rootScope); expect(collected).toBe(true); }); it('should not prevent to compile interpolated expressions', function() { element = $compile('{{"text "+"interpolated"}}')($rootScope); $rootScope.$apply(); expect(element.text()).toBe('text interpolated'); }); it('should interpolate expressions inside class attribute', function() { $rootScope.interpolateMe = 'interpolated'; var html = '
'; element = $compile(html)($rootScope); $rootScope.$apply(); expect(element).toHaveClass('interpolated'); }); }); }); it('should configure comment directives true by default', module(function($compileProvider) { var commentDirectivesEnabled = $compileProvider.commentDirectivesEnabled(); expect(commentDirectivesEnabled).toBe(true); }) ); it('should return self when setting commentDirectivesEnabled', module(function($compileProvider) { var self = $compileProvider.commentDirectivesEnabled(true); expect(self).toBe($compileProvider); }) ); it('should cache commentDirectivesEnabled value when configure ends', function() { var $compileProvider; module(function(_$compileProvider_) { $compileProvider = _$compileProvider_; $compileProvider.commentDirectivesEnabled(false); }); inject(function($compile, $rootScope) { $compileProvider.commentDirectivesEnabled(true); var html = ''; element = $compile('
' + html + '
')($rootScope); expect(collected).toBe(false); }); }); it('should configure css class directives true by default', module(function($compileProvider) { var cssClassDirectivesEnabled = $compileProvider.cssClassDirectivesEnabled(); expect(cssClassDirectivesEnabled).toBe(true); }) ); it('should return self when setting cssClassDirectivesEnabled', module(function($compileProvider) { var self = $compileProvider.cssClassDirectivesEnabled(true); expect(self).toBe($compileProvider); }) ); it('should cache cssClassDirectivesEnabled value when configure ends', function() { var $compileProvider; module(function(_$compileProvider_) { $compileProvider = _$compileProvider_; $compileProvider.cssClassDirectivesEnabled(false); }); inject(function($compile, $rootScope) { $compileProvider.cssClassDirectivesEnabled(true); element = $compile('
')($rootScope); expect(collected).toBe(false); }); }); }); 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'); }); }); it('should throw multilink error when linking the same element more then once', function() { var linker = $compile('
'); linker($rootScope).remove(); expect(function() { linker($rootScope); }).toThrowMinErr('$compile', 'multilink', 'This element has already been linked.'); }); }); 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].calls.reset(); spies[1].calls.reset(); $rootScope.$apply(function() { $rootScope.items[0].id = 5; }); expect(spies[0]).toHaveBeenCalledOnceWith('id_5'); }); }); describe('$set', function() { var attr; beforeEach(function() { module(function() { // Create directives that capture the `attr` object ['input', 'a', 'img'].forEach(function(tag) { directive(tag, 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')).toBeUndefined(); attr.$set('ngMyAttr', 'value'); attr.$set('ngMyAttr', null); expect(element.attr('ng-my-attr')).toBeUndefined(); }); 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'); }); it('should not automatically sanitize a[href]', inject(function($compile, $rootScope) { // Breaking change in https://github.com/angular/angular.js/pull/16378 element = $compile('')($rootScope); $rootScope.attr.$set('href', 'evil:foo()'); expect(element.attr('href')).toEqual('evil:foo()'); expect($rootScope.attr.href).toEqual('evil:foo()'); })); it('should not automatically sanitize img[src]', inject(function($compile, $rootScope) { // Breaking change in https://github.com/angular/angular.js/pull/16378 element = $compile('')($rootScope); $rootScope.attr.$set('img', 'evil:foo()'); expect(element.attr('img')).toEqual('evil:foo()'); expect($rootScope.attr.img).toEqual('evil:foo()'); })); it('should automatically sanitize img[srcset]', inject(function($compile, $rootScope) { element = $compile('')($rootScope); $rootScope.attr.$set('srcset', 'evil:foo()'); expect(element.attr('srcset')).toEqual('unsafe:evil:foo()'); expect($rootScope.attr.srcset).toEqual('unsafe:evil:foo()'); })); it('should not accept trusted values for img[srcset]', inject(function($compile, $rootScope, $sce) { var trusted = $sce.trustAsMediaUrl('trustme:foo()'); element = $compile('')($rootScope); expect(function() { $rootScope.attr.$set('srcset', trusted); }).toThrowMinErr('$compile', 'srcset', 'Can\'t pass trusted values to `$set(\'srcset\', value)`: "trustme:foo()"'); })); }); }); describe('controller lifecycle hooks', function() { describe('$onInit', function() { it('should call `$onInit`, if provided, after all the controllers on the element have been initialized', function() { function check() { expect(this.element.controller('d1').id).toEqual(1); expect(this.element.controller('d2').id).toEqual(2); } function Controller1($element) { this.id = 1; this.element = $element; } Controller1.prototype.$onInit = jasmine.createSpy('$onInit').and.callFake(check); function Controller2($element) { this.id = 2; this.element = $element; } Controller2.prototype.$onInit = jasmine.createSpy('$onInit').and.callFake(check); angular.module('my', []) .directive('d1', valueFn({ controller: Controller1 })) .directive('d2', valueFn({ controller: Controller2 })); module('my'); inject(function($compile, $rootScope) { element = $compile('
')($rootScope); expect(Controller1.prototype.$onInit).toHaveBeenCalledOnce(); expect(Controller2.prototype.$onInit).toHaveBeenCalledOnce(); }); }); it('should continue to trigger other `$onInit` hooks if one throws an error', function() { function ThrowingController() { this.$onInit = function() { throw new Error('bad hook'); }; } function LoggingController($log) { this.$onInit = function() { $log.info('onInit'); }; } angular.module('my', []) .component('c1', { controller: ThrowingController, bindings: {'prop': '<'} }) .component('c2', { controller: LoggingController, bindings: {'prop': '<'} }) .config(function($exceptionHandlerProvider) { // We need to test with the exceptionHandler not rethrowing... $exceptionHandlerProvider.mode('log'); }); module('my'); inject(function($compile, $rootScope, $exceptionHandler, $log) { // Setup the directive with bindings that will keep updating the bound value forever element = $compile('
')($rootScope); // The first component's error should be logged expect($exceptionHandler.errors.pop()).toEqual(new Error('bad hook')); // The second component's hook should still be called expect($log.info.logs.pop()).toEqual(['onInit']); }); }); }); describe('$onDestroy', function() { it('should call `$onDestroy`, if provided, on the controller when its scope is destroyed', function() { function TestController() { this.count = 0; } TestController.prototype.$onDestroy = function() { this.count++; }; angular.module('my', []) .directive('d1', valueFn({ scope: true, controller: TestController })) .directive('d2', valueFn({ scope: {}, controller: TestController })) .directive('d3', valueFn({ controller: TestController })); module('my'); inject(function($compile, $rootScope) { element = $compile('
')($rootScope); $rootScope.$apply('show = [true, true, true]'); var d1Controller = element.find('d1').controller('d1'); var d2Controller = element.find('d2').controller('d2'); var d3Controller = element.find('d3').controller('d3'); expect([d1Controller.count, d2Controller.count, d3Controller.count]).toEqual([0,0,0]); $rootScope.$apply('show = [false, true, true]'); expect([d1Controller.count, d2Controller.count, d3Controller.count]).toEqual([1,0,0]); $rootScope.$apply('show = [false, false, true]'); expect([d1Controller.count, d2Controller.count, d3Controller.count]).toEqual([1,1,0]); $rootScope.$apply('show = [false, false, false]'); expect([d1Controller.count, d2Controller.count, d3Controller.count]).toEqual([1,1,1]); }); }); it('should call `$onDestroy` top-down (the same as `scope.$broadcast`)', function() { var log = []; function ParentController() { log.push('parent created'); } ParentController.prototype.$onDestroy = function() { log.push('parent destroyed'); }; function ChildController() { log.push('child created'); } ChildController.prototype.$onDestroy = function() { log.push('child destroyed'); }; function GrandChildController() { log.push('grand child created'); } GrandChildController.prototype.$onDestroy = function() { log.push('grand child destroyed'); }; angular.module('my', []) .directive('parent', valueFn({ scope: true, controller: ParentController })) .directive('child', valueFn({ scope: true, controller: ChildController })) .directive('grandChild', valueFn({ scope: true, controller: GrandChildController })); module('my'); inject(function($compile, $rootScope) { element = $compile('')($rootScope); $rootScope.$apply('show = true'); expect(log).toEqual(['parent created', 'child created', 'grand child created']); log = []; $rootScope.$apply('show = false'); expect(log).toEqual(['parent destroyed', 'child destroyed', 'grand child destroyed']); }); }); }); describe('$postLink', function() { it('should call `$postLink`, if provided, after the element has completed linking (i.e. post-link)', function() { var log = []; function Controller1() { } Controller1.prototype.$postLink = function() { log.push('d1 view init'); }; function Controller2() { } Controller2.prototype.$postLink = function() { log.push('d2 view init'); }; angular.module('my', []) .directive('d1', valueFn({ controller: Controller1, link: { pre: function(s, e) { log.push('d1 pre: ' + e.text()); }, post: function(s, e) { log.push('d1 post: ' + e.text()); } }, template: '' })) .directive('d2', valueFn({ controller: Controller2, link: { pre: function(s, e) { log.push('d2 pre: ' + e.text()); }, post: function(s, e) { log.push('d2 post: ' + e.text()); } }, template: 'loaded' })); module('my'); inject(function($compile, $rootScope) { element = $compile('')($rootScope); expect(log).toEqual([ 'd1 pre: loaded', 'd2 pre: loaded', 'd2 post: loaded', 'd2 view init', 'd1 post: loaded', 'd1 view init' ]); }); }); }); describe('$doCheck', function() { it('should call `$doCheck`, if provided, for each digest cycle, after $onChanges and $onInit', function() { var log = []; function TestController() { } TestController.prototype.$doCheck = function() { log.push('$doCheck'); }; TestController.prototype.$onChanges = function() { log.push('$onChanges'); }; TestController.prototype.$onInit = function() { log.push('$onInit'); }; angular.module('my', []) .component('dcc', { controller: TestController, bindings: { 'prop1': '<' } }); module('my'); inject(function($compile, $rootScope) { element = $compile('')($rootScope); expect(log).toEqual([ '$onChanges', '$onInit', '$doCheck' ]); // Clear log log = []; $rootScope.$apply(); expect(log).toEqual([ '$doCheck', '$doCheck' ]); // Clear log log = []; $rootScope.$apply('val = 2'); expect(log).toEqual([ '$doCheck', '$onChanges', '$doCheck' ]); }); }); it('should work if $doCheck is provided in the constructor', function() { var log = []; function TestController() { this.$doCheck = function() { log.push('$doCheck'); }; this.$onChanges = function() { log.push('$onChanges'); }; this.$onInit = function() { log.push('$onInit'); }; } angular.module('my', []) .component('dcc', { controller: TestController, bindings: { 'prop1': '<' } }); module('my'); inject(function($compile, $rootScope) { element = $compile('')($rootScope); expect(log).toEqual([ '$onChanges', '$onInit', '$doCheck' ]); // Clear log log = []; $rootScope.$apply(); expect(log).toEqual([ '$doCheck', '$doCheck' ]); // Clear log log = []; $rootScope.$apply('val = 2'); expect(log).toEqual([ '$doCheck', '$onChanges', '$doCheck' ]); }); }); }); describe('$onChanges', function() { it('should call `$onChanges`, if provided, when a one-way (`<`) or interpolation (`@`) bindings are updated', function() { var log = []; function TestController() { } TestController.prototype.$onChanges = function(change) { log.push(change); }; angular.module('my', []) .component('c1', { controller: TestController, bindings: { 'prop1': '<', 'prop2': '<', 'other': '=', 'attr': '@' } }); module('my'); inject(function($compile, $rootScope) { // Setup a watch to indicate some complicated updated logic $rootScope.$watch('val', function(val, oldVal) { $rootScope.val2 = val * 2; }); // Setup the directive with two bindings element = $compile('')($rootScope); expect(log).toEqual([ { prop1: jasmine.objectContaining({currentValue: undefined}), prop2: jasmine.objectContaining({currentValue: undefined}), attr: jasmine.objectContaining({currentValue: ''}) } ]); // Clear the initial changes from the log log = []; // Update val to trigger the onChanges $rootScope.$apply('val = 42'); // Now we should have a single changes entry in the log expect(log).toEqual([ { prop1: jasmine.objectContaining({currentValue: 42}), prop2: jasmine.objectContaining({currentValue: 84}) } ]); // Clear the log log = []; // Update val to trigger the onChanges $rootScope.$apply('val = 17'); // Now we should have a single changes entry in the log expect(log).toEqual([ { prop1: jasmine.objectContaining({previousValue: 42, currentValue: 17}), prop2: jasmine.objectContaining({previousValue: 84, currentValue: 34}) } ]); // Clear the log log = []; // Update val3 to trigger the "other" two-way binding $rootScope.$apply('val3 = 63'); // onChanges should not have been called expect(log).toEqual([]); // Update val4 to trigger the "attr" interpolation binding $rootScope.$apply('val4 = 22'); // onChanges should not have been called expect(log).toEqual([ { attr: jasmine.objectContaining({previousValue: '', currentValue: '22'}) } ]); }); }); it('should trigger `$onChanges` even if the inner value already equals the new outer value', function() { var log = []; function TestController() { } TestController.prototype.$onChanges = function(change) { log.push(change); }; angular.module('my', []) .component('c1', { controller: TestController, bindings: { 'prop1': '<' } }); module('my'); inject(function($compile, $rootScope) { element = $compile('')($rootScope); $rootScope.$apply('val = 1'); expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: undefined, currentValue: 1})}); element.isolateScope().$ctrl.prop1 = 2; $rootScope.$apply('val = 2'); expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: 1, currentValue: 2})}); }); }); it('should trigger `$onChanges` for literal expressions when expression input value changes (simple value)', function() { var log = []; function TestController() { } TestController.prototype.$onChanges = function(change) { log.push(change); }; angular.module('my', []) .component('c1', { controller: TestController, bindings: { 'prop1': '<' } }); module('my'); inject(function($compile, $rootScope) { element = $compile('')($rootScope); $rootScope.$apply('val = 1'); expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [undefined], currentValue: [1]})}); $rootScope.$apply('val = 2'); expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [1], currentValue: [2]})}); }); }); it('should trigger `$onChanges` for literal expressions when expression input value changes (complex value)', function() { var log = []; function TestController() { } TestController.prototype.$onChanges = function(change) { log.push(change); }; angular.module('my', []) .component('c1', { controller: TestController, bindings: { 'prop1': '<' } }); module('my'); inject(function($compile, $rootScope) { element = $compile('')($rootScope); $rootScope.$apply('val = [1]'); expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [undefined], currentValue: [[1]]})}); $rootScope.$apply('val = [2]'); expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [[1]], currentValue: [[2]]})}); }); }); it('should trigger `$onChanges` for literal expressions when expression input value changes instances, even when equal', function() { var log = []; function TestController() { } TestController.prototype.$onChanges = function(change) { log.push(change); }; angular.module('my', []) .component('c1', { controller: TestController, bindings: { 'prop1': '<' } }); module('my'); inject(function($compile, $rootScope) { element = $compile('')($rootScope); $rootScope.$apply('val = [1]'); expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [undefined], currentValue: [[1]]})}); $rootScope.$apply('val = [1]'); expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [[1]], currentValue: [[1]]})}); }); }); it('should pass the original value as `previousValue` even if there were multiple changes in a single digest', function() { var log = []; function TestController() { } TestController.prototype.$onChanges = function(change) { log.push(change); }; angular.module('my', []) .component('c1', { controller: TestController, bindings: { 'prop': '<' } }); module('my'); inject(function($compile, $rootScope) { element = $compile('')($rootScope); // We add this watch after the compilation to ensure that it will run after the binding watchers // therefore triggering the thing that this test is hoping to enforce $rootScope.$watch('a', function(val) { $rootScope.b = val * 2; }); expect(log).toEqual([{prop: jasmine.objectContaining({currentValue: undefined})}]); // Clear the initial values from the log log = []; // Update val to trigger the onChanges $rootScope.$apply('a = 42'); // Now the change should have the real previous value (undefined), not the intermediate one (42) expect(log).toEqual([{prop: jasmine.objectContaining({currentValue: 126})}]); // Clear the log log = []; // Update val to trigger the onChanges $rootScope.$apply('a = 7'); // Now the change should have the real previous value (126), not the intermediate one, (91) expect(log).toEqual([{prop: jasmine.objectContaining({previousValue: 126, currentValue: 21})}]); }); }); it('should trigger an initial onChanges call for each binding with the `isFirstChange()` returning true', function() { var log = []; function TestController() { } TestController.prototype.$onChanges = function(change) { log.push(change); }; angular.module('my', []) .component('c1', { controller: TestController, bindings: { 'prop': '<', attr: '@' } }); module('my'); inject(function($compile, $rootScope) { $rootScope.$apply('a = 7'); element = $compile('')($rootScope); expect(log).toEqual([ { prop: jasmine.objectContaining({currentValue: 7}), attr: jasmine.objectContaining({currentValue: '7'}) } ]); expect(log[0].prop.isFirstChange()).toEqual(true); expect(log[0].attr.isFirstChange()).toEqual(true); log = []; $rootScope.$apply('a = 9'); expect(log).toEqual([ { prop: jasmine.objectContaining({previousValue: 7, currentValue: 9}), attr: jasmine.objectContaining({previousValue: '7', currentValue: '9'}) } ]); expect(log[0].prop.isFirstChange()).toEqual(false); expect(log[0].attr.isFirstChange()).toEqual(false); }); }); it('should trigger an initial onChanges call for each binding even if the hook is defined in the constructor', function() { var log = []; function TestController() { this.$onChanges = function(change) { log.push(change); }; } angular.module('my', []) .component('c1', { controller: TestController, bindings: { 'prop': '<', attr: '@' } }); module('my'); inject(function($compile, $rootScope) { $rootScope.$apply('a = 7'); element = $compile('')($rootScope); expect(log).toEqual([ { prop: jasmine.objectContaining({currentValue: 7}), attr: jasmine.objectContaining({currentValue: '7'}) } ]); expect(log[0].prop.isFirstChange()).toEqual(true); expect(log[0].attr.isFirstChange()).toEqual(true); log = []; $rootScope.$apply('a = 10'); expect(log).toEqual([ { prop: jasmine.objectContaining({previousValue: 7, currentValue: 10}), attr: jasmine.objectContaining({previousValue: '7', currentValue: '10'}) } ]); expect(log[0].prop.isFirstChange()).toEqual(false); expect(log[0].attr.isFirstChange()).toEqual(false); }); }); it('should clean up `@`-binding observers when re-assigning bindings', function() { var constructorSpy = jasmine.createSpy('constructor'); var prototypeSpy = jasmine.createSpy('prototype'); function TestController() { return {$onChanges: constructorSpy}; } TestController.prototype.$onChanges = prototypeSpy; module(function($compileProvider) { $compileProvider.component('test', { bindings: {attr: '@'}, controller: TestController }); }); inject(function($compile, $rootScope) { var template = ''; $rootScope.a = 'foo'; element = $compile(template)($rootScope); $rootScope.$digest(); expect(constructorSpy).toHaveBeenCalled(); expect(prototypeSpy).not.toHaveBeenCalled(); constructorSpy.calls.reset(); $rootScope.$apply('a = "bar"'); expect(constructorSpy).toHaveBeenCalled(); expect(prototypeSpy).not.toHaveBeenCalled(); }); }); it('should not call `$onChanges` twice even when the initial value is `NaN`', function() { var onChangesSpy = jasmine.createSpy('$onChanges'); module(function($compileProvider) { $compileProvider.component('test', { bindings: {prop: '<', attr: '@'}, controller: function TestController() { this.$onChanges = onChangesSpy; } }); }); inject(function($compile, $rootScope) { var template = '' + ''; $rootScope.a = 'foo'; $rootScope.b = NaN; element = $compile(template)($rootScope); $rootScope.$digest(); expect(onChangesSpy).toHaveBeenCalledTimes(2); expect(onChangesSpy.calls.argsFor(0)[0]).toEqual({ prop: jasmine.objectContaining({currentValue: 'foo'}), attr: jasmine.objectContaining({currentValue: 'foo'}) }); expect(onChangesSpy.calls.argsFor(1)[0]).toEqual({ prop: jasmine.objectContaining({currentValue: NaN}), attr: jasmine.objectContaining({currentValue: 'NaN'}) }); onChangesSpy.calls.reset(); $rootScope.$apply('a = "bar"; b = 42'); expect(onChangesSpy).toHaveBeenCalledTimes(2); expect(onChangesSpy.calls.argsFor(0)[0]).toEqual({ prop: jasmine.objectContaining({previousValue: 'foo', currentValue: 'bar'}), attr: jasmine.objectContaining({previousValue: 'foo', currentValue: 'bar'}) }); expect(onChangesSpy.calls.argsFor(1)[0]).toEqual({ prop: jasmine.objectContaining({previousValue: NaN, currentValue: 42}), attr: jasmine.objectContaining({previousValue: 'NaN', currentValue: '42'}) }); }); }); it('should only trigger one extra digest however many controllers have changes', function() { var log = []; function TestController1() { } TestController1.prototype.$onChanges = function(change) { log.push(['TestController1', change]); }; function TestController2() { } TestController2.prototype.$onChanges = function(change) { log.push(['TestController2', change]); }; angular.module('my', []) .component('c1', { controller: TestController1, bindings: {'prop': '<'} }) .component('c2', { controller: TestController2, bindings: {'prop': '<'} }); module('my'); inject(function($compile, $rootScope) { // Create a watcher to count the number of digest cycles var watchCount = 0; $rootScope.$watch(function() { watchCount++; }); // Setup two sibling components with bindings that will change element = $compile('
')($rootScope); // Clear out initial changes log = []; // Update val to trigger the onChanges $rootScope.$apply('val1 = 42; val2 = 17'); expect(log).toEqual([ ['TestController1', {prop: jasmine.objectContaining({currentValue: 42})}], ['TestController2', {prop: jasmine.objectContaining({currentValue: 17})}] ]); // A single apply should only trigger three turns of the digest loop expect(watchCount).toEqual(3); }); }); it('should cope with changes occurring inside `$onChanges()` hooks', function() { var log = []; function OuterController() {} OuterController.prototype.$onChanges = function(change) { log.push(['OuterController', change]); // Make a change to the inner component this.b = this.prop1 * 2; }; function InnerController() { } InnerController.prototype.$onChanges = function(change) { log.push(['InnerController', change]); }; angular.module('my', []) .component('outer', { controller: OuterController, bindings: {'prop1': '<'}, template: '' }) .component('inner', { controller: InnerController, bindings: {'prop2': '<'} }); module('my'); inject(function($compile, $rootScope) { // Setup the directive with two bindings element = $compile('')($rootScope); // Clear out initial changes log = []; // Update val to trigger the onChanges $rootScope.$apply('a = 42'); expect(log).toEqual([ ['OuterController', {prop1: jasmine.objectContaining({previousValue: undefined, currentValue: 42})}], ['InnerController', {prop2: jasmine.objectContaining({previousValue: NaN, currentValue: 84})}] ]); }); }); it('should throw an error if `$onChanges()` hooks are not stable', function() { function TestController() {} TestController.prototype.$onChanges = function(change) { this.onChange(); }; angular.module('my', []) .component('c1', { controller: TestController, bindings: {'prop': '<', onChange: '&'} }); module('my'); inject(function($compile, $rootScope) { // Setup the directive with bindings that will keep updating the bound value forever element = $compile('')($rootScope); // Update val to trigger the unstable onChanges, which will result in an error expect(function() { $rootScope.$apply('a = 42'); }).toThrowMinErr('$compile', 'infchng'); dealoc(element); element = $compile('')($rootScope); $rootScope.$apply('b = 24'); $rootScope.$apply('b = 48'); }); }); it('should log an error if `$onChanges()` hooks are not stable', function() { function TestController() {} TestController.prototype.$onChanges = function(change) { this.onChange(); }; angular.module('my', []) .component('c1', { controller: TestController, bindings: {'prop': '<', onChange: '&'} }) .config(function($exceptionHandlerProvider) { // We need to test with the exceptionHandler not rethrowing... $exceptionHandlerProvider.mode('log'); }); module('my'); inject(function($compile, $rootScope, $exceptionHandler) { // Setup the directive with bindings that will keep updating the bound value forever element = $compile('')($rootScope); // Update val to trigger the unstable onChanges, which will result in an error $rootScope.$apply('a = 42'); expect($exceptionHandler.errors.length).toEqual(1); expect($exceptionHandler.errors[0]). toEqualMinErr('$compile', 'infchng', '10 $onChanges() iterations reached.'); }); }); it('should continue to trigger other `$onChanges` hooks if one throws an error', function() { function ThrowingController() { this.$onChanges = function(change) { throw new Error('bad hook'); }; } function LoggingController($log) { this.$onChanges = function(change) { $log.info('onChange'); }; } angular.module('my', []) .component('c1', { controller: ThrowingController, bindings: {'prop': '<'} }) .component('c2', { controller: LoggingController, bindings: {'prop': '<'} }) .config(function($exceptionHandlerProvider) { // We need to test with the exceptionHandler not rethrowing... $exceptionHandlerProvider.mode('log'); }); module('my'); inject(function($compile, $rootScope, $exceptionHandler, $log) { // Setup the directive with bindings that will keep updating the bound value forever element = $compile('
')($rootScope); // The first component's error should be logged expect($exceptionHandler.errors.pop()).toEqual(new Error('bad hook')); // The second component's changes should still be called expect($log.info.logs.pop()).toEqual(['onChange']); $rootScope.$apply('a = 42'); // The first component's error should be logged expect($exceptionHandler.errors.pop()).toEqual(new Error('bad hook')); // The second component's changes should still be called expect($log.info.logs.pop()).toEqual(['onChange']); }); }); it('should throw `$onChanges` errors immediately', function() { function ThrowingController() { this.$onChanges = function(change) { throw new Error('bad hook: ' + this.prop); }; } angular.module('my', []) .component('c1', { controller: ThrowingController, bindings: {'prop': '<'} }) .config(function($exceptionHandlerProvider) { // We need to test with the exceptionHandler not rethrowing... $exceptionHandlerProvider.mode('log'); }); module('my'); inject(function($compile, $rootScope, $exceptionHandler, $log) { // Setup the directive with bindings that will keep updating the bound value forever element = $compile('
')($rootScope); // Both component's errors should be logged expect($exceptionHandler.errors.pop()).toEqual(new Error('bad hook: NaN')); expect($exceptionHandler.errors.pop()).toEqual(new Error('bad hook: undefined')); $rootScope.$apply('a = 42'); // Both component's error should be logged individually expect($exceptionHandler.errors.pop()).toEqual(new Error('bad hook: 84')); expect($exceptionHandler.errors.pop()).toEqual(new Error('bad hook: 42')); }); }); }); }); describe('isolated locals', function() { var componentScope, regularScope; beforeEach(module(function() { directive('myComponent', function() { return { scope: { attr: '@', attrAlias: '@attr', $attrAlias: '@$attr$', ref: '=', refAlias: '= ref', $refAlias: '= $ref$', reference: '=', optref: '=?', optrefAlias: '=? optref', $optrefAlias: '=? $optref$', optreference: '=?', colref: '=*', colrefAlias: '=* colref', $colrefAlias: '=* $colref$', owRef: '<', owRefAlias: '< owRef', $owRefAlias: '< $owRef$', owOptref: '
'); $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'); }); }); it('should update parent scope when "="-bound NaN changes', inject(function($compile, $rootScope) { $rootScope.num = NaN; compile('
'); var isolateScope = element.isolateScope(); expect(isolateScope.reference).toBeNaN(); isolateScope.$apply(function(scope) { scope.reference = 64; }); expect($rootScope.num).toBe(64); })); it('should update isolate scope when "="-bound NaN changes', inject(function($compile, $rootScope) { $rootScope.num = NaN; compile('
'); var isolateScope = element.isolateScope(); expect(isolateScope.reference).toBeNaN(); $rootScope.$apply(function(scope) { scope.num = 64; }); expect(isolateScope.reference).toBe(64); })); it('should be able to bind attribute names which are present in Object.prototype', function() { module(function() { directive('inProtoAttr', valueFn({ scope: { 'constructor': '@', 'toString': '&', // Spidermonkey extension, may be obsolete in the future 'watch': '=' } })); }); inject(function($rootScope) { expect(function() { compile('
'); }).not.toThrow(); var isolateScope = element.isolateScope(); expect(typeof isolateScope.constructor).toBe('string'); expect(isArray(isolateScope.watch)).toBe(true); expect(typeof isolateScope.toString).toBe('function'); expect($rootScope.value).toBeUndefined(); isolateScope.toString(); expect($rootScope.value).toBe(true); }); }); it('should be able to interpolate attribute names which are present in Object.prototype', function() { var attrs; module(function() { directive('attrExposer', valueFn({ link: function($scope, $element, $attrs) { attrs = $attrs; } })); }); inject(function($compile, $rootScope) { $compile('
')($rootScope); $rootScope.$apply(); expect(attrs.toString).toBe('2'); }); }); it('should not initialize scope value if optional expression binding is not passed', inject(function($compile) { compile('
'); var isolateScope = element.isolateScope(); expect(isolateScope.optExpr).toBeUndefined(); })); it('should not initialize scope value if optional expression binding with Object.prototype name is not passed', inject(function($compile) { compile('
'); var isolateScope = element.isolateScope(); expect(isolateScope.constructor).toBe($rootScope.constructor); })); it('should initialize scope value if optional expression binding is passed', inject(function($compile) { compile('
'); var isolateScope = element.isolateScope(); expect(typeof isolateScope.optExpr).toBe('function'); expect(isolateScope.optExpr()).toBe('did!'); expect($rootScope.value).toBe('did!'); })); it('should initialize scope value if optional expression binding with Object.prototype name is passed', inject(function($compile) { compile('
'); var isolateScope = element.isolateScope(); expect(typeof isolateScope.constructor).toBe('function'); expect(isolateScope.constructor()).toBe('did!'); expect($rootScope.value).toBe('did!'); })); it('should not overwrite @-bound property each digest when not present', function() { module(function($compileProvider) { $compileProvider.directive('testDir', valueFn({ scope: {prop: '@'}, controller: function($scope) { $scope.prop = $scope.prop || 'default'; this.getProp = function() { return $scope.prop; }; }, controllerAs: 'ctrl', template: '

' })); }); inject(function($compile, $rootScope) { element = $compile('
')($rootScope); var scope = element.isolateScope(); expect(scope.ctrl.getProp()).toBe('default'); $rootScope.$digest(); expect(scope.ctrl.getProp()).toBe('default'); }); }); it('should ignore optional "="-bound property if value is the empty string', function() { module(function($compileProvider) { $compileProvider.directive('testDir', valueFn({ scope: {prop: '=?'}, controller: function($scope) { $scope.prop = $scope.prop || 'default'; this.getProp = function() { return $scope.prop; }; }, controllerAs: 'ctrl', template: '

' })); }); inject(function($compile, $rootScope) { element = $compile('
')($rootScope); var scope = element.isolateScope(); expect(scope.ctrl.getProp()).toBe('default'); $rootScope.$digest(); expect(scope.ctrl.getProp()).toBe('default'); scope.prop = 'foop'; $rootScope.$digest(); expect(scope.ctrl.getProp()).toBe('foop'); }); }); describe('bind-once', function() { function countWatches(scope) { var result = 0; while (scope !== null) { result += (scope.$$watchers && scope.$$watchers.length) || 0; result += countWatches(scope.$$childHead); scope = scope.$$nextSibling; } return result; } it('should be possible to one-time bind a parameter on a component with a template', function() { module(function() { directive('otherTplDir', function() { return { scope: {param1: '=', param2: '='}, template: '1:{{param1}};2:{{param2}};3:{{::param1}};4:{{::param2}}' }; }); }); inject(function($rootScope) { compile('
'); expect(countWatches($rootScope)).toEqual(6); // 4 -> template watch group, 2 -> '=' $rootScope.$digest(); expect(element.html()).toBe('1:;2:;3:;4:'); expect(countWatches($rootScope)).toEqual(6); $rootScope.foo = 'foo'; $rootScope.$digest(); expect(element.html()).toBe('1:foo;2:;3:foo;4:'); expect(countWatches($rootScope)).toEqual(4); $rootScope.foo = 'baz'; $rootScope.bar = 'bar'; $rootScope.$digest(); expect(element.html()).toBe('1:foo;2:bar;3:foo;4:bar'); expect(countWatches($rootScope)).toEqual(3); $rootScope.bar = 'baz'; $rootScope.$digest(); expect(element.html()).toBe('1:foo;2:baz;3:foo;4:bar'); }); }); it('should be possible to one-time bind a parameter on a component with a template', function() { module(function() { directive('otherTplDir', function() { return { scope: {param1: '@', param2: '@'}, template: '1:{{param1}};2:{{param2}};3:{{::param1}};4:{{::param2}}' }; }); }); inject(function($rootScope) { compile('
'); expect(countWatches($rootScope)).toEqual(6); // 4 -> template watch group, 2 -> {{ }} $rootScope.$digest(); expect(element.html()).toBe('1:;2:;3:;4:'); expect(countWatches($rootScope)).toEqual(4); // (- 2) -> bind-once in template $rootScope.foo = 'foo'; $rootScope.$digest(); expect(element.html()).toBe('1:foo;2:;3:;4:'); expect(countWatches($rootScope)).toEqual(3); $rootScope.foo = 'baz'; $rootScope.bar = 'bar'; $rootScope.$digest(); expect(element.html()).toBe('1:foo;2:bar;3:;4:'); expect(countWatches($rootScope)).toEqual(3); $rootScope.bar = 'baz'; $rootScope.$digest(); expect(element.html()).toBe('1:foo;2:baz;3:;4:'); }); }); it('should be possible to one-time bind a parameter on a component with a template', function() { module(function() { directive('otherTplDir', function() { return { scope: {param1: '=', param2: '='}, templateUrl: 'other.html' }; }); }); inject(function($rootScope, $templateCache) { $templateCache.put('other.html', '1:{{param1}};2:{{param2}};3:{{::param1}};4:{{::param2}}'); compile('
'); $rootScope.$digest(); expect(element.html()).toBe('1:;2:;3:;4:'); expect(countWatches($rootScope)).toEqual(6); // 4 -> template watch group, 2 -> '=' $rootScope.foo = 'foo'; $rootScope.$digest(); expect(element.html()).toBe('1:foo;2:;3:foo;4:'); expect(countWatches($rootScope)).toEqual(4); $rootScope.foo = 'baz'; $rootScope.bar = 'bar'; $rootScope.$digest(); expect(element.html()).toBe('1:foo;2:bar;3:foo;4:bar'); expect(countWatches($rootScope)).toEqual(3); $rootScope.bar = 'baz'; $rootScope.$digest(); expect(element.html()).toBe('1:foo;2:baz;3:foo;4:bar'); }); }); it('should be possible to one-time bind a parameter on a component with a template', function() { module(function() { directive('otherTplDir', function() { return { scope: {param1: '@', param2: '@'}, templateUrl: 'other.html' }; }); }); inject(function($rootScope, $templateCache) { $templateCache.put('other.html', '1:{{param1}};2:{{param2}};3:{{::param1}};4:{{::param2}}'); compile('
'); $rootScope.$digest(); expect(element.html()).toBe('1:;2:;3:;4:'); expect(countWatches($rootScope)).toEqual(4); // (4 - 2) -> template watch group, 2 -> {{ }} $rootScope.foo = 'foo'; $rootScope.$digest(); expect(element.html()).toBe('1:foo;2:;3:;4:'); expect(countWatches($rootScope)).toEqual(3); $rootScope.foo = 'baz'; $rootScope.bar = 'bar'; $rootScope.$digest(); expect(element.html()).toBe('1:foo;2:bar;3:;4:'); expect(countWatches($rootScope)).toEqual(3); $rootScope.bar = 'baz'; $rootScope.$digest(); expect(element.html()).toBe('1:foo;2:baz;3:;4:'); }); }); it('should continue with a digets cycle when there is a two-way binding from the child to the parent', function() { module(function() { directive('hello', function() { return { restrict: 'E', scope: { greeting: '=' }, template: '', link: function(scope) { scope.setGreeting = function() { scope.greeting = 'Hello!'; }; } }; }); }); inject(function($rootScope) { compile('
' + '

{{greeting}}

' + '
' + '
'); $rootScope.$digest(); browserTrigger(element.find('button'), 'click'); expect(element.find('p').text()).toBe('Hello!'); }); }); }); 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('some other text'); expect(componentScope.attrAlias).toEqual(componentScope.attr); })); it('should copy an attribute with spaces', inject(function() { compile('
'); expect(componentScope.attr).toEqual(' some text '); expect(componentScope.attrAlias).toEqual(' some text '); expect(componentScope.$attrAlias).toEqual(' some other 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'); expect(componentScope.$attrAlias).toEqual('hi 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'); expect(componentScope.$attrAlias).toEqual('hi igor'); })); }); describe('object reference', function() { it('should update local when origin changes', inject(function() { compile('
'); expect(componentScope.ref).toBeUndefined(); expect(componentScope.refAlias).toBe(componentScope.ref); 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'); expect(componentScope.$refAlias).toBe('misko'); $rootScope.name = {}; $rootScope.$apply(); expect(componentScope.ref).toBe($rootScope.name); expect(componentScope.refAlias).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); 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); 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 within the same digest cycle componentScope.ref = 'same'; $rootScope.name = 'same'; $rootScope.$apply(); //change origin back to its 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(function() { $rootScope.$apply(); }). toThrowMinErr('$compile', 'nonassign', 'Expression \'\'hello \' + name\' in attribute \'ref\' 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'); })); it('should complain if assigning to undefined', inject(function() { compile('
'); $rootScope.$apply(); expect(componentScope.ref).toBeUndefined(); componentScope.ref = 'ignore me'; expect(function() { $rootScope.$apply(); }). toThrowMinErr('$compile', 'nonassign', 'Expression \'undefined\' in attribute \'ref\' used with directive \'myComponent\' is non-assignable!'); expect(componentScope.ref).toBeUndefined(); $rootScope.$$phase = null; // reset since the exception was rethrown which prevented phase clearing $rootScope.$apply(); expect(componentScope.ref).toBeUndefined(); })); // 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}\' in attribute \'reference\' 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'); test('true', true); 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).toBeUndefined(); expect(componentScope.optRefAlias).toBe(componentScope.optRef); expect(componentScope.$optRefAlias).toBe(componentScope.optRef); $rootScope.name = 'misko'; $rootScope.$apply(); expect(componentScope.optref).toBe($rootScope.name); expect(componentScope.optrefAlias).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); expect(componentScope.$optrefAlias).toBe($rootScope.name); })); it('should not throw exception when reference does not exist', inject(function() { compile('
'); expect(componentScope.optref).toBeUndefined(); expect(componentScope.optrefAlias).toBeUndefined(); expect(componentScope.$optrefAlias).toBeUndefined(); expect(componentScope.optreference).toBeUndefined(); })); }); describe('collection object reference', function() { it('should update isolate scope when origin scope changes', inject(function() { $rootScope.collection = [{ name: 'Gabriel', value: 18 }, { name: 'Tony', value: 91 }]; $rootScope.query = ''; $rootScope.$apply(); compile('
'); expect(componentScope.colref).toEqual($rootScope.collection); expect(componentScope.colrefAlias).toEqual(componentScope.colref); expect(componentScope.$colrefAlias).toEqual(componentScope.colref); $rootScope.query = 'Gab'; $rootScope.$apply(); expect(componentScope.colref).toEqual([$rootScope.collection[0]]); expect(componentScope.colrefAlias).toEqual([$rootScope.collection[0]]); expect(componentScope.$colrefAlias).toEqual([$rootScope.collection[0]]); })); it('should update origin scope when isolate scope changes', inject(function() { $rootScope.collection = [{ name: 'Gabriel', value: 18 }, { name: 'Tony', value: 91 }]; compile('
'); var newItem = { name: 'Pablo', value: 10 }; componentScope.colref.push(newItem); componentScope.$apply(); expect($rootScope.collection[2]).toEqual(newItem); })); }); describe('one-way binding', function() { it('should update isolate when the identity of origin changes', inject(function() { compile('
'); expect(componentScope.owRef).toBeUndefined(); expect(componentScope.owRefAlias).toBe(componentScope.owRef); expect(componentScope.$owRefAlias).toBe(componentScope.owRef); $rootScope.obj = {value: 'initial'}; $rootScope.$apply(); expect($rootScope.obj).toEqual({value: 'initial'}); expect(componentScope.owRef).toEqual({value: 'initial'}); expect(componentScope.owRefAlias).toBe(componentScope.owRef); expect(componentScope.$owRefAlias).toBe(componentScope.owRef); // This changes in both scopes because of reference $rootScope.obj.value = 'origin1'; $rootScope.$apply(); expect(componentScope.owRef.value).toBe('origin1'); expect(componentScope.owRefAlias.value).toBe('origin1'); expect(componentScope.$owRefAlias.value).toBe('origin1'); componentScope.owRef = {value: 'isolate1'}; componentScope.$apply(); expect($rootScope.obj.value).toBe('origin1'); // Change does not propagate because object identity hasn't changed $rootScope.obj.value = 'origin2'; $rootScope.$apply(); expect(componentScope.owRef.value).toBe('isolate1'); expect(componentScope.owRefAlias.value).toBe('origin2'); expect(componentScope.$owRefAlias.value).toBe('origin2'); // Change does propagate because object identity changes $rootScope.obj = {value: 'origin3'}; $rootScope.$apply(); expect(componentScope.owRef.value).toBe('origin3'); expect(componentScope.owRef).toBe($rootScope.obj); expect(componentScope.owRefAlias).toBe($rootScope.obj); expect(componentScope.$owRefAlias).toBe($rootScope.obj); })); it('should update isolate when both change', inject(function() { compile('
'); $rootScope.name = {mark:123}; componentScope.owRef = 'misko'; $rootScope.$apply(); expect($rootScope.name).toEqual({mark:123}); expect(componentScope.owRef).toBe($rootScope.name); expect(componentScope.owRefAlias).toBe($rootScope.name); expect(componentScope.$owRefAlias).toBe($rootScope.name); $rootScope.name = 'igor'; componentScope.owRef = {}; $rootScope.$apply(); expect($rootScope.name).toEqual('igor'); expect(componentScope.owRef).toBe($rootScope.name); expect(componentScope.owRefAlias).toBe($rootScope.name); expect(componentScope.$owRefAlias).toBe($rootScope.name); })); describe('initialization', function() { var component, log; beforeEach(function() { log = []; angular.module('owComponentTest', []) .component('owComponent', { bindings: { input: '<' }, controller: function() { component = this; this.input = 'constructor'; log.push('constructor'); this.$onInit = function() { this.input = '$onInit'; log.push('$onInit'); }; this.$onChanges = function(changes) { if (changes.input) { log.push(['$onChanges', copy(changes.input)]); } }; } }); }); it('should not update isolate again after $onInit if outer has not changed', function() { module('owComponentTest'); inject(function() { $rootScope.name = 'outer'; compile(''); expect($rootScope.name).toEqual('outer'); expect(component.input).toEqual('$onInit'); $rootScope.$digest(); expect($rootScope.name).toEqual('outer'); expect(component.input).toEqual('$onInit'); expect(log).toEqual([ 'constructor', ['$onChanges', jasmine.objectContaining({ currentValue: 'outer' })], '$onInit' ]); }); }); it('should not update isolate again after $onInit if outer object reference has not changed', function() { module('owComponentTest'); inject(function() { $rootScope.name = ['outer']; compile(''); expect($rootScope.name).toEqual(['outer']); expect(component.input).toEqual('$onInit'); $rootScope.name[0] = 'inner'; $rootScope.$digest(); expect($rootScope.name).toEqual(['inner']); expect(component.input).toEqual('$onInit'); expect(log).toEqual([ 'constructor', ['$onChanges', jasmine.objectContaining({ currentValue: ['outer'] })], '$onInit' ]); }); }); it('should update isolate again after $onInit if outer object reference changes even if equal', function() { module('owComponentTest'); inject(function() { $rootScope.name = ['outer']; compile(''); expect($rootScope.name).toEqual(['outer']); expect(component.input).toEqual('$onInit'); $rootScope.name = ['outer']; $rootScope.$digest(); expect($rootScope.name).toEqual(['outer']); expect(component.input).toEqual(['outer']); expect(log).toEqual([ 'constructor', ['$onChanges', jasmine.objectContaining({ currentValue: ['outer'] })], '$onInit', ['$onChanges', jasmine.objectContaining({ previousValue: ['outer'], currentValue: ['outer'] })] ]); }); }); it('should not update isolate again after $onInit if outer is a literal', function() { module('owComponentTest'); inject(function() { $rootScope.name = 'outer'; compile(''); expect(component.input).toEqual('$onInit'); // No outer change $rootScope.$apply('name = "outer"'); expect(component.input).toEqual('$onInit'); // Outer change $rootScope.$apply('name = "re-outer"'); expect(component.input).toEqual(['re-outer']); expect(log).toEqual([ 'constructor', [ '$onChanges', jasmine.objectContaining({currentValue: ['outer']}) ], '$onInit', [ '$onChanges', jasmine.objectContaining({previousValue: ['outer'], currentValue: ['re-outer']}) ] ]); }); }); it('should update isolate again after $onInit if outer has changed (before initial watchAction call)', function() { module('owComponentTest'); inject(function() { $rootScope.name = 'outer1'; compile(''); expect(component.input).toEqual('$onInit'); $rootScope.$apply('name = "outer2"'); expect($rootScope.name).toEqual('outer2'); expect(component.input).toEqual('outer2'); expect(log).toEqual([ 'constructor', ['$onChanges', jasmine.objectContaining({ currentValue: 'outer1' })], '$onInit', ['$onChanges', jasmine.objectContaining({ currentValue: 'outer2', previousValue: 'outer1' })] ]); }); }); it('should update isolate again after $onInit if outer has changed (before initial watchAction call)', function() { angular.module('owComponentTest') .directive('changeInput', function() { return function(scope, elem, attrs) { scope.name = 'outer2'; }; }); module('owComponentTest'); inject(function() { $rootScope.name = 'outer1'; compile(''); expect(component.input).toEqual('$onInit'); $rootScope.$digest(); expect($rootScope.name).toEqual('outer2'); expect(component.input).toEqual('outer2'); expect(log).toEqual([ 'constructor', ['$onChanges', jasmine.objectContaining({ currentValue: 'outer1' })], '$onInit', ['$onChanges', jasmine.objectContaining({ currentValue: 'outer2', previousValue: 'outer1' })] ]); }); }); }); it('should not break when isolate and origin both change to the same value', inject(function() { $rootScope.name = 'aaa'; compile('
'); //change both sides to the same item within the same digest cycle componentScope.owRef = 'same'; $rootScope.name = 'same'; $rootScope.$apply(); //change origin back to its previous value $rootScope.name = 'aaa'; $rootScope.$apply(); expect($rootScope.name).toBe('aaa'); expect(componentScope.owRef).toBe('aaa'); })); it('should not update origin when identity of isolate changes', inject(function() { $rootScope.name = {mark:123}; compile('
'); expect($rootScope.name).toEqual({mark:123}); expect(componentScope.owRef).toBe($rootScope.name); expect(componentScope.owRefAlias).toBe($rootScope.name); expect(componentScope.$owRefAlias).toBe($rootScope.name); componentScope.owRef = 'martin'; $rootScope.$apply(); expect($rootScope.name).toEqual({mark: 123}); expect(componentScope.owRef).toBe('martin'); expect(componentScope.owRefAlias).toEqual({mark: 123}); expect(componentScope.$owRefAlias).toEqual({mark: 123}); })); it('should update origin when property of isolate object reference changes', inject(function() { $rootScope.obj = {mark:123}; compile('
'); expect($rootScope.obj).toEqual({mark:123}); expect(componentScope.owRef).toBe($rootScope.obj); componentScope.owRef.mark = 789; $rootScope.$apply(); expect($rootScope.obj).toEqual({mark: 789}); expect(componentScope.owRef).toBe($rootScope.obj); })); it('should not throw on non assignable expressions in the parent', inject(function() { compile('
'); $rootScope.name = 'world'; $rootScope.$apply(); expect(componentScope.owRef).toBe('hello world'); componentScope.owRef = 'ignore me'; expect(componentScope.owRef).toBe('ignore me'); expect($rootScope.name).toBe('world'); $rootScope.name = 'misko'; $rootScope.$apply(); expect(componentScope.owRef).toBe('hello misko'); })); it('should not throw when assigning to undefined', inject(function() { compile('
'); expect(componentScope.owRef).toBeUndefined(); componentScope.owRef = 'ignore me'; expect(componentScope.owRef).toBe('ignore me'); $rootScope.$apply(); expect(componentScope.owRef).toBe('ignore me'); })); it('should update isolate scope when "<"-bound NaN changes', inject(function() { $rootScope.num = NaN; compile('
'); var isolateScope = element.isolateScope(); expect(isolateScope.owRef).toBeNaN(); $rootScope.num = 64; $rootScope.$apply(); expect(isolateScope.owRef).toBe(64); })); describe('literal objects', function() { it('should copy parent changes', inject(function() { compile('
'); $rootScope.name = 'a'; $rootScope.$apply(); expect(componentScope.owRef).toEqual({name: 'a'}); $rootScope.name = 'b'; $rootScope.$apply(); expect(componentScope.owRef).toEqual({name: 'b'}); })); it('should not change the isolated scope when origin does not change', inject(function() { compile('
'); $rootScope.name = 'a'; $rootScope.$apply(); var lastComponentValue = componentScope.owRef; $rootScope.$apply(); expect(componentScope.owRef).toBe(lastComponentValue); })); it('should watch input values to array literals', inject(function() { $rootScope.name = 'georgios'; $rootScope.obj = {name: 'pete'}; compile('
'); expect(componentScope.owRef).toEqual([{name: 'georgios'}, {name: 'pete'}]); $rootScope.name = 'lucas'; $rootScope.obj = {name: 'martin'}; $rootScope.$apply(); expect(componentScope.owRef).toEqual([{name: 'lucas'}, {name: 'martin'}]); })); it('should watch input values object literals', inject(function() { $rootScope.name = 'georgios'; $rootScope.obj = {name: 'pete'}; compile('
'); expect(componentScope.owRef).toEqual({name: 'georgios', item: {name: 'pete'}}); $rootScope.name = 'lucas'; $rootScope.obj = {name: 'martin'}; $rootScope.$apply(); expect(componentScope.owRef).toEqual({name: 'lucas', item: {name: 'martin'}}); })); // https://github.com/angular/angular.js/issues/15833 it('should work with ng-model inputs', function() { var componentScope; module(function($compileProvider) { $compileProvider.directive('undi', function() { return { restrict: 'A', scope: { undi: '<' }, link: function($scope) { componentScope = $scope; } }; }); }); inject(function($compile, $rootScope) { element = $compile('
')($rootScope); $rootScope.$apply(); expect(componentScope.undi).toBeDefined(); }); }); it('should not complain when the isolated scope changes', inject(function() { compile('
'); $rootScope.name = 'a'; $rootScope.$apply(); componentScope.owRef = {name: 'b'}; componentScope.$apply(); expect(componentScope.owRef).toEqual({name: 'b'}); expect($rootScope.name).toBe('a'); $rootScope.name = 'c'; $rootScope.$apply(); expect(componentScope.owRef).toEqual({name: 'c'}); })); it('should work for primitive literals', inject(function() { test('1', 1); test('null', null); test('undefined', undefined); test('\'someString\'', 'someString'); test('true', true); function test(literalString, literalValue) { compile('
'); expect(componentScope.owRef).toBe(literalValue); dealoc(element); } })); describe('optional one-way binding', function() { it('should update local when origin changes', inject(function() { compile('
'); expect(componentScope.owOptref).toBeUndefined(); expect(componentScope.owOptrefAlias).toBe(componentScope.owOptref); expect(componentScope.$owOptrefAlias).toBe(componentScope.owOptref); $rootScope.name = 'misko'; $rootScope.$apply(); expect(componentScope.owOptref).toBe($rootScope.name); expect(componentScope.owOptrefAlias).toBe($rootScope.name); expect(componentScope.$owOptrefAlias).toBe($rootScope.name); $rootScope.name = {}; $rootScope.$apply(); expect(componentScope.owOptref).toBe($rootScope.name); expect(componentScope.owOptrefAlias).toBe($rootScope.name); expect(componentScope.$owOptrefAlias).toBe($rootScope.name); })); it('should not throw exception when reference does not exist', inject(function() { compile('
'); expect(componentScope.owOptref).toBeUndefined(); expect(componentScope.owOptrefAlias).toBeUndefined(); expect(componentScope.$owOptrefAlias).toBeUndefined(); })); }); }); }); describe('one-way collection bindings', function() { it('should update isolate scope when origin scope changes', inject(function() { $rootScope.collection = [{ name: 'Gabriel', value: 18 }, { name: 'Tony', value: 91 }]; $rootScope.query = ''; $rootScope.$apply(); compile('
'); expect(componentScope.owColref).toEqual($rootScope.collection); expect(componentScope.owColrefAlias).toEqual(componentScope.owColref); expect(componentScope.$owColrefAlias).toEqual(componentScope.owColref); $rootScope.query = 'Gab'; $rootScope.$apply(); expect(componentScope.owColref).toEqual([$rootScope.collection[0]]); expect(componentScope.owColrefAlias).toEqual([$rootScope.collection[0]]); expect(componentScope.$owColrefAlias).toEqual([$rootScope.collection[0]]); })); it('should not update isolate scope when deep state within origin scope changes', inject(function() { $rootScope.collection = [{ name: 'Gabriel', value: 18 }, { name: 'Tony', value: 91 }]; $rootScope.$apply(); compile('
'); expect(componentScope.owColref).toEqual($rootScope.collection); expect(componentScope.owColrefAlias).toEqual(componentScope.owColref); expect(componentScope.$owColrefAlias).toEqual(componentScope.owColref); componentScope.owColref = componentScope.owColrefAlias = componentScope.$owColrefAlias = undefined; $rootScope.collection[0].name = 'Joe'; $rootScope.$apply(); expect(componentScope.owColref).toBeUndefined(); expect(componentScope.owColrefAlias).toBeUndefined(); expect(componentScope.$owColrefAlias).toBeUndefined(); })); it('should update isolate scope when origin scope changes', inject(function() { $rootScope.gab = { name: 'Gabriel', value: 18 }; $rootScope.tony = { name: 'Tony', value: 91 }; $rootScope.query = ''; $rootScope.$apply(); compile('
'); expect(componentScope.owColref).toEqual([$rootScope.gab, $rootScope.tony]); expect(componentScope.owColrefAlias).toEqual([$rootScope.gab, $rootScope.tony]); expect(componentScope.$owColrefAlias).toEqual([$rootScope.gab, $rootScope.tony]); $rootScope.query = 'Gab'; $rootScope.$apply(); expect(componentScope.owColref).toEqual([$rootScope.gab]); expect(componentScope.owColrefAlias).toEqual([$rootScope.gab]); expect(componentScope.$owColrefAlias).toEqual([$rootScope.gab]); })); it('should update isolate scope when origin literal object content changes', inject(function() { $rootScope.gab = { name: 'Gabriel', value: 18 }; $rootScope.tony = { name: 'Tony', value: 91 }; $rootScope.$apply(); compile('
'); expect(componentScope.owColref).toEqual([$rootScope.gab, $rootScope.tony]); expect(componentScope.owColrefAlias).toEqual([$rootScope.gab, $rootScope.tony]); expect(componentScope.$owColrefAlias).toEqual([$rootScope.gab, $rootScope.tony]); $rootScope.tony = { name: 'Bob', value: 42 }; $rootScope.$apply(); expect(componentScope.owColref).toEqual([$rootScope.gab, $rootScope.tony]); expect(componentScope.owColrefAlias).toEqual([$rootScope.gab, $rootScope.tony]); expect(componentScope.$owColrefAlias).toEqual([$rootScope.gab, $rootScope.tony]); })); }); 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(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(componentScope.$exprAlias({offset: 10})).toEqual(23); expect($rootScope.count).toEqual(23); })); }); 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.mode).toBe('@'); expect(componentScope.$$isolateBindings.attr.attrName).toBe('attr'); expect(componentScope.$$isolateBindings.attrAlias.attrName).toBe('attr'); expect(componentScope.$$isolateBindings.$attrAlias.attrName).toBe('$attr$'); expect(componentScope.$$isolateBindings.ref.mode).toBe('='); expect(componentScope.$$isolateBindings.ref.attrName).toBe('ref'); expect(componentScope.$$isolateBindings.refAlias.attrName).toBe('ref'); expect(componentScope.$$isolateBindings.$refAlias.attrName).toBe('$ref$'); expect(componentScope.$$isolateBindings.reference.mode).toBe('='); expect(componentScope.$$isolateBindings.reference.attrName).toBe('reference'); expect(componentScope.$$isolateBindings.owRef.mode).toBe('<'); expect(componentScope.$$isolateBindings.owRef.attrName).toBe('owRef'); expect(componentScope.$$isolateBindings.owRefAlias.attrName).toBe('owRef'); expect(componentScope.$$isolateBindings.$owRefAlias.attrName).toBe('$owRef$'); expect(componentScope.$$isolateBindings.expr.mode).toBe('&'); expect(componentScope.$$isolateBindings.expr.attrName).toBe('expr'); expect(componentScope.$$isolateBindings.exprAlias.attrName).toBe('expr'); expect(componentScope.$$isolateBindings.$exprAlias.attrName).toBe('$expr$'); var firstComponentScope = componentScope, first$$isolateBindings = componentScope.$$isolateBindings; dealoc(element); compile('
'); expect(componentScope).not.toBe(firstComponentScope); expect(componentScope.$$isolateBindings).toBe(first$$isolateBindings); })); it('should expose isolate scope variables on controller with controllerAs when bindToController is true (template)', function() { var controllerCalled = false; module(function($compileProvider) { $compileProvider.directive('fooDir', valueFn({ template: '

isolate

', scope: { 'data': '=dirData', 'oneway': '
')($rootScope); expect(controllerCalled).toBe(true); }); }); it('should not pre-assign bound properties to the controller', function() { var controllerCalled = false, onInitCalled = false; module(function($compileProvider) { $compileProvider.directive('fooDir', valueFn({ template: '

isolate

', scope: { 'data': '=dirData', 'oneway': '
')($rootScope); expect(controllerCalled).toBe(true); expect(onInitCalled).toBe(true); }); }); it('should eventually expose isolate scope variables on ES6 class controller with controllerAs when bindToController is true', function() { if (!support.classes) return; var controllerCalled = false; // eslint-disable-next-line no-eval var Controller = eval('(\n' + 'class Foo {\n' + ' constructor($scope) {}\n' + ' $onInit() {\n' + ' expect(this.data).toEqualData({\n' + ' \'foo\': \'bar\',\n' + ' \'baz\': \'biz\'\n' + ' });\n' + ' expect(this.oneway).toEqualData({\n' + ' \'foo\': \'bar\',\n' + ' \'baz\': \'biz\'\n' + ' });\n' + ' expect(this.str).toBe(\'Hello, world!\');\n' + ' expect(this.fn()).toBe(\'called!\');\n' + ' controllerCalled = true;\n' + ' }\n' + '}\n' + ')'); spyOn(Controller.prototype, '$onInit').and.callThrough(); module(function($compileProvider) { $compileProvider.directive('fooDir', valueFn({ template: '

isolate

', scope: { 'data': '=dirData', 'oneway': '
')($rootScope); expect(Controller.prototype.$onInit).toHaveBeenCalled(); expect(controllerCalled).toBe(true); }); }); it('should update @-bindings on controller when bindToController and attribute change observed', function() { module(function($compileProvider) { $compileProvider.directive('atBinding', valueFn({ template: '

{{At.text}}

', scope: { text: '@atBinding' }, controller: function($scope) {}, bindToController: true, controllerAs: 'At' })); }); inject(function($compile, $rootScope) { element = $compile('
')($rootScope); var p = element.find('p'); $rootScope.$digest(); expect(p.text()).toBe('Test: '); $rootScope.text = 'Kittens'; $rootScope.$digest(); expect(p.text()).toBe('Test: Kittens'); }); }); it('should expose isolate scope variables on controller with controllerAs when bindToController is true (templateUrl)', function() { var controllerCalled = false; module(function($compileProvider) { $compileProvider.directive('fooDir', valueFn({ templateUrl: 'test.html', scope: { 'data': '=dirData', 'oneway': 'isolate

'); $rootScope.fn = valueFn('called!'); $rootScope.whom = 'world'; $rootScope.remoteData = { 'foo': 'bar', 'baz': 'biz' }; element = $compile('
')($rootScope); $rootScope.$digest(); expect(controllerCalled).toBe(true); }); }); it('should throw noctrl when missing controller', function() { module(function($compileProvider) { $compileProvider.directive('noCtrl', valueFn({ templateUrl: 'test.html', scope: { 'data': '=dirData', 'oneway': '')($rootScope); }).toThrowMinErr('$compile', 'noctrl', 'Cannot bind to controller without directive \'noCtrl\'s controller.'); }); }); it('should throw badrestrict on first compilation when restrict is invalid', function() { module(function($compileProvider, $exceptionHandlerProvider) { $compileProvider.directive('invalidRestrictBadString', valueFn({restrict: '"'})); $compileProvider.directive('invalidRestrictTrue', valueFn({restrict: true})); $compileProvider.directive('invalidRestrictObject', valueFn({restrict: {}})); $compileProvider.directive('invalidRestrictNumber', valueFn({restrict: 42})); // We need to test with the exceptionHandler not rethrowing... $exceptionHandlerProvider.mode('log'); }); inject(function($exceptionHandler, $compile, $rootScope) { $compile('
')($rootScope); expect($exceptionHandler.errors.length).toBe(1); expect($exceptionHandler.errors[0]).toMatch(/\$compile.*badrestrict.*'true'/); $compile('
')($rootScope); $compile('
')($rootScope); expect($exceptionHandler.errors.length).toBe(2); expect($exceptionHandler.errors[1]).toMatch(/\$compile.*badrestrict.*'"'/); $compile('
')($rootScope); expect($exceptionHandler.errors.length).toBe(3); expect($exceptionHandler.errors[2]).toMatch(/\$compile.*badrestrict.*'{}'/); $compile('
')($rootScope); expect($exceptionHandler.errors.length).toBe(4); expect($exceptionHandler.errors[3]).toMatch(/\$compile.*badrestrict.*'42'/); }); }); describe('should bind to controller via object notation', function() { var controllerOptions = [{ description: 'no controller identifier', controller: 'myCtrl' }, { description: '"Ctrl as ident" syntax', controller: 'myCtrl as myCtrl' }, { description: 'controllerAs setting', controller: 'myCtrl', controllerAs: 'myCtrl' }], scopeOptions = [{ description: 'isolate scope', scope: {} }, { description: 'new scope', scope: true }, { description: 'no scope', scope: false }], templateOptions = [{ description: 'inline template', template: '

template

' }, { description: 'templateUrl setting', templateUrl: 'test.html' }, { description: 'no template' }]; forEach(controllerOptions, function(controllerOption) { forEach(scopeOptions, function(scopeOption) { forEach(templateOptions, function(templateOption) { var description = [], ddo = { bindToController: { 'data': '=dirData', 'oneway': 'template

'); $rootScope.fn = valueFn('called!'); $rootScope.whom = 'world'; $rootScope.remoteData = { 'foo': 'bar', 'baz': 'biz' }; element = $compile('
')($rootScope); $rootScope.$digest(); expect(controllerCalled).toBe(true); if (ddo.controllerAs || ddo.controller.indexOf(' as ') !== -1) { if (ddo.scope) { expect($rootScope.myCtrl).toBeUndefined(); } else { // The controller identifier was added to the containing scope. expect($rootScope.myCtrl).toBeDefined(); } } }); }); }); }); }); }); it('should bind to multiple directives controllers via object notation (no scope)', function() { var controller1Called = false; var controller2Called = false; module(function($compileProvider, $controllerProvider) { $compileProvider.directive('foo', valueFn({ bindToController: { 'data': '=fooData', 'oneway': ' ' + '
')($rootScope); $rootScope.$digest(); expect(controller1Called).toBe(true); expect(controller2Called).toBe(true); }); }); it('should bind to multiple directives controllers via object notation (new iso scope)', function() { var controller1Called = false; var controller2Called = false; module(function($compileProvider, $controllerProvider) { $compileProvider.directive('foo', valueFn({ bindToController: { 'data': '=fooData', 'oneway': ' ' + '
')($rootScope); $rootScope.$digest(); expect(controller1Called).toBe(true); expect(controller2Called).toBe(true); }); }); it('should bind to multiple directives controllers via object notation (new scope)', function() { var controller1Called = false; var controller2Called = false; module(function($compileProvider, $controllerProvider) { $compileProvider.directive('foo', valueFn({ bindToController: { 'data': '=fooData', 'oneway': ' ' + '
')($rootScope); $rootScope.$digest(); expect(controller1Called).toBe(true); expect(controller2Called).toBe(true); }); }); it('should evaluate against the correct scope, when using `bindToController` (new scope)', function() { module(function($compileProvider, $controllerProvider) { $controllerProvider.register({ 'ParentCtrl': function() { this.value1 = 'parent1'; this.value2 = 'parent2'; this.value3 = function() { return 'parent3'; }; this.value4 = 'parent4'; }, 'ChildCtrl': function() { this.value1 = 'child1'; this.value2 = 'child2'; this.value3 = function() { return 'child3'; }; this.value4 = 'child4'; } }); $compileProvider.directive('child', valueFn({ scope: true, controller: 'ChildCtrl as ctrl', bindToController: { fromParent1: '@', fromParent2: '=', fromParent3: '&', fromParent4: '<' }, template: '' })); }); inject(function($compile, $rootScope) { element = $compile( '
' + '' + '' + '
')($rootScope); $rootScope.$digest(); var parentCtrl = element.controller('ngController'); var childCtrl = element.find('child').controller('child'); expect(childCtrl.fromParent1).toBe(parentCtrl.value1); expect(childCtrl.fromParent1).not.toBe(childCtrl.value1); expect(childCtrl.fromParent2).toBe(parentCtrl.value2); expect(childCtrl.fromParent2).not.toBe(childCtrl.value2); expect(childCtrl.fromParent3()()).toBe(parentCtrl.value3()); expect(childCtrl.fromParent3()()).not.toBe(childCtrl.value3()); expect(childCtrl.fromParent4).toBe(parentCtrl.value4); expect(childCtrl.fromParent4).not.toBe(childCtrl.value4); childCtrl.fromParent2 = 'modified'; $rootScope.$digest(); expect(parentCtrl.value2).toBe('modified'); expect(childCtrl.value2).toBe('child2'); }); } ); it('should evaluate against the correct scope, when using `bindToController` (new iso scope)', function() { module(function($compileProvider, $controllerProvider) { $controllerProvider.register({ 'ParentCtrl': function() { this.value1 = 'parent1'; this.value2 = 'parent2'; this.value3 = function() { return 'parent3'; }; this.value4 = 'parent4'; }, 'ChildCtrl': function() { this.value1 = 'child1'; this.value2 = 'child2'; this.value3 = function() { return 'child3'; }; this.value4 = 'child4'; } }); $compileProvider.directive('child', valueFn({ scope: {}, controller: 'ChildCtrl as ctrl', bindToController: { fromParent1: '@', fromParent2: '=', fromParent3: '&', fromParent4: '<' }, template: '' })); }); inject(function($compile, $rootScope) { element = $compile( '
' + '' + '' + '
')($rootScope); $rootScope.$digest(); var parentCtrl = element.controller('ngController'); var childCtrl = element.find('child').controller('child'); expect(childCtrl.fromParent1).toBe(parentCtrl.value1); expect(childCtrl.fromParent1).not.toBe(childCtrl.value1); expect(childCtrl.fromParent2).toBe(parentCtrl.value2); expect(childCtrl.fromParent2).not.toBe(childCtrl.value2); expect(childCtrl.fromParent3()()).toBe(parentCtrl.value3()); expect(childCtrl.fromParent3()()).not.toBe(childCtrl.value3()); expect(childCtrl.fromParent4).toBe(parentCtrl.value4); expect(childCtrl.fromParent4).not.toBe(childCtrl.value4); childCtrl.fromParent2 = 'modified'; $rootScope.$digest(); expect(parentCtrl.value2).toBe('modified'); expect(childCtrl.value2).toBe('child2'); }); } ); it('should put controller in scope when controller identifier present but not using controllerAs', function() { var controllerCalled = false; var myCtrl; module(function($compileProvider, $controllerProvider) { $controllerProvider.register('myCtrl', function() { controllerCalled = true; myCtrl = this; }); $compileProvider.directive('fooDir', valueFn({ templateUrl: 'test.html', bindToController: {}, scope: true, controller: 'myCtrl as theCtrl' })); }); inject(function($compile, $rootScope, $templateCache) { $templateCache.put('test.html', '

isolate

'); element = $compile('
')($rootScope); $rootScope.$digest(); expect(controllerCalled).toBe(true); var childScope = element.children().scope(); expect(childScope).not.toBe($rootScope); expect(childScope.theCtrl).toBe(myCtrl); }); }); it('should re-install controllerAs and bindings for returned value from controller (new scope)', function() { var controllerCalled = false; var myCtrl; function MyCtrl() { } MyCtrl.prototype.test = function() { expect(this.data).toEqualData({ 'foo': 'bar', 'baz': 'biz' }); expect(this.oneway).toEqualData({ 'foo': 'bar', 'baz': 'biz' }); expect(this.str).toBe('Hello, world!'); expect(this.fn()).toBe('called!'); }; module(function($compileProvider, $controllerProvider) { $controllerProvider.register('myCtrl', function() { controllerCalled = true; myCtrl = this; return new MyCtrl(); }); $compileProvider.directive('fooDir', valueFn({ templateUrl: 'test.html', bindToController: { 'data': '=dirData', 'oneway': 'isolate

'); $rootScope.fn = valueFn('called!'); $rootScope.whom = 'world'; $rootScope.remoteData = { 'foo': 'bar', 'baz': 'biz' }; element = $compile('
')($rootScope); $rootScope.$digest(); expect(controllerCalled).toBe(true); var childScope = element.children().scope(); expect(childScope).not.toBe($rootScope); expect(childScope.theCtrl).not.toBe(myCtrl); expect(childScope.theCtrl.constructor).toBe(MyCtrl); childScope.theCtrl.test(); }); }); it('should re-install controllerAs and bindings for returned value from controller (isolate scope)', function() { var controllerCalled = false; var myCtrl; function MyCtrl() { } MyCtrl.prototype.test = function() { expect(this.data).toEqualData({ 'foo': 'bar', 'baz': 'biz' }); expect(this.oneway).toEqualData({ 'foo': 'bar', 'baz': 'biz' }); expect(this.str).toBe('Hello, world!'); expect(this.fn()).toBe('called!'); }; module(function($compileProvider, $controllerProvider) { $controllerProvider.register('myCtrl', function() { controllerCalled = true; myCtrl = this; return new MyCtrl(); }); $compileProvider.directive('fooDir', valueFn({ templateUrl: 'test.html', bindToController: true, scope: { 'data': '=dirData', 'oneway': 'isolate

'); $rootScope.fn = valueFn('called!'); $rootScope.whom = 'world'; $rootScope.remoteData = { 'foo': 'bar', 'baz': 'biz' }; element = $compile('
')($rootScope); $rootScope.$digest(); expect(controllerCalled).toBe(true); var childScope = element.children().scope(); expect(childScope).not.toBe($rootScope); expect(childScope.theCtrl).not.toBe(myCtrl); expect(childScope.theCtrl.constructor).toBe(MyCtrl); childScope.theCtrl.test(); }); }); describe('should not overwrite @-bound property each digest when not present', function() { it('when creating new scope', function() { module(function($compileProvider) { $compileProvider.directive('testDir', valueFn({ scope: true, bindToController: { prop: '@' }, controller: function() { var self = this; this.$onInit = function() { this.prop = this.prop || 'default'; }; this.getProp = function() { return self.prop; }; }, controllerAs: 'ctrl', template: '

' })); }); inject(function($compile, $rootScope) { element = $compile('
')($rootScope); var scope = element.scope(); expect(scope.ctrl.getProp()).toBe('default'); $rootScope.$digest(); expect(scope.ctrl.getProp()).toBe('default'); }); }); it('when creating isolate scope', function() { module(function($compileProvider) { $compileProvider.directive('testDir', valueFn({ scope: {}, bindToController: { prop: '@' }, controller: function() { var self = this; this.$onInit = function() { this.prop = this.prop || 'default'; }; this.getProp = function() { return self.prop; }; }, controllerAs: 'ctrl', template: '

' })); }); inject(function($compile, $rootScope) { element = $compile('
')($rootScope); var scope = element.isolateScope(); expect(scope.ctrl.getProp()).toBe('default'); $rootScope.$digest(); expect(scope.ctrl.getProp()).toBe('default'); }); }); }); }); describe('require', 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 respect explicit return value from controller', function() { var expectedController; module(function() { directive('logControllerProp', function(log) { return { controller: function($scope) { this.foo = 'baz'; // value should not be used. expectedController = {foo: 'bar'}; return expectedController; }, link: function(scope, element, attrs, controller) { expect(expectedController).toBeDefined(); expect(controller).toBe(expectedController); expect(controller.foo).toBe('bar'); log('done'); } }; }); }); inject(function(log, $compile, $rootScope) { element = $compile('')($rootScope); expect(log).toEqual('done'); expect(element.data('$logControllerPropController')).toBe(expectedController); }); }); it('should get explicit return value of required parent controller', function() { var expectedController; module(function() { directive('nested', function(log) { return { require: '^^?nested', controller: function() { if (!expectedController) expectedController = {foo: 'bar'}; return expectedController; }, link: function(scope, element, attrs, controller) { if (element.parent().length) { expect(expectedController).toBeDefined(); expect(controller).toBe(expectedController); expect(controller.foo).toBe('bar'); log('done'); } } }; }); }); inject(function(log, $compile, $rootScope) { element = $compile('
')($rootScope); expect(log).toEqual('done'); expect(element.data('$nestedController')).toBe(expectedController); }); }); it('should respect explicit controller return value when using controllerAs', function() { module(function() { directive('main', function() { return { templateUrl: 'main.html', scope: {}, controller: function() { this.name = 'lucas'; return {name: 'george'}; }, controllerAs: 'mainCtrl' }; }); }); inject(function($templateCache, $compile, $rootScope) { $templateCache.put('main.html', 'template:{{mainCtrl.name}}'); element = $compile('
')($rootScope); $rootScope.$apply(); expect(element.text()).toBe('template:george'); }); }); it('transcluded children should receive explicit return value of parent controller', function() { var expectedController; module(function() { directive('nester', valueFn({ transclude: true, controller: function($transclude) { this.foo = 'baz'; expectedController = {transclude:$transclude, foo: 'bar'}; return expectedController; }, link: function(scope, el, attr, ctrl) { ctrl.transclude(cloneAttach); function cloneAttach(clone) { el.append(clone); } } })); directive('nested', function(log) { return { require: '^^nester', link: function(scope, element, attrs, controller) { expect(controller).toBeDefined(); expect(controller).toBe(expectedController); log('done'); } }; }); }); inject(function(log, $compile) { element = $compile('
')($rootScope); $rootScope.$apply(); expect(log.toString()).toBe('done'); expect(element.data('$nesterController')).toBe(expectedController); }); }); it('explicit controller return values are ignored if they are primitives', function() { module(function() { directive('logControllerProp', function(log) { return { controller: function($scope) { this.foo = 'baz'; // value *will* be used. return 'bar'; }, link: function(scope, element, attrs, controller) { log(controller.foo); } }; }); }); inject(function(log, $compile, $rootScope) { element = $compile('')($rootScope); expect(log).toEqual('baz'); expect(element.data('$logControllerPropController').foo).toEqual('baz'); }); }); it('should correctly assign controller return values for multiple directives', function() { var directiveController, otherDirectiveController; module(function() { directive('myDirective', function(log) { return { scope: true, controller: function($scope) { directiveController = { foo: 'bar' }; return directiveController; } }; }); directive('myOtherDirective', function(log) { return { controller: function($scope) { otherDirectiveController = { baz: 'luh' }; return otherDirectiveController; } }; }); }); inject(function(log, $compile, $rootScope) { element = $compile('')($rootScope); expect(element.data('$myDirectiveController')).toBe(directiveController); expect(element.data('$myOtherDirectiveController')).toBe(otherDirectiveController); }); }); it('should get required parent controller', function() { module(function() { directive('nested', function(log) { return { require: '^^?nested', controller: function($scope) {}, link: function(scope, element, attrs, controller) { log(!!controller); } }; }); }); inject(function(log, $compile, $rootScope) { element = $compile('
')($rootScope); expect(log).toEqual('true; false'); }); }); it('should get required parent controller when the question mark precedes the ^^', function() { module(function() { directive('nested', function(log) { return { require: '?^^nested', controller: function($scope) {}, link: function(scope, element, attrs, controller) { log(!!controller); } }; }); }); inject(function(log, $compile, $rootScope) { element = $compile('
')($rootScope); expect(log).toEqual('true; false'); }); }); it('should throw if required parent is not found', function() { module(function() { directive('nested', function() { return { require: '^^nested', controller: function($scope) {}, link: function(scope, element, attrs, controller) {} }; }); }); inject(function($compile, $rootScope) { expect(function() { element = $compile('
')($rootScope); }).toThrowMinErr('$compile', 'ctreq', 'Controller \'nested\', required by directive \'nested\', can\'t be found!'); }); }); 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 bind the required controllers to the directive controller, if provided as an object and bindToController is truthy', function() { var parentController, siblingController; function ParentController() { this.name = 'Parent'; } function SiblingController() { this.name = 'Sibling'; } function MeController() { this.name = 'Me'; } MeController.prototype.$onInit = function() { parentController = this.container; siblingController = this.friend; }; spyOn(MeController.prototype, '$onInit').and.callThrough(); angular.module('my', []) .directive('me', function() { return { restrict: 'E', scope: {}, require: { container: '^parent', friend: 'sibling' }, bindToController: true, controller: MeController, controllerAs: '$ctrl' }; }) .directive('parent', function() { return { restrict: 'E', scope: {}, controller: ParentController }; }) .directive('sibling', function() { return { controller: SiblingController }; }); module('my'); inject(function($compile, $rootScope, meDirective) { element = $compile('')($rootScope); expect(MeController.prototype.$onInit).toHaveBeenCalled(); expect(parentController).toEqual(jasmine.any(ParentController)); expect(siblingController).toEqual(jasmine.any(SiblingController)); }); }); it('should use the key if the name of a required controller is omitted', function() { function ParentController() { this.name = 'Parent'; } function ParentOptController() { this.name = 'ParentOpt'; } function ParentOrSiblingController() { this.name = 'ParentOrSibling'; } function ParentOrSiblingOptController() { this.name = 'ParentOrSiblingOpt'; } function SiblingController() { this.name = 'Sibling'; } function SiblingOptController() { this.name = 'SiblingOpt'; } angular.module('my', []) .component('me', { require: { parent: '^^', parentOpt: '?^^', parentOrSibling1: '^', parentOrSiblingOpt1: '?^', parentOrSibling2: '^', parentOrSiblingOpt2: '?^', sibling: '', siblingOpt: '?' } }) .directive('parent', function() { return {controller: ParentController}; }) .directive('parentOpt', function() { return {controller: ParentOptController}; }) .directive('parentOrSibling1', function() { return {controller: ParentOrSiblingController}; }) .directive('parentOrSiblingOpt1', function() { return {controller: ParentOrSiblingOptController}; }) .directive('parentOrSibling2', function() { return {controller: ParentOrSiblingController}; }) .directive('parentOrSiblingOpt2', function() { return {controller: ParentOrSiblingOptController}; }) .directive('sibling', function() { return {controller: SiblingController}; }) .directive('siblingOpt', function() { return {controller: SiblingOptController}; }); module('my'); inject(function($compile, $rootScope) { var template = '
' + // With optional '' + '' + '' + // Without optional '' + '' + '' + '
'; element = $compile(template)($rootScope); var ctrl1 = element.find('me').eq(0).controller('me'); expect(ctrl1.parent).toEqual(jasmine.any(ParentController)); expect(ctrl1.parentOpt).toEqual(jasmine.any(ParentOptController)); expect(ctrl1.parentOrSibling1).toEqual(jasmine.any(ParentOrSiblingController)); expect(ctrl1.parentOrSiblingOpt1).toEqual(jasmine.any(ParentOrSiblingOptController)); expect(ctrl1.parentOrSibling2).toEqual(jasmine.any(ParentOrSiblingController)); expect(ctrl1.parentOrSiblingOpt2).toEqual(jasmine.any(ParentOrSiblingOptController)); expect(ctrl1.sibling).toEqual(jasmine.any(SiblingController)); expect(ctrl1.siblingOpt).toEqual(jasmine.any(SiblingOptController)); var ctrl2 = element.find('me').eq(1).controller('me'); expect(ctrl2.parent).toEqual(jasmine.any(ParentController)); expect(ctrl2.parentOpt).toBe(null); expect(ctrl2.parentOrSibling1).toEqual(jasmine.any(ParentOrSiblingController)); expect(ctrl2.parentOrSiblingOpt1).toBe(null); expect(ctrl2.parentOrSibling2).toEqual(jasmine.any(ParentOrSiblingController)); expect(ctrl2.parentOrSiblingOpt2).toBe(null); expect(ctrl2.sibling).toEqual(jasmine.any(SiblingController)); expect(ctrl2.siblingOpt).toBe(null); }); }); it('should not bind required controllers if bindToController is falsy', function() { var parentController, siblingController; function ParentController() { this.name = 'Parent'; } function SiblingController() { this.name = 'Sibling'; } function MeController() { this.name = 'Me'; } MeController.prototype.$onInit = function() { parentController = this.container; siblingController = this.friend; }; spyOn(MeController.prototype, '$onInit').and.callThrough(); angular.module('my', []) .directive('me', function() { return { restrict: 'E', scope: {}, require: { container: '^parent', friend: 'sibling' }, controller: MeController }; }) .directive('parent', function() { return { restrict: 'E', scope: {}, controller: ParentController }; }) .directive('sibling', function() { return { controller: SiblingController }; }); module('my'); inject(function($compile, $rootScope, meDirective) { element = $compile('')($rootScope); expect(MeController.prototype.$onInit).toHaveBeenCalled(); expect(parentController).toBeUndefined(); expect(siblingController).toBeUndefined(); }); }); it('should bind required controllers to controller that has an explicit constructor return value', function() { var parentController, siblingController, meController; function ParentController() { this.name = 'Parent'; } function SiblingController() { this.name = 'Sibling'; } function MeController() { meController = { name: 'Me', $onInit: function() { parentController = this.container; siblingController = this.friend; } }; spyOn(meController, '$onInit').and.callThrough(); return meController; } angular.module('my', []) .directive('me', function() { return { restrict: 'E', scope: {}, require: { container: '^parent', friend: 'sibling' }, bindToController: true, controller: MeController, controllerAs: '$ctrl' }; }) .directive('parent', function() { return { restrict: 'E', scope: {}, controller: ParentController }; }) .directive('sibling', function() { return { controller: SiblingController }; }); module('my'); inject(function($compile, $rootScope, meDirective) { element = $compile('')($rootScope); expect(meController.$onInit).toHaveBeenCalled(); expect(parentController).toEqual(jasmine.any(ParentController)); expect(siblingController).toEqual(jasmine.any(SiblingController)); }); }); it('should bind required controllers to controllers that return an explicit constructor return value', function() { var parentController, containerController, siblingController, friendController, meController; function MeController() { this.name = 'Me'; this.$onInit = function() { containerController = this.container; friendController = this.friend; }; } function ParentController() { parentController = { name: 'Parent' }; return parentController; } function SiblingController() { siblingController = { name: 'Sibling' }; return siblingController; } angular.module('my', []) .directive('me', function() { return { priority: 1, // make sure it is run before sibling to test this case correctly restrict: 'E', scope: {}, require: { container: '^parent', friend: 'sibling' }, bindToController: true, controller: MeController, controllerAs: '$ctrl' }; }) .directive('parent', function() { return { restrict: 'E', scope: {}, controller: ParentController }; }) .directive('sibling', function() { return { controller: SiblingController }; }); module('my'); inject(function($compile, $rootScope, meDirective) { element = $compile('')($rootScope); expect(containerController).toEqual(parentController); expect(friendController).toEqual(siblingController); }); }); 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 pass null if required controller can\'t be found and is optional',function() { module(function() { directive('dep', function(log) { return { require: '?^main', link: function(scope, element, attrs, controller) { log('dep:' + controller); } }; }); }); inject(function(log, $compile, $rootScope) { $compile('
')($rootScope); expect(log).toEqual('dep:null'); }); }); it('should pass null if required controller can\'t be found and is optional with the question mark on the right',function() { module(function() { directive('dep', function(log) { return { require: '^?main', link: function(scope, element, attrs, controller) { log('dep:' + controller); } }; }); }); inject(function(log, $compile, $rootScope) { $compile('
')($rootScope); expect(log).toEqual('dep:null'); }); }); 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 support multiple controllers as an object hash', 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: { myC1: '^c1', myC2: '^c2' }, link: function(scope, element, attrs, controllers) { log('dep:' + controllers.myC1.name + '-' + controllers.myC2.name); } }; }); }); inject(function(log, $compile, $rootScope) { element = $compile('
')($rootScope); expect(log).toEqual('dep:c1-c2'); }); }); it('should support omitting the name of the required controller if it is the same as the key', function() { module(function() { directive('myC1', valueFn({ controller: function() { this.name = 'c1'; } })); directive('myC2', valueFn({ controller: function() { this.name = 'c2'; } })); directive('dep', function(log) { return { require: { myC1: '^', myC2: '^' }, link: function(scope, element, attrs, controllers) { log('dep:' + controllers.myC1.name + '-' + controllers.myC2.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 transclusion, 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('
childContentText;
')($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 transclusion, 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'); }); }); it('should throw ctreq with correct directive name, regardless of order', function() { module(function($compileProvider) { $compileProvider.directive('aDir', valueFn({ restrict: 'E', require: 'ngModel', link: noop })); }); inject(function($compile, $rootScope) { expect(function() { // a-dir will cause a ctreq error to be thrown. Previously, the error would reference // the last directive in the chain (which in this case would be ngClick), based on // priority and alphabetical ordering. This test verifies that the ordering does not // affect which directive is referenced in the minErr message. element = $compile('')($rootScope); }).toThrowMinErr('$compile', 'ctreq', 'Controller \'ngModel\', required by directive \'aDir\', can\'t be found!'); }); }); }); describe('transclude', function() { describe('content transclusion', function() { it('should support transclude directive', function() { module(function() { directive('trans', function() { return { transclude: 'content', replace: true, scope: {}, link: function(scope) { scope.x = 'iso'; }, template: '
  • W:{{x}}-{{$parent.$id}}-{{$id}};
' }; }); }); inject(function(log, $rootScope, $compile) { element = $compile('
T:{{x}}-{{$parent.$id}}-{{$id}};
')($rootScope); $rootScope.x = 'root'; $rootScope.$apply(); expect(element.text()).toEqual('W:iso-1-2;T:root-2-3;'); expect(jqLite(jqLite(element.find('li')[1]).contents()[0]).text()).toEqual('T:root-2-3'); expect(jqLite(element.find('span')[0]).text()).toEqual(';'); }); }); it('should transclude transcluded content', function() { module(function() { directive('book', valueFn({ transclude: 'content', template: '
book-
(
)
' })); directive('chapter', valueFn({ transclude: 'content', templateUrl: 'chapter.html' })); directive('section', valueFn({ transclude: 'content', template: '
section-!
!
' })); return function($httpBackend) { $httpBackend. expect('GET', 'chapter.html'). respond('
chapter-
[
]
'); }; }); inject(function(log, $rootScope, $compile, $httpBackend) { element = $compile('
paragraph
')($rootScope); $rootScope.$apply(); expect(element.text()).toEqual('book-'); $httpBackend.flush(); $rootScope.$apply(); expect(element.text()).toEqual('book-chapter-section-![(paragraph)]!'); }); }); it('should compile directives with lower priority than ngTransclude', function() { var ngTranscludePriority; var lowerPriority = -1; module(function($provide) { $provide.decorator('ngTranscludeDirective', function($delegate) { ngTranscludePriority = $delegate[0].priority; return $delegate; }); directive('lower', function(log) { return { priority: lowerPriority, link: { pre: function() { log('pre'); }, post: function() { log('post'); } } }; }); directive('trans', function(log) { return { transclude: true, template: '
' }; }); }); inject(function(log, $rootScope, $compile) { element = $compile('
transcluded content
')($rootScope); expect(lowerPriority).toBeLessThan(ngTranscludePriority); $rootScope.$apply(); expect(element.text()).toEqual('transcluded content'); expect(log).toEqual('pre; post'); }); }); it('should not merge text elements from transcluded content', function() { module(function() { directive('foo', valueFn({ transclude: 'content', template: '
This is before {{before}}.
', link: function(scope, element, attr, ctrls, $transclude) { var futureParent = element.children().eq(0); $transclude(function(clone) { futureParent.append(clone); }, futureParent); }, scope: true })); }); inject(function($rootScope, $compile) { element = $compile('
This is after {{after}}
')($rootScope); $rootScope.before = 'BEFORE'; $rootScope.after = 'AFTER'; $rootScope.$apply(); expect(element.text()).toEqual('This is before BEFORE. This is after AFTER'); $rootScope.before = 'Not-Before'; $rootScope.after = 'AfTeR'; $rootScope.$$childHead.before = 'BeFoRe'; $rootScope.$$childHead.after = 'Not-After'; $rootScope.$apply(); expect(element.text()).toEqual('This is before BeFoRe. This is after AfTeR'); }); }); 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:
]', transclude: true })); directive('bar', valueFn({ template: '[
|
]', transclude: { header: 'header', footer: 'footer' } })); }); inject(function($compile, $rootScope) { var tmplWithFoo = '' + '
Hello,
' + '
world!
' + '
'; var tmplWithBar = '' + '
This is a
' + '
header!
' + '
This is a
' + '
footer!
' + '
'; var elem1 = $compile(tmplWithFoo)($rootScope); var elem2 = $compile(tmplWithBar)($rootScope); $rootScope.$digest(); expect(elem1.text()).toBe('[Hello, world!]'); expect(elem2.text()).toBe('[This is a header!|This is a footer!]'); dealoc(elem1); dealoc(elem2); }); }); //see issue https://github.com/angular/angular.js/issues/12936 it('should use the proper scope when it is on the root element of a replaced directive template', function() { module(function() { directive('isolate', valueFn({ scope: {}, replace: true, template: '
{{x}}
', link: function(scope, element, attr, ctrl) { scope.x = 'iso'; } })); directive('trans', valueFn({ transclude: 'content', link: function(scope, element, attr, ctrl, $transclude) { $transclude(function(clone) { element.append(clone); }); } })); }); inject(function($rootScope, $compile) { element = $compile('')($rootScope); $rootScope.x = 'root'; $rootScope.$apply(); expect(element.text()).toEqual('iso'); }); }); //see issue https://github.com/angular/angular.js/issues/12936 it('should use the proper scope when it is on the root element of a replaced directive template with child scope', function() { module(function() { directive('child', valueFn({ scope: true, replace: true, template: '
{{x}}
', link: function(scope, element, attr, ctrl) { scope.x = 'child'; } })); directive('trans', valueFn({ transclude: 'content', link: function(scope, element, attr, ctrl, $transclude) { $transclude(function(clone) { element.append(clone); }); } })); }); inject(function($rootScope, $compile) { element = $compile('')($rootScope); $rootScope.x = 'root'; $rootScope.$apply(); expect(element.text()).toEqual('child'); }); }); it('should throw if a transcluded node is transcluded again', function() { module(function() { directive('trans', valueFn({ transclude: true, link: function(scope, element, attr, ctrl, $transclude) { $transclude(); $transclude(); } })); }); inject(function($rootScope, $compile) { expect(function() { $compile('')($rootScope); }).toThrowMinErr('$compile', 'multilink', 'This element has already been linked.'); }); }); it('should not leak if two "element" transclusions are on the same element (with debug info)', function() { if (jQuery) { // jQuery 2.x doesn't expose the cache storage. return; } module(function($compileProvider) { $compileProvider.debugInfoEnabled(true); }); inject(function($compile, $rootScope) { var cacheSize = jqLiteCacheSize(); element = $compile('
{{x}}
')($rootScope); expect(jqLiteCacheSize()).toEqual(cacheSize + 1); $rootScope.$apply('xs = [0,1]'); expect(jqLiteCacheSize()).toEqual(cacheSize + 2); $rootScope.$apply('xs = [0]'); expect(jqLiteCacheSize()).toEqual(cacheSize + 1); $rootScope.$apply('xs = []'); expect(jqLiteCacheSize()).toEqual(cacheSize + 1); element.remove(); expect(jqLiteCacheSize()).toEqual(cacheSize + 0); }); }); it('should not leak if two "element" transclusions are on the same element (without debug info)', function() { if (jQuery) { // jQuery 2.x doesn't expose the cache storage. return; } module(function($compileProvider) { $compileProvider.debugInfoEnabled(false); }); inject(function($compile, $rootScope) { var cacheSize = jqLiteCacheSize(); element = $compile('
{{x}}
')($rootScope); expect(jqLiteCacheSize()).toEqual(cacheSize); $rootScope.$apply('xs = [0,1]'); expect(jqLiteCacheSize()).toEqual(cacheSize); $rootScope.$apply('xs = [0]'); expect(jqLiteCacheSize()).toEqual(cacheSize); $rootScope.$apply('xs = []'); expect(jqLiteCacheSize()).toEqual(cacheSize); element.remove(); expect(jqLiteCacheSize()).toEqual(cacheSize); }); }); it('should not leak if two "element" transclusions are on the same element (with debug info)', function() { if (jQuery) { // jQuery 2.x doesn't expose the cache storage. return; } module(function($compileProvider) { $compileProvider.debugInfoEnabled(true); }); inject(function($compile, $rootScope) { var cacheSize = jqLiteCacheSize(); element = $compile('
{{x}}
')($rootScope); $rootScope.$apply('xs = [0,1]'); // At this point we have a bunch of comment placeholders but no real transcluded elements // So the cache only contains the root element's data expect(jqLiteCacheSize()).toEqual(cacheSize + 1); $rootScope.$apply('val = true'); // Now we have two concrete transcluded elements plus some comments so two more cache items expect(jqLiteCacheSize()).toEqual(cacheSize + 3); $rootScope.$apply('val = false'); // Once again we only have comments so no transcluded elements and the cache is back to just // the root element expect(jqLiteCacheSize()).toEqual(cacheSize + 1); element.remove(); // Now we've even removed the root element along with its cache expect(jqLiteCacheSize()).toEqual(cacheSize + 0); }); }); it('should not leak when continuing the compilation of elements on a scope that was destroyed', function() { if (jQuery) { // jQuery 2.x doesn't expose the cache storage. return; } var linkFn = jasmine.createSpy('linkFn'); module(function($controllerProvider, $compileProvider) { $controllerProvider.register('Leak', function($scope, $timeout) { $scope.code = 'red'; $timeout(function() { $scope.code = 'blue'; }); }); $compileProvider.directive('isolateRed', function() { return { restrict: 'A', scope: {}, template: '
' }; }); $compileProvider.directive('red', function() { return { restrict: 'A', templateUrl: 'red.html', scope: {}, link: linkFn }; }); }); inject(function($compile, $rootScope, $httpBackend, $timeout, $templateCache) { var cacheSize = jqLiteCacheSize(); $httpBackend.whenGET('red.html').respond('

red.html

'); var template = $compile( '
' + '
' + '
' + '
' + '
' + '
' + '
'); element = template($rootScope, noop); $rootScope.$digest(); $timeout.flush(); $httpBackend.flush(); expect(linkFn).not.toHaveBeenCalled(); expect(jqLiteCacheSize()).toEqual(cacheSize + 2); $templateCache.removeAll(); var destroyedScope = $rootScope.$new(); destroyedScope.$destroy(); var clone = template(destroyedScope, noop); $rootScope.$digest(); $timeout.flush(); expect(linkFn).not.toHaveBeenCalled(); clone.remove(); }); }); describe('cleaning up after a replaced element', function() { var $compile, xs; beforeEach(inject(function(_$compile_) { $compile = _$compile_; xs = [0, 1]; })); function testCleanup() { var privateData, firstRepeatedElem; element = $compile('
{{x}}
')($rootScope); $rootScope.$apply('xs = [' + xs + ']'); firstRepeatedElem = element.children('.ng-scope').eq(0); expect(firstRepeatedElem.data('$scope')).toBeDefined(); privateData = jqLite._data(firstRepeatedElem[0]); expect(privateData.events).toBeDefined(); expect(privateData.events.click).toBeDefined(); expect(privateData.events.click[0]).toBeDefined(); // Ensure the AngularJS $destroy event is still sent var destroyCount = 0; element.find('div').on('$destroy', function() { destroyCount++; }); $rootScope.$apply('xs = null'); expect(destroyCount).toBe(2); expect(firstRepeatedElem.data('$scope')).not.toBeDefined(); privateData = jqLite._data(firstRepeatedElem[0]); expect(privateData && privateData.events).not.toBeDefined(); } it('should work without external libraries (except jQuery)', testCleanup); it('should work with another library patching jqLite/jQuery.cleanData after AngularJS', function() { var cleanedCount = 0; var currentCleanData = jqLite.cleanData; jqLite.cleanData = function(elems) { cleanedCount += elems.length; // Don't return the output and explicitly pass only the first parameter // so that we're sure we're not relying on either of them. jQuery UI patch // behaves in this way. currentCleanData(elems); }; testCleanup(); // The ng-repeat template is removed/cleaned (the +1) // and each clone of the ng-repeat template is also removed (xs.length) expect(cleanedCount).toBe(xs.length + 1); // Restore the previous cleanData. jqLite.cleanData = currentCleanData; }); }); it('should add a $$transcluded property onto the transcluded scope', function() { module(function() { directive('trans', function() { return { transclude: true, replace: true, scope: true, template: '
I:{{$$transcluded}}
' }; }); }); inject(function($rootScope, $compile) { element = $compile('
T:{{$$transcluded}}
')($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-transclude element before appending transcluded content' + ' if transcluded content exists', function() { module(function() { directive('trans', function() { return { transclude: true, template: '
old stuff!
' }; }); }); inject(function($rootScope, $compile) { element = $compile('
unicorn!
')($rootScope); $rootScope.$apply(); expect(sortedHtml(element.html())).toEqual('
unicorn!
'); }); }); it('should NOT clear contents of the ng-transclude element before appending transcluded content' + ' if transcluded content does NOT exist', function() { module(function() { directive('trans', function() { return { transclude: true, template: '
old stuff!
' }; }); }); inject(function(log, $rootScope, $compile) { element = $compile('
')($rootScope); $rootScope.$apply(); expect(sortedHtml(element.html())).toEqual('
old stuff!
'); }); }); it('should clear the fallback content from the element during compile and before linking', function() { module(function() { directive('trans', function() { return { transclude: true, template: '
fallback content
' }; }); }); inject(function(log, $rootScope, $compile) { element = jqLite('
'); var linkfn = $compile(element); expect(element.html()).toEqual('
'); linkfn($rootScope); $rootScope.$apply(); expect(sortedHtml(element.html())).toEqual('
fallback content
'); }); }); it('should allow cloning of the fallback via ngRepeat', function() { module(function() { directive('trans', function() { return { transclude: true, template: '
{{i}}
' }; }); }); inject(function(log, $rootScope, $compile) { element = $compile('
')($rootScope); $rootScope.$apply(); expect(element.text()).toEqual('012'); }); }); it('should not link the fallback content if transcluded content is provided', function() { var linkSpy = jasmine.createSpy('postlink'); module(function() { directive('inner', function() { return { restrict: 'E', template: 'old stuff! ', link: linkSpy }; }); directive('trans', function() { return { transclude: true, template: '
' }; }); }); inject(function($rootScope, $compile) { element = $compile('
unicorn!
')($rootScope); $rootScope.$apply(); expect(sortedHtml(element.html())).toEqual('
unicorn!
'); expect(linkSpy).not.toHaveBeenCalled(); }); }); it('should compile and link the fallback content if no transcluded content is provided', function() { var linkSpy = jasmine.createSpy('postlink'); module(function() { directive('inner', function() { return { restrict: 'E', template: 'old stuff! ', link: linkSpy }; }); directive('trans', function() { return { transclude: true, template: '
' }; }); }); inject(function(log, $rootScope, $compile) { element = $compile('
')($rootScope); $rootScope.$apply(); expect(sortedHtml(element.html())).toEqual('
old stuff!
'); expect(linkSpy).toHaveBeenCalled(); }); }); it('should compile and link the fallback content if only whitespace transcluded content is provided', function() { var linkSpy = jasmine.createSpy('postlink'); module(function() { directive('inner', function() { return { restrict: 'E', template: 'old stuff! ', link: linkSpy }; }); directive('trans', function() { return { transclude: true, template: '
' }; }); }); inject(function(log, $rootScope, $compile) { element = $compile('
\n \n
')($rootScope); $rootScope.$apply(); expect(sortedHtml(element.html())).toEqual('
old stuff!
'); expect(linkSpy).toHaveBeenCalled(); }); }); it('should not link the fallback content if only whitespace and comments are provided as transclude content', function() { var linkSpy = jasmine.createSpy('postlink'); module(function() { directive('inner', function() { return { restrict: 'E', template: 'old stuff! ', link: linkSpy }; }); directive('trans', function() { return { transclude: true, template: '
' }; }); }); inject(function(log, $rootScope, $compile) { element = $compile('
\n \n
')($rootScope); $rootScope.$apply(); expect(sortedHtml(element.html())).toEqual('
\n \n
'); expect(linkSpy).not.toHaveBeenCalled(); }); }); it('should compile and link the fallback content if an optional transclusion slot is not provided', function() { var linkSpy = jasmine.createSpy('postlink'); module(function() { directive('inner', function() { return { restrict: 'E', template: 'old stuff! ', link: linkSpy }; }); directive('trans', function() { return { transclude: { optionalSlot: '?optional'}, template: '
' }; }); }); inject(function(log, $rootScope, $compile) { element = $compile('
')($rootScope); $rootScope.$apply(); expect(sortedHtml(element.html())).toEqual('
old stuff!
'); expect(linkSpy).toHaveBeenCalled(); }); }); it('should cope if there is neither transcluded content nor fallback content', function() { module(function() { directive('trans', function() { return { transclude: true, template: '
' }; }); }); inject(function($rootScope, $compile) { element = $compile('
')($rootScope); $rootScope.$apply(); expect(sortedHtml(element.html())).toEqual('
'); }); }); it('should throw on an ng-transclude element inside no transclusion directive', function() { inject(function($rootScope, $compile) { var error; try { $compile('
')($rootScope); } catch (e) { error = e; } expect(error).toEqualMinErr('ngTransclude', 'orphan', 'Illegal use of ngTransclude directive in the template! ' + 'No parent directive that requires a transclusion found. ' + 'Element:
' + '
' + '
this one should get replaced with content
' + '
' + '
', transclude: true })); $compileProvider.directive('noTransBar', valueFn({ template: '
' + // This ng-transclude is invalid. It should throw an error. '
' + '
', transclude: false })); }); inject(function($compile, $rootScope) { expect(function() { $compile('
content
')($rootScope); }).toThrowMinErr('ngTransclude', 'orphan', 'Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found. Element:
'); }); }); it('should not pass transclusion into a templateUrl directive', function() { module(function($compileProvider) { $compileProvider.directive('transFoo', valueFn({ template: '
' + '
' + '
this one should get replaced with content
' + '
' + '
', transclude: true })); $compileProvider.directive('noTransBar', valueFn({ templateUrl: 'noTransBar.html', transclude: false })); }); inject(function($compile, $rootScope, $templateCache) { $templateCache.put('noTransBar.html', '
' + // This ng-transclude is invalid. It should throw an error. '
' + '
'); expect(function() { element = $compile('
content
')($rootScope); $rootScope.$digest(); }).toThrowMinErr('ngTransclude', 'orphan', 'Illegal use of ngTransclude directive in the template! ' + 'No parent directive that requires a transclusion found. ' + 'Element:
'); }); }); it('should expose transcludeFn in compile fn even for templateUrl', function() { module(function() { directive('transInCompile', valueFn({ transclude: true, // template: '
whatever
', templateUrl: 'foo.html', compile: function(_, __, transclude) { return function(scope, element) { transclude(scope, function(clone, scope) { element.html(''); element.append(clone); }); }; } })); }); inject(function($compile, $rootScope, $templateCache) { $templateCache.put('foo.html', '
whatever
'); compile('
transcluded content
'); $rootScope.$apply(); expect(trim(element.text())).toBe('transcluded content'); }); }); it('should make the result of a transclusion available to the parent directive in post-linking phase' + '(template)', function() { module(function() { directive('trans', function(log) { return { transclude: true, template: '
', link: { pre: function($scope, $element) { log('pre(' + $element.text() + ')'); }, post: function($scope, $element) { log('post(' + $element.text() + ')'); } } }; }); }); inject(function(log, $rootScope, $compile) { element = $compile('
unicorn!
')($rootScope); $rootScope.$apply(); expect(log).toEqual('pre(); post(unicorn!)'); }); }); it('should make the result of a transclusion available to the parent directive in post-linking phase' + '(templateUrl)', function() { // when compiling an async directive the transclusion is always processed before the directive // this is different compared to sync directive. delaying the transclusion makes little sense. module(function() { directive('trans', function(log) { return { transclude: true, templateUrl: 'trans.html', link: { pre: function($scope, $element) { log('pre(' + $element.text() + ')'); }, post: function($scope, $element) { log('post(' + $element.text() + ')'); } } }; }); }); inject(function(log, $rootScope, $compile, $templateCache) { $templateCache.put('trans.html', '
'); element = $compile('
unicorn!
')($rootScope); $rootScope.$apply(); expect(log).toEqual('pre(); post(unicorn!)'); }); }); it('should make the result of a transclusion available to the parent *replace* directive in post-linking phase' + '(template)', function() { module(function() { directive('replacedTrans', function(log) { return { transclude: true, replace: true, template: '
', link: { pre: function($scope, $element) { log('pre(' + $element.text() + ')'); }, post: function($scope, $element) { log('post(' + $element.text() + ')'); } } }; }); }); inject(function(log, $rootScope, $compile) { element = $compile('
unicorn!
')($rootScope); $rootScope.$apply(); expect(log).toEqual('pre(); post(unicorn!)'); }); }); it('should make the result of a transclusion available to the parent *replace* directive in post-linking phase' + ' (templateUrl)', function() { module(function() { directive('replacedTrans', function(log) { return { transclude: true, replace: true, templateUrl: 'trans.html', link: { pre: function($scope, $element) { log('pre(' + $element.text() + ')'); }, post: function($scope, $element) { log('post(' + $element.text() + ')'); } } }; }); }); inject(function(log, $rootScope, $compile, $templateCache) { $templateCache.put('trans.html', '
'); element = $compile('
unicorn!
')($rootScope); $rootScope.$apply(); expect(log).toEqual('pre(); post(unicorn!)'); }); }); it('should copy the directive controller to all clones', function() { var transcludeCtrl, cloneCount = 2; module(function() { directive('transclude', valueFn({ transclude: 'content', controller: function($transclude) { transcludeCtrl = this; }, link: function(scope, el, attr, ctrl, $transclude) { var i; for (i = 0; i < cloneCount; i++) { $transclude(cloneAttach); } function cloneAttach(clone) { el.append(clone); } } })); }); inject(function($compile) { element = $compile('
')($rootScope); var children = element.children(), i; expect(transcludeCtrl).toBeDefined(); expect(element.data('$transcludeController')).toBe(transcludeCtrl); for (i = 0; i < cloneCount; i++) { expect(children.eq(i).data('$transcludeController')).toBeUndefined(); } }); }); it('should provide the $transclude controller local as 5th argument to the pre and post-link function', function() { var ctrlTransclude, preLinkTransclude, postLinkTransclude; module(function() { directive('transclude', valueFn({ transclude: 'content', controller: function($transclude) { ctrlTransclude = $transclude; }, compile: function() { return { pre: function(scope, el, attr, ctrl, $transclude) { preLinkTransclude = $transclude; }, post: function(scope, el, attr, ctrl, $transclude) { postLinkTransclude = $transclude; } }; } })); }); inject(function($compile) { element = $compile('
')($rootScope); expect(ctrlTransclude).toBeDefined(); expect(ctrlTransclude).toBe(preLinkTransclude); expect(ctrlTransclude).toBe(postLinkTransclude); }); }); it('should allow an optional scope argument in $transclude', function() { var capturedChildCtrl; module(function() { directive('transclude', valueFn({ transclude: 'content', link: function(scope, element, attr, ctrl, $transclude) { $transclude(scope, function(clone) { element.append(clone); }); } })); }); inject(function($compile) { element = $compile('
{{$id}}
')($rootScope); $rootScope.$apply(); expect(element.text()).toBe('' + $rootScope.$id); }); }); it('should expose the directive controller to transcluded children', function() { var capturedChildCtrl; module(function() { directive('transclude', valueFn({ transclude: 'content', controller: function() { }, link: function(scope, element, attr, ctrl, $transclude) { $transclude(function(clone) { element.append(clone); }); } })); directive('child', valueFn({ require: '^transclude', link: function(scope, element, attr, ctrl) { capturedChildCtrl = ctrl; } })); }); inject(function($compile) { element = $compile('
')($rootScope); expect(capturedChildCtrl).toBeTruthy(); }); }); // See issue https://github.com/angular/angular.js/issues/14924 it('should not process top-level transcluded text nodes merged into their sibling', function() { module(function() { directive('transclude', valueFn({ template: '', transclude: true, scope: {} })); }); inject(function($compile) { element = jqLite('
'); element[0].appendChild(document.createTextNode('1{{ value }}')); element[0].appendChild(document.createTextNode('2{{ value }}')); element[0].appendChild(document.createTextNode('3{{ value }}')); var initialWatcherCount = $rootScope.$countWatchers(); $compile(element)($rootScope); $rootScope.$apply('value = 0'); var newWatcherCount = $rootScope.$countWatchers() - initialWatcherCount; expect(element.text()).toBe('102030'); expect(newWatcherCount).toBe(3); }); } ); // see issue https://github.com/angular/angular.js/issues/9413 describe('passing a parent bound transclude function to the link ' + 'function returned from `$compile`', function() { beforeEach(module(function() { directive('lazyCompile', function($compile) { return { compile: function(tElement, tAttrs) { var content = tElement.contents(); tElement.empty(); return function(scope, element, attrs, ctrls, transcludeFn) { element.append(content); $compile(content)(scope, undefined, { parentBoundTranscludeFn: transcludeFn }); }; } }; }); directive('toggle', valueFn({ scope: {t: '=toggle'}, transclude: true, template: '
' })); })); it('should preserve the bound scope', function() { inject(function($compile, $rootScope) { element = $compile( '
' + '
' + '
' + 'SuccessError' + '
' + '
')($rootScope); $rootScope.$apply('t = false'); expect($rootScope.$countChildScopes()).toBe(1); expect(element.text()).toBe(''); $rootScope.$apply('t = true'); expect($rootScope.$countChildScopes()).toBe(4); expect(element.text()).toBe('Success'); $rootScope.$apply('t = false'); expect($rootScope.$countChildScopes()).toBe(1); expect(element.text()).toBe(''); $rootScope.$apply('t = true'); expect($rootScope.$countChildScopes()).toBe(4); expect(element.text()).toBe('Success'); }); }); it('should preserve the bound scope when using recursive transclusion', function() { directive('recursiveTransclude', valueFn({ transclude: true, template: '
' })); inject(function($compile, $rootScope) { element = $compile( '
' + '
' + '
' + '
' + 'SuccessError' + '
' + '
' + '
')($rootScope); $rootScope.$apply('t = false'); expect($rootScope.$countChildScopes()).toBe(1); expect(element.text()).toBe(''); $rootScope.$apply('t = true'); expect($rootScope.$countChildScopes()).toBe(4); expect(element.text()).toBe('Success'); $rootScope.$apply('t = false'); expect($rootScope.$countChildScopes()).toBe(1); expect(element.text()).toBe(''); $rootScope.$apply('t = true'); expect($rootScope.$countChildScopes()).toBe(4); expect(element.text()).toBe('Success'); }); }); }); // see issue https://github.com/angular/angular.js/issues/9095 describe('removing a transcluded element', function() { beforeEach(module(function() { directive('toggle', function() { return { transclude: true, template: '
' }; }); })); it('should not leak the transclude scope when the transcluded content is an element transclusion directive', inject(function($compile, $rootScope) { element = $compile( '
' + '
{{ msg }}
' + '
' )($rootScope); $rootScope.$apply('t = true'); expect(element.text()).toContain('msg-1'); // Expected scopes: $rootScope, ngIf, transclusion, ngRepeat expect($rootScope.$countChildScopes()).toBe(3); $rootScope.$apply('t = false'); expect(element.text()).not.toContain('msg-1'); // Expected scopes: $rootScope expect($rootScope.$countChildScopes()).toBe(0); $rootScope.$apply('t = true'); expect(element.text()).toContain('msg-1'); // Expected scopes: $rootScope, ngIf, transclusion, ngRepeat expect($rootScope.$countChildScopes()).toBe(3); $rootScope.$apply('t = false'); expect(element.text()).not.toContain('msg-1'); // Expected scopes: $rootScope expect($rootScope.$countChildScopes()).toBe(0); })); it('should not leak the transclude scope when the transcluded content is an multi-element transclusion directive', inject(function($compile, $rootScope) { element = $compile( '
' + '
{{ msg }}
' + '
{{ msg }}
' + '
' )($rootScope); $rootScope.$apply('t = true'); expect(element.text()).toContain('msg-1msg-1'); // Expected scopes: $rootScope, ngIf, transclusion, ngRepeat expect($rootScope.$countChildScopes()).toBe(3); $rootScope.$apply('t = false'); expect(element.text()).not.toContain('msg-1msg-1'); // Expected scopes: $rootScope expect($rootScope.$countChildScopes()).toBe(0); $rootScope.$apply('t = true'); expect(element.text()).toContain('msg-1msg-1'); // Expected scopes: $rootScope, ngIf, transclusion, ngRepeat expect($rootScope.$countChildScopes()).toBe(3); $rootScope.$apply('t = false'); expect(element.text()).not.toContain('msg-1msg-1'); // Expected scopes: $rootScope expect($rootScope.$countChildScopes()).toBe(0); })); it('should not leak the transclude scope if the transcluded contains only comments', inject(function($compile, $rootScope) { element = $compile( '
' + '' + '
' )($rootScope); $rootScope.$apply('t = true'); expect(element.html()).toContain('some comment'); // Expected scopes: $rootScope, ngIf, transclusion expect($rootScope.$countChildScopes()).toBe(2); $rootScope.$apply('t = false'); expect(element.html()).not.toContain('some comment'); // Expected scopes: $rootScope expect($rootScope.$countChildScopes()).toBe(0); $rootScope.$apply('t = true'); expect(element.html()).toContain('some comment'); // Expected scopes: $rootScope, ngIf, transclusion expect($rootScope.$countChildScopes()).toBe(2); $rootScope.$apply('t = false'); expect(element.html()).not.toContain('some comment'); // Expected scopes: $rootScope expect($rootScope.$countChildScopes()).toBe(0); })); it('should not leak the transclude scope if the transcluded contains only text nodes', inject(function($compile, $rootScope) { element = $compile( '
' + 'some text' + '
' )($rootScope); $rootScope.$apply('t = true'); expect(element.html()).toContain('some text'); // Expected scopes: $rootScope, ngIf, transclusion expect($rootScope.$countChildScopes()).toBe(2); $rootScope.$apply('t = false'); expect(element.html()).not.toContain('some text'); // Expected scopes: $rootScope expect($rootScope.$countChildScopes()).toBe(0); $rootScope.$apply('t = true'); expect(element.html()).toContain('some text'); // Expected scopes: $rootScope, ngIf, transclusion expect($rootScope.$countChildScopes()).toBe(2); $rootScope.$apply('t = false'); expect(element.html()).not.toContain('some text'); // Expected scopes: $rootScope expect($rootScope.$countChildScopes()).toBe(0); })); it('should mark as destroyed all sub scopes of the scope being destroyed', inject(function($compile, $rootScope) { element = $compile( '
' + '
{{ msg }}
' + '
' )($rootScope); $rootScope.$apply('t = true'); var childScopes = getChildScopes($rootScope); $rootScope.$apply('t = false'); for (var i = 0; i < childScopes.length; ++i) { expect(childScopes[i].$$destroyed).toBe(true); } })); }); describe('nested transcludes', function() { beforeEach(module(function($compileProvider) { $compileProvider.directive('noop', valueFn({})); $compileProvider.directive('sync', valueFn({ template: '
', transclude: true })); $compileProvider.directive('async', valueFn({ templateUrl: 'async', transclude: true })); $compileProvider.directive('syncSync', valueFn({ template: '
', transclude: true })); $compileProvider.directive('syncAsync', valueFn({ template: '
', transclude: true })); $compileProvider.directive('asyncSync', valueFn({ templateUrl: 'asyncSync', transclude: true })); $compileProvider.directive('asyncAsync', valueFn({ templateUrl: 'asyncAsync', transclude: true })); })); beforeEach(inject(function($templateCache) { $templateCache.put('async', '
'); $templateCache.put('asyncSync', '
'); $templateCache.put('asyncAsync', '
'); })); it('should allow nested transclude directives with sync template containing sync template', inject(function($compile, $rootScope) { element = $compile('
transcluded content
')($rootScope); $rootScope.$digest(); expect(element.text()).toEqual('transcluded content'); })); it('should allow nested transclude directives with sync template containing async template', inject(function($compile, $rootScope) { element = $compile('
transcluded content
')($rootScope); $rootScope.$digest(); expect(element.text()).toEqual('transcluded content'); })); it('should allow nested transclude directives with async template containing sync template', inject(function($compile, $rootScope) { element = $compile('
transcluded content
')($rootScope); $rootScope.$digest(); expect(element.text()).toEqual('transcluded content'); })); it('should allow nested transclude directives with async template containing asynch template', inject(function($compile, $rootScope) { element = $compile('
transcluded content
')($rootScope); $rootScope.$digest(); expect(element.text()).toEqual('transcluded content'); })); it('should not leak memory with nested transclusion', function() { inject(function($compile, $rootScope) { var size, initialSize = jqLiteCacheSize(); element = jqLite('
  • {{n}} => EvenOdd
'); $compile(element)($rootScope.$new()); $rootScope.nums = [0,1,2]; $rootScope.$apply(); size = jqLiteCacheSize(); $rootScope.nums = [3,4,5]; $rootScope.$apply(); expect(jqLiteCacheSize()).toEqual(size); element.remove(); expect(jqLiteCacheSize()).toEqual(initialSize); }); }); }); describe('nested isolated scope transcludes', function() { beforeEach(module(function($compileProvider) { $compileProvider.directive('trans', valueFn({ restrict: 'E', template: '
', transclude: true })); $compileProvider.directive('transAsync', valueFn({ restrict: 'E', templateUrl: 'transAsync', transclude: true })); $compileProvider.directive('iso', valueFn({ restrict: 'E', transclude: true, template: '', scope: {} })); $compileProvider.directive('isoAsync1', valueFn({ restrict: 'E', transclude: true, template: '', scope: {} })); $compileProvider.directive('isoAsync2', valueFn({ restrict: 'E', transclude: true, templateUrl: 'isoAsync', scope: {} })); })); beforeEach(inject(function($templateCache) { $templateCache.put('transAsync', '
'); $templateCache.put('isoAsync', ''); })); it('should pass the outer scope to the transclude on the isolated template sync-sync', inject(function($compile, $rootScope) { $rootScope.val = 'transcluded content'; element = $compile('')($rootScope); $rootScope.$digest(); expect(element.text()).toEqual('transcluded content'); })); it('should pass the outer scope to the transclude on the isolated template async-sync', inject(function($compile, $rootScope) { $rootScope.val = 'transcluded content'; element = $compile('')($rootScope); $rootScope.$digest(); expect(element.text()).toEqual('transcluded content'); })); it('should pass the outer scope to the transclude on the isolated template async-async', inject(function($compile, $rootScope) { $rootScope.val = 'transcluded content'; element = $compile('')($rootScope); $rootScope.$digest(); expect(element.text()).toEqual('transcluded content'); })); }); describe('multiple siblings receiving transclusion', function() { it('should only receive transclude from parent', function() { module(function($compileProvider) { $compileProvider.directive('myExample', valueFn({ scope: {}, link: function link(scope, element, attrs) { var foo = element[0].querySelector('.foo'); scope.children = angular.element(foo).children().length; }, template: '
' + '
myExample {{children}}!
' + '
has children
' + '
' + '
', transclude: true })); }); inject(function($compile, $rootScope) { var element = $compile('
')($rootScope); $rootScope.$digest(); expect(element.text()).toEqual('myExample 0!'); dealoc(element); element = $compile('

')($rootScope); $rootScope.$digest(); expect(element.text()).toEqual('myExample 1!has children'); dealoc(element); }); }); }); }); describe('element transclusion', function() { it('should support basic element transclusion', function() { module(function() { directive('trans', function(log) { return { transclude: 'element', priority: 2, controller: function($transclude) { this.$transclude = $transclude; }, compile: function(element, attrs, template) { log('compile: ' + angular.mock.dump(element)); return function(scope, element, attrs, ctrl) { log('link'); var cursor = element; template(scope.$new(), function(clone) {cursor.after(cursor = clone);}); ctrl.$transclude(function(clone) {cursor.after(clone);}); }; } }; }); }); inject(function(log, $rootScope, $compile) { element = $compile('
{{$parent.$id}}-{{$id}};
')($rootScope); $rootScope.$apply(); expect(log).toEqual('compile: ; link; LOG; LOG; HIGH'); expect(element.text()).toEqual('1-2;1-3;'); }); }); it('should only allow one element transclusion per element', function() { module(function() { directive('first', valueFn({ transclude: 'element' })); directive('second', valueFn({ transclude: 'element' })); }); inject(function($compile) { expect(function() { $compile('
'); }).toThrowMinErr('$compile', 'multidir', 'Multiple directives [first, second] asking for transclusion on: ' + ''); }); }); it('should only allow one element transclusion per element when directives have different priorities', function() { // we restart compilation in this case and we need to remember the duplicates during the second compile // regression #3893 module(function() { directive('first', valueFn({ transclude: 'element', priority: 100 })); directive('second', valueFn({ transclude: 'element' })); }); inject(function($compile) { expect(function() { $compile('
'); }).toThrowMinErr('$compile', 'multidir', /Multiple directives \[first, second] asking for transclusion on:
template.html

'); expect(function() { $compile('
'); $httpBackend.flush(); }).toThrowMinErr('$compile', 'multidir', 'Multiple directives [first, second] asking for transclusion on:

', replace: true })); directive('first', valueFn({ transclude: 'element', priority: 100 })); directive('second', valueFn({ transclude: 'element' })); }); inject(function($compile) { expect(function() { $compile('
'); }).toThrowMinErr('$compile', 'multidir', /Multiple directives \[first, second] asking for transclusion on:

before

after
').contents(); expect(element.length).toEqual(3); expect(nodeName_(element[1])).toBe('div'); $compile(element)($rootScope); expect(nodeName_(element[1])).toBe('#comment'); expect(nodeName_(comment)).toBe('#comment'); }); }); it('should terminate compilation only for element transclusion', function() { module(function() { directive('elementTrans', function(log) { return { transclude: 'element', priority: 50, compile: log.fn('compile:elementTrans') }; }); directive('regularTrans', function(log) { return { transclude: true, priority: 50, compile: log.fn('compile:regularTrans') }; }); }); inject(function(log, $compile, $rootScope) { $compile('
')($rootScope); expect(log).toEqual('compile:elementTrans; compile:regularTrans; regular'); }); }); it('should instantiate high priority controllers only once, but low priority ones each time we transclude', function() { module(function() { directive('elementTrans', function(log) { return { transclude: 'element', priority: 50, controller: function($transclude, $element) { log('controller:elementTrans'); $transclude(function(clone) { $element.after(clone); }); $transclude(function(clone) { $element.after(clone); }); $transclude(function(clone) { $element.after(clone); }); } }; }); directive('normalDir', function(log) { return { controller: function() { log('controller:normalDir'); } }; }); }); inject(function($compile, $rootScope, log) { element = $compile('
')($rootScope); expect(log).toEqual([ 'controller:elementTrans', 'controller:normalDir', 'controller:normalDir', 'controller:normalDir' ]); }); }); it('should allow to access $transclude in the same directive', function() { var _$transclude; module(function() { directive('transclude', valueFn({ transclude: 'element', controller: function($transclude) { _$transclude = $transclude; } })); }); inject(function($compile) { element = $compile('
')($rootScope); expect(_$transclude).toBeDefined(); }); }); it('should copy the directive controller to all clones', function() { var transcludeCtrl, cloneCount = 2; module(function() { directive('transclude', valueFn({ transclude: 'element', controller: function() { transcludeCtrl = this; }, link: function(scope, el, attr, ctrl, $transclude) { var i; for (i = 0; i < cloneCount; i++) { $transclude(cloneAttach); } function cloneAttach(clone) { el.after(clone); } } })); }); inject(function($compile) { element = $compile('
')($rootScope); var children = element.children(), i; for (i = 0; i < cloneCount; i++) { expect(children.eq(i).data('$transcludeController')).toBe(transcludeCtrl); } }); }); it('should expose the directive controller to transcluded children', function() { var capturedTranscludeCtrl; module(function() { directive('transclude', valueFn({ transclude: 'element', controller: function() { }, link: function(scope, element, attr, ctrl, $transclude) { $transclude(scope, function(clone) { element.after(clone); }); } })); directive('child', valueFn({ require: '^transclude', link: function(scope, element, attr, ctrl) { capturedTranscludeCtrl = ctrl; } })); }); inject(function($compile) { // We need to wrap the transclude directive's element in a parent element so that the // cloned element gets deallocated/cleaned up correctly element = $compile('
')($rootScope); expect(capturedTranscludeCtrl).toBeTruthy(); }); }); it('should allow access to $transclude in a templateUrl directive', function() { var transclude; module(function() { directive('template', valueFn({ templateUrl: 'template.html', replace: true })); directive('transclude', valueFn({ transclude: 'content', controller: function($transclude) { transclude = $transclude; } })); }); inject(function($compile, $httpBackend) { $httpBackend.expectGET('template.html').respond('
'); element = $compile('
')($rootScope); $httpBackend.flush(); expect(transclude).toBeDefined(); }); }); // issue #6006 it('should link directive with $element as a comment node', function() { module(function($provide) { directive('innerAgain', function(log) { return { transclude: 'element', link: function(scope, element, attr, controllers, transclude) { log('innerAgain:' + lowercase(nodeName_(element)) + ':' + trim(element[0].data)); transclude(scope, function(clone) { element.parent().append(clone); }); } }; }); directive('inner', function(log) { return { replace: true, templateUrl: 'inner.html', link: function(scope, element) { log('inner:' + lowercase(nodeName_(element)) + ':' + trim(element[0].data)); } }; }); directive('outer', function(log) { return { transclude: 'element', link: function(scope, element, attrs, controllers, transclude) { log('outer:' + lowercase(nodeName_(element)) + ':' + trim(element[0].data)); transclude(scope, function(clone) { element.parent().append(clone); }); } }; }); }); inject(function(log, $compile, $rootScope, $templateCache) { $templateCache.put('inner.html', '

Content

'); element = $compile('
')($rootScope); $rootScope.$digest(); var child = element.children(); expect(log.toArray()).toEqual([ 'outer:#comment:outer:', 'innerAgain:#comment:innerAgain:', 'inner:#comment:innerAgain:' ]); expect(child.length).toBe(1); expect(child.contents().length).toBe(2); expect(lowercase(nodeName_(child.contents().eq(0)))).toBe('#comment'); expect(lowercase(nodeName_(child.contents().eq(1)))).toBe('div'); }); }); }); it('should be possible to change the scope of a directive using $provide', function() { module(function($provide) { directive('foo', function() { return { scope: {}, template: '
' }; }); $provide.decorator('fooDirective', function($delegate) { var directive = $delegate[0]; directive.scope.something = '='; directive.template = '{{something}}'; return $delegate; }); }); inject(function($compile, $rootScope) { element = $compile('
')($rootScope); $rootScope.bar = 'bar'; $rootScope.$digest(); expect(element.text()).toBe('bar'); }); }); it('should distinguish different bindings with the same binding name', function() { module(function() { directive('foo', function() { return { scope: { foo: '=', bar: '=' }, template: '
{{foo}}
{{bar}}
' }; }); }); inject(function($compile, $rootScope) { element = $compile('
')($rootScope); $rootScope.$digest(); expect(element.text()).toBe('foobar'); }); }); it('should safely create transclude comment node and not break with "-->"', inject(function($rootScope) { // see: https://github.com/angular/angular.js/issues/1740 element = $compile('
  • {{item}}|
')($rootScope); $rootScope.$digest(); expect(element.text()).toBe('-->|x|'); })); describe('lazy compilation', function() { // See https://github.com/angular/angular.js/issues/7183 it('should pass transclusion through to template of a \'replace\' directive', function() { module(function() { directive('transSync', function() { return { transclude: true, link: function(scope, element, attr, ctrl, transclude) { expect(transclude).toEqual(jasmine.any(Function)); transclude(function(child) { element.append(child); }); } }; }); directive('trans', function($timeout) { return { transclude: true, link: function(scope, element, attrs, ctrl, transclude) { // We use timeout here to simulate how ng-if works $timeout(function() { transclude(function(child) { element.append(child); }); }); } }; }); directive('replaceWithTemplate', function() { return { templateUrl: 'template.html', replace: true }; }); }); inject(function($compile, $rootScope, $templateCache, $timeout) { $templateCache.put('template.html', '
Content To Be Transcluded
'); expect(function() { element = $compile('
')($rootScope); $timeout.flush(); }).not.toThrow(); expect(element.text()).toEqual('Content To Be Transcluded'); }); }); it('should lazily compile the contents of directives that are transcluded', function() { var innerCompilationCount = 0, transclude; module(function() { directive('trans', valueFn({ transclude: true, controller: function($transclude) { transclude = $transclude; } })); directive('inner', valueFn({ template: 'FooBar', compile: function() { innerCompilationCount += 1; } })); }); inject(function($compile, $rootScope) { element = $compile('')($rootScope); expect(innerCompilationCount).toBe(0); transclude(function(child) { element.append(child); }); expect(innerCompilationCount).toBe(1); expect(element.text()).toBe('FooBar'); }); }); it('should lazily compile the contents of directives that are transcluded with a template', function() { var innerCompilationCount = 0, transclude; module(function() { directive('trans', valueFn({ transclude: true, template: '
Baz
', controller: function($transclude) { transclude = $transclude; } })); directive('inner', valueFn({ template: 'FooBar', compile: function() { innerCompilationCount += 1; } })); }); inject(function($compile, $rootScope) { element = $compile('')($rootScope); expect(innerCompilationCount).toBe(0); transclude(function(child) { element.append(child); }); expect(innerCompilationCount).toBe(1); expect(element.text()).toBe('BazFooBar'); }); }); it('should lazily compile the contents of directives that are transcluded with a templateUrl', function() { var innerCompilationCount = 0, transclude; module(function() { directive('trans', valueFn({ transclude: true, templateUrl: 'baz.html', controller: function($transclude) { transclude = $transclude; } })); directive('inner', valueFn({ template: 'FooBar', compile: function() { innerCompilationCount += 1; } })); }); inject(function($compile, $rootScope, $httpBackend) { $httpBackend.expectGET('baz.html').respond('
Baz
'); element = $compile('')($rootScope); $httpBackend.flush(); expect(innerCompilationCount).toBe(0); transclude(function(child) { element.append(child); }); expect(innerCompilationCount).toBe(1); expect(element.text()).toBe('BazFooBar'); }); }); it('should lazily compile the contents of directives that are transclude element', function() { var innerCompilationCount = 0, transclude; module(function() { directive('trans', valueFn({ transclude: 'element', controller: function($transclude) { transclude = $transclude; } })); directive('inner', valueFn({ template: 'FooBar', compile: function() { innerCompilationCount += 1; } })); }); inject(function($compile, $rootScope) { element = $compile('
')($rootScope); expect(innerCompilationCount).toBe(0); transclude(function(child) { element.append(child); }); expect(innerCompilationCount).toBe(1); expect(element.text()).toBe('FooBar'); }); }); it('should lazily compile transcluded directives with ngIf on them', function() { var innerCompilationCount = 0, outerCompilationCount = 0, transclude; module(function() { directive('outer', valueFn({ transclude: true, compile: function() { outerCompilationCount += 1; }, controller: function($transclude) { transclude = $transclude; } })); directive('inner', valueFn({ template: 'FooBar', compile: function() { innerCompilationCount += 1; } })); }); inject(function($compile, $rootScope) { $rootScope.shouldCompile = false; element = $compile('
')($rootScope); expect(outerCompilationCount).toBe(0); expect(innerCompilationCount).toBe(0); expect(transclude).toBeUndefined(); $rootScope.$apply('shouldCompile=true'); expect(outerCompilationCount).toBe(1); expect(innerCompilationCount).toBe(0); expect(transclude).toBeDefined(); transclude(function(child) { element.append(child); }); expect(outerCompilationCount).toBe(1); expect(innerCompilationCount).toBe(1); expect(element.text()).toBe('FooBar'); }); }); it('should eagerly compile multiple directives with transclusion and templateUrl/replace', function() { var innerCompilationCount = 0; module(function() { directive('outer', valueFn({ transclude: true })); directive('outer', valueFn({ templateUrl: 'inner.html', replace: true })); directive('inner', valueFn({ compile: function() { innerCompilationCount += 1; } })); }); inject(function($compile, $rootScope, $httpBackend) { $httpBackend.expectGET('inner.html').respond(''); element = $compile('')($rootScope); $httpBackend.flush(); expect(innerCompilationCount).toBe(1); }); }); }); }); describe('multi-slot transclude', function() { it('should only include elements without a matching transclusion element in default transclusion slot', function() { module(function() { directive('minionComponent', function() { return { restrict: 'E', scope: {}, transclude: { bossSlot: 'boss' }, template: '
' }; }); }); inject(function($rootScope, $compile) { element = $compile( '' + 'stuart' + 'bob' + 'gru' + 'kevin' + '')($rootScope); $rootScope.$apply(); expect(element.text()).toEqual('stuartbobkevin'); }); }); it('should use the default transclusion slot if the ng-transclude attribute has the same value as its key', function() { module(function() { directive('minionComponent', function() { return { restrict: 'E', scope: {}, transclude: {}, template: '
' + '
' + '
' }; }); }); inject(function($rootScope, $compile) { element = $compile( '' + 'stuart' + 'bob' + 'kevin' + '')($rootScope); $rootScope.$apply(); var a = element.children().eq(0); var b = element.children().eq(1); var c = element.children().eq(2); expect(a).toHaveClass('a'); expect(b).toHaveClass('b'); expect(c).toHaveClass('c'); expect(a.text()).toEqual('stuartbobkevin'); expect(b.text()).toEqual('stuartbobkevin'); expect(c.text()).toEqual('stuartbobkevin'); }); }); it('should include non-element nodes in the default transclusion', function() { module(function() { directive('minionComponent', function() { return { restrict: 'E', scope: {}, transclude: { bossSlot: 'boss' }, template: '
' }; }); }); inject(function($rootScope, $compile) { element = $compile( '' + 'text1' + 'stuart' + 'bob' + 'gru' + 'text2' + 'kevin' + '')($rootScope); $rootScope.$apply(); expect(element.text()).toEqual('text1stuartbobtext2kevin'); }); }); it('should transclude elements to an `ng-transclude` with a matching transclusion slot name', function() { module(function() { directive('minionComponent', function() { return { restrict: 'E', scope: {}, transclude: { minionSlot: 'minion', bossSlot: 'boss' }, template: '
' + '
' + '
' }; }); }); inject(function($rootScope, $compile) { element = $compile( '' + 'stuart' + 'dorothy' + 'gru' + 'kevin' + '')($rootScope); $rootScope.$apply(); expect(element.children().eq(0).text()).toEqual('gru'); expect(element.children().eq(1).text()).toEqual('stuartkevin'); expect(element.children().eq(2).text()).toEqual('dorothy'); }); }); it('should use the `ng-transclude-slot` attribute if ng-transclude is used as an element', function() { module(function() { directive('minionComponent', function() { return { restrict: 'E', scope: {}, transclude: { minionSlot: 'minion', bossSlot: 'boss' }, template: '' + '' + '' }; }); }); inject(function($rootScope, $compile) { element = $compile( '' + 'stuart' + 'dorothy' + 'gru' + 'kevin' + '')($rootScope); $rootScope.$apply(); expect(element.children().eq(0).text()).toEqual('gru'); expect(element.children().eq(1).text()).toEqual('stuartkevin'); expect(element.children().eq(2).text()).toEqual('dorothy'); }); }); it('should error if a required transclude slot is not filled', function() { module(function() { directive('minionComponent', function() { return { restrict: 'E', scope: {}, transclude: { minionSlot: 'minion', bossSlot: 'boss' }, template: '
' + '
' + '
' }; }); }); inject(function($rootScope, $compile) { expect(function() { element = $compile( '' + 'stuart' + 'dorothy' + '')($rootScope); }).toThrowMinErr('$compile', 'reqslot', 'Required transclusion slot `bossSlot` was not filled.'); }); }); it('should not error if an optional transclude slot is not filled', function() { module(function() { directive('minionComponent', function() { return { restrict: 'E', scope: {}, transclude: { minionSlot: 'minion', bossSlot: '?boss' }, template: '
' + '
' + '
' }; }); }); inject(function($rootScope, $compile) { element = $compile( '' + 'stuart' + 'dorothy' + '')($rootScope); $rootScope.$apply(); expect(element.children().eq(1).text()).toEqual('stuart'); expect(element.children().eq(2).text()).toEqual('dorothy'); }); }); it('should error if we try to transclude a slot that was not declared by the directive', function() { module(function() { directive('minionComponent', function() { return { restrict: 'E', scope: {}, transclude: { minionSlot: 'minion' }, template: '
' + '
' + '
' }; }); }); inject(function($rootScope, $compile) { expect(function() { element = $compile( '' + 'stuart' + 'dorothy' + '')($rootScope); }).toThrowMinErr('$compile', 'noslot', 'No parent directive that requires a transclusion with slot name "bossSlot". ' + 'Element:
'); }); }); it('should allow the slot name to equal the element name', function() { module(function() { directive('foo', function() { return { restrict: 'E', scope: {}, transclude: { bar: 'bar' }, template: '
' }; }); }); inject(function($rootScope, $compile) { element = $compile( '' + 'baz' + '')($rootScope); $rootScope.$apply(); expect(element.text()).toEqual('baz'); }); }); it('should match the normalized form of the element name', function() { module(function() { directive('foo', function() { return { restrict: 'E', scope: {}, transclude: { fooBarSlot: 'fooBar', mooKarSlot: 'mooKar' }, template: '
' + '
' }; }); }); inject(function($rootScope, $compile) { element = $compile( '' + 'bar1' + 'bar2' + 'baz1' + 'baz2' + '')($rootScope); $rootScope.$apply(); expect(element.children().eq(0).text()).toEqual('bar1bar2'); expect(element.children().eq(1).text()).toEqual('baz1baz2'); }); }); it('should return true from `isSlotFilled(slotName) for slots that have content in the transclusion', function() { var capturedTranscludeFn; module(function() { directive('minionComponent', function() { return { restrict: 'E', scope: {}, transclude: { minionSlot: 'minion', bossSlot: '?boss' }, template: '
' + '
' + '
', link: function(s, e, a, c, transcludeFn) { capturedTranscludeFn = transcludeFn; } }; }); }); inject(function($rootScope, $compile, log) { element = $compile( '' + ' stuart' + ' bob' + ' dorothy' + '')($rootScope); $rootScope.$apply(); var hasMinions = capturedTranscludeFn.isSlotFilled('minionSlot'); var hasBosses = capturedTranscludeFn.isSlotFilled('bossSlot'); expect(hasMinions).toBe(true); expect(hasBosses).toBe(false); }); }); it('should not overwrite the contents of an `ng-transclude` element, if the matching optional slot is not filled', function() { module(function() { directive('minionComponent', function() { return { restrict: 'E', scope: {}, transclude: { minionSlot: 'minion', bossSlot: '?boss' }, template: '
default boss content
' + '
default minion content
' + '
default content
' }; }); }); inject(function($rootScope, $compile) { element = $compile( '' + 'stuart' + 'dorothy' + 'kevin' + '')($rootScope); $rootScope.$apply(); expect(element.children().eq(0).text()).toEqual('default boss content'); expect(element.children().eq(1).text()).toEqual('stuartkevin'); expect(element.children().eq(2).text()).toEqual('dorothy'); }); }); // See issue https://github.com/angular/angular.js/issues/14924 it('should not process top-level transcluded text nodes merged into their sibling', function() { module(function() { directive('transclude', valueFn({ template: '', transclude: {}, scope: {} })); }); inject(function($compile) { element = jqLite('
'); element[0].appendChild(document.createTextNode('1{{ value }}')); element[0].appendChild(document.createTextNode('2{{ value }}')); element[0].appendChild(document.createTextNode('3{{ value }}')); var initialWatcherCount = $rootScope.$countWatchers(); $compile(element)($rootScope); $rootScope.$apply('value = 0'); var newWatcherCount = $rootScope.$countWatchers() - initialWatcherCount; expect(element.text()).toBe('102030'); expect(newWatcherCount).toBe(3); // Support: IE 11 only // See #11781 and #14924 if (msie === 11) { expect(element.find('ng-transclude').contents().length).toBe(1); } }); } ); }); ['img', 'audio', 'video'].forEach(function(tag) { // Support: IE 9 only // IE9 rejects the `video` / `audio` tags with "Error: Not implemented" if (msie !== 9 || tag === 'img') { describe(tag + '[src] context requirement', function() { it('should NOT require trusted values for whitelisted URIs', inject(function($rootScope, $compile) { element = $compile('<' + tag + ' src="{{testUrl}}">')($rootScope); $rootScope.testUrl = 'http://example.com/image.mp4'; // `http` is whitelisted $rootScope.$digest(); expect(element.attr('src')).toEqual('http://example.com/image.mp4'); })); it('should accept trusted values', inject(function($rootScope, $compile, $sce) { // As a MEDIA_URL URL element = $compile('<' + tag + ' src="{{testUrl}}">')($rootScope); // Some browsers complain if you try to write `javascript:` into an `img[src]` // So for the test use something different $rootScope.testUrl = $sce.trustAsMediaUrl('untrusted:foo()'); $rootScope.$digest(); expect(element.attr('src')).toEqual('untrusted:foo()'); // As a URL element = $compile('<' + tag + ' src="{{testUrl}}">')($rootScope); $rootScope.testUrl = $sce.trustAsUrl('untrusted:foo()'); $rootScope.$digest(); expect(element.attr('src')).toEqual('untrusted:foo()'); // As a RESOURCE URL element = $compile('<' + tag + ' src="{{testUrl}}">')($rootScope); $rootScope.testUrl = $sce.trustAsResourceUrl('untrusted:foo()'); $rootScope.$digest(); expect(element.attr('src')).toEqual('untrusted:foo()'); })); }); } }); // Support: IE 9 only // IE 9 rejects the `source` / `track` tags with // "Unable to get value of the property 'childNodes': object is null or undefined" if (msie !== 9) { ['source', 'track'].forEach(function(tag) { describe(tag + '[src]', function() { it('should NOT require trusted values for whitelisted URIs', inject(function($rootScope, $compile) { element = $compile('')($rootScope); $rootScope.testUrl = 'http://example.com/image.mp4'; // `http` is whitelisted $rootScope.$digest(); expect(element.find(tag).attr('src')).toEqual('http://example.com/image.mp4'); })); it('should accept trusted values', inject(function($rootScope, $compile, $sce) { // As a MEDIA_URL URL element = $compile('')($rootScope); $rootScope.testUrl = $sce.trustAsMediaUrl('javascript:foo()'); $rootScope.$digest(); expect(element.find(tag).attr('src')).toEqual('javascript:foo()'); // As a URL element = $compile('')($rootScope); $rootScope.testUrl = $sce.trustAsUrl('javascript:foo()'); $rootScope.$digest(); expect(element.find(tag).attr('src')).toEqual('javascript:foo()'); // As a RESOURCE URL element = $compile('')($rootScope); $rootScope.testUrl = $sce.trustAsResourceUrl('javascript:foo()'); $rootScope.$digest(); expect(element.find(tag).attr('src')).toEqual('javascript:foo()'); })); }); }); } describe('img[src] sanitization', function() { it('should accept trusted values', inject(function($rootScope, $compile, $sce) { element = $compile('')($rootScope); // Some browsers complain if you try to write `javascript:` into an `img[src]` // So for the test use something different $rootScope.testUrl = $sce.trustAsMediaUrl('someUntrustedThing:foo();'); $rootScope.$digest(); expect(element.attr('src')).toEqual('someUntrustedThing:foo();'); })); it('should sanitize concatenated values even if they are trusted', inject(function($rootScope, $compile, $sce) { element = $compile('')($rootScope); $rootScope.testUrl = $sce.trustAsUrl('untrusted:foo();'); $rootScope.$digest(); expect(element.attr('src')).toEqual('unsafe:untrusted:foo();ponies'); element = $compile('')($rootScope); $rootScope.testUrl2 = $sce.trustAsUrl('xyz;'); $rootScope.$digest(); expect(element.attr('src')).toEqual('http://xyz;'); element = $compile('')($rootScope); $rootScope.testUrl3 = $sce.trustAsUrl('untrusted:foo();'); $rootScope.$digest(); expect(element.attr('src')).toEqual('unsafe:untrusted:foo();untrusted:foo();'); })); it('should not sanitize attributes other than src', inject(function($compile, $rootScope) { element = $compile('')($rootScope); $rootScope.testUrl = 'javascript:doEvilStuff()'; $rootScope.$apply(); expect(element.attr('title')).toBe('javascript:doEvilStuff()'); })); it('should use $$sanitizeUri', function() { var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri'); module(function($provide) { $provide.value('$$sanitizeUri', $$sanitizeUri); }); inject(function($compile, $rootScope) { element = $compile('')($rootScope); $rootScope.testUrl = 'someUrl'; $$sanitizeUri.and.returnValue('someSanitizedUrl'); $rootScope.$apply(); expect(element.attr('src')).toBe('someSanitizedUrl'); expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, true); }); }); it('should use $$sanitizeUri on concatenated trusted values', function() { var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('someSanitizedUrl'); module(function($provide) { $provide.value('$$sanitizeUri', $$sanitizeUri); }); inject(function($compile, $rootScope, $sce) { element = $compile('')($rootScope); $rootScope.testUrl = $sce.trustAsUrl('javascript:foo();'); $rootScope.$digest(); expect(element.attr('src')).toEqual('someSanitizedUrl'); element = $compile('')($rootScope); $rootScope.testUrl = $sce.trustAsUrl('xyz'); $rootScope.$digest(); expect(element.attr('src')).toEqual('someSanitizedUrl'); }); }); it('should not use $$sanitizeUri with trusted values', function() { var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.throwError('Should not have been called'); module(function($provide) { $provide.value('$$sanitizeUri', $$sanitizeUri); }); inject(function($compile, $rootScope, $sce) { element = $compile('')($rootScope); // Assigning javascript:foo to src makes at least IE9-11 complain, so use another // protocol name. $rootScope.testUrl = $sce.trustAsMediaUrl('untrusted:foo();'); $rootScope.$apply(); expect(element.attr('src')).toEqual('untrusted:foo();'); }); }); }); describe('img[srcset] sanitization', function() { it('should not error if srcset is undefined', function() { var linked = false; module(function() { directive('setter', valueFn(function(scope, elem, attrs) { // Set srcset to a value attrs.$set('srcset', 'http://example.com/'); expect(attrs.srcset).toBe('http://example.com/'); // Now set it to undefined attrs.$set('srcset', undefined); expect(attrs.srcset).toBeUndefined(); linked = true; })); }); inject(function($compile, $rootScope) { element = $compile('')($rootScope); expect(linked).toBe(true); expect(element.attr('srcset')).toBeUndefined(); }); }); it('should NOT require trusted values for whitelisted values', inject(function($rootScope, $compile, $sce) { element = $compile('')($rootScope); $rootScope.testUrl = 'http://example.com/image.png'; // `http` is whitelisted $rootScope.$digest(); expect(element.attr('srcset')).toEqual('http://example.com/image.png'); })); it('should accept trusted values, if they are also whitelisted', inject(function($rootScope, $compile, $sce) { element = $compile('')($rootScope); $rootScope.testUrl = $sce.trustAsUrl('http://example.com'); $rootScope.$digest(); expect(element.attr('srcset')).toEqual('http://example.com'); })); it('does not work with trusted values', inject(function($rootScope, $compile, $sce) { // A limitation of the approach used for srcset is that you cannot use `trustAsUrl`. // Use trustAsHtml and ng-bind-html to work around this. element = $compile('')($rootScope); $rootScope.testUrl = $sce.trustAsUrl('javascript:something'); $rootScope.$digest(); expect(element.attr('srcset')).toEqual('unsafe:javascript:something'); element = $compile('')($rootScope); $rootScope.testUrl = $sce.trustAsUrl('javascript:something'); $rootScope.$digest(); expect(element.attr('srcset')).toEqual( 'unsafe:javascript:something ,unsafe:javascript:something'); })); it('should use $$sanitizeUri', function() { var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('someSanitizedUrl'); module(function($provide) { $provide.value('$$sanitizeUri', $$sanitizeUri); }); inject(function($compile, $rootScope) { element = $compile('')($rootScope); $rootScope.testUrl = 'someUrl'; $rootScope.$apply(); expect(element.attr('srcset')).toBe('someSanitizedUrl'); expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, true); element = $compile('')($rootScope); $rootScope.testUrl = 'javascript:yay'; $rootScope.$apply(); expect(element.attr('srcset')).toEqual('someSanitizedUrl ,someSanitizedUrl'); element = $compile('')($rootScope); $rootScope.testUrl = 'script:yay, javascript:nay'; $rootScope.$apply(); expect(element.attr('srcset')).toEqual('someSanitizedUrl ,someSanitizedUrl'); }); }); it('should sanitize all uris in srcset', inject(function($rootScope, $compile) { element = $compile('')($rootScope); var testSet = { 'http://example.com/image.png':'http://example.com/image.png', ' http://example.com/image.png':'http://example.com/image.png', 'http://example.com/image.png ':'http://example.com/image.png', 'http://example.com/image.png 128w':'http://example.com/image.png 128w', 'http://example.com/image.png 2x':'http://example.com/image.png 2x', 'http://example.com/image.png 1.5x':'http://example.com/image.png 1.5x', 'http://example.com/image1.png 1x,http://example.com/image2.png 2x':'http://example.com/image1.png 1x,http://example.com/image2.png 2x', 'http://example.com/image1.png 1x ,http://example.com/image2.png 2x':'http://example.com/image1.png 1x ,http://example.com/image2.png 2x', 'http://example.com/image1.png 1x, http://example.com/image2.png 2x':'http://example.com/image1.png 1x,http://example.com/image2.png 2x', 'http://example.com/image1.png 1x , http://example.com/image2.png 2x':'http://example.com/image1.png 1x ,http://example.com/image2.png 2x', 'http://example.com/image1.png 48w,http://example.com/image2.png 64w':'http://example.com/image1.png 48w,http://example.com/image2.png 64w', //Test regex to make sure doesn't mistake parts of url for width descriptors 'http://example.com/image1.png?w=48w,http://example.com/image2.png 64w':'http://example.com/image1.png?w=48w,http://example.com/image2.png 64w', 'http://example.com/image1.png 1x,http://example.com/image2.png 64w':'http://example.com/image1.png 1x,http://example.com/image2.png 64w', 'http://example.com/image1.png,http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png', 'http://example.com/image1.png ,http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png', 'http://example.com/image1.png, http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png', 'http://example.com/image1.png , http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png', 'http://example.com/image1.png 1x, http://example.com/image2.png 2x, http://example.com/image3.png 3x': 'http://example.com/image1.png 1x,http://example.com/image2.png 2x,http://example.com/image3.png 3x', 'javascript:doEvilStuff() 2x': 'unsafe:javascript:doEvilStuff() 2x', 'http://example.com/image1.png 1x,javascript:doEvilStuff() 2x':'http://example.com/image1.png 1x,unsafe:javascript:doEvilStuff() 2x', 'http://example.com/image1.jpg?x=a,b 1x,http://example.com/ima,ge2.jpg 2x':'http://example.com/image1.jpg?x=a,b 1x,http://example.com/ima,ge2.jpg 2x', //Test regex to make sure doesn't mistake parts of url for pixel density descriptors 'http://example.com/image1.jpg?x=a2x,b 1x,http://example.com/ima,ge2.jpg 2x':'http://example.com/image1.jpg?x=a2x,b 1x,http://example.com/ima,ge2.jpg 2x' }; forEach(testSet, function(ref, url) { $rootScope.testUrl = url; $rootScope.$digest(); expect(element.attr('srcset')).toEqual(ref); }); })); }); describe('a[href] sanitization', function() { it('should NOT require trusted values for whitelisted values', inject(function($rootScope, $compile) { $rootScope.testUrl = 'http://example.com/image.png'; // `http` is whitelisted element = $compile('')($rootScope); $rootScope.$digest(); expect(element.attr('href')).toEqual('http://example.com/image.png'); element = $compile('')($rootScope); $rootScope.$digest(); expect(element.attr('ng-href')).toEqual('http://example.com/image.png'); })); it('should accept trusted values for non-whitelisted values', inject(function($rootScope, $compile, $sce) { $rootScope.testUrl = $sce.trustAsUrl('javascript:foo()'); // `javascript` is not whitelisted element = $compile('')($rootScope); $rootScope.$digest(); expect(element.attr('href')).toEqual('javascript:foo()'); element = $compile('')($rootScope); $rootScope.$digest(); expect(element.attr('ng-href')).toEqual('javascript:foo()'); })); it('should sanitize non-whitelisted values', inject(function($rootScope, $compile) { $rootScope.testUrl = 'javascript:foo()'; // `javascript` is not whitelisted element = $compile('')($rootScope); $rootScope.$digest(); expect(element.attr('href')).toEqual('unsafe:javascript:foo()'); element = $compile('')($rootScope); $rootScope.$digest(); expect(element.attr('href')).toEqual('unsafe:javascript:foo()'); })); it('should not sanitize href on elements other than anchor', inject(function($compile, $rootScope) { element = $compile('
')($rootScope); $rootScope.testUrl = 'javascript:doEvilStuff()'; $rootScope.$apply(); expect(element.attr('href')).toBe('javascript:doEvilStuff()'); })); it('should not sanitize attributes other than href/ng-href', inject(function($compile, $rootScope) { element = $compile('')($rootScope); $rootScope.testUrl = 'javascript:doEvilStuff()'; $rootScope.$apply(); expect(element.attr('title')).toBe('javascript:doEvilStuff()'); })); it('should use $$sanitizeUri', function() { var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('someSanitizedUrl'); module(function($provide) { $provide.value('$$sanitizeUri', $$sanitizeUri); }); inject(function($compile, $rootScope) { element = $compile('')($rootScope); $rootScope.testUrl = 'someUrl'; $rootScope.$apply(); expect(element.attr('href')).toBe('someSanitizedUrl'); expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, false); $$sanitizeUri.calls.reset(); element = $compile('')($rootScope); $rootScope.$apply(); expect(element.attr('href')).toBe('someSanitizedUrl'); expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, false); }); }); it('should use $$sanitizeUri when working with svg and xlink:href', function() { var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('https://clean.example.org'); module(function($provide) { $provide.value('$$sanitizeUri', $$sanitizeUri); }); inject(function($compile, $rootScope) { // This URL would fail the RESOURCE_URL whitelist, but that test shouldn't be run // because these interpolations will be resolved against the URL context instead $rootScope.testUrl = 'https://bad.example.org'; var elementA = $compile('')($rootScope); $rootScope.$apply(); expect(elementA.find('a').attr('xlink:href')).toBe('https://clean.example.org'); expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl + 'aTag', false); var elementImage = $compile('')($rootScope); $rootScope.$apply(); expect(elementImage.find('image').attr('xlink:href')).toBe('https://clean.example.org'); expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl + 'imageTag', true); }); }); it('should use $$sanitizeUri when working with svg and xlink:href through ng-href', function() { var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('https://clean.example.org'); module(function($provide) { $provide.value('$$sanitizeUri', $$sanitizeUri); }); inject(function($compile, $rootScope) { // This URL would fail the RESOURCE_URL whitelist, but that test shouldn't be run // because these interpolations will be resolved against the URL context instead $rootScope.testUrl = 'https://bad.example.org'; element = $compile('')($rootScope); $rootScope.$apply(); expect(element.find('a').prop('href').baseVal).toBe('https://clean.example.org'); expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, false); }); }); it('should require a RESOURCE_URL context for xlink:href by if not on an anchor or image', function() { inject(function($compile, $rootScope) { element = $compile('')($rootScope); $rootScope.testUrl = 'https://bad.example.org'; expect(function() { $rootScope.$apply(); }).toThrowMinErr('$interpolate', 'interr', 'Can\'t interpolate: {{ testUrl }}\n' + 'Error: [$sce:insecurl] Blocked loading resource from url not allowed by $sceDelegate policy. ' + 'URL: https://bad.example.org'); }); }); it('should not have endless digests when given arrays in concatenable context', inject(function($compile, $rootScope) { element = $compile('' + '')($rootScope); $rootScope.testUrl = [1]; $rootScope.$digest(); $rootScope.testUrl = []; $rootScope.$digest(); $rootScope.testUrl = {a:'b'}; $rootScope.$digest(); $rootScope.testUrl = {}; $rootScope.$digest(); })); }); describe('interpolation on HTML DOM event handler attributes onclick, onXYZ, formaction', function() { it('should disallow interpolation on onclick', inject(function($compile, $rootScope) { // All interpolations are disallowed. $rootScope.onClickJs = ''; expect(function() { $compile('