From c5e41a0325d0476f8e0f02fce3b0712050e2571b Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 17 Mar 2014 15:50:24 -0700 Subject: [PATCH 01/15] chore(log): add `log.empty()` method to the testing logger `log.empty()` is the same as `log.reset()`, except thati `empty()` also returns the current array with messages instead of: ``` // do work expect(log).toEqual(['bar']); log.reset(); ``` do: ``` // do work expect(log.empty()).toEqual(['bar']); ``` --- test/helpers/testabilityPatch.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/helpers/testabilityPatch.js b/test/helpers/testabilityPatch.js index f61be4eeb13f..34b6b78a0141 100644 --- a/test/helpers/testabilityPatch.js +++ b/test/helpers/testabilityPatch.js @@ -269,21 +269,27 @@ function provideLog($provide) { log.toString = function() { return messages.join('; '); - } + }; log.toArray = function() { return messages; - } + }; log.reset = function() { messages = []; + }; + + log.empty = function() { + var currentMessages = messages; + messages = []; + return currentMessages; } log.fn = function(msg) { return function() { log(msg); - } - } + }; + }; log.$$log = true; From 78057a945ef84cbb05f9417fe884cb8c28e67b44 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 17 Mar 2014 15:48:09 -0700 Subject: [PATCH 02/15] fix(Scope): $watchCollection should call listener with oldValue Originally we destroyed the oldValue by incrementaly copying over portions of the newValue into the oldValue during dirty-checking, this resulted in oldValue to be equal to newValue by the time we called the watchCollection listener. The fix creates a copy of the newValue each time a change is detected and then uses that copy *the next time* a change is detected. To make `$watchCollection` behave the same way as `$watch`, during the first iteration the listener is called with newValue and oldValue being identical. Since many of the corner-cases are already covered by existing tests, I refactored the test logging to include oldValue and made the tests more readable. Closes #2621 Closes #5661 Closes #5688 Closes #6736 --- src/ng/rootScope.js | 51 +++++++++++++++--- test/ng/rootScopeSpec.js | 114 ++++++++++++++++++++++++++------------- 2 files changed, 119 insertions(+), 46 deletions(-) diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 2bb965bb7b6f..ce0f8ad54cb3 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -398,30 +398,40 @@ function $RootScopeProvider(){ * {@link ng.$rootScope.Scope#$digest $digest} cycle. Any shallow change within the * collection will trigger a call to the `listener`. * - * @param {function(newCollection, oldCollection, scope)} listener a callback function that is - * fired with both the `newCollection` and `oldCollection` as parameters. - * The `newCollection` object is the newly modified data obtained from the `obj` expression - * and the `oldCollection` object is a copy of the former collection data. - * The `scope` refers to the current scope. + * @param {function(newCollection, oldCollection, scope)} listener a callback function called + * when a change is detected. + * - The `newCollection` object is the newly modified data obtained from the `obj` expression + * - The `oldCollection` object is a copy of the former collection data. + * Due to performance considerations, the`oldCollection` value is computed only if the + * `listener` function declares two or more arguments. + * - The `scope` argument refers to the current scope. * * @returns {function()} Returns a de-registration function for this listener. When the * de-registration function is executed, the internal watch operation is terminated. */ $watchCollection: function(obj, listener) { var self = this; - var oldValue; + // the current value, updated on each dirty-check run var newValue; + // a shallow copy of the newValue from the last dirty-check run, + // updated to match newValue during dirty-check run + var oldValue; + // a shallow copy of the newValue from when the last change happened + var veryOldValue; + // only track veryOldValue if the listener is asking for it + var trackVeryOldValue = (listener.length > 1); var changeDetected = 0; var objGetter = $parse(obj); var internalArray = []; var internalObject = {}; + var initRun = true; var oldLength = 0; function $watchCollectionWatch() { newValue = objGetter(self); var newLength, key; - if (!isObject(newValue)) { + if (!isObject(newValue)) { // if primitive if (oldValue !== newValue) { oldValue = newValue; changeDetected++; @@ -487,7 +497,32 @@ function $RootScopeProvider(){ } function $watchCollectionAction() { - listener(newValue, oldValue, self); + if (initRun) { + initRun = false; + listener(newValue, newValue, self); + } else { + listener(newValue, veryOldValue, self); + } + + // make a copy for the next time a collection is changed + if (trackVeryOldValue) { + if (!isObject(newValue)) { + //primitive + veryOldValue = newValue; + } else if (isArrayLike(newValue)) { + veryOldValue = new Array(newValue.length); + for (var i = 0; i < newValue.length; i++) { + veryOldValue[i] = newValue[i]; + } + } else { // if object + veryOldValue = {}; + for (var key in newValue) { + if (hasOwnProperty.call(newValue, key)) { + veryOldValue[key] = newValue[key]; + } + } + } + } } return this.$watch($watchCollectionWatch, $watchCollectionAction); diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js index f9cf9412c605..251a8ce882c4 100644 --- a/test/ng/rootScopeSpec.js +++ b/test/ng/rootScopeSpec.js @@ -483,104 +483,127 @@ describe('Scope', function() { describe('$watchCollection', function() { var log, $rootScope, deregister; - beforeEach(inject(function(_$rootScope_) { - log = []; + beforeEach(inject(function(_$rootScope_, _log_) { $rootScope = _$rootScope_; - deregister = $rootScope.$watchCollection('obj', function logger(obj) { - log.push(toJson(obj)); + log = _log_; + deregister = $rootScope.$watchCollection('obj', function logger(newVal, oldVal) { + var msg = {newVal: newVal, oldVal: oldVal}; + + if (newVal === oldVal) { + msg.identical = true; + } + + log(msg); }); })); it('should not trigger if nothing change', inject(function($rootScope) { $rootScope.$digest(); - expect(log).toEqual([undefined]); + expect(log).toEqual([{ newVal : undefined, oldVal : undefined, identical : true }]); + log.reset(); $rootScope.$digest(); - expect(log).toEqual([undefined]); + expect(log).toEqual([]); })); - it('should allow deregistration', inject(function($rootScope) { + it('should allow deregistration', function() { $rootScope.obj = []; $rootScope.$digest(); - - expect(log).toEqual(['[]']); + expect(log.toArray().length).toBe(1); + log.reset(); $rootScope.obj.push('a'); deregister(); $rootScope.$digest(); - expect(log).toEqual(['[]']); - })); + expect(log).toEqual([]); + }); describe('array', function() { + + it('should return oldCollection === newCollection only on the first listener call', + inject(function($rootScope, log) { + + // first time should be identical + $rootScope.obj = ['a', 'b']; + $rootScope.$digest(); + expect(log).toEqual([{newVal: ['a', 'b'], oldVal: ['a', 'b'], identical: true}]); + log.reset(); + + // second time should be different + $rootScope.obj[1] = 'c'; + $rootScope.$digest(); + expect(log).toEqual([{newVal: ['a', 'c'], oldVal: ['a', 'b']}]); + })); + + it('should trigger when property changes into array', function() { $rootScope.obj = 'test'; $rootScope.$digest(); - expect(log).toEqual(['"test"']); + expect(log.empty()).toEqual([{newVal: "test", oldVal: "test", identical: true}]); $rootScope.obj = []; $rootScope.$digest(); - expect(log).toEqual(['"test"', '[]']); + expect(log.empty()).toEqual([{newVal: [], oldVal: "test"}]); $rootScope.obj = {}; $rootScope.$digest(); - expect(log).toEqual(['"test"', '[]', '{}']); + expect(log.empty()).toEqual([{newVal: {}, oldVal: []}]); $rootScope.obj = []; $rootScope.$digest(); - expect(log).toEqual(['"test"', '[]', '{}', '[]']); + expect(log.empty()).toEqual([{newVal: [], oldVal: {}}]); $rootScope.obj = undefined; $rootScope.$digest(); - expect(log).toEqual(['"test"', '[]', '{}', '[]', undefined]); + expect(log.empty()).toEqual([{newVal: undefined, oldVal: []}]); }); it('should not trigger change when object in collection changes', function() { $rootScope.obj = [{}]; $rootScope.$digest(); - expect(log).toEqual(['[{}]']); + expect(log.empty()).toEqual([{newVal: [{}], oldVal: [{}], identical: true}]); $rootScope.obj[0].name = 'foo'; $rootScope.$digest(); - expect(log).toEqual(['[{}]']); + expect(log).toEqual([]); }); it('should watch array properties', function() { $rootScope.obj = []; $rootScope.$digest(); - expect(log).toEqual(['[]']); + expect(log.empty()).toEqual([{newVal: [], oldVal: [], identical: true}]); $rootScope.obj.push('a'); $rootScope.$digest(); - expect(log).toEqual(['[]', '["a"]']); + expect(log.empty()).toEqual([{newVal: ['a'], oldVal: []}]); $rootScope.obj[0] = 'b'; $rootScope.$digest(); - expect(log).toEqual(['[]', '["a"]', '["b"]']); + expect(log.empty()).toEqual([{newVal: ['b'], oldVal: ['a']}]); $rootScope.obj.push([]); $rootScope.obj.push({}); - log = []; $rootScope.$digest(); - expect(log).toEqual(['["b",[],{}]']); + expect(log.empty()).toEqual([{newVal: ['b', [], {}], oldVal: ['b']}]); var temp = $rootScope.obj[1]; $rootScope.obj[1] = $rootScope.obj[2]; $rootScope.obj[2] = temp; $rootScope.$digest(); - expect(log).toEqual([ '["b",[],{}]', '["b",{},[]]' ]); + expect(log.empty()).toEqual([{newVal: ['b', {}, []], oldVal: ['b', [], {}]}]); $rootScope.obj.shift(); - log = []; $rootScope.$digest(); - expect(log).toEqual([ '[{},[]]' ]); + expect(log.empty()).toEqual([{newVal: [{}, []], oldVal: ['b', {}, []]}]); }); + it('should watch array-like objects like arrays', function () { var arrayLikelog = []; $rootScope.$watchCollection('arrayLikeObject', function logger(obj) { @@ -601,57 +624,72 @@ describe('Scope', function() { describe('object', function() { + + it('should return oldCollection === newCollection only on the first listener call', + inject(function($rootScope, log) { + + $rootScope.obj = {'a': 'b'}; + // first time should be identical + $rootScope.$digest(); + expect(log.empty()).toEqual([{newVal: {'a': 'b'}, oldVal: {'a': 'b'}, identical: true}]); + + // second time not identical + $rootScope.obj.a = 'c'; + $rootScope.$digest(); + expect(log).toEqual([{newVal: {'a': 'c'}, oldVal: {'a': 'b'}}]); + })); + + it('should trigger when property changes into object', function() { $rootScope.obj = 'test'; $rootScope.$digest(); - expect(log).toEqual(['"test"']); + expect(log.empty()).toEqual([{newVal: 'test', oldVal: 'test', identical: true}]); $rootScope.obj = {}; $rootScope.$digest(); - expect(log).toEqual(['"test"', '{}']); + expect(log.empty()).toEqual([{newVal: {}, oldVal: 'test'}]); }); it('should not trigger change when object in collection changes', function() { $rootScope.obj = {name: {}}; $rootScope.$digest(); - expect(log).toEqual(['{"name":{}}']); + expect(log.empty()).toEqual([{newVal: {name: {}}, oldVal: {name: {}}, identical: true}]); $rootScope.obj.name.bar = 'foo'; $rootScope.$digest(); - expect(log).toEqual(['{"name":{}}']); + expect(log.empty()).toEqual([]); }); it('should watch object properties', function() { $rootScope.obj = {}; $rootScope.$digest(); - expect(log).toEqual(['{}']); + expect(log.empty()).toEqual([{newVal: {}, oldVal: {}, identical: true}]); $rootScope.obj.a= 'A'; $rootScope.$digest(); - expect(log).toEqual(['{}', '{"a":"A"}']); + expect(log.empty()).toEqual([{newVal: {a: 'A'}, oldVal: {}}]); $rootScope.obj.a = 'B'; $rootScope.$digest(); - expect(log).toEqual(['{}', '{"a":"A"}', '{"a":"B"}']); + expect(log.empty()).toEqual([{newVal: {a: 'B'}, oldVal: {a: 'A'}}]); $rootScope.obj.b = []; $rootScope.obj.c = {}; - log = []; $rootScope.$digest(); - expect(log).toEqual(['{"a":"B","b":[],"c":{}}']); + expect(log.empty()).toEqual([{newVal: {a: 'B', b: [], c: {}}, oldVal: {a: 'B'}}]); var temp = $rootScope.obj.a; $rootScope.obj.a = $rootScope.obj.b; $rootScope.obj.c = temp; $rootScope.$digest(); - expect(log).toEqual([ '{"a":"B","b":[],"c":{}}', '{"a":[],"b":[],"c":"B"}' ]); + expect(log.empty()). + toEqual([{newVal: {a: [], b: {}, c: 'B'}, oldVal: {a: 'B', b: [], c: {}}}]); delete $rootScope.obj.a; - log = []; $rootScope.$digest(); - expect(log).toEqual([ '{"b":[],"c":"B"}' ]); + expect(log.empty()).toEqual([{newVal: {b: {}, c: 'B'}, oldVal: {a: [], b: {}, c: 'B'}}]); }); }); }); From 299b220f5e05e1d4e26bfd58d0b2fd7329ca76b1 Mon Sep 17 00:00:00 2001 From: Caio Cunha Date: Thu, 2 Jan 2014 23:02:12 -0500 Subject: [PATCH 03/15] feat($compile): add support for $observer deregistration In order to make the behavior compatible with $rootScope.$watch and $rootScope.$on methods, and make it possible to deregister an attribute observer, Attributes.$observe method now returns a deregistration function instead of the observer itself. BREAKING CHANGE: calling attr.$observe no longer returns the observer function, but a deregistration function instead. To migrate the code follow the example below: Before: ``` directive('directiveName', function() { return { link: function(scope, elm, attr) { var observer = attr.$observe('someAttr', function(value) { console.log(value); }); } }; }); ``` After: ``` directive('directiveName', function() { return { link: function(scope, elm, attr) { var observer = function(value) { console.log(value); }; attr.$observe('someAttr', observer); } }; }); ``` Closes #5609 --- src/ng/compile.js | 7 +++++-- test/ng/compileSpec.js | 17 ++++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index c7cd08bce127..8cc518d5e78f 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -775,7 +775,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * @param {function(interpolatedValue)} fn Function that will be called whenever the interpolated value of the attribute changes. * See the {@link guide/directive#Attributes Directives} guide for more info. - * @returns {function()} the `fn` parameter. + * @returns {function()} Returns a deregistration function for this observer. */ $observe: function(key, fn) { var attrs = this, @@ -789,7 +789,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { fn(attrs[key]); } }); - return fn; + + return function() { + arrayRemove(listeners, fn); + }; } }; diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 5110c4d62634..96d3c18bc7ee 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -1914,15 +1914,14 @@ describe('$compile', function() { describe('interpolation', function() { - var observeSpy, directiveAttrs; + var observeSpy, directiveAttrs, deregisterObserver; beforeEach(module(function() { directive('observer', function() { return function(scope, elm, attr) { directiveAttrs = attr; observeSpy = jasmine.createSpy('$observe attr'); - - expect(attr.$observe('someAttr', observeSpy)).toBe(observeSpy); + deregisterObserver = attr.$observe('someAttr', observeSpy); }; }); directive('replaceSomeAttr', valueFn({ @@ -2020,6 +2019,18 @@ describe('$compile', function() { })); + 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) { $rootScope.whatever = 'test value'; $compile('
')($rootScope); From d6cfcacee101f2738e0a224a3377232ff85f78a4 Mon Sep 17 00:00:00 2001 From: Caio Cunha Date: Tue, 22 Oct 2013 09:41:51 -0400 Subject: [PATCH 04/15] feat(ngMock.$httpBackend): added support for function as URL matcher It's now possible to pass a function to match the URL in $httpBackend mocked expectations. This gives a more sophisticate control over the URL matching without requiring complex RegExp mantainance or the workaround of creating an object with a `test` function in order to mimic RegExp interface. This approach was suggested in [this thread](https://groups.google.com/d/msg/angular/3QsCUEvvxlM/Q4C4ZIqNIuEJ) Closes #4580 --- src/ngMock/angular-mocks.js | 70 +++++++++++++++++++++----------- test/ngMock/angular-mocksSpec.js | 11 +++++ 2 files changed, 58 insertions(+), 23 deletions(-) diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index 67decaceb33f..bb2ac754d020 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -1178,7 +1178,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * Creates a new backend definition. * * @param {string} method HTTP method. - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives * data string and returns true if the data is as expected. * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header @@ -1216,7 +1217,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for GET requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. @@ -1228,7 +1230,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for HEAD requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. @@ -1240,7 +1243,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for DELETE requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. @@ -1252,7 +1256,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for POST requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives * data string and returns true if the data is as expected. * @param {(Object|function(Object))=} headers HTTP headers. @@ -1266,7 +1271,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for PUT requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives * data string and returns true if the data is as expected. * @param {(Object|function(Object))=} headers HTTP headers. @@ -1280,7 +1286,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for JSONP requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. */ @@ -1294,7 +1301,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * Creates a new request expectation. * * @param {string} method HTTP method. - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body * is in JSON format. @@ -1326,7 +1334,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for GET requests. For more info see `expect()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @param {Object=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. See #expect for more info. @@ -1338,7 +1347,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for HEAD requests. For more info see `expect()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @param {Object=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. @@ -1350,7 +1360,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for DELETE requests. For more info see `expect()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @param {Object=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. @@ -1362,7 +1373,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for POST requests. For more info see `expect()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body * is in JSON format. @@ -1377,7 +1389,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for PUT requests. For more info see `expect()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body * is in JSON format. @@ -1392,7 +1405,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for PATCH requests. For more info see `expect()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body * is in JSON format. @@ -1407,7 +1421,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for JSONP requests. For more info see `expect()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. */ @@ -1531,6 +1546,7 @@ function MockHttpExpectation(method, url, data, headers) { this.matchUrl = function(u) { if (!url) return true; if (angular.isFunction(url.test)) return url.test(u); + if (angular.isFunction(url)) return url(u); return url == u; }; @@ -1808,7 +1824,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * Creates a new backend definition. * * @param {string} method HTTP method. - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @param {(string|RegExp)=} data HTTP request body. * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header * object and returns true if the headers match the current definition. @@ -1832,7 +1849,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for GET requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that * control how a matched request is handled. @@ -1845,7 +1863,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for HEAD requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that * control how a matched request is handled. @@ -1858,7 +1877,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for DELETE requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that * control how a matched request is handled. @@ -1871,7 +1891,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for POST requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @param {(string|RegExp)=} data HTTP request body. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that @@ -1885,7 +1906,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for PUT requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @param {(string|RegExp)=} data HTTP request body. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that @@ -1899,7 +1921,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for PATCH requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @param {(string|RegExp)=} data HTTP request body. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that @@ -1913,7 +1936,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for JSONP requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that * control how a matched request is handled. */ diff --git a/test/ngMock/angular-mocksSpec.js b/test/ngMock/angular-mocksSpec.js index c2b6108dfb33..b3608013c5ad 100644 --- a/test/ngMock/angular-mocksSpec.js +++ b/test/ngMock/angular-mocksSpec.js @@ -1431,6 +1431,17 @@ describe('ngMock', function() { }); + it('should accept url as function', function() { + var urlValidator = function(url) { + return url !== '/not-accepted'; + }; + var exp = new MockHttpExpectation('POST', urlValidator); + + expect(exp.match('POST', '/url')).toBe(true); + expect(exp.match('POST', '/not-accepted')).toBe(false); + }); + + it('should accept data as regexp', function() { var exp = new MockHttpExpectation('POST', '/url', /\{.*?\}/); From 2b84f43a6d6f05b0ed328e8bd39c83db5989ea40 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 18 Mar 2014 15:54:17 -0700 Subject: [PATCH 05/15] style(ngMocks): remove ws --- src/ngMock/angular-mocks.js | 46 ++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index bb2ac754d020..02e5432961ca 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -1178,7 +1178,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * Creates a new backend definition. * * @param {string} method HTTP method. - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url * and returns true if the url match the current definition. * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives * data string and returns true if the data is as expected. @@ -1217,7 +1217,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for GET requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url * and returns true if the url match the current definition. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched @@ -1230,7 +1230,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for HEAD requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url * and returns true if the url match the current definition. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched @@ -1243,7 +1243,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for DELETE requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url * and returns true if the url match the current definition. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched @@ -1256,7 +1256,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for POST requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url * and returns true if the url match the current definition. * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives * data string and returns true if the data is as expected. @@ -1271,7 +1271,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for PUT requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url * and returns true if the url match the current definition. * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives * data string and returns true if the data is as expected. @@ -1286,7 +1286,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for JSONP requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url * and returns true if the url match the current definition. * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. @@ -1301,7 +1301,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * Creates a new request expectation. * * @param {string} method HTTP method. - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url * and returns true if the url match the current definition. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body @@ -1334,7 +1334,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for GET requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url * and returns true if the url match the current definition. * @param {Object=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched @@ -1347,7 +1347,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for HEAD requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url * and returns true if the url match the current definition. * @param {Object=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched @@ -1360,7 +1360,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for DELETE requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url * and returns true if the url match the current definition. * @param {Object=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched @@ -1373,7 +1373,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for POST requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url * and returns true if the url match the current definition. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body @@ -1389,7 +1389,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for PUT requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url * and returns true if the url match the current definition. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body @@ -1405,7 +1405,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for PATCH requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url * and returns true if the url match the current definition. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body @@ -1421,7 +1421,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for JSONP requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url * and returns true if the url match the current definition. * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. @@ -1824,7 +1824,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * Creates a new backend definition. * * @param {string} method HTTP method. - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url * and returns true if the url match the current definition. * @param {(string|RegExp)=} data HTTP request body. * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header @@ -1849,7 +1849,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for GET requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url * and returns true if the url match the current definition. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that @@ -1863,7 +1863,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for HEAD requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url * and returns true if the url match the current definition. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that @@ -1877,7 +1877,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for DELETE requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url * and returns true if the url match the current definition. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that @@ -1891,7 +1891,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for POST requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url * and returns true if the url match the current definition. * @param {(string|RegExp)=} data HTTP request body. * @param {(Object|function(Object))=} headers HTTP headers. @@ -1906,7 +1906,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for PUT requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url * and returns true if the url match the current definition. * @param {(string|RegExp)=} data HTTP request body. * @param {(Object|function(Object))=} headers HTTP headers. @@ -1921,7 +1921,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for PATCH requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url * and returns true if the url match the current definition. * @param {(string|RegExp)=} data HTTP request body. * @param {(Object|function(Object))=} headers HTTP headers. @@ -1936,7 +1936,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for JSONP requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url * and returns true if the url match the current definition. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that * control how a matched request is handled. From 4cf2adfeda3d2726a426be7089f71c0533a20515 Mon Sep 17 00:00:00 2001 From: frandroid Date: Tue, 18 Mar 2014 17:19:31 -0400 Subject: [PATCH 06/15] docs(tutorial/step_05): removed stray "a" --- docs/content/tutorial/step_05.ngdoc | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/content/tutorial/step_05.ngdoc b/docs/content/tutorial/step_05.ngdoc index 17941cd14e14..dccd1d18849e 100644 --- a/docs/content/tutorial/step_05.ngdoc +++ b/docs/content/tutorial/step_05.ngdoc @@ -20,7 +20,6 @@ You should now see a list of 20 phones. The most important changes are listed below. You can see the full diff on [GitHub](https://github.com/angular/angular-phonecat/compare/step-4...step-5): ## Data -a The `app/phones/phones.json` file in your project is a dataset that contains a larger list of phones stored in the JSON format. From f7ce415c676dc0d458c9f0508dde01f92cf6f00a Mon Sep 17 00:00:00 2001 From: frandroid Date: Tue, 18 Mar 2014 17:22:20 -0400 Subject: [PATCH 07/15] docs(tutorial/step_05): fix services link --- docs/content/tutorial/step_05.ngdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/tutorial/step_05.ngdoc b/docs/content/tutorial/step_05.ngdoc index dccd1d18849e..031d97b751d8 100644 --- a/docs/content/tutorial/step_05.ngdoc +++ b/docs/content/tutorial/step_05.ngdoc @@ -7,7 +7,7 @@ Enough of building an app with three phones in a hard-coded dataset! Let's fetch a larger dataset -from our server using one of Angular's built-in {@link guide/dev_guide.services services} called {@link +from our server using one of Angular's built-in {@link guide/services services} called {@link ng.$http $http}. We will use Angular's {@link guide/di dependency injection (DI)} to provide the service to the `PhoneListCtrl` controller. From c839f78b8f2d8d910bc2bfc9e41b3e3b67090ec1 Mon Sep 17 00:00:00 2001 From: Traxmaxx Date: Tue, 4 Mar 2014 18:54:08 +0100 Subject: [PATCH 08/15] fix($$RAFProvider): check for webkitCancelRequestAnimationFrame Android 4.3 only supports webkitCancelRequestAnimationFrame. Closes #6526 --- src/ng/raf.js | 3 ++- test/ng/rafSpec.js | 32 +++++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/ng/raf.js b/src/ng/raf.js index e07adbfed5ce..bb1f47ed5e65 100644 --- a/src/ng/raf.js +++ b/src/ng/raf.js @@ -8,7 +8,8 @@ function $$RAFProvider(){ //rAF var cancelAnimationFrame = $window.cancelAnimationFrame || $window.webkitCancelAnimationFrame || - $window.mozCancelAnimationFrame; + $window.mozCancelAnimationFrame || + $window.webkitCancelRequestAnimationFrame; var rafSupported = !!requestAnimationFrame; var raf = rafSupported diff --git a/test/ng/rafSpec.js b/test/ng/rafSpec.js index 8bf76efd03b0..7c67b8c9409f 100644 --- a/test/ng/rafSpec.js +++ b/test/ng/rafSpec.js @@ -38,11 +38,8 @@ describe('$$rAF', function() { //we need to create our own injector to work around the ngMock overrides var injector = createInjector(['ng', function($provide) { $provide.value('$timeout', timeoutSpy); - $provide.decorator('$window', function($delegate) { - $delegate.requestAnimationFrame = false; - $delegate.webkitRequestAnimationFrame = false; - $delegate.mozRequestAnimationFrame = false; - return $delegate; + $provide.value('$window', { + location : window.location, }); }]); @@ -76,4 +73,29 @@ describe('$$rAF', function() { } })); }); + + describe('mobile', function() { + it('should provide a cancellation method for an older version of Android', function() { + //we need to create our own injector to work around the ngMock overrides + var injector = createInjector(['ng', function($provide) { + $provide.value('$window', { + location : window.location, + webkitRequestAnimationFrame: jasmine.createSpy('$window.webkitRequestAnimationFrame'), + webkitCancelRequestAnimationFrame: jasmine.createSpy('$window.webkitCancelRequestAnimationFrame') + }); + }]); + + var $$rAF = injector.get('$$rAF'); + var $window = injector.get('$window'); + var cancel = $$rAF(function() {}); + + expect($$rAF.supported).toBe(true); + + try { + cancel(); + } catch(e) {} + + expect($window.webkitCancelRequestAnimationFrame).toHaveBeenCalled(); + }); + }); }); From 6680b7b97c0326a80bdccaf0a35031e4af641e0e Mon Sep 17 00:00:00 2001 From: Caitlin Potter Date: Tue, 18 Mar 2014 11:11:39 -0400 Subject: [PATCH 09/15] fix($httpBackend): don't error when JSONP callback called with no parameter This change brings Angular's JSONP behaviour closer in line with jQuery's. It will no longer treat a callback called with no data as an error, and will no longer support IE8 via the onreadystatechanged event. BREAKING CHANGE: Previously, the JSONP backend code would support IE8 by relying on the readystatechanged events. This is no longer the case, as these events do not provide adequate useful information for deeming whether or not a response is an error. Previously, a JSONP response which did not pass data into the callback would be given a status of -2, and treated as an error. Now, this situation will instead be given a status of 200, despite the lack of data. This is useful for interaction with certain APIs. Previously, the onload and onerror callbacks were added to the JSONP script tag. These have been replaced with jQuery events, in order to gain access to the event object. This means that it is now difficult to test if the callbacks are registered or not. This is possible with jQuery, using the $.data("events") method, however it is currently impossible with jqLite. This is not expected to break applications. Closes #4987 Closes #6735 --- src/ng/httpBackend.js | 59 ++++++++++++++++--------------- test/ng/httpBackendSpec.js | 71 +++----------------------------------- 2 files changed, 35 insertions(+), 95 deletions(-) diff --git a/src/ng/httpBackend.js b/src/ng/httpBackend.js index 28107966f643..9b2d7361e057 100644 --- a/src/ng/httpBackend.js +++ b/src/ng/httpBackend.js @@ -49,16 +49,13 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc var callbackId = '_' + (callbacks.counter++).toString(36); callbacks[callbackId] = function(data) { callbacks[callbackId].data = data; + callbacks[callbackId].called = true; }; var jsonpDone = jsonpReq(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId), - function() { - if (callbacks[callbackId].data) { - completeRequest(callback, 200, callbacks[callbackId].data); - } else { - completeRequest(callback, status || -2); - } - callbacks[callbackId] = angular.noop; + callbackId, function(status, text) { + completeRequest(callback, status, callbacks[callbackId].data, "", text); + callbacks[callbackId] = noop; }); } else { @@ -158,33 +155,39 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc } }; - function jsonpReq(url, done) { + function jsonpReq(url, callbackId, done) { // we can't use jQuery/jqLite here because jQuery does crazy shit with script elements, e.g.: // - fetches local scripts via XHR and evals them // - adds and immediately removes script elements from the document - var script = rawDocument.createElement('script'), - doneWrapper = function() { - script.onreadystatechange = script.onload = script.onerror = null; - rawDocument.body.removeChild(script); - if (done) done(); - }; - - script.type = 'text/javascript'; + var script = rawDocument.createElement('script'), callback = null; + script.type = "text/javascript"; script.src = url; - - if (msie && msie <= 8) { - script.onreadystatechange = function() { - if (/loaded|complete/.test(script.readyState)) { - doneWrapper(); + script.async = true; + + callback = function(event) { + removeEventListenerFn(script, "load", callback); + removeEventListenerFn(script, "error", callback); + rawDocument.body.removeChild(script); + script = null; + var status = -1; + var text = "unknown"; + + if (event) { + if (event.type === "load" && !callbacks[callbackId].called) { + event = { type: "error" }; } - }; - } else { - script.onload = script.onerror = function() { - doneWrapper(); - }; - } + text = event.type; + status = event.type === "error" ? 404 : 200; + } + + if (done) { + done(status, text); + } + }; + addEventListenerFn(script, "load", callback); + addEventListenerFn(script, "error", callback); rawDocument.body.appendChild(script); - return doneWrapper; + return callback; } } diff --git a/test/ng/httpBackendSpec.js b/test/ng/httpBackendSpec.js index 5c9cbf586c36..837fbab9967a 100644 --- a/test/ng/httpBackendSpec.js +++ b/test/ng/httpBackendSpec.js @@ -39,7 +39,8 @@ describe('$httpBackend', function() { fakeDocument = { $$scripts: [], createElement: jasmine.createSpy('createElement').andCallFake(function() { - return {}; + // Return a proper script element... + return document.createElement(arguments[0]); }), body: { appendChild: jasmine.createSpy('body.appendChild').andCallFake(function(script) { @@ -325,13 +326,7 @@ describe('$httpBackend', function() { expect(url[1]).toBe('http://example.org/path'); callbacks[url[2]]('some-data'); - - if (script.onreadystatechange) { - script.readyState = 'complete'; - script.onreadystatechange(); - } else { - script.onload(); - } + browserTrigger(script, "load"); expect(callback).toHaveBeenCalledOnce(); }); @@ -346,71 +341,13 @@ describe('$httpBackend', function() { callbackId = script.src.match(SCRIPT_URL)[2]; callbacks[callbackId]('some-data'); - - if (script.onreadystatechange) { - script.readyState = 'complete'; - script.onreadystatechange(); - } else { - script.onload(); - } + browserTrigger(script, "load"); expect(callbacks[callbackId]).toBe(angular.noop); expect(fakeDocument.body.removeChild).toHaveBeenCalledOnceWith(script); }); - if(msie<=8) { - - it('should attach onreadystatechange handler to the script object', function() { - $backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, noop); - - expect(fakeDocument.$$scripts[0].onreadystatechange).toEqual(jasmine.any(Function)); - - var script = fakeDocument.$$scripts[0]; - - script.readyState = 'complete'; - script.onreadystatechange(); - - expect(script.onreadystatechange).toBe(null); - }); - - } else { - - it('should attach onload and onerror handlers to the script object', function() { - $backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, noop); - - expect(fakeDocument.$$scripts[0].onload).toEqual(jasmine.any(Function)); - expect(fakeDocument.$$scripts[0].onerror).toEqual(jasmine.any(Function)); - - var script = fakeDocument.$$scripts[0]; - script.onload(); - - expect(script.onload).toBe(null); - expect(script.onerror).toBe(null); - }); - - } - - it('should call callback with status -2 when script fails to load', function() { - callback.andCallFake(function(status, response) { - expect(status).toBe(-2); - expect(response).toBeUndefined(); - }); - - $backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, callback); - expect(fakeDocument.$$scripts.length).toBe(1); - - var script = fakeDocument.$$scripts.shift(); - if (script.onreadystatechange) { - script.readyState = 'complete'; - script.onreadystatechange(); - } else { - script.onload(); - } - expect(callback).toHaveBeenCalledOnce(); - }); - - it('should set url to current location if not specified or empty string', function() { $backend('JSONP', undefined, null, callback); expect(fakeDocument.$$scripts[0].src).toBe($browser.url()); From bc42950b514b60f319812eeb87aae2915e394237 Mon Sep 17 00:00:00 2001 From: Chris Constantin Date: Mon, 17 Feb 2014 16:10:36 -0800 Subject: [PATCH 10/15] fix(ngTouch): update workaround for desktop Webkit quirk Fix click busting of input click triggered by a label click quickly following a touch event on a different element, in desktop and mobile WebKit To reproduce the issue fixed by this commit set up a page with - an element with ng-click - a radio button (with hg-model) and associated label In a quick sequence tap on the element and then on the label. The radio button will not be checked, unless PREVENT_DURATION has passed Closes #6302 --- src/ngTouch/directive/ngClick.js | 16 +++- test/ngTouch/directive/ngClickSpec.js | 106 ++++++++++++++++++++------ 2 files changed, 98 insertions(+), 24 deletions(-) diff --git a/src/ngTouch/directive/ngClick.js b/src/ngTouch/directive/ngClick.js index 6cd0ff763032..7e558defc9f5 100644 --- a/src/ngTouch/directive/ngClick.js +++ b/src/ngTouch/directive/ngClick.js @@ -53,6 +53,7 @@ ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement', var ACTIVE_CLASS_NAME = 'ng-click-active'; var lastPreventedTime; var touchCoordinates; + var lastLabelClickCoordinates; // TAP EVENTS AND GHOST CLICKS @@ -124,10 +125,23 @@ ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement', var y = touches[0].clientY; // Work around desktop Webkit quirk where clicking a label will fire two clicks (on the label // and on the input element). Depending on the exact browser, this second click we don't want - // to bust has either (0,0) or negative coordinates. + // to bust has either (0,0), negative coordinates, or coordinates equal to triggering label + // click event if (x < 1 && y < 1) { return; // offscreen } + if (lastLabelClickCoordinates && + lastLabelClickCoordinates[0] === x && lastLabelClickCoordinates[1] === y) { + return; // input click triggered by label click + } + // reset label click coordinates on first subsequent click + if (lastLabelClickCoordinates) { + lastLabelClickCoordinates = null; + } + // remember label click coordinates to prevent click busting of trigger click event on input + if (event.target.tagName.toLowerCase() === 'label') { + lastLabelClickCoordinates = [x, y]; + } // Look for an allowable region containing this click. // If we find one, that means it was created by touchstart and not removed by diff --git a/test/ngTouch/directive/ngClickSpec.js b/test/ngTouch/directive/ngClickSpec.js index 43735709abea..921c64578b2b 100644 --- a/test/ngTouch/directive/ngClickSpec.js +++ b/test/ngTouch/directive/ngClickSpec.js @@ -370,40 +370,100 @@ describe('ngClick (touch)', function() { })); - it('should not cancel clicks that come long after', inject(function($rootScope, $compile) { - element1 = $compile('
')($rootScope); + describe('when clicking on a label immediately following a touch event', function() { + var touch = function(element, x, y) { + time = 10; + browserTrigger(element, 'touchstart',{ + keys: [], + x: x, + y: y + }); - $rootScope.count = 0; + time = 50; + browserTrigger(element, 'touchend',{ + keys: [], + x: x, + y: y + }); + }; - $rootScope.$digest(); + var click = function(element, x, y) { + browserTrigger(element, 'click',{ + keys: [], + x: x, + y: y + }); + }; - expect($rootScope.count).toBe(0); + var $rootScope; + var container, otherElement, input, label; + beforeEach(inject(function(_$rootScope_, $compile, $rootElement) { + $rootScope = _$rootScope_; + var container = $compile('
' + + '' + + '
')($rootScope); + $rootElement.append(container); + otherElement = container.children()[0]; + input = container.children()[1]; + label = container.children()[2]; - time = 10; - browserTrigger(element1, 'touchstart',{ - keys: [], - x: 10, - y: 10 + $rootScope.selection = 'initial'; + + $rootScope.$digest(); + })); + + + afterEach(function() { + dealoc(label); + dealoc(input); + dealoc(otherElement); + dealoc(container); }); - time = 50; - browserTrigger(element1, 'touchend',{ - keys: [], - x: 10, - y: 10 + + it('should not cancel input clicks with (0,0) coordinates', function() { + touch(otherElement, 100, 100); + + time = 500; + click(label, 10, 10); + click(input, 0, 0); + + expect($rootScope.selection).toBe('radio1'); }); - expect($rootScope.count).toBe(1); - time = 2700; - browserTrigger(element1, 'click',{ - keys: [], - x: 10, - y: 10 + it('should not cancel input clicks with negative coordinates', function() { + touch(otherElement, 100, 100); + + time = 500; + click(label, 10, 10); + click(input, -1, -1); + + expect($rootScope.selection).toBe('radio1'); }); - expect($rootScope.count).toBe(2); - })); + + it('should not cancel input clicks with positive coordinates identical to label click', function() { + touch(otherElement, 100, 100); + + time = 500; + click(label, 10, 10); + click(input, 10, 10); + + expect($rootScope.selection).toBe('radio1'); + }); + + + it('should cancel input clicks with positive coordinates different than label click', function() { + touch(otherElement, 100, 100); + + time = 500; + click(label, 10, 10); + click(input, 11, 11); + + expect($rootScope.selection).toBe('initial'); + }); + }); }); From 3652831084c3788f786046b907a7361d2e89c520 Mon Sep 17 00:00:00 2001 From: Caitlin Potter Date: Tue, 18 Mar 2014 13:13:08 -0400 Subject: [PATCH 11/15] fix(ngCookie): convert non-string values to string Previously, non-string values stored in $cookies would be removed, without warning the user, and causing difficulty debugging. Now, the value is converted to string before being stored, and the value is not dropped. Serialization may be customized using the toString() method of an object's prototype. Closes #6151 Closes #6220 --- src/ngCookies/cookies.js | 10 ++++------ test/ngCookies/cookiesSpec.js | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/ngCookies/cookies.js b/src/ngCookies/cookies.js index ccfae23be86e..4f97c8dd17f3 100644 --- a/src/ngCookies/cookies.js +++ b/src/ngCookies/cookies.js @@ -95,12 +95,10 @@ angular.module('ngCookies', ['ng']). for(name in cookies) { value = cookies[name]; if (!angular.isString(value)) { - if (angular.isDefined(lastCookies[name])) { - cookies[name] = lastCookies[name]; - } else { - delete cookies[name]; - } - } else if (value !== lastCookies[name]) { + value = '' + value; + cookies[name] = value; + } + if (value !== lastCookies[name]) { $browser.cookies(name, value); updated = true; } diff --git a/test/ngCookies/cookiesSpec.js b/test/ngCookies/cookiesSpec.js index 674c27748f11..1d669c1c6b8a 100644 --- a/test/ngCookies/cookiesSpec.js +++ b/test/ngCookies/cookiesSpec.js @@ -45,15 +45,25 @@ describe('$cookies', function() { })); - it('should drop or reset any cookie that was set to a non-string value', + it('should convert non-string values to string', inject(function($cookies, $browser, $rootScope) { $cookies.nonString = [1, 2, 3]; $cookies.nullVal = null; $cookies.undefVal = undefined; - $cookies.preexisting = function() {}; + var preexisting = $cookies.preexisting = function() {}; $rootScope.$digest(); - expect($browser.cookies()).toEqual({'preexisting': 'oldCookie'}); - expect($cookies).toEqual({'preexisting': 'oldCookie'}); + expect($browser.cookies()).toEqual({ + 'preexisting': '' + preexisting, + 'nonString': '1,2,3', + 'nullVal': 'null', + 'undefVal': 'undefined' + }); + expect($cookies).toEqual({ + 'preexisting': '' + preexisting, + 'nonString': '1,2,3', + 'nullVal': 'null', + 'undefVal': 'undefined' + }); })); From 37bc5ef4d87f19da47d3ab454c43d1e532c4f924 Mon Sep 17 00:00:00 2001 From: Caitlin Potter Date: Wed, 5 Feb 2014 23:50:58 -0500 Subject: [PATCH 12/15] fix(orderBy): support string predicates containing non-ident characters The orderBy filter now allows string predicates passed to the orderBy filter to make use property name predicates containing non-ident strings, such as spaces or percent signs, or non-latin characters. This behaviour requires the predicate string to be double-quoted. In markup, this might look like so: ```html
...
``` Or in JS: ```js var sorted = $filter('orderBy')(array, ['"Tip %"', '-"Subtotal $"'], false); ``` Closes #6143 Closes #6144 --- src/ng/filter/orderBy.js | 6 ++++++ test/ng/filter/orderBySpec.js | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/ng/filter/orderBy.js b/src/ng/filter/orderBy.js index b62626988fc5..faeb8ed1e570 100644 --- a/src/ng/filter/orderBy.js +++ b/src/ng/filter/orderBy.js @@ -74,6 +74,12 @@ function orderByFilter($parse){ predicate = predicate.substring(1); } get = $parse(predicate); + if (get.constant) { + var key = get(); + return reverseComparator(function(a,b) { + return compare(a[key], b[key]); + }, descending); + } } return reverseComparator(function(a,b){ return compare(get(a),get(b)); diff --git a/test/ng/filter/orderBySpec.js b/test/ng/filter/orderBySpec.js index 5c1178910255..5dc966777419 100644 --- a/test/ng/filter/orderBySpec.js +++ b/test/ng/filter/orderBySpec.js @@ -31,4 +31,16 @@ describe('Filter: orderBy', function() { toEqual([{a:2, b:1},{a:15, b:1}]); }); + it('should support string predicates with names containing non-identifier characters', function() { + expect(orderBy([{"Tip %": .25}, {"Tip %": .15}, {"Tip %": .40}], '"Tip %"')) + .toEqualData([{"Tip %": .15}, {"Tip %": .25}, {"Tip %": .40}]); + expect(orderBy([{"원": 76000}, {"원": 31000}, {"원": 156000}], '"원"')) + .toEqualData([{"원": 31000}, {"원": 76000}, {"원": 156000}]) + }); + + it('should throw if quoted string predicate is quoted incorrectly', function() { + expect(function() { + return orderBy([{"Tip %": .15}, {"Tip %": .25}, {"Tip %": .40}], '"Tip %\''); + }).toThrow(); + }); }); From f40f54c6da4a5399fe18a89d068634bb491e9f1a Mon Sep 17 00:00:00 2001 From: Jeff Balboni Date: Sun, 26 Jan 2014 16:17:36 -0500 Subject: [PATCH 13/15] fix(select): avoid checking option element selected properties in render In Firefox, hovering over an option in an open select menu updates the selected property of option elements. This means that when a render is triggered by the digest cycle, and the list of options is being rendered, the selected properties are reset to the values from the model and the option hovered over changes. This fix changes the code to only use DOM elements' selected properties in a comparison when a change event has been fired. Otherwise, the internal new and existing option arrays are used. Closes #2448 Closes #5994 --- src/ng/directive/select.js | 8 +++++++- test/ng/directive/selectSpec.js | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/ng/directive/select.js b/src/ng/directive/select.js index 0b562cca686c..628d21776b85 100644 --- a/src/ng/directive/select.js +++ b/src/ng/directive/select.js @@ -394,6 +394,12 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { value = valueFn(scope, locals); } } + // Update the null option's selected property here so $render cleans it up correctly + if (optionGroupsCache[0].length > 1) { + if (optionGroupsCache[0][1].id !== key) { + optionGroupsCache[0][1].selected = false; + } + } } ctrl.$setViewValue(value); }); @@ -531,7 +537,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { lastElement.val(existingOption.id = option.id); } // lastElement.prop('selected') provided by jQuery has side-effects - if (lastElement[0].selected !== option.selected) { + if (existingOption.selected !== option.selected) { lastElement.prop('selected', (existingOption.selected = option.selected)); } } else { diff --git a/test/ng/directive/selectSpec.js b/test/ng/directive/selectSpec.js index 6fcd1fe05f82..d270f438704f 100644 --- a/test/ng/directive/selectSpec.js +++ b/test/ng/directive/selectSpec.js @@ -733,6 +733,27 @@ describe('select', function() { expect(sortedHtml(options[2])).toEqual(''); }); + it('should not update selected property of an option element on digest with no change event', + function() { + createSingleSelect(); + + scope.$apply(function() { + scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}]; + scope.selected = scope.values[0]; + }); + + var options = element.find('option'); + var optionToSelect = options.eq(1); + + expect(optionToSelect.text()).toBe('B'); + + optionToSelect.prop('selected', true); + scope.$digest(); + + expect(optionToSelect.prop('selected')).toBe(true); + expect(scope.selected).toBe(scope.values[0]); + }); + describe('binding', function() { it('should bind to scope value', function() { From 0c65f1ae3e86cd3eed078df4e691402d591cc757 Mon Sep 17 00:00:00 2001 From: Brett Porter Date: Tue, 18 Mar 2014 15:36:00 +1100 Subject: [PATCH 14/15] test(ngMock): workaround issue with negative timestamps In some specific timezones and operating systems, it seems that getTimezoneOffset() can return an incorrect value for negative timestamps, as described in #5017. While this isn't something easily fixed in the mock code, the tests can avoid that particular timeframe by using a positive timestamp. Closes #5017 Closes #6730 --- test/ngMock/angular-mocksSpec.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test/ngMock/angular-mocksSpec.js b/test/ngMock/angular-mocksSpec.js index b3608013c5ad..33c8f34aa512 100644 --- a/test/ngMock/angular-mocksSpec.js +++ b/test/ngMock/angular-mocksSpec.js @@ -52,16 +52,19 @@ describe('ngMock', function() { it('should fake getHours method', function() { - //0 in -3h - var t0 = new angular.mock.TzDate(-3, 0); + // avoid going negative due to #5017, so use Jan 2, 1970 00:00 UTC + var jan2 = 24 * 60 * 60 * 1000; + + //0:00 in -3h + var t0 = new angular.mock.TzDate(-3, jan2); expect(t0.getHours()).toBe(3); - //0 in +0h - var t1 = new angular.mock.TzDate(0, 0); + //0:00 in +0h + var t1 = new angular.mock.TzDate(0, jan2); expect(t1.getHours()).toBe(0); - //0 in +3h - var t2 = new angular.mock.TzDate(3, 0); + //0:00 in +3h + var t2 = new angular.mock.TzDate(3, jan2); expect(t2.getHours()).toMatch(21); }); From fb6062fb9d83545730b993e94ac7482ffd43a62c Mon Sep 17 00:00:00 2001 From: Sekib Omazic Date: Sun, 9 Feb 2014 17:58:11 +0100 Subject: [PATCH 15/15] fix($rootScope): ng-repeat can't handle NaN values. #4605 $watchCollection checks if oldValue !== newValue which does not work for NaN. This was causing infinite digest errors, since comparing NaN to NaN in $watchCollection would always return false, indicating that a change was occuring on each loop. This fix adds a simple check to see if the current value and previous value are both NaN, and if so, does not count it as a change. Closes #4605 --- src/ng/rootScope.js | 4 +++- test/ng/rootScopeSpec.js | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index ce0f8ad54cb3..dbb93000cd72 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -453,7 +453,9 @@ function $RootScopeProvider(){ } // copy the items to oldValue and look for changes. for (var i = 0; i < newLength; i++) { - if (oldValue[i] !== newValue[i]) { + var bothNaN = (oldValue[i] !== oldValue[i]) && + (newValue[i] !== newValue[i]); + if (!bothNaN && (oldValue[i] !== newValue[i])) { changeDetected++; oldValue[i] = newValue[i]; } diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js index 251a8ce882c4..2ea414893032 100644 --- a/test/ng/rootScopeSpec.js +++ b/test/ng/rootScopeSpec.js @@ -603,6 +603,10 @@ describe('Scope', function() { expect(log.empty()).toEqual([{newVal: [{}, []], oldVal: ['b', {}, []]}]); }); + it('should not infinitely digest when current value is NaN', function() { + $rootScope.obj = [NaN]; + $rootScope.$digest(); + }); it('should watch array-like objects like arrays', function () { var arrayLikelog = [];