diff --git a/src/.jshintrc b/src/.jshintrc index e2d6c01b7474..d11760dc1e26 100644 --- a/src/.jshintrc +++ b/src/.jshintrc @@ -92,6 +92,13 @@ "skipDestroyOnNextJQueryCleanData": true, + "NODE_TYPE_ELEMENT": false, + "NODE_TYPE_TEXT": false, + "NODE_TYPE_COMMENT": false, + "NODE_TYPE_COMMENT": false, + "NODE_TYPE_DOCUMENT": false, + "NODE_TYPE_DOCUMENT_FRAGMENT": false, + /* filters.js */ "getFirstThursdayOfYear": false, diff --git a/src/Angular.js b/src/Angular.js index 2a7540eb224e..2e37d8925bf8 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -83,6 +83,12 @@ getBlockNodes: true, hasOwnProperty: true, createMap: true, + + NODE_TYPE_ELEMENT: true, + NODE_TYPE_TEXT: true, + NODE_TYPE_COMMENT: true, + NODE_TYPE_DOCUMENT: true, + NODE_TYPE_DOCUMENT_FRAGMENT: true, */ //////////////////////////////////// @@ -192,7 +198,7 @@ function isArrayLike(obj) { var length = obj.length; - if (obj.nodeType === 1 && length) { + if (obj.nodeType === NODE_TYPE_ELEMENT && length) { return true; } @@ -1028,11 +1034,9 @@ function startingTag(element) { // are not allowed to have children. So we just ignore it. element.empty(); } catch(e) {} - // As Per DOM Standards - var TEXT_NODE = 3; var elemHtml = jqLite('
test
'); + + $rootScope.$apply(function() { + $animate.addClass(element, 'test-class1'); + expect(element).not.toHaveClass('test-class1'); + + $animate.removeClass(element, 'test-class1'); + + $animate.addClass(element, 'test-class2'); + expect(element).not.toHaveClass('test-class2'); + + $animate.setClass(element, 'test-class3', 'test-class4'); + expect(element).not.toHaveClass('test-class3'); + expect(element).not.toHaveClass('test-class4'); + expect(log).toEqual([]); + }); + + expect(element).not.toHaveClass('test-class1'); + expect(element).not.toHaveClass('test-class4'); + expect(element).toHaveClass('test-class2'); + expect(element).toHaveClass('test-class3'); + expect(log).toEqual(['addClass(test-class2 test-class3)']); + expect(addClass.callCount).toBe(1); + expect(removeClass.callCount).toBe(0); + })); + + + it('should defer class manipulation until postDigest when outside of digest', inject(function($rootScope, $animate, log) { + setupClassManipulationLogger(log); + element = jqLite('test
'); + + $animate.addClass(element, 'test-class1'); + $animate.removeClass(element, 'test-class1'); + $animate.addClass(element, 'test-class2'); + $animate.setClass(element, 'test-class3', 'test-class4'); + + expect(log).toEqual([]); + $rootScope.$digest(); + + + expect(log).toEqual(['addClass(test-class2 test-class3)', 'removeClass(test-class4)']); + expect(element).not.toHaveClass('test-class1'); + expect(element).toHaveClass('test-class2'); + expect(element).toHaveClass('test-class3'); + expect(addClass.callCount).toBe(1); + expect(removeClass.callCount).toBe(1); + })); + + + it('should perform class manipulation in expected order at end of digest', inject(function($rootScope, $animate, log) { + element = jqLite('test
'); + + setupClassManipulationLogger(log); + + $rootScope.$apply(function() { + $animate.addClass(element, 'test-class1'); + $animate.addClass(element, 'test-class2'); + $animate.removeClass(element, 'test-class1'); + $animate.removeClass(element, 'test-class3'); + $animate.addClass(element, 'test-class3'); + }); + expect(log).toEqual(['addClass(test-class2)']); + })); + + + it('should return a promise which is resolved on a different turn', inject(function(log, $animate, $browser, $rootScope) { + element = jqLite('test
'); + + $animate.addClass(element, 'test1').then(log.fn('addClass(test1)')); + $animate.removeClass(element, 'test2').then(log.fn('removeClass(test2)')); + + $rootScope.$digest(); + expect(log).toEqual([]); + $browser.defer.flush(); + expect(log).toEqual(['addClass(test1)', 'removeClass(test2)']); + + log.reset(); + element = jqLite('test
'); + + $rootScope.$apply(function() { + $animate.addClass(element, 'test3').then(log.fn('addClass(test3)')); + $animate.removeClass(element, 'test4').then(log.fn('removeClass(test4)')); + expect(log).toEqual([]); + }); + + $browser.defer.flush(); + expect(log).toEqual(['addClass(test3)', 'removeClass(test4)']); + })); + + + it('should defer class manipulation until end of digest for SVG', inject(function($rootScope, $animate) { + if (!window.SVGElement) return; + setupClassManipulationSpies(); + element = jqLite(''); + var target = element.children().eq(0); + + $rootScope.$apply(function() { + $animate.addClass(target, 'test-class1'); + expect(target).not.toHaveClass('test-class1'); + + $animate.removeClass(target, 'test-class1'); + + $animate.addClass(target, 'test-class2'); + expect(target).not.toHaveClass('test-class2'); + + $animate.setClass(target, 'test-class3', 'test-class4'); + expect(target).not.toHaveClass('test-class3'); + expect(target).not.toHaveClass('test-class4'); + }); + + expect(target).not.toHaveClass('test-class1'); + expect(target).toHaveClass('test-class2'); + expect(addClass.callCount).toBe(1); + expect(removeClass.callCount).toBe(0); + })); + + + it('should defer class manipulation until postDigest when outside of digest for SVG', inject(function($rootScope, $animate, log) { + if (!window.SVGElement) return; + setupClassManipulationLogger(log); + element = jqLite(''); + var target = element.children().eq(0); + + $animate.addClass(target, 'test-class1'); + $animate.removeClass(target, 'test-class1'); + $animate.addClass(target, 'test-class2'); + $animate.setClass(target, 'test-class3', 'test-class4'); + + expect(log).toEqual([]); + $rootScope.$digest(); + + expect(log).toEqual(['addClass(test-class2 test-class3)', 'removeClass(test-class4)']); + expect(target).not.toHaveClass('test-class1'); + expect(target).toHaveClass('test-class2'); + expect(target).toHaveClass('test-class3'); + expect(addClass.callCount).toBe(1); + expect(removeClass.callCount).toBe(1); + })); + + + it('should perform class manipulation in expected order at end of digest for SVG', inject(function($rootScope, $animate, log) { + if (!window.SVGElement) return; + element = jqLite(''); + var target = element.children().eq(0); + + setupClassManipulationLogger(log); + + $rootScope.$apply(function() { + $animate.addClass(target, 'test-class1'); + $animate.addClass(target, 'test-class2'); + $animate.removeClass(target, 'test-class1'); + $animate.removeClass(target, 'test-class3'); + $animate.addClass(target, 'test-class3'); + }); + expect(log).toEqual(['addClass(test-class2)']); + })); + }); }); diff --git a/test/ng/directive/formSpec.js b/test/ng/directive/formSpec.js index febe0d8ebe54..6595ef9d6457 100644 --- a/test/ng/directive/formSpec.js +++ b/test/ng/directive/formSpec.js @@ -554,17 +554,20 @@ describe('form', function() { expect(doc).toBeValid(); control.$setValidity('error', false); + scope.$digest(); expect(doc).toBeInvalid(); expect(doc.hasClass('ng-valid-error')).toBe(false); expect(doc.hasClass('ng-invalid-error')).toBe(true); control.$setValidity('another', false); + scope.$digest(); expect(doc.hasClass('ng-valid-error')).toBe(false); expect(doc.hasClass('ng-invalid-error')).toBe(true); expect(doc.hasClass('ng-valid-another')).toBe(false); expect(doc.hasClass('ng-invalid-another')).toBe(true); control.$setValidity('error', true); + scope.$digest(); expect(doc).toBeInvalid(); expect(doc.hasClass('ng-valid-error')).toBe(true); expect(doc.hasClass('ng-invalid-error')).toBe(false); @@ -572,6 +575,7 @@ describe('form', function() { expect(doc.hasClass('ng-invalid-another')).toBe(true); control.$setValidity('another', true); + scope.$digest(); expect(doc).toBeValid(); expect(doc.hasClass('ng-valid-error')).toBe(true); expect(doc.hasClass('ng-invalid-error')).toBe(false); @@ -581,6 +585,7 @@ describe('form', function() { // validators are skipped, e.g. becuase of a parser error control.$setValidity('error', null); control.$setValidity('another', null); + scope.$digest(); expect(doc.hasClass('ng-valid-error')).toBe(false); expect(doc.hasClass('ng-invalid-error')).toBe(false); expect(doc.hasClass('ng-valid-another')).toBe(false); @@ -652,7 +657,9 @@ describe('form', function() { expect(input1).toBeDirty(); expect(input2).toBeDirty(); + formCtrl.$setPristine(); + scope.$digest(); expect(form).toBePristine(); expect(formCtrl.$pristine).toBe(true); expect(formCtrl.$dirty).toBe(false); @@ -685,6 +692,7 @@ describe('form', function() { expect(input).toBeDirty(); formCtrl.$setPristine(); + scope.$digest(); expect(form).toBePristine(); expect(formCtrl.$pristine).toBe(true); expect(formCtrl.$dirty).toBe(false); @@ -719,7 +727,9 @@ describe('form', function() { expect(nestedInput).toBeDirty(); formCtrl.$setPristine(); + scope.$digest(); expect(form).toBePristine(); + scope.$digest(); expect(formCtrl.$pristine).toBe(true); expect(formCtrl.$dirty).toBe(false); expect(nestedForm).toBePristine(); diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 6b0dd0d1df43..17aab79183c4 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -892,6 +892,45 @@ describe('NgModelController', function() { dealoc(element); })); + + it('should minimize janky setting of classes during $validate() and ngModelWatch', inject(function($animate, $compile, $rootScope) { + var addClass = $animate.$$addClassImmediately; + var removeClass = $animate.$$removeClassImmediately; + var addClassCallCount = 0; + var removeClassCallCount = 0; + var input; + $animate.$$addClassImmediately = function(element, className) { + if (input && element[0] === input[0]) ++addClassCallCount; + return addClass.call($animate, element, className); + }; + + $animate.$$removeClassImmediately = function(element, className) { + if (input && element[0] === input[0]) ++removeClassCallCount; + return removeClass.call($animate, element, className); + }; + + dealoc(element); + + $rootScope.value = "123456789"; + element = $compile( + '' + )($rootScope); + + var form = $rootScope.form; + input = element.children().eq(0); + + $rootScope.$digest(); + + expect(input).toBeValid(); + expect(input).not.toHaveClass('ng-invalid-maxlength'); + expect(input).toHaveClass('ng-valid-maxlength'); + expect(addClassCallCount).toBe(1); + expect(removeClassCallCount).toBe(0); + + dealoc(element); + })); }); }); diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index e2440b343c1a..43748bfdee98 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -1,7 +1,13 @@ 'use strict'; describe("ngAnimate", function() { - + var $originalAnimate; + beforeEach(module(function($provide) { + $provide.decorator('$animate', function($delegate) { + $originalAnimate = $delegate; + return $delegate; + }); + })); beforeEach(module('ngAnimate')); beforeEach(module('ngAnimateMock')); @@ -4871,4 +4877,202 @@ describe("ngAnimate", function() { })); }); }); + + + describe('CSS class DOM manipulation', function() { + var element; + var addClass; + var removeClass; + + beforeEach(module(provideLog)); + + afterEach(function() { + dealoc(element); + }); + + function setupClassManipulationSpies() { + inject(function($animate) { + addClass = spyOn($originalAnimate, '$$addClassImmediately').andCallThrough(); + removeClass = spyOn($originalAnimate, '$$removeClassImmediately').andCallThrough(); + }); + } + + function setupClassManipulationLogger(log) { + inject(function($animate) { + var addClassImmediately = $originalAnimate.$$addClassImmediately; + var removeClassImmediately = $originalAnimate.$$removeClassImmediately; + addClass = spyOn($originalAnimate, '$$addClassImmediately').andCallFake(function(element, classes) { + var names = classes; + if (Object.prototype.toString.call(classes) === '[object Array]') names = classes.join( ' '); + log('addClass(' + names + ')'); + return addClassImmediately.call($originalAnimate, element, classes); + }); + removeClass = spyOn($originalAnimate, '$$removeClassImmediately').andCallFake(function(element, classes) { + var names = classes; + if (Object.prototype.toString.call(classes) === '[object Array]') names = classes.join( ' '); + log('removeClass(' + names + ')'); + return removeClassImmediately.call($originalAnimate, element, classes); + }); + }); + } + + + it('should defer class manipulation until end of digest', inject(function($rootScope, $animate, log) { + setupClassManipulationLogger(log); + element = jqLite('test
'); + + $rootScope.$apply(function() { + $animate.addClass(element, 'test-class1'); + expect(element).not.toHaveClass('test-class1'); + + $animate.removeClass(element, 'test-class1'); + + $animate.addClass(element, 'test-class2'); + expect(element).not.toHaveClass('test-class2'); + + $animate.setClass(element, 'test-class3', 'test-class4'); + expect(element).not.toHaveClass('test-class3'); + expect(element).not.toHaveClass('test-class4'); + expect(log).toEqual([]); + }); + + expect(element).not.toHaveClass('test-class1'); + expect(element).not.toHaveClass('test-class4'); + expect(element).toHaveClass('test-class2'); + expect(element).toHaveClass('test-class3'); + expect(log).toEqual(['addClass(test-class2 test-class3)']); + expect(addClass.callCount).toBe(1); + expect(removeClass.callCount).toBe(0); + })); + + + it('should defer class manipulation until postDigest when outside of digest', inject(function($rootScope, $animate, log) { + setupClassManipulationLogger(log); + element = jqLite('test
'); + + $animate.addClass(element, 'test-class1'); + $animate.removeClass(element, 'test-class1'); + $animate.addClass(element, 'test-class2'); + $animate.setClass(element, 'test-class3', 'test-class4'); + + expect(log).toEqual([]); + $rootScope.$digest(); + + expect(log).toEqual(['addClass(test-class2 test-class3)', 'removeClass(test-class4)']); + expect(element).not.toHaveClass('test-class1'); + expect(element).toHaveClass('test-class2'); + expect(element).toHaveClass('test-class3'); + expect(addClass.callCount).toBe(1); + expect(removeClass.callCount).toBe(1); + })); + + + it('should perform class manipulation in expected order at end of digest', inject(function($rootScope, $animate, log) { + element = jqLite('test
'); + + setupClassManipulationLogger(log); + + $rootScope.$apply(function() { + $animate.addClass(element, 'test-class1'); + $animate.addClass(element, 'test-class2'); + $animate.removeClass(element, 'test-class1'); + $animate.removeClass(element, 'test-class3'); + $animate.addClass(element, 'test-class3'); + }); + expect(log).toEqual(['addClass(test-class2)']); + })); + + + it('should return a promise which is resolved on a different turn', inject(function(log, $animate, $browser, $rootScope) { + element = jqLite('test
'); + + $animate.addClass(element, 'test1').then(log.fn('addClass(test1)')); + $animate.removeClass(element, 'test2').then(log.fn('removeClass(test2)')); + + $rootScope.$digest(); + expect(log).toEqual([]); + $browser.defer.flush(); + expect(log).toEqual(['addClass(test1)', 'removeClass(test2)']); + + log.reset(); + element = jqLite('test
'); + + $rootScope.$apply(function() { + $animate.addClass(element, 'test3').then(log.fn('addClass(test3)')); + $animate.removeClass(element, 'test4').then(log.fn('removeClass(test4)')); + expect(log).toEqual([]); + }); + + $browser.defer.flush(); + expect(log).toEqual(['addClass(test3)', 'removeClass(test4)']); + })); + + + it('should defer class manipulation until end of digest for SVG', inject(function($rootScope, $animate) { + if (!window.SVGElement) return; + setupClassManipulationSpies(); + element = jqLite(''); + var target = element.children().eq(0); + + $rootScope.$apply(function() { + $animate.addClass(target, 'test-class1'); + expect(target).not.toHaveClass('test-class1'); + + $animate.removeClass(target, 'test-class1'); + + $animate.addClass(target, 'test-class2'); + expect(target).not.toHaveClass('test-class2'); + + $animate.setClass(target, 'test-class3', 'test-class4'); + expect(target).not.toHaveClass('test-class3'); + expect(target).not.toHaveClass('test-class4'); + }); + + expect(target).not.toHaveClass('test-class1'); + expect(target).toHaveClass('test-class2'); + expect(addClass.callCount).toBe(1); + expect(removeClass.callCount).toBe(0); + })); + + + it('should defer class manipulation until postDigest when outside of digest for SVG', inject(function($rootScope, $animate, log) { + if (!window.SVGElement) return; + setupClassManipulationLogger(log); + element = jqLite(''); + var target = element.children().eq(0); + + $animate.addClass(target, 'test-class1'); + $animate.removeClass(target, 'test-class1'); + $animate.addClass(target, 'test-class2'); + $animate.setClass(target, 'test-class3', 'test-class4'); + + expect(log).toEqual([]); + $rootScope.$digest(); + + expect(log).toEqual(['addClass(test-class2 test-class3)', 'removeClass(test-class4)']); + expect(target).not.toHaveClass('test-class1'); + expect(target).toHaveClass('test-class2'); + expect(target).toHaveClass('test-class3'); + expect(addClass.callCount).toBe(1); + expect(removeClass.callCount).toBe(1); + })); + + + it('should perform class manipulation in expected order at end of digest for SVG', inject(function($rootScope, $animate, log) { + if (!window.SVGElement) return; + element = jqLite(''); + var target = element.children().eq(0); + + setupClassManipulationLogger(log); + + $rootScope.$apply(function() { + $animate.addClass(target, 'test-class1'); + $animate.addClass(target, 'test-class2'); + $animate.removeClass(target, 'test-class1'); + $animate.removeClass(target, 'test-class3'); + $animate.addClass(target, 'test-class3'); + }); + expect(log).toEqual(['addClass(test-class2)']); + })); + }); });