diff --git a/.travis.yml b/.travis.yml index b8d925b15d37..38ac9be188db 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,10 +18,10 @@ before_script: - grunt bower - grunt bower - grunt package-without-bower - - grunt ci-checks - ./lib/sauce/sauce_connect_block.sh script: + - grunt ci-checks - ./travis_build.sh after_script: diff --git a/TRIAGING.md b/TRIAGING.md new file mode 100644 index 000000000000..ec7ac6e89a95 --- /dev/null +++ b/TRIAGING.md @@ -0,0 +1,55 @@ +# Triage new issues/PRs on github + +This document shows the steps the Angular team is using to triage issues. +The labels are used later on for planning releases. + +## Tips ## + +* install [github pr helper extension](https://github.com/petebacondarwin/github-pr-helper) and become 356% more productive +* Label "resolution:*" + * these tags can be used for labeling a closed issue/PR with a reason why it was closed. (we can add reasons as we need them, right there are only a few rejection reasons. it doesn't make sense to label issues that were fixed or prs that were merged) + + +## Process ## + +1. Open list of [non triaged issues](https://github.com/angular/angular.js/issues?direction=desc&milestone=none&page=1&sort=created&state=open) +1. Assign yourself: Pick an issue that is not assigned to anyone and assign it to you +1. Assign milestone: + * "Docs only" milestone - for documentation PR -> **Done**. + * Current/next milestone - regressions + * 1.2.x - everything else +1. Label "GH: *" (to be automated via Mary Poppins) + * PR - issue is a PR + * issue - otherwise +1. Bugs: + * Label "Type: Bug" + * Label "Type: Regression" - if the bug is a regression + * Duplicate? - Check if there are comments pointing out that this is a dupe, if they do exist verify that this is indeed a dupe and close it and go to the last step + * Reproducible? - Steps to reproduce the bug are clear, if not ask for clarification (ideally plunker or fiddle) + * Reproducible on master? - http://code.angularjs.org/snapshot/ + +1. Non bugs: + * Label "Type: Feature" or "Type: Chore" + * Label "needs: breaking change" - if needed + * Understandable? - verify if the description of the request is clear. if not ask for clarification + * Goals of angular core? - Often new features should be implemented as a third-party module rather than an addition to the core. + +1. Label "component: *" + * In rare cases, it's ok to have multiple components. +1. Label "impact: *" + * small - obscure issue affecting one or handful of developers + * medium - impacts some usage patterns + * large - impacts most or all of angular apps +1. Label "complexity: *" + * small - trivial change + * medium - non-trivial but straightforward change + * large - changes to many components in angular or any changes to $compile, ngRepeat or other "fun" components +1. Label "PRs welcome" for "GH: issue" + * if complexity is small or medium and the problem as well as solution are well captured in the issue +1. Label "cla: yes" for "GH: PR": + * otherwise prompt the contributor to sign the CLA +1. Label "origin: google" for issues from Google +1. Label "high priority" for security issues, major performance regressions or memory leaks + +1. Unassign yourself from the issue + diff --git a/closure/angular.js b/closure/angular.js index 0e520c535aca..d39b2ab876f3 100644 --- a/closure/angular.js +++ b/closure/angular.js @@ -1709,7 +1709,8 @@ angular.$routeProvider.when = function(path, route) {}; * resolve: (Object.|angular.$q.Promise * )>|undefined), - * redirectTo: (string|function()|undefined), + * redirectTo: ( + * string|function(Object., string, Object): string|undefined), * reloadOnSearch: (boolean|undefined) * }} */ @@ -1732,7 +1733,7 @@ angular.$routeProvider.Params.templateUrl; */ angular.$routeProvider.Params.resolve; -/** @type {string|function()} */ +/** @type {string|function(Object., string, Object): string} */ angular.$routeProvider.Params.redirectTo; /** @type {boolean} */ diff --git a/docs/content/tutorial/step_02.ngdoc b/docs/content/tutorial/step_02.ngdoc index f1906018046e..2489162c6ad3 100644 --- a/docs/content/tutorial/step_02.ngdoc +++ b/docs/content/tutorial/step_02.ngdoc @@ -184,7 +184,11 @@ http://pivotal.github.com/jasmine/ Jasmine home page} and at the {@link http://pivotal.github.io/jasmine/ Jasmine docs}. The angular-seed project is pre-configured to run all unit tests using {@link -http://karma-runner.github.io/ Karma}. To run the test, do the following: +http://karma-runner.github.io/ Karma}. Ensure that the necessary karma plugins are installed. +You can do this by issuing `npm install` into your terminal. + + +To run the test, do the following: 1. In a _separate_ terminal window or tab, go to the `angular-phonecat` directory and run `./scripts/test.sh` to start the Karma server (the config file necessary to start the server diff --git a/docs/content/tutorial/step_03.ngdoc b/docs/content/tutorial/step_03.ngdoc index a26a43a189b9..686f0854c25b 100644 --- a/docs/content/tutorial/step_03.ngdoc +++ b/docs/content/tutorial/step_03.ngdoc @@ -127,8 +127,8 @@ end-to-end tests! Use `./scripts/e2e-test.sh` script for that. End-to-end tests with unit tests, Karma will exit after the test run and will not automatically rerun the test suite on every file change. To rerun the test suite, execute the `e2e-test.sh` script again. -Note: You must ensure you've installed karma-ng-scenario prior to running the `e2e-test.sh` script. -You can do this by issuing `npm install karma-ng-scenario` into your terminal. +Note: You must ensure you've installed the karma-ng-scenario framework plugin prior to running the +`e2e-test.sh` script. You can do this by issuing `npm install` into your terminal. This test verifies that the search box and the repeater are correctly wired together. Notice how easy it is to write end-to-end tests in Angular. Although this example is for a simple test, it @@ -154,7 +154,7 @@ really is that easy to set up any functional, readable, end-to-end test. `ngController` declaration to the HTML element because it is the common parent of both the body and title elements: - + Be sure to __remove__ the `ng-controller` declaration from the body element. diff --git a/docs/content/tutorial/step_06.ngdoc b/docs/content/tutorial/step_06.ngdoc index 40e22dfce4cc..eaf0ad3cbbf7 100644 --- a/docs/content/tutorial/step_06.ngdoc +++ b/docs/content/tutorial/step_06.ngdoc @@ -64,7 +64,7 @@ We also added phone images next to each record using an image tag with the {@lin api/ng.directive:ngSrc ngSrc} directive. That directive prevents the browser from treating the angular `{{ expression }}` markup literally, and initiating a request to invalid url `http://localhost:8000/app/{{phone.imageUrl}}`, which it would have done if we had only -specified an attribute binding in a regular `src` attribute (``). +specified an attribute binding in a regular `src` attribute (``). Using the `ngSrc` directive prevents the browser from making an http request to an invalid location. diff --git a/docs/content/tutorial/step_12.ngdoc b/docs/content/tutorial/step_12.ngdoc index bd4333e4c012..1fabfd1c1b8c 100644 --- a/docs/content/tutorial/step_12.ngdoc +++ b/docs/content/tutorial/step_12.ngdoc @@ -43,7 +43,7 @@ __`app/index.html`.__
 ...
   
-  
+  
 
   
   
@@ -56,6 +56,10 @@ __`app/index.html`.__
 ...
 
+
+ **Important:** Be sure to use jQuery version `1.10.x`. AngularJS does not yet support jQuery `2.x`. +
+ Animations can now be created within the CSS code (`animations.css`) as well as the JavaScript code (`animations.js`). But before we start, let's create a new module which uses the ngAnimate module as a dependency just like we did before with `ngResource`. @@ -383,10 +387,6 @@ isn't required to do JavaScript animations with AngularJS, but we're going to us your own JavaScript animation library is beyond the scope of this tutorial. For more on `jQuery.animate`, see the {@link http://api.jquery.com/animate/ jQuery documentation}. -
- **Important:** Be sure to use jQuery version `1.10.x`. AngularJS does not yet support jQuery `2.x`. -
- The `addClass` and `removeClass` callback functions are called whenever an a class is added or removed on the element that contains the class we registered, which is in this case `.phone`. When the `.active` class is added to the element (via the `ng-class` directive) the `addClass` JavaScript callback will diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 8307f7e816e8..9ff50364ac97 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -951,39 +951,6 @@ var VALID_CLASS = 'ng-valid', * * - * ## Isolated Scope Pitfall - * - * Note that if you have a directive with an isolated scope, you cannot require `ngModel` - * since the model value will be looked up on the isolated scope rather than the outer scope. - * When the directive updates the model value, calling `ngModel.$setViewValue()` the property - * on the outer scope will not be updated. However you can get around this by using $parent. - * - * Here is an example of this situation. You'll notice that the first div is not updating the input. - * However the second div can update the input properly. - * - * - - angular.module('badIsolatedDirective', []).directive('isolate', function() { - return { - require: 'ngModel', - scope: { }, - template: '', - link: function(scope, element, attrs, ngModel) { - scope.$watch('innerModel', function(value) { - console.log(value); - ngModel.$setViewValue(value); - }); - } - }; - }); - - - -
-
-
- *
- * * */ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', @@ -1130,7 +1097,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * It will update the $viewValue, then pass this value through each of the functions in `$parsers`, * which includes any validators. The value that comes out of this `$parsers` pipeline, be applied to * `$modelValue` and the **expression** specified in the `ng-model` attribute. - * + * * Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called. * * Note that calling this function does not trigger a `$digest`. @@ -1187,6 +1154,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ ctrl.$render(); } } + + return value; }); }]; diff --git a/src/ng/directive/ngInit.js b/src/ng/directive/ngInit.js index d3d0f3c1931c..934b31cca1e2 100644 --- a/src/ng/directive/ngInit.js +++ b/src/ng/directive/ngInit.js @@ -16,6 +16,8 @@ * to initialize values on a scope. * * + * @priority 450 + * * @element ANY * @param {expression} ngInit {@link guide/expression Expression} to eval. * @@ -47,6 +49,7 @@ */ var ngInitDirective = ngDirective({ + priority: 450, compile: function() { return { pre: function(scope, element, attrs) { diff --git a/src/ng/interpolate.js b/src/ng/interpolate.js index 43593f91fc14..b78f44a4c9c5 100644 --- a/src/ng/interpolate.js +++ b/src/ng/interpolate.js @@ -103,8 +103,8 @@ function $InterpolateProvider() { *
          var $interpolate = ...; // injected
-         var exp = $interpolate('Hello {{name}}!');
-         expect(exp({name:'Angular'}).toEqual('Hello Angular!');
+         var exp = $interpolate('Hello {{name | uppercase}}!');
+         expect(exp({name:'Angular'}).toEqual('Hello ANGULAR!');
        
* * diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 13ee4c70a0b1..a54fdc98fc1e 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -670,11 +670,12 @@ function $RootScopeProvider(){ */ $destroy: function() { // we can't destroy the root scope or a scope that has been already destroyed - if ($rootScope == this || this.$$destroyed) return; + if (this.$$destroyed) return; var parent = this.$parent; this.$broadcast('$destroy'); this.$$destroyed = true; + if (this === $rootScope) return; if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling; diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index bbe056ed45fa..8ff7b4298725 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -258,6 +258,19 @@ angular.module('ngAnimate', ['ng']) var NG_ANIMATE_CLASS_NAME = 'ng-animate'; var rootAnimateState = {running: true}; + function extractElementNode(element) { + for(var i = 0; i < element.length; i++) { + var elm = element[i]; + if(elm.nodeType == ELEMENT_NODE) { + return elm; + } + } + } + + function isMatchingElement(elm1, elm2) { + return extractElementNode(elm1) == extractElementNode(elm2); + } + $provide.decorator('$animate', ['$delegate', '$injector', '$sniffer', '$rootElement', '$timeout', '$rootScope', '$document', function($delegate, $injector, $sniffer, $rootElement, $timeout, $rootScope, $document) { @@ -556,7 +569,16 @@ angular.module('ngAnimate', ['ng']) and the onComplete callback will be fired once the animation is fully complete. */ function performAnimation(animationEvent, className, element, parentElement, afterElement, domOperation, doneCallback) { - var currentClassName = element.attr('class') || ''; + var node = extractElementNode(element); + //transcluded directives may sometimes fire an animation using only comment nodes + //best to catch this early on to prevent any animation operations from occurring + if(!node) { + fireDOMOperation(); + closeAnimation(); + return; + } + + var currentClassName = node.className; var classes = currentClassName + ' ' + className; var animationLookup = (' ' + classes).replace(/\s+/g,'.'); if (!parentElement) { @@ -760,11 +782,7 @@ angular.module('ngAnimate', ['ng']) } function cancelChildAnimations(element) { - var node = element[0]; - if(node.nodeType != ELEMENT_NODE) { - return; - } - + var node = extractElementNode(element); forEach(node.querySelectorAll('.' + NG_ANIMATE_CLASS_NAME), function(element) { element = angular.element(element); var data = element.data(NG_ANIMATE_STATE); @@ -788,7 +806,7 @@ angular.module('ngAnimate', ['ng']) } function cleanup(element) { - if(element[0] == $rootElement[0]) { + if(isMatchingElement(element, $rootElement)) { if(!rootAnimateState.disabled) { rootAnimateState.running = false; rootAnimateState.structural = false; @@ -802,7 +820,7 @@ angular.module('ngAnimate', ['ng']) function animationsDisabled(element, parentElement) { if (rootAnimateState.disabled) return true; - if(element[0] == $rootElement[0]) { + if(isMatchingElement(element, $rootElement)) { return rootAnimateState.disabled || rootAnimateState.running; } @@ -812,7 +830,7 @@ angular.module('ngAnimate', ['ng']) //any animations on it if(parentElement.length === 0) break; - var isRoot = parentElement[0] == $rootElement[0]; + var isRoot = isMatchingElement(parentElement, $rootElement); var state = isRoot ? rootAnimateState : parentElement.data(NG_ANIMATE_STATE); var result = state && (!!state.disabled || !!state.running); if(isRoot || result) { @@ -865,6 +883,7 @@ angular.module('ngAnimate', ['ng']) var NG_ANIMATE_CSS_DATA_KEY = '$$ngAnimateCSS3Data'; var NG_ANIMATE_FALLBACK_CLASS_NAME = 'ng-animate-start'; var NG_ANIMATE_FALLBACK_ACTIVE_CLASS_NAME = 'ng-animate-active'; + var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3; var lookupCache = {}; var parentCounter = 0; @@ -959,7 +978,7 @@ angular.module('ngAnimate', ['ng']) parentElement.data(NG_ANIMATE_PARENT_KEY, ++parentCounter); parentID = parentCounter; } - return parentID + '-' + element[0].className; + return parentID + '-' + extractElementNode(element).className; } function animateSetup(element, className) { @@ -994,7 +1013,6 @@ angular.module('ngAnimate', ['ng']) return false; } - var node = element[0]; //temporarily disable the transition so that the enter styles //don't animate twice (this is here to avoid a bug in Chrome/FF). var activeClassName = ''; @@ -1024,35 +1042,37 @@ angular.module('ngAnimate', ['ng']) } function blockTransitions(element) { - element[0].style[TRANSITION_PROP + PROPERTY_KEY] = 'none'; + extractElementNode(element).style[TRANSITION_PROP + PROPERTY_KEY] = 'none'; } function blockKeyframeAnimations(element) { - element[0].style[ANIMATION_PROP] = 'none 0s'; + extractElementNode(element).style[ANIMATION_PROP] = 'none 0s'; } function unblockTransitions(element) { - var node = element[0], prop = TRANSITION_PROP + PROPERTY_KEY; + var prop = TRANSITION_PROP + PROPERTY_KEY; + var node = extractElementNode(element); if(node.style[prop] && node.style[prop].length > 0) { node.style[prop] = ''; } } function unblockKeyframeAnimations(element) { - var node = element[0], prop = ANIMATION_PROP; + var prop = ANIMATION_PROP; + var node = extractElementNode(element); if(node.style[prop] && node.style[prop].length > 0) { - element[0].style[prop] = ''; + node.style[prop] = ''; } } function animateRun(element, className, activeAnimationComplete) { var data = element.data(NG_ANIMATE_CSS_DATA_KEY); - if(!element.hasClass(className) || !data) { + var node = extractElementNode(element); + if(node.className.indexOf(className) == -1 || !data) { activeAnimationComplete(); return; } - var node = element[0]; var timings = data.timings; var stagger = data.stagger; var maxDuration = data.maxDuration; @@ -1095,6 +1115,9 @@ angular.module('ngAnimate', ['ng']) } if(appliedStyles.length > 0) { + //the element being animated may sometimes contain comment nodes in + //the jqLite object, so we're safe to use a single variable to house + //the styles since there is always only one element being animated var oldStyle = node.getAttribute('style') || ''; node.setAttribute('style', oldStyle + ' ' + style); } @@ -1109,6 +1132,7 @@ angular.module('ngAnimate', ['ng']) element.off(css3AnimationEvents, onAnimationProgress); element.removeClass(activeClassName); animateClose(element, className); + var node = extractElementNode(element); for (var i in appliedStyles) { node.style.removeProperty(appliedStyles[i]); } @@ -1118,6 +1142,11 @@ angular.module('ngAnimate', ['ng']) event.stopPropagation(); var ev = event.originalEvent || event; var timeStamp = ev.$manualTimeStamp || ev.timeStamp || Date.now(); + + /* Firefox (or possibly just Gecko) likes to not round values up + * when a ms measurement is used for the animation */ + var elapsedTime = parseFloat(ev.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES)); + /* $manualTimeStamp is a mocked timeStamp value which is set * within browserTrigger(). This is only here so that tests can * mock animations properly. Real events fallback to event.timeStamp, @@ -1125,7 +1154,7 @@ angular.module('ngAnimate', ['ng']) * We're checking to see if the timeStamp surpasses the expected delay, * but we're using elapsedTime instead of the timeStamp on the 2nd * pre-condition since animations sometimes close off early */ - if(Math.max(timeStamp - startTime, 0) >= maxDelayTime && ev.elapsedTime >= maxDuration) { + if(Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) { activeAnimationComplete(); } } @@ -1203,7 +1232,7 @@ angular.module('ngAnimate', ['ng']) } var parentElement = element.parent(); - var clone = angular.element(element[0].cloneNode()); + var clone = angular.element(extractElementNode(element).cloneNode()); //make the element super hidden and override any CSS style values clone.attr('style','position:absolute; top:-9999px; left:-9999px'); diff --git a/src/ngResource/resource.js b/src/ngResource/resource.js index e2499864ff6f..8e63c4e8f4cd 100644 --- a/src/ngResource/resource.js +++ b/src/ngResource/resource.js @@ -24,6 +24,25 @@ function lookupDottedPath(obj, path) { return obj; } +/** + * Create a shallow copy of an object and clear other fields from the destination + */ +function shallowClearAndCopy(src, dst) { + dst = dst || {}; + + angular.forEach(dst, function(value, key){ + delete dst[key]; + }); + + for (var key in src) { + if (src.hasOwnProperty(key) && key.substr(0, 2) !== '$$') { + dst[key] = src[key]; + } + } + + return dst; +} + /** * @ngdoc overview * @name ngResource @@ -393,7 +412,7 @@ angular.module('ngResource', ['ng']). } function Resource(value){ - copy(value || {}, this); + shallowClearAndCopy(value || {}, this); } forEach(actions, function(action, name) { @@ -465,7 +484,7 @@ angular.module('ngResource', ['ng']). if (data) { // Need to convert action.isArray to boolean in case it is undefined // jshint -W018 - if ( angular.isArray(data) !== (!!action.isArray) ) { + if (angular.isArray(data) !== (!!action.isArray)) { throw $resourceMinErr('badcfg', 'Error in resource configuration. Expected ' + 'response to contain an {0} but got an {1}', action.isArray?'array':'object', angular.isArray(data)?'array':'object'); @@ -477,7 +496,7 @@ angular.module('ngResource', ['ng']). value.push(new Resource(item)); }); } else { - copy(data, value); + shallowClearAndCopy(data, value); value.$promise = promise; } } diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 892c1b7f534d..c568e807d112 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -383,6 +383,29 @@ describe('ngModel', function() { dealoc(element); }); }); + + it('should keep previously defined watches consistent when changes in validity are made', + inject(function($compile, $rootScope) { + + var isFormValid; + $rootScope.$watch('myForm.$valid', function(value) { isFormValid = value; }); + + var element = $compile('
' + + '' + + '
')($rootScope); + + $rootScope.$apply(); + expect(isFormValid).toBe(false); + expect($rootScope.myForm.$valid).toBe(false); + + $rootScope.value='value'; + $rootScope.$apply(); + expect(isFormValid).toBe(true); + expect($rootScope.myForm.$valid).toBe(true); + + dealoc(element); + })); + }); diff --git a/test/ng/directive/ngInitSpec.js b/test/ng/directive/ngInitSpec.js index 00038621a495..9ed930ad39eb 100644 --- a/test/ng/directive/ngInitSpec.js +++ b/test/ng/directive/ngInitSpec.js @@ -13,4 +13,30 @@ describe('ngInit', function() { element = $compile('
')($rootScope); expect($rootScope.a).toEqual(123); })); + + + it("should be evaluated before ngInclude", inject(function($rootScope, $templateCache, $compile) { + $templateCache.put('template1.tpl', '1'); + $templateCache.put('template2.tpl', '2'); + $rootScope.template = 'template1.tpl'; + element = $compile('
')($rootScope); + $rootScope.$digest(); + expect($rootScope.template).toEqual('template2.tpl'); + expect(element.find('span').text()).toEqual('2'); + })); + + + it("should be evaluated after ngController", function() { + module(function($controllerProvider) { + $controllerProvider.register('TestCtrl', function($scope) {}); + }); + inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.$digest(); + expect($rootScope.test).toBeUndefined(); + expect(element.children('div').scope().test).toEqual(123); + }); + }); }); diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js index 69447a3c20b9..287b535634da 100644 --- a/test/ng/rootScopeSpec.js +++ b/test/ng/rootScopeSpec.js @@ -599,10 +599,14 @@ describe('Scope', function() { })); - it('should ignore remove on root', inject(function($rootScope) { + it('should broadcast $destroy on rootScope', inject(function($rootScope) { + var spy = spyOn(angular, 'noop'); + $rootScope.$on('$destroy', angular.noop); $rootScope.$destroy(); $rootScope.$digest(); expect(log).toEqual('123'); + expect(spy).toHaveBeenCalled(); + expect($rootScope.$$destroyed).toBe(true); })); diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index b4635bd0907c..44b623b43ef1 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -2852,5 +2852,79 @@ describe("ngAnimate", function() { $timeout.flush(); }); }); + + it('should round up long elapsedTime values to close off a CSS3 animation', + inject(function($rootScope, $compile, $rootElement, $document, $animate, $sniffer, $timeout, $window) { + if (!$sniffer.animations) return; + + ss.addRule('.millisecond-transition.ng-leave', '-webkit-transition:510ms linear all;' + + 'transition:510ms linear all;'); + + var element = $compile('
')($rootScope); + $rootElement.append(element); + jqLite($document[0].body).append($rootElement); + + $animate.leave(element); + $rootScope.$digest(); + + $timeout.flush(); + + browserTrigger(element, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 0.50999999991 }); + + expect($rootElement.children().length).toBe(0); + })); + + it('should properly animate elements with compound directives', function() { + var capturedAnimation; + module(function($animateProvider) { + $animateProvider.register('.special', function() { + return { + enter : function(element, done) { + capturedAnimation = 'enter'; + done(); + }, + leave : function(element, done) { + capturedAnimation = 'leave'; + done(); + } + } + }); + }); + inject(function($rootScope, $compile, $rootElement, $document, $timeout, $templateCache, $sniffer) { + if(!$sniffer.transitions) return; + + $templateCache.put('item-template', 'item: #{{ item }} '); + var element = $compile('
' + + '
' + + '
')($rootScope); + + ss.addRule('.special', '-webkit-transition:1s linear all;' + + 'transition:1s linear all;'); + + $rootElement.append(element); + jqLite($document[0].body).append($rootElement); + + $rootScope.tpl = 'item-template'; + $rootScope.items = [1,2,3]; + $rootScope.$digest(); + $timeout.flush(); + + expect(capturedAnimation).toBe('enter'); + expect(element.text()).toContain('item: #1'); + + forEach(element.children(), function(kid) { + browserTrigger(kid, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 }); + }); + $timeout.flush(); + + $rootScope.items = []; + $rootScope.$digest(); + $timeout.flush(); + + expect(capturedAnimation).toBe('leave'); + }); + }); }); });