diff --git a/docs/app/src/examples.js b/docs/app/src/examples.js index 5db8bf4e88f7..1f0f2aa4e83e 100644 --- a/docs/app/src/examples.js +++ b/docs/app/src/examples.js @@ -24,7 +24,26 @@ angular.module('examples', []) .factory('openPlunkr', ['formPostData', '$http', '$q', function(formPostData, $http, $q) { - return function(exampleFolder, clickEvent) { + + var COPYRIGHT = 'Copyright ' + (new Date()).getFullYear() + ' Google Inc. All Rights Reserved.\n' + + 'Use of this source code is governed by an MIT-style license that\n' + + 'can be found in the LICENSE file at http://angular.io/license'; + var COPYRIGHT_JS_CSS = '\n\n/*\n' + COPYRIGHT + '\n*/'; + var COPYRIGHT_HTML = '\n\n'; + function getCopyright(filename) { + switch (filename.substr(filename.lastIndexOf('.'))) { + case '.html': + return COPYRIGHT_HTML; + case '.js': + case '.css': + return COPYRIGHT_JS_CSS; + case '.md': + return COPYRIGHT; + } + return ''; + } + + return function(exampleFolder, clickEvent) { var exampleName = 'AngularJS Example'; var newWindow = clickEvent.ctrlKey || clickEvent.metaKey; @@ -67,7 +86,7 @@ angular.module('examples', []) var postData = {}; angular.forEach(files, function(file) { - postData['files[' + file.name + ']'] = file.content; + postData['files[' + file.name + ']'] = file.content + getCopyright(file.name); }); postData['tags[0]'] = "angularjs"; diff --git a/src/ng/animateCss.js b/src/ng/animateCss.js index 552e51095ebf..2e133167a6c3 100644 --- a/src/ng/animateCss.js +++ b/src/ng/animateCss.js @@ -15,10 +15,14 @@ var $CoreAnimateCssProvider = function() { this.$get = ['$$rAF', '$q', '$$AnimateRunner', function($$rAF, $q, $$AnimateRunner) { return function(element, initialOptions) { - // we always make a copy of the options since - // there should never be any side effects on - // the input data when running `$animateCss`. - var options = copy(initialOptions); + // all of the animation functions should create + // a copy of the options data, however, if a + // parent service has already created a copy then + // we should stick to using that + var options = initialOptions || {}; + if (!options.$$prepared) { + options = copy(options); + } // there is no point in applying the styles since // there is no animation that goes on at all in diff --git a/src/ng/compile.js b/src/ng/compile.js index 98c233206f65..1049aa6c8c53 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1052,6 +1052,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * See also {@link ng.$compileProvider#directive $compileProvider.directive()}. */ this.component = function registerComponent(name, options) { + var controller = options.controller || function() {}; + function factory($injector) { function makeInjectable(fn) { if (isFunction(fn) || isArray(fn)) { @@ -1065,7 +1067,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var template = (!options.template && !options.templateUrl ? '' : options.template); return { - controller: options.controller || function() {}, + controller: controller, controllerAs: identifierForController(options.controller) || options.controllerAs || '$ctrl', template: makeInjectable(template), templateUrl: makeInjectable(options.templateUrl), diff --git a/src/ng/controller.js b/src/ng/controller.js index 4e64f3417a88..389f869c9888 100644 --- a/src/ng/controller.js +++ b/src/ng/controller.js @@ -3,7 +3,7 @@ var $controllerMinErr = minErr('$controller'); -var CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/; +var CNTRL_REG = /^(\S+)(\s+as\s+([\w$]+))?$/; function identifierForController(controller, ident) { if (ident && isString(ident)) return ident; if (isString(controller)) { diff --git a/src/ngAnimate/animateCss.js b/src/ngAnimate/animateCss.js index 278814b1f744..b9757f27777c 100644 --- a/src/ngAnimate/animateCss.js +++ b/src/ngAnimate/animateCss.js @@ -447,10 +447,14 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { } return function init(element, initialOptions) { - // we always make a copy of the options since - // there should never be any side effects on - // the input data when running `$animateCss`. - var options = copy(initialOptions); + // all of the animation functions should create + // a copy of the options data, however, if a + // parent service has already created a copy then + // we should stick to using that + var options = initialOptions || {}; + if (!options.$$prepared) { + options = prepareAnimationOptions(copy(options)); + } var restoreStyles = {}; var node = getDomNode(element); @@ -460,8 +464,6 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { return closeAndReturnNoopAnimator(); } - options = prepareAnimationOptions(options); - var temporaryStyles = []; var classes = element.attr('class'); var styles = packageStyles(options); diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index 6997d83a5dd0..500aa9ac8782 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -797,7 +797,10 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng']) var animateJsConstructor = function() { var animator = $delegate.apply($delegate, arguments); - runners.push(animator); + // If no javascript animation is found, animator is undefined + if (animator) { + runners.push(animator); + } return animator; }; @@ -2161,6 +2164,49 @@ angular.mock.$ControllerDecorator = ['$delegate', function($delegate) { }; }]; +/** + * @ngdoc service + * @name $componentController + * @description + * A service that can be used to create instances of component controllers. + *
+ * Be aware that the controller will be instantiated and attached to the scope as specified in + * the component definition object. That means that you must always provide a `$scope` object + * in the `locals` param. + *
+ * @param {string} componentName the name of the component whose controller we want to instantiate + * @param {Object} locals Injection locals for Controller. + * @param {Object=} bindings Properties to add to the controller before invoking the constructor. This is used + * to simulate the `bindToController` feature and simplify certain kinds of tests. + * @param {string=} ident Override the property name to use when attaching the controller to the scope. + * @return {Object} Instance of requested controller. + */ +angular.mock.$ComponentControllerProvider = ['$compileProvider', function($compileProvider) { + return { + $get: ['$controller','$injector', function($controller,$injector) { + return function $componentController(componentName, locals, bindings, ident) { + // get all directives associated to the component name + var directives = $injector.get(componentName + 'Directive'); + // look for those directives that are components + var candidateDirectives = directives.filter(function(directiveInfo) { + // components have controller, controllerAs and restrict:'E' compatible + return directiveInfo.controller && directiveInfo.controllerAs && directiveInfo.restrict.indexOf('E') >= 0; + }); + // check if valid directives found + if (candidateDirectives.length === 0) { + throw new Error('No component found'); + } + if (candidateDirectives.length > 1) { + throw new Error('Too many components found'); + } + // get the info of the component + var directiveInfo = candidateDirectives[0]; + return $controller(directiveInfo.controller, locals, bindings, ident || directiveInfo.controllerAs); + }; + }] + }; +}]; + /** * @ngdoc module @@ -2184,7 +2230,8 @@ angular.module('ngMock', ['ng']).provider({ $log: angular.mock.$LogProvider, $interval: angular.mock.$IntervalProvider, $httpBackend: angular.mock.$HttpBackendProvider, - $rootElement: angular.mock.$RootElementProvider + $rootElement: angular.mock.$RootElementProvider, + $componentController: angular.mock.$ComponentControllerProvider }).config(['$provide', function($provide) { $provide.decorator('$timeout', angular.mock.$TimeoutDecorator); $provide.decorator('$$rAF', angular.mock.$RAFDecorator); diff --git a/test/helpers/privateMocks.js b/test/helpers/privateMocks.js index 637f19ebf97c..ae57f116cc0e 100644 --- a/test/helpers/privateMocks.js +++ b/test/helpers/privateMocks.js @@ -40,9 +40,8 @@ function browserSupportsCssAnimations() { return true; } -function createMockStyleSheet(doc, wind) { +function createMockStyleSheet(doc, prefix) { doc = doc ? doc[0] : document; - wind = wind || window; var node = doc.createElement('style'); var head = doc.getElementsByTagName('head')[0]; @@ -63,6 +62,18 @@ function createMockStyleSheet(doc, wind) { } }, + addPossiblyPrefixedRule: function(selector, styles) { + if (prefix) { + var prefixedStyles = styles.split(/\s*;\s*/g).map(function(style) { + return !style ? '' : prefix + style; + }).join('; '); + + this.addRule(selector, prefixedStyles); + } + + this.addRule(selector, styles); + }, + destroy: function() { head.removeChild(node); } diff --git a/test/helpers/privateMocksSpec.js b/test/helpers/privateMocksSpec.js index 495dcb60c6ef..c584b1d33c39 100644 --- a/test/helpers/privateMocksSpec.js +++ b/test/helpers/privateMocksSpec.js @@ -211,7 +211,7 @@ describe('private mocks', function() { var doc = $document[0]; var count = doc.styleSheets.length; - var stylesheet = createMockStyleSheet($document, $window); + var stylesheet = createMockStyleSheet($document); var elm; runs(function() { expect(doc.styleSheets.length).toBe(count + 1); diff --git a/test/ng/animateCssSpec.js b/test/ng/animateCssSpec.js index f0a027805cfa..dcc37b7fc9c9 100644 --- a/test/ng/animateCssSpec.js +++ b/test/ng/animateCssSpec.js @@ -31,6 +31,28 @@ describe("$animateCss", function() { expect(copiedOptions).toEqual(initialOptions); })); + it("should not create a copy of the provided options if they have already been prepared earlier", + inject(function($animateCss, $$rAF) { + + var options = { + from: { height: '50px' }, + to: { width: '50px' }, + addClass: 'one', + removeClass: 'two' + }; + + options.$$prepared = true; + var runner = $animateCss(element, options).start(); + runner.end(); + + $$rAF.flush(); + + expect(options.addClass).toBeFalsy(); + expect(options.removeClass).toBeFalsy(); + expect(options.to).toBeFalsy(); + expect(options.from).toBeFalsy(); + })); + it("should apply the provided [from] CSS to the element", inject(function($animateCss) { $animateCss(element, { from: { height: '50px' }}).start(); expect(element.css('height')).toBe('50px'); diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 884f6c8846c4..d79d55a0774c 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -230,6 +230,9 @@ describe('$compile', function() { 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); @@ -300,17 +303,19 @@ describe('$compile', function() { })); // NOTE: This test may be redundant. - 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]); - })); + 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() { diff --git a/test/ng/controllerSpec.js b/test/ng/controllerSpec.js index 839e3310c5e6..253623d05a42 100644 --- a/test/ng/controllerSpec.js +++ b/test/ng/controllerSpec.js @@ -209,5 +209,16 @@ describe('$controller', function() { "Badly formed controller string 'ctrl as'. " + "Must match `__name__ as __id__` or `__name__`."); }); + + + it('should allow identifiers containing `$`', function() { + var scope = {}; + + $controllerProvider.register('FooCtrl', function() { this.mark = 'foo'; }); + + var foo = $controller('FooCtrl as $foo', {$scope: scope}); + expect(scope.$foo).toBe(foo); + expect(scope.$foo.mark).toBe('foo'); + }); }); }); diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 58689c0eea5f..b8ba4ed924d2 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -1196,7 +1196,7 @@ describe('input', function() { it('should validate if max is empty', function() { $rootScope.maxVal = undefined; - $rootScope.value = new Date(9999, 11, 31, 23, 59, 59); + $rootScope.value = new Date(3000, 11, 31, 23, 59, 59); $rootScope.$digest(); expect($rootScope.form.alias.$error.max).toBeFalsy(); diff --git a/test/ng/directive/ngRepeatSpec.js b/test/ng/directive/ngRepeatSpec.js index 4523718b64f0..e91939ca3194 100644 --- a/test/ng/directive/ngRepeatSpec.js +++ b/test/ng/directive/ngRepeatSpec.js @@ -1491,11 +1491,11 @@ describe('ngRepeat animations', function() { })); it('should not change the position of the block that is being animated away via a leave animation', - inject(function($compile, $rootScope, $animate, $document, $window, $sniffer, $timeout) { + inject(function($compile, $rootScope, $animate, $document, $sniffer, $timeout) { if (!$sniffer.transitions) return; var item; - var ss = createMockStyleSheet($document, $window); + var ss = createMockStyleSheet($document); try { diff --git a/test/ng/snifferSpec.js b/test/ng/snifferSpec.js index c16d4802d966..b1fafceff36d 100644 --- a/test/ng/snifferSpec.js +++ b/test/ng/snifferSpec.js @@ -88,7 +88,9 @@ describe('$sniffer', function() { inject(function($sniffer, $window) { var expectedPrefix; var ua = $window.navigator.userAgent.toLowerCase(); - if (/chrome/i.test(ua) || /safari/i.test(ua) || /webkit/i.test(ua)) { + if (/edge/i.test(ua)) { + expectedPrefix = 'Ms'; + } else if (/chrome/i.test(ua) || /safari/i.test(ua) || /webkit/i.test(ua)) { expectedPrefix = 'Webkit'; } else if (/firefox/i.test(ua)) { expectedPrefix = 'Moz'; diff --git a/test/ngAnimate/animateCssDriverSpec.js b/test/ngAnimate/animateCssDriverSpec.js index 97567e4c053e..abc4eefa7a89 100644 --- a/test/ngAnimate/animateCssDriverSpec.js +++ b/test/ngAnimate/animateCssDriverSpec.js @@ -69,11 +69,11 @@ describe("ngAnimate $$animateCssDriver", function() { element = jqLite('
'); - return function($$animateCssDriver, $document, $window) { + return function($$animateCssDriver, $document) { driver = function(details, cb) { return $$animateCssDriver(details, cb || noop); }; - ss = createMockStyleSheet($document, $window); + ss = createMockStyleSheet($document); }; })); diff --git a/test/ngAnimate/animateCssSpec.js b/test/ngAnimate/animateCssSpec.js index e43dd89a912c..7de34ee58615 100644 --- a/test/ngAnimate/animateCssSpec.js +++ b/test/ngAnimate/animateCssSpec.js @@ -12,6 +12,13 @@ describe("ngAnimate $animateCss", function() { : expect(className).not.toMatch(regex); } + function getPossiblyPrefixedStyleValue(element, styleProp) { + var value = element.css(prefix + styleProp); + if (isUndefined(value)) value = element.css(styleProp); + + return value; + } + function keyframeProgress(element, duration, delay) { browserTrigger(element, 'animationend', { timeStamp: Date.now() + ((delay || 1) * 1000), elapsedTime: duration }); @@ -28,9 +35,10 @@ describe("ngAnimate $animateCss", function() { var ss, prefix, triggerAnimationStartFrame; beforeEach(module(function() { - return function($document, $window, $sniffer, $$rAF, $animate) { + return function($document, $sniffer, $$rAF, $animate) { prefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; - ss = createMockStyleSheet($document, $window); + ss = createMockStyleSheet($document, prefix); + $animate.enabled(true); triggerAnimationStartFrame = function() { $$rAF.flush(); @@ -325,7 +333,7 @@ describe("ngAnimate $animateCss", function() { triggerAnimationStartFrame(); runner.pause(); - expect(element.css(prefix + 'animation-play-state')).toEqual('paused'); + expect(getPossiblyPrefixedStyleValue(element, 'animation-play-state')).toEqual('paused'); runner.resume(); expect(element.attr('style')).toBeFalsy(); })); @@ -346,7 +354,7 @@ describe("ngAnimate $animateCss", function() { triggerAnimationStartFrame(); runner.pause(); - expect(element.css(prefix + 'animation-play-state')).toEqual('paused'); + expect(getPossiblyPrefixedStyleValue(element, 'animation-play-state')).toEqual('paused'); runner.end(); expect(element.attr('style')).toBeFalsy(); })); @@ -784,8 +792,8 @@ describe("ngAnimate $animateCss", function() { jqLite($document[0].body).append($rootElement); - ss.addRule('.ng-enter-stagger', prefix + 'animation-delay:0.2s'); - ss.addRule('.ng-enter', prefix + 'animation:my_animation 2s;'); + ss.addPossiblyPrefixedRule('.ng-enter-stagger', 'animation-delay:0.2s'); + ss.addPossiblyPrefixedRule('.ng-enter', 'animation:my_animation 2s;'); var i, element, elements = []; for (i = 0; i < 5; i++) { @@ -803,7 +811,7 @@ describe("ngAnimate $animateCss", function() { if (i === 0) { // the first element is always run right away expect(element.attr('style')).toBeFalsy(); } else { - expect(element.css(prefix + 'animation-play-state')).toBe('paused'); + expect(getPossiblyPrefixedStyleValue(element, 'animation-play-state')).toBe('paused'); } } })); @@ -879,7 +887,7 @@ describe("ngAnimate $animateCss", function() { jqLite($document[0].body).append($rootElement); ss.addRule('.ng-enter-stagger', 'transition-delay:0.2s'); - ss.addRule('.transition-animation', prefix + 'animation:2s 5s my_animation;'); + ss.addPossiblyPrefixedRule('.transition-animation', 'animation: 2s 5s my_animation;'); for (var i = 0; i < 5; i++) { var elm = jqLite('
'); @@ -898,11 +906,11 @@ describe("ngAnimate $animateCss", function() { jqLite($document[0].body).append($rootElement); - ss.addRule('.ng-enter-stagger', 'transition-delay:0.5s;' + - prefix + 'animation-delay:1s'); + ss.addPossiblyPrefixedRule('.ng-enter-stagger', 'transition-delay: 0.5s; ' + + 'animation-delay: 1s'); - ss.addRule('.ng-enter', 'transition:10s linear all;' + - prefix + 'animation:my_animation 20s'); + ss.addPossiblyPrefixedRule('.ng-enter', 'transition: 10s linear all; ' + + 'animation: 20s my_animation'); var i, elm, elements = []; for (i = 0; i < 5; i++) { @@ -1258,14 +1266,14 @@ describe("ngAnimate $animateCss", function() { // At this point, the animation should still be running (closing timeout is 7500ms ... duration * 1.5 + delay => 7.5) $timeout.flush(7000); - expect(element.css(prefix + 'transition-delay')).toBe('3s'); - expect(element.css(prefix + 'transition-duration')).toBe('3s'); + expect(getPossiblyPrefixedStyleValue(element, 'transition-delay')).toBe('3s'); + expect(getPossiblyPrefixedStyleValue(element, 'transition-duration')).toBe('3s'); // Let's flush the remaining amount of time for the timeout timer to kick in $timeout.flush(500); - expect(element.css(prefix + 'transition-duration')).toBeOneOf('', '0s'); - expect(element.css(prefix + 'transition-delay')).toBeOneOf('', '0s'); + expect(getPossiblyPrefixedStyleValue(element, 'transition-duration')).toBeOneOf('', '0s'); + expect(getPossiblyPrefixedStyleValue(element, 'transition-delay')).toBeOneOf('', '0s'); })); it("should ignore a boolean options.delay value for the closing timeout", @@ -1291,14 +1299,14 @@ describe("ngAnimate $animateCss", function() { // At this point, the animation should still be running (closing timeout is 4500ms ... duration * 1.5 => 4.5) $timeout.flush(4000); - expect(element.css(prefix + 'transition-delay')).toBeOneOf('initial', '0s'); - expect(element.css(prefix + 'transition-duration')).toBe('3s'); + expect(getPossiblyPrefixedStyleValue(element, 'transition-delay')).toBeOneOf('initial', '0s'); + expect(getPossiblyPrefixedStyleValue(element, 'transition-duration')).toBe('3s'); // Let's flush the remaining amount of time for the timeout timer to kick in $timeout.flush(500); - expect(element.css(prefix + 'transition-duration')).toBeOneOf('', '0s'); - expect(element.css(prefix + 'transition-delay')).toBeOneOf('', '0s'); + expect(getPossiblyPrefixedStyleValue(element, 'transition-duration')).toBeOneOf('', '0s'); + expect(getPossiblyPrefixedStyleValue(element, 'transition-delay')).toBeOneOf('', '0s'); })); }); @@ -2028,6 +2036,28 @@ describe("ngAnimate $animateCss", function() { expect(copiedOptions).toEqual(initialOptions); })); + it("should not create a copy of the provided options if they have already been prepared earlier", + inject(function($animate, $animateCss) { + + var options = { + from: { height: '50px' }, + to: { width: '50px' }, + addClass: 'one', + removeClass: 'two' + }; + + options.$$prepared = true; + var runner = $animateCss(element, options).start(); + runner.end(); + + $animate.flush(); + + expect(options.addClass).toBeFalsy(); + expect(options.removeClass).toBeFalsy(); + expect(options.to).toBeFalsy(); + expect(options.from).toBeFalsy(); + })); + describe("[$$skipPreparationClasses]", function() { it('should not apply and remove the preparation classes to the element when true', inject(function($animateCss) { @@ -2104,7 +2134,7 @@ describe("ngAnimate $animateCss", function() { triggerAnimationStartFrame(); - expect(element.css(prefix + 'animation-duration')).toEqual('5s'); + expect(getPossiblyPrefixedStyleValue(element, 'animation-duration')).toEqual('5s'); })); it("should remove all inline keyframe styling when an animation completes if a custom duration was applied", @@ -2145,7 +2175,7 @@ describe("ngAnimate $animateCss", function() { triggerAnimationStartFrame(); - expect(element.css(prefix + 'animation-delay')).toEqual('5s'); + expect(getPossiblyPrefixedStyleValue(element, 'animation-delay')).toEqual('5s'); browserTrigger(element, 'animationend', { timeStamp: Date.now() + 5000, elapsedTime: 1.5 }); @@ -2299,7 +2329,7 @@ describe("ngAnimate $animateCss", function() { triggerAnimationStartFrame(); - expect(element.css(prefix + 'animation-delay')).toEqual('400s'); + expect(getPossiblyPrefixedStyleValue(element, 'animation-delay')).toEqual('400s'); expect(element.attr('style')).not.toContain('transition-delay'); })); @@ -2323,7 +2353,7 @@ describe("ngAnimate $animateCss", function() { triggerAnimationStartFrame(); - expect(element.css(prefix + 'animation-delay')).toEqual('10s'); + expect(getPossiblyPrefixedStyleValue(element, 'animation-delay')).toEqual('10s'); expect(element.css('transition-delay')).toEqual('10s'); })); @@ -2363,7 +2393,7 @@ describe("ngAnimate $animateCss", function() { var assertionsRun = false; classSpy = function() { assertionsRun = true; - expect(element.css(prefix + 'animation-delay')).toEqual('2s'); + expect(getPossiblyPrefixedStyleValue(element, 'animation-delay')).toEqual('2s'); expect(element.css('transition-delay')).toEqual('2s'); expect(element).not.toHaveClass('superman'); }; @@ -2397,8 +2427,8 @@ describe("ngAnimate $animateCss", function() { it("should consider a negative value when delay:true is used with a keyframe animation", inject(function($animateCss, $rootElement) { - ss.addRule('.ng-enter', prefix + 'animation:2s keyframe_animation; ' + - prefix + 'animation-delay: -1s;'); + ss.addPossiblyPrefixedRule('.ng-enter', 'animation: 2s keyframe_animation; ' + + 'animation-delay: -1s;'); var options = { delay: true, @@ -2411,7 +2441,7 @@ describe("ngAnimate $animateCss", function() { animator.start(); triggerAnimationStartFrame(); - expect(element.css(prefix + 'animation-delay')).toContain('-1s'); + expect(getPossiblyPrefixedStyleValue(element, 'animation-delay')).toContain('-1s'); })); they("should consider a negative value when a negative option delay is provided for a $prop animation", { @@ -2421,17 +2451,17 @@ describe("ngAnimate $animateCss", function() { css: 'transition:2s linear all' }; }, - 'keyframe': function(prefix) { + 'keyframe': function() { return { - prop: prefix + 'animation-delay', - css: prefix + 'animation:2s keyframe_animation' + prop: 'animation-delay', + css: 'animation: 2s keyframe_animation' }; } }, function(testDetailsFactory) { inject(function($animateCss, $rootElement) { var testDetails = testDetailsFactory(prefix); - ss.addRule('.ng-enter', testDetails.css); + ss.addPossiblyPrefixedRule('.ng-enter', testDetails.css); var options = { delay: -2, event: 'enter', @@ -2443,7 +2473,7 @@ describe("ngAnimate $animateCss", function() { animator.start(); triggerAnimationStartFrame(); - expect(element.css(testDetails.prop)).toContain('-2s'); + expect(getPossiblyPrefixedStyleValue(element, testDetails.prop)).toContain('-2s'); }); }); @@ -2454,18 +2484,18 @@ describe("ngAnimate $animateCss", function() { css: 'transition:5s linear all; transition-delay: -2s' }; }, - 'animation': function(prefix) { + 'animation': function() { return { event: 'animationend', - css: prefix + 'animation:5s keyframe_animation; ' + prefix + 'animation-delay: -2s;' + css: 'animation: 5s keyframe_animation; animation-delay: -2s;' }; } }, function(testDetailsFactory) { inject(function($animateCss, $rootElement) { - var testDetails = testDetailsFactory(prefix); + var testDetails = testDetailsFactory(); var event = testDetails.event; - ss.addRule('.ng-enter', testDetails.css); + ss.addPossiblyPrefixedRule('.ng-enter', testDetails.css); var options = { event: 'enter', structural: true }; var animator = $animateCss(element, options); @@ -2571,15 +2601,15 @@ describe("ngAnimate $animateCss", function() { $animateCss(element, options).start(); triggerAnimationStartFrame(); - expect(element.css(prefix + 'transition-delay')).not.toEqual('4s'); - expect(element.css(prefix + 'transition-duration')).not.toEqual('6s'); + expect(getPossiblyPrefixedStyleValue(element, 'transition-delay')).not.toEqual('4s'); + expect(getPossiblyPrefixedStyleValue(element, 'transition-duration')).not.toEqual('6s'); options.to = { color: 'brown' }; $animateCss(element, options).start(); triggerAnimationStartFrame(); - expect(element.css(prefix + 'transition-delay')).toEqual('4s'); - expect(element.css(prefix + 'transition-duration')).toEqual('6s'); + expect(getPossiblyPrefixedStyleValue(element, 'transition-delay')).toEqual('4s'); + expect(getPossiblyPrefixedStyleValue(element, 'transition-duration')).toEqual('6s'); })); }); @@ -2652,9 +2682,9 @@ describe("ngAnimate $animateCss", function() { triggerAnimationStartFrame(); - expect(element.css(prefix + 'animation-delay')).toEqual('50s'); - expect(element.css(prefix + 'animation-duration')).toEqual('5.5s'); - expect(element.css(prefix + 'animation-name')).toEqual('my_animation'); + expect(getPossiblyPrefixedStyleValue(element, 'animation-delay')).toEqual('50s'); + expect(getPossiblyPrefixedStyleValue(element, 'animation-duration')).toEqual('5.5s'); + expect(getPossiblyPrefixedStyleValue(element, 'animation-name')).toEqual('my_animation'); })); it("should be able to execute the animation if it is the only provided value", @@ -2669,9 +2699,9 @@ describe("ngAnimate $animateCss", function() { animator.start(); triggerAnimationStartFrame(); - expect(element.css(prefix + 'animation-delay')).toEqual('10s'); - expect(element.css(prefix + 'animation-duration')).toEqual('5.5s'); - expect(element.css(prefix + 'animation-name')).toEqual('my_animation'); + expect(getPossiblyPrefixedStyleValue(element, 'animation-delay')).toEqual('10s'); + expect(getPossiblyPrefixedStyleValue(element, 'animation-duration')).toEqual('5.5s'); + expect(getPossiblyPrefixedStyleValue(element, 'animation-name')).toEqual('my_animation'); })); }); @@ -2964,7 +2994,7 @@ describe("ngAnimate $animateCss", function() { expect(element.css('transition-duration')).toMatch('10s'); - expect(element.css(prefix + 'animation-duration')).toEqual('10s'); + expect(getPossiblyPrefixedStyleValue(element, 'animation-duration')).toEqual('10s'); })); }); @@ -3004,7 +3034,7 @@ describe("ngAnimate $animateCss", function() { inject(function($animateCss) { ss.addRule('.red', 'transition: 1s linear all;'); - ss.addRule('.blue', prefix + 'animation:my_keyframe 1s;'); + ss.addPossiblyPrefixedRule('.blue', 'animation: 1s my_keyframe;'); var easing = 'ease-out'; var animator = $animateCss(element, { addClass: 'red blue', easing: easing }); animator.start(); diff --git a/test/ngAnimate/integrationSpec.js b/test/ngAnimate/integrationSpec.js index 5c67897c8492..f3758250a00a 100644 --- a/test/ngAnimate/integrationSpec.js +++ b/test/ngAnimate/integrationSpec.js @@ -7,10 +7,10 @@ describe('ngAnimate integration tests', function() { var element, html, ss; beforeEach(module(function() { - return function($rootElement, $document, $window, $animate) { + return function($rootElement, $document, $animate) { $animate.enabled(true); - ss = createMockStyleSheet($document, $window); + ss = createMockStyleSheet($document); var body = jqLite($document[0].body); html = function(element) { @@ -28,6 +28,27 @@ describe('ngAnimate integration tests', function() { describe('CSS animations', function() { if (!browserSupportsCssAnimations()) return; + it("should only create a single copy of the provided animation options", + inject(function($rootScope, $rootElement, $animate) { + + ss.addRule('.animate-me', 'transition:2s linear all;'); + + var element = jqLite('
'); + html(element); + + var myOptions = {to: { 'color': 'red' }}; + + var spy = spyOn(window, 'copy'); + expect(spy).not.toHaveBeenCalled(); + + var animation = $animate.leave(element, myOptions); + $rootScope.$digest(); + $animate.flush(); + + expect(spy).toHaveBeenCalledOnce(); + dealoc(element); + })); + they('should render an $prop animation', ['enter', 'leave', 'move', 'addClass', 'removeClass', 'setClass'], function(event) { @@ -426,6 +447,32 @@ describe('ngAnimate integration tests', function() { expect(element).not.toHaveClass('hide'); })); + it('should handle ng-if & ng-class with a class that is removed before its add animation has concluded', function() { + inject(function($animate, $rootScope, $compile, $timeout, $$rAF) { + + ss.addRule('.animate-me', 'transition: all 0.5s;'); + + element = jqLite('
'); + + html(element); + $rootScope.blue = true; + $rootScope.red = true; + $compile(element)($rootScope); + $rootScope.$digest(); + + var child = element.find('div'); + + // Trigger class removal before the add animation has been concluded + $rootScope.blue = false; + $animate.closeAndFlush(); + + expect(child).toHaveClass('red'); + expect(child).not.toHaveClass('blue'); + }); + }); }); describe('JS animations', function() { diff --git a/test/ngMock/angular-mocksSpec.js b/test/ngMock/angular-mocksSpec.js index 14fb543eb374..30b096a35b69 100644 --- a/test/ngMock/angular-mocksSpec.js +++ b/test/ngMock/angular-mocksSpec.js @@ -1857,6 +1857,198 @@ describe('ngMock', function() { }); }); }); + + + describe('$componentController', function() { + it('should instantiate a simple controller defined inline in a component', function() { + function TestController($scope, a, b) { + this.$scope = $scope; + this.a = a; + this.b = b; + } + module(function($compileProvider) { + $compileProvider.component('test', { + controller: TestController + }); + }); + inject(function($componentController, $rootScope) { + var $scope = {}; + var ctrl = $componentController('test', { $scope: $scope, a: 'A', b: 'B' }, { x: 'X', y: 'Y' }); + expect(ctrl).toEqual({ $scope: $scope, a: 'A', b: 'B', x: 'X', y: 'Y' }); + expect($scope.$ctrl).toBe(ctrl); + }); + }); + + it('should instantiate a controller with $$inject annotation defined inline in a component', function() { + function TestController(x, y, z) { + this.$scope = x; + this.a = y; + this.b = z; + } + TestController.$inject = ['$scope', 'a', 'b']; + module(function($compileProvider) { + $compileProvider.component('test', { + controller: TestController + }); + }); + inject(function($componentController, $rootScope) { + var $scope = {}; + var ctrl = $componentController('test', { $scope: $scope, a: 'A', b: 'B' }, { x: 'X', y: 'Y' }); + expect(ctrl).toEqual({ $scope: $scope, a: 'A', b: 'B', x: 'X', y: 'Y' }); + expect($scope.$ctrl).toBe(ctrl); + }); + }); + + it('should instantiate a named controller defined in a component', function() { + function TestController($scope, a, b) { + this.$scope = $scope; + this.a = a; + this.b = b; + } + module(function($controllerProvider, $compileProvider) { + $controllerProvider.register('TestController', TestController); + $compileProvider.component('test', { + controller: 'TestController' + }); + }); + inject(function($componentController, $rootScope) { + var $scope = {}; + var ctrl = $componentController('test', { $scope: $scope, a: 'A', b: 'B' }, { x: 'X', y: 'Y' }); + expect(ctrl).toEqual({ $scope: $scope, a: 'A', b: 'B', x: 'X', y: 'Y' }); + expect($scope.$ctrl).toBe(ctrl); + }); + }); + + it('should instantiate a named controller with `controller as` syntax defined in a component', function() { + function TestController($scope, a, b) { + this.$scope = $scope; + this.a = a; + this.b = b; + } + module(function($controllerProvider, $compileProvider) { + $controllerProvider.register('TestController', TestController); + $compileProvider.component('test', { + controller: 'TestController as testCtrl' + }); + }); + inject(function($componentController, $rootScope) { + var $scope = {}; + var ctrl = $componentController('test', { $scope: $scope, a: 'A', b: 'B' }, { x: 'X', y: 'Y' }); + expect(ctrl).toEqual({ $scope: $scope, a: 'A', b: 'B', x: 'X', y: 'Y' }); + expect($scope.testCtrl).toBe(ctrl); + }); + }); + + it('should instantiate the controller of the restrict:\'E\' component if there are more directives with the same name but not \'E\' restrictness', function() { + function TestController() { + this.r = 6779; + } + module(function($compileProvider) { + $compileProvider.directive('test', function() { + return { restrict: 'A' }; + }); + $compileProvider.component('test', { + controller: TestController + }); + }); + inject(function($componentController, $rootScope) { + var ctrl = $componentController('test', { $scope: {} }); + expect(ctrl).toEqual({ r: 6779 }); + }); + }); + + it('should instantiate the controller of the restrict:\'E\' component if there are more directives with the same name and \'E\' restrictness but no controller', function() { + function TestController() { + this.r = 22926; + } + module(function($compileProvider) { + $compileProvider.directive('test', function() { + return { restrict: 'E' }; + }); + $compileProvider.component('test', { + controller: TestController + }); + }); + inject(function($componentController, $rootScope) { + var ctrl = $componentController('test', { $scope: {} }); + expect(ctrl).toEqual({ r: 22926 }); + }); + }); + + it('should instantiate the controller of the directive with controller if there are more directives', function() { + function TestController() { + this.r = 18842; + } + module(function($compileProvider) { + $compileProvider.directive('test', function() { + return { }; + }); + $compileProvider.directive('test', function() { + return { + controller: TestController, + controllerAs: '$ctrl' + }; + }); + }); + inject(function($componentController, $rootScope) { + var ctrl = $componentController('test', { $scope: {} }); + expect(ctrl).toEqual({ r: 18842 }); + }); + }); + + it('should fail if there is no directive with restrict:\'E\' compatible and controller', function() { + function TestController() { + this.r = 31145; + } + module(function($compileProvider) { + $compileProvider.directive('test', function() { + return { + restrict: 'AC', + controller: TestController + }; + }); + $compileProvider.directive('test', function() { + return { + restrict: 'E', + controller: TestController + }; + }); + $compileProvider.directive('test', function() { + return { restrict: 'E' }; + }); + }); + inject(function($componentController, $rootScope) { + expect(function() { + $componentController('test', { $scope: {} }); + }).toThrow('No component found'); + }); + }); + + it('should fail if there more than two components with same name', function() { + function TestController($scope, a, b) { + this.$scope = $scope; + this.a = a; + this.b = b; + } + module(function($compileProvider) { + $compileProvider.directive('test', function() { + return { + controller: TestController, + controllerAs: '$ctrl' + }; + }); + $compileProvider.component('test', { + controller: TestController + }); + }); + inject(function($componentController, $rootScope) { + expect(function() { + var $scope = {}; + $componentController('test', { $scope: $scope, a: 'A', b: 'B' }, { x: 'X', y: 'Y' }); + }).toThrow('Too many components found'); + }); + }); + }); }); @@ -1964,8 +2156,8 @@ describe('ngMockE2E', function() { } }); - return function($animate, $rootElement, $document, $rootScope, $window) { - ss = createMockStyleSheet($document, $window); + return function($animate, $rootElement, $document, $rootScope) { + ss = createMockStyleSheet($document); element = angular.element('
'); $rootElement.append(element); @@ -2148,12 +2340,36 @@ describe('ngMockE2E', function() { expect(animationLog).toEqual(['start leave', 'end leave']); })); + it('should not throw when a regular animation has no javascript animation', + inject(function($animate, $$animation, $rootElement) { + + if (!browserSupportsCssAnimations()) return; + + var element = jqLite('
'); + $rootElement.append(element); + + // Make sure the animation has valid $animateCss options + $$animation(element, null, { + from: { background: 'red' }, + to: { background: 'blue' }, + duration: 1, + transitionStyle: 'all 1s' + }); + + expect(function() { + $animate.closeAndFlush(); + }).not.toThrow(); + + dealoc(element); + })); + it('should throw an error if there are no animations to close and flush', inject(function($animate) { expect(function() { $animate.closeAndFlush(); }).toThrow('No pending animations ready to be closed or flushed'); + })); }); }); diff --git a/test/ngSanitize/sanitizeSpec.js b/test/ngSanitize/sanitizeSpec.js index b5c9d33bc0b2..6455ba94d6ee 100644 --- a/test/ngSanitize/sanitizeSpec.js +++ b/test/ngSanitize/sanitizeSpec.js @@ -1,6 +1,8 @@ 'use strict'; describe('HTML', function() { + var ua = window.navigator.userAgent; + var isChrome = /Chrome/.test(ua) && !/Edge/.test(ua); var expectHTML; @@ -222,7 +224,7 @@ describe('HTML', function() { .toEqual(''); }); - if (/Chrome/.test(window.navigator.userAgent)) { + if (isChrome) { it('should prevent mXSS attacks', function() { expectHTML('CLICKME').toBe('CLICKME'); }); @@ -245,7 +247,8 @@ describe('HTML', function() { expectHTML('') .toBeOneOf('', '', - ''); + '', + ''); }); it('should not ignore white-listed svg camelCased attributes', function() { @@ -283,6 +286,7 @@ describe('HTML', function() { it('should not accept SVG animation tags', function() { expectHTML('Click me') .toBeOneOf('Click me', + 'Click me', 'Click me'); expectHTML('' +