diff --git a/CHANGELOG.md b/CHANGELOG.md index 85809d7573ba..2fb3c1064bea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ + +# 1.6.8 beneficial-tincture (2017-12-18) + + +## Bug Fixes +- **$location:** + - always decode special chars in `$location.url(value)` + ([2bdf71](https://github.com/angular/angular.js/commit/2bdf7126878c87474bb7588ce093d0a3c57b0026)) + - decode non-component special chars in Hashbang URLS + ([57b626](https://github.com/angular/angular.js/commit/57b626a673b7530399d3377dfe770165bec35f8a)) +- **ngModelController:** allow $overrideModelOptions to set updateOn + ([55516d](https://github.com/angular/angular.js/commit/55516da2dfc7c5798dce24e9fa930c5ac90c900c), + [#16351](https://github.com/angular/angular.js/issues/16351), + [#16364](https://github.com/angular/angular.js/issues/16364)) + + +## New Features +- **$parse:** add a hidden interface to retrieve an expression's AST + ([f33d95](https://github.com/angular/angular.js/commit/f33d95cfcff6fd0270f92a142df8794cca2013ad), + [#16253](https://github.com/angular/angular.js/issues/16253), + [#16260](https://github.com/angular/angular.js/issues/16260)) + # 1.6.7 imperial-backstroke (2017-11-24) diff --git a/src/ng/filter/filters.js b/src/ng/filter/filters.js index 5a79a7799929..482b31897c79 100644 --- a/src/ng/filter/filters.js +++ b/src/ng/filter/filters.js @@ -68,11 +68,14 @@ function currencyFilter($locale) { fractionSize = formats.PATTERNS[1].maxFrac; } + // If the currency symbol is empty, trim whitespace around the symbol + var currencySymbolRe = !currencySymbol ? /\s*\u00A4\s*/g : /\u00A4/g; + // if null or undefined pass it through return (amount == null) ? amount : formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, fractionSize). - replace(/\u00A4/g, currencySymbol); + replace(currencySymbolRe, currencySymbol); }; } diff --git a/src/ngResource/resource.js b/src/ngResource/resource.js index 55760d2f77e9..c8a79274ca2b 100644 --- a/src/ngResource/resource.js +++ b/src/ngResource/resource.js @@ -185,11 +185,12 @@ function shallowClearAndCopy(src, dst) { * for more information. * - **`responseType`** - `{string}` - see * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType). - * - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods - - * `response` and `responseError`. Both `response` and `responseError` interceptors get called - * with `http response` object. See {@link ng.$http $http interceptors}. In addition, the - * resource instance or array object is accessible by the `resource` property of the - * `http response` object. + * - **`interceptor`** - `{Object=}` - The interceptor object has four optional methods - + * `request`, `requestError`, `response`, and `responseError`. See + * {@link ng.$http $http interceptors} for details. Note that `request`/`requestError` + * interceptors are applied before calling `$http`, thus before any global `$http` interceptors. + * The resource instance or array object is accessible by the `resource` property of the + * `http response` object passed to response interceptors. * Keep in mind that the associated promise will be resolved with the value returned by the * response interceptor, if one is specified. The default response interceptor returns * `response.resource` (i.e. the resource instance or array). @@ -707,6 +708,9 @@ angular.module('ngResource', ['ng']). var isInstanceCall = this instanceof Resource; var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data)); var httpConfig = {}; + var requestInterceptor = action.interceptor && action.interceptor.request || undefined; + var requestErrorInterceptor = action.interceptor && action.interceptor.requestError || + undefined; var responseInterceptor = action.interceptor && action.interceptor.response || defaultResponseInterceptor; var responseErrorInterceptor = action.interceptor && action.interceptor.responseError || @@ -743,7 +747,14 @@ angular.module('ngResource', ['ng']). extend({}, extractParams(data, action.params || {}), params), action.url); - var promise = $http(httpConfig).then(function(response) { + // Start the promise chain + var promise = $q. + resolve(httpConfig). + then(requestInterceptor). + catch(requestErrorInterceptor). + then($http); + + promise = promise.then(function(response) { var data = response.data; if (data) { diff --git a/test/ng/filter/filtersSpec.js b/test/ng/filter/filtersSpec.js index 8e3a54a0b2df..0646dfa656af 100644 --- a/test/ng/filter/filtersSpec.js +++ b/test/ng/filter/filtersSpec.js @@ -186,6 +186,19 @@ describe('filters', function() { expect(currency(1.07)).toBe('$1.1'); })); + + it('should trim whitespace around the currency symbol if it is empty', + inject(function($locale) { + var pattern = $locale.NUMBER_FORMATS.PATTERNS[1]; + pattern.posPre = pattern.posSuf = ' \u00A4 '; + pattern.negPre = pattern.negSuf = ' - \u00A4 - '; + + expect(currency(+1.07, '$')).toBe(' $ 1.07 $ '); + expect(currency(-1.07, '$')).toBe(' - $ - 1.07 - $ - '); + expect(currency(+1.07, '')).toBe('1.07'); + expect(currency(-1.07, '')).toBe(' -- 1.07 -- '); + }) + ); }); describe('number', function() { diff --git a/test/ngResource/resourceSpec.js b/test/ngResource/resourceSpec.js index c472ad63f9f4..00fce4b662a8 100644 --- a/test/ngResource/resourceSpec.js +++ b/test/ngResource/resourceSpec.js @@ -3,7 +3,7 @@ describe('resource', function() { describe('basic usage', function() { - var $resource, CreditCard, callback, $httpBackend, resourceProvider; + var $resource, CreditCard, callback, $httpBackend, resourceProvider, $q; beforeEach(module('ngResource')); @@ -14,6 +14,7 @@ describe('basic usage', function() { beforeEach(inject(function($injector) { $httpBackend = $injector.get('$httpBackend'); $resource = $injector.get('$resource'); + $q = $injector.get('$q'); CreditCard = $resource('/CreditCard/:id:verb', {id:'@id.key'}, { charge:{ method:'post', @@ -1129,6 +1130,188 @@ describe('basic usage', function() { }); + describe('requestInterceptor', function() { + var rejectReason = {'lol':'cat'}; + var successSpy, failureSpy; + + beforeEach(function() { + successSpy = jasmine.createSpy('successSpy'); + failureSpy = jasmine.createSpy('failureSpy'); + }); + + it('should allow per action request interceptor that gets full configuration', function() { + var CreditCard = $resource('/CreditCard', {}, { + query: { + method: 'get', + isArray: true, + interceptor: { + request: function(httpConfig) { + callback(httpConfig); + return httpConfig; + } + } + } + }); + + $httpBackend.expect('GET', '/CreditCard').respond([{id: 1}]); + + var resource = CreditCard.query(); + resource.$promise.then(successSpy, failureSpy); + + $httpBackend.flush(); + expect(callback).toHaveBeenCalledOnce(); + expect(successSpy).toHaveBeenCalledOnce(); + expect(failureSpy).not.toHaveBeenCalled(); + + expect(callback).toHaveBeenCalledWith({ + 'method': 'get', + 'url': '/CreditCard' + }); + }); + + it('should call $http with the value returned from requestInterceptor', function() { + var CreditCard = $resource('/CreditCard', {}, { + query: { + method: 'get', + isArray: true, + interceptor: { + request: function(httpConfig) { + httpConfig.url = '/DebitCard'; + return httpConfig; + } + } + } + }); + + $httpBackend.expect('GET', '/DebitCard').respond([{id: 1}]); + + var resource = CreditCard.query(); + resource.$promise.then(successSpy, failureSpy); + + $httpBackend.flush(); + expect(successSpy).toHaveBeenCalledOnceWith(jasmine.arrayContaining([ + jasmine.objectContaining({id: 1}) + ])); + expect(failureSpy).not.toHaveBeenCalled(); + }); + + it('should abort the operation if the requestInterceptor rejects the operation', function() { + var CreditCard = $resource('/CreditCard', {}, { + query: { + method: 'get', + isArray: true, + interceptor: { + request: function() { + return $q.reject(rejectReason); + } + } + } + }); + + var resource = CreditCard.query(); + resource.$promise.then(successSpy, failureSpy); + + // Make sure all promises resolve. + $rootScope.$apply(); + + // Ensure the resource promise was rejected + expect(resource.$resolved).toBeTruthy(); + expect(successSpy).not.toHaveBeenCalled(); + expect(failureSpy).toHaveBeenCalledOnceWith(rejectReason); + + // Ensure that no requests were made. + $httpBackend.verifyNoOutstandingRequest(); + }); + + it('should call requestErrorInterceptor if requestInterceptor rejects the operation', function() { + var CreditCard = $resource('/CreditCard', {}, { + query: { + method: 'get', + isArray: true, + interceptor: { + request: function() { + return $q.reject(rejectReason); + }, + requestError: function(rejection) { + callback(rejection); + return $q.reject(rejection); + } + } + } + }); + + var resource = CreditCard.query(); + resource.$promise.then(successSpy, failureSpy); + $rootScope.$digest(); + + expect(callback).toHaveBeenCalledOnceWith(rejectReason); + expect(successSpy).not.toHaveBeenCalled(); + expect(failureSpy).toHaveBeenCalledOnceWith(rejectReason); + + // Ensure that no requests were made. + $httpBackend.verifyNoOutstandingRequest(); + }); + + it('should abort the operation if a requestErrorInterceptor rejects the operation', function() { + var CreditCard = $resource('/CreditCard', {}, { + query: { + method: 'get', + isArray: true, + interceptor: { + request: function() { + return $q.reject(rejectReason); + }, + requestError: function(rejection) { + return $q.reject(rejection); + } + } + } + }); + + var resource = CreditCard.query(); + resource.$promise.then(successSpy, failureSpy); + $rootScope.$apply(); + + expect(resource.$resolved).toBeTruthy(); + expect(successSpy).not.toHaveBeenCalled(); + expect(failureSpy).toHaveBeenCalledOnceWith(rejectReason); + + // Ensure that no requests were made. + $httpBackend.verifyNoOutstandingRequest(); + }); + + it('should continue the operation if a requestErrorInterceptor rescues it', function() { + var CreditCard = $resource('/CreditCard', {}, { + query: { + method: 'get', + isArray: true, + interceptor: { + request: function(httpConfig) { + return $q.reject(httpConfig); + }, + requestError: function(httpConfig) { + return $q.resolve(httpConfig); + } + } + } + }); + + $httpBackend.expect('GET', '/CreditCard').respond([{id: 1}]); + + var resource = CreditCard.query(); + resource.$promise.then(successSpy, failureSpy); + $httpBackend.flush(); + + expect(resource.$resolved).toBeTruthy(); + expect(successSpy).toHaveBeenCalledOnceWith(jasmine.arrayContaining([ + jasmine.objectContaining({id: 1}) + ])); + expect(failureSpy).not.toHaveBeenCalled(); + + $httpBackend.verifyNoOutstandingRequest(); + }); + }); + it('should allow per action response interceptor that gets full response', function() { CreditCard = $resource('/CreditCard', {}, { query: { @@ -1584,6 +1767,7 @@ describe('extra params', function() { var $http; var $httpBackend; var $resource; + var $rootScope; beforeEach(module('ngResource')); @@ -1593,10 +1777,11 @@ describe('extra params', function() { }); })); - beforeEach(inject(function(_$http_, _$httpBackend_, _$resource_) { + beforeEach(inject(function(_$http_, _$httpBackend_, _$resource_, _$rootScope_) { $http = _$http_; $httpBackend = _$httpBackend_; $resource = _$resource_; + $rootScope = _$rootScope_; })); afterEach(function() { @@ -1610,6 +1795,7 @@ describe('extra params', function() { var R = $resource('/:foo'); R.get({foo: 'bar', baz: 'qux'}); + $rootScope.$digest(); expect($http).toHaveBeenCalledWith(jasmine.objectContaining({params: {baz: 'qux'}})); }); @@ -1624,7 +1810,7 @@ describe('extra params', function() { }); describe('errors', function() { - var $httpBackend, $resource, $q; + var $httpBackend, $resource, $q, $rootScope; beforeEach(module(function($exceptionHandlerProvider) { $exceptionHandlerProvider.mode('log'); @@ -1636,6 +1822,7 @@ describe('errors', function() { $httpBackend = $injector.get('$httpBackend'); $resource = $injector.get('$resource'); $q = $injector.get('$q'); + $rootScope = $injector.get('$rootScope'); })); @@ -1838,6 +2025,81 @@ describe('handling rejections', function() { expect($exceptionHandler.errors[0]).toMatch(/^Error: should be caught/); } ); + + describe('requestInterceptor', function() { + var rejectReason = {'lol':'cat'}; + var $q, $rootScope; + var successSpy, failureSpy, callback; + + beforeEach(inject(function(_$q_, _$rootScope_) { + $q = _$q_; + $rootScope = _$rootScope_; + + successSpy = jasmine.createSpy('successSpy'); + failureSpy = jasmine.createSpy('failureSpy'); + callback = jasmine.createSpy(); + })); + + it('should call requestErrorInterceptor if requestInterceptor throws an error', function() { + var CreditCard = $resource('/CreditCard', {}, { + query: { + method: 'get', + isArray: true, + interceptor: { + request: function() { + throw rejectReason; + }, + requestError: function(rejection) { + callback(rejection); + return $q.reject(rejection); + } + } + } + }); + + var resource = CreditCard.query(); + resource.$promise.then(successSpy, failureSpy); + $rootScope.$apply(); + + expect(callback).toHaveBeenCalledOnce(); + expect(callback).toHaveBeenCalledWith(rejectReason); + expect(successSpy).not.toHaveBeenCalled(); + expect(failureSpy).toHaveBeenCalledOnce(); + expect(failureSpy).toHaveBeenCalledWith(rejectReason); + + // Ensure that no requests were made. + $httpBackend.verifyNoOutstandingRequest(); + }); + + it('should abort the operation if a requestErrorInterceptor throws an exception', function() { + var CreditCard = $resource('/CreditCard', {}, { + query: { + method: 'get', + isArray: true, + interceptor: { + request: function() { + return $q.reject(); + }, + requestError: function() { + throw rejectReason; + } + } + } + }); + + var resource = CreditCard.query(); + resource.$promise.then(successSpy, failureSpy); + $rootScope.$apply(); + + expect(resource.$resolved).toBeTruthy(); + expect(successSpy).not.toHaveBeenCalled(); + expect(failureSpy).toHaveBeenCalledOnce(); + expect(failureSpy).toHaveBeenCalledWith(rejectReason); + + // Ensure that no requests were made. + $httpBackend.verifyNoOutstandingRequest(); + }); + }); }); describe('cancelling requests', function() { @@ -1902,7 +2164,7 @@ describe('cancelling requests', function() { ); it('should use `cancellable` value if passed a non-numeric `timeout` in an action', - inject(function($log, $q) { + inject(function($log, $q, $rootScope) { spyOn($log, 'debug'); $httpBackend.whenGET('/CreditCard').respond({}); @@ -1915,6 +2177,7 @@ describe('cancelling requests', function() { }); var creditCard = CreditCard.get(); + $rootScope.$digest(); expect(creditCard.$cancelRequest).toBeDefined(); expect(httpSpy.calls.argsFor(0)[0].timeout).toEqual(jasmine.any($q)); expect(httpSpy.calls.argsFor(0)[0].timeout.then).toBeDefined();