diff --git a/app/scripts/angular-apimock.js b/app/scripts/angular-apimock.js index c4f47f8..c330656 100644 --- a/app/scripts/angular-apimock.js +++ b/app/scripts/angular-apimock.js @@ -42,6 +42,7 @@ angular.module('apiMock', []) var $location; var $log; var $q; + var $filter; var config = { defaultMock: false, mockDataPath: '/mock_data', @@ -68,7 +69,28 @@ angular.module('apiMock', []) return keys; } - // Taken from Angular 1.4.x: https://github.com/angular/angular.js/blob/f13852c179ffd9ec18b7a94df27dec39eb5f19fc/src/Angular.js#L296 + // TODO: IE8: remove when we drop IE8/Angular 1.2 support. + // Date.prototype.toISOString isn't supported in IE8. Which we need to support as long as we support Angular 1.2. + // Modified from MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString + function toISOString(date) { + function pad(number) { + if (number < 10) { + return '0' + number; + } + return number; + } + + return date.getUTCFullYear() + + '-' + pad(date.getUTCMonth() + 1) + + '-' + pad(date.getUTCDate()) + + 'T' + pad(date.getUTCHours()) + + ':' + pad(date.getUTCMinutes()) + + ':' + pad(date.getUTCSeconds()) + + '.' + (date.getUTCMilliseconds() / 1000).toFixed(3).slice(2, 5) + + 'Z'; + } + + // Taken as-is from Angular 1.4.x: https://github.com/angular/angular.js/blob/f13852c179ffd9ec18b7a94df27dec39eb5f19fc/src/Angular.js#L296 function forEachSorted(obj, iterator, context) { var keys = objectKeys(obj).sort(); for (var i = 0; i < keys.length; i++) { @@ -77,50 +99,49 @@ angular.module('apiMock', []) return keys; } - // Taken from Angular 1.4.x: https://github.com/angular/angular.js/blob/929ec6ba5a60e926654583033a90aebe716123c0/src/ng/http.js#L18 + // Modified from Angular 1.4.x: https://github.com/angular/angular.js/blob/929ec6ba5a60e926654583033a90aebe716123c0/src/ng/http.js#L18 function serializeValue(v) { - if (angular.isObject(v)) { - return angular.isDate(v) ? v.toISOString() : angular.toJson(v); + if (angular.isDate(v)) { + return toISOString(v); } + return v; } - // Taken from Angular 1.4.x: https://github.com/angular/angular.js/blob/720012eab6fef5e075a1d6876dd2e508c8e95b73/src/ngResource/resource.js#L405 - function encodeUriQuery(val, pctEncodeSpaces) { + // Modified from Angular 1.4.x: https://github.com/angular/angular.js/blob/720012eab6fef5e075a1d6876dd2e508c8e95b73/src/ngResource/resource.js#L405 + function encodeUriQuery(val) { return encodeURIComponent(val). replace(/%40/gi, '@'). replace(/%3A/gi, ':'). replace(/%24/g, '$'). replace(/%2C/gi, ','). - replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); + replace(/%20/g, '+'); } // TODO: replace with a $httpParamSerializerJQLikeProvider() call when we require Angular 1.4 (i.e. when we drop 1.2 and 1.3). - // Taken from Angular 1.4.x: https://github.com/angular/angular.js/blob/929ec6ba5a60e926654583033a90aebe716123c0/src/ng/http.js#L108 + // Modified from Angular 1.4.x: https://github.com/angular/angular.js/blob/929ec6ba5a60e926654583033a90aebe716123c0/src/ng/http.js#L108 function jQueryLikeParamSerializer(params) { - if (!params) { - return ''; - } - var parts = []; function serialize(toSerialize, prefix, topLevel) { - if (toSerialize === null || angular.isUndefined(toSerialize)) { - return; - } - if (angular.isArray(toSerialize)) { + // Serialize arrays. angular.forEach(toSerialize, function (value, index) { serialize(value, prefix + '[' + (angular.isObject(value) ? index : '') + ']'); }); } else if (angular.isObject(toSerialize) && !angular.isDate(toSerialize)) { + // Serialize objects (not dates, because that's covered by the default case). forEachSorted(toSerialize, function (value, key) { serialize(value, prefix + (topLevel ? '' : '[') + key + (topLevel ? '' : ']')); }); + } else if (toSerialize === undefined || toSerialize === '') { + // Keep empty parameters as it still affects the mock file path. + parts.push(encodeUriQuery(prefix)); } else { + // Serialize everything else (including dates). parts.push(encodeUriQuery(prefix) + '=' + encodeUriQuery(serializeValue(toSerialize))); } } @@ -247,15 +268,13 @@ angular.module('apiMock', []) } function removeFallback(res) { - var found = false; - angular.forEach(fallbacks, function (fallback, index) { - if (fallback.method === res.method && fallback.url === res.url) { - found = true; - fallbacks.splice(index, 1); - } - }); + var startLength = fallbacks.length; + fallbacks = $filter('filter')(fallbacks, { + method: '!' + res.method, + url: '!' + res.url + }, true); - return found; + return startLength > fallbacks.length; } function reroute(req) { @@ -309,10 +328,11 @@ angular.module('apiMock', []) // Expose public interface for provider instance // - function ApiMock(_$location, _$log, _$q) { + function ApiMock(_$location, _$log, _$q, _$filter) { $location = _$location; $log = _$log; $q = _$q; + $filter = _$filter; } var p = ApiMock.prototype; @@ -342,18 +362,13 @@ angular.module('apiMock', []) case 'respond': return httpStatusResponse(command.value); case 'ignore': - return req; - + /* falls through */ default: return req; } }; p.onResponse = function (res) { - if (config.disable) { - return res; - } - removeFallback(res); return res; }; @@ -382,8 +397,8 @@ angular.module('apiMock', []) angular.extend(config, options); }; - this.$get = function ($location, $log, $q) { - return new ApiMock($location, $log, $q); + this.$get = function ($location, $log, $q, $filter) { + return new ApiMock($location, $log, $q, $filter); }; }) @@ -393,10 +408,7 @@ angular.module('apiMock', []) * `apiMock` to determine if a mock should be done, then do the actual mocking. */ this.request = function (req) { - req = apiMock.onRequest(req); - - // Return the request or promise. - return req || $q.when(req); + return apiMock.onRequest(req); }; this.response = function (res) { diff --git a/karma.conf.js b/karma.conf.js index 63b0762..db78a9f 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -106,6 +106,8 @@ module.exports = function (config) { // You need to set `SAUCE_USERNAME` and `SAUCE_ACCESS_KEY` as environment variables. sauceLabs: { testName: 'Angular ApiMock', + recordVideo: false, + recordScreenshots: false, startConnect: true }, diff --git a/test/spec/services/angular-apimock.js b/test/spec/services/angular-apimock.js index 0022ed9..bf7cd06 100644 --- a/test/spec/services/angular-apimock.js +++ b/test/spec/services/angular-apimock.js @@ -708,6 +708,52 @@ describe('Service: apiMock', function () { expectHttpSuccess(); }); + + it('should serialize objects nested inside arrays', function () { + defaultRequest.url = '/api/pokemon'; + defaultRequest.params = { + 'moves': [ { + 'Thunderbolt': { + power: 95, + type: 'Electric' + }}, { + 'Double Edge': { + power: 120, + type: 'Normal' + } + } ] + }; + defaultExpectPath = '/mock_data/pokemon/moves%5b0%5d%5bthunderbolt%5d%5bpower%5d=95&moves%5b0%5d%5bthunderbolt%5d%5btype%5d=electric&moves%5b1%5d%5bdouble+edge%5d%5bpower%5d=120&moves%5b1%5d%5bdouble+edge%5d%5btype%5d=normal.get.json'; + + expectHttpSuccess(); + }); + + it('should handle empty value', function () { + defaultRequest.url = '/api/pokemon?releaseDate'; + defaultExpectPath = '/mock_data/pokemon/releasedate.get.json'; + + expectHttpSuccess(); + }); + + it('should handle undefined value', function () { + defaultRequest.url = '/api/pokemon'; + defaultRequest.params = { + 'releaseDate': undefined + }; + defaultExpectPath = '/mock_data/pokemon/releasedate.get.json'; + + expectHttpSuccess(); + }); + + it('should serialize date type', function () { + defaultRequest.url = '/api/pokemon'; + defaultRequest.params = { + 'releaseDate': new Date(Date.UTC(96, 1, 27, 0, 0, 0)) + }; + defaultExpectPath = '/mock_data/pokemon/releasedate=1996-02-27t00:00:00.000z.get.json'; + + expectHttpSuccess(); + }); }); describe('delay option', function () {