From 8218c4b60b82927234cf545253266f288fa936c2 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Mon, 26 Mar 2012 21:18:01 -0700 Subject: chore(Rakefile): get ready for modules --- Rakefile | 116 +++++------ angularFiles.js | 88 ++++---- gen_jstd_configs.js | 6 +- src/AngularPublic.js | 1 - src/ng/http.js | 2 +- src/ng/httpBackend.js | 2 +- src/ng/resource.js | 368 ---------------------------------- src/ngResource/resource.js | 419 +++++++++++++++++++++++++++++++++++++++ src/publishExternalApis.js | 3 + test-mocks.sh | 4 - test-modules.sh | 4 + test/ng/resourceSpec.js | 325 ------------------------------ test/ngMock/angular-mocksSpec.js | 3 +- test/ngResource/resourceSpec.js | 326 ++++++++++++++++++++++++++++++ test/testabilityPatch.js | 2 - 15 files changed, 860 insertions(+), 809 deletions(-) delete mode 100644 src/ng/resource.js create mode 100644 src/ngResource/resource.js create mode 100644 src/publishExternalApis.js delete mode 100755 test-mocks.sh create mode 100755 test-modules.sh delete mode 100644 test/ng/resourceSpec.js create mode 100644 test/ngResource/resourceSpec.js diff --git a/Rakefile b/Rakefile index 1ca59cf1..a61c99ea 100644 --- a/Rakefile +++ b/Rakefile @@ -2,7 +2,7 @@ require 'yaml' include FileUtils content = File.open('angularFiles.js', 'r') {|f| f.read } -files = eval(content.gsub(/\};(\s|\S)*/, '}').gsub(/angularFiles = /, '').gsub(/:/, '=>')); +files = eval(content.gsub(/\};(\s|\S)*/, '}').gsub(/angularFiles = /, '').gsub(/:/, '=>').gsub(/\/\//, '#')); BUILD_DIR = 'build' @@ -34,38 +34,24 @@ end desc 'Compile Scenario' task :compile_scenario => :init do - - deps = [ + + concatFile('angular-scenario.js', [ 'lib/jquery/jquery.js', 'src/ngScenario/angular.prefix', files['angularSrc'], files['angularScenario'], 'src/ngScenario/angular.suffix', - ] - - concat = 'cat ' + deps.flatten.join(' ') - - File.open(path_to('angular-scenario.js'), 'w') do |f| - f.write(%x{#{concat}}.gsub('"NG_VERSION_FULL"', NG_VERSION.full)) - f.write(gen_css('css/angular.css') + "\n") - f.write(gen_css('css/angular-scenario.css')) - end + ], gen_css('css/angular.css') + "\n" + gen_css('css/angular-scenario.css')) end desc 'Compile JSTD Scenario Adapter' task :compile_jstd_scenario_adapter => :init do - deps = [ + concatFile('jstd-scenario-adapter.js', [ 'src/ngScenario/jstd-scenario-adapter/angular.prefix', 'src/ngScenario/jstd-scenario-adapter/Adapter.js', 'src/ngScenario/jstd-scenario-adapter/angular.suffix', - ] - - concat = 'cat ' + deps.flatten.join(' ') - - File.open(path_to('jstd-scenario-adapter.js'), 'w') do |f| - f.write(%x{#{concat}}.gsub('"NG_VERSION_FULL"', NG_VERSION.full)) - end + ]) # TODO(vojta) use jstd configuration when implemented # (instead of including jstd-adapter-config.js) @@ -80,55 +66,24 @@ end desc 'Compile JavaScript' task :compile => [:init, :compile_scenario, :compile_jstd_scenario_adapter] do - deps = [ - 'src/angular.prefix', - files['angularSrc'], - 'src/angular.suffix', - ] - - File.open(path_to('angular.js'), 'w') do |f| - concat = 'cat ' + deps.flatten.join(' ') - - content = %x{#{concat}}. - gsub('"NG_VERSION_FULL"', NG_VERSION.full). - gsub('"NG_VERSION_MAJOR"', NG_VERSION.major). - gsub('"NG_VERSION_MINOR"', NG_VERSION.minor). - gsub('"NG_VERSION_DOT"', NG_VERSION.dot). - gsub('"NG_VERSION_CODENAME"', NG_VERSION.codename). - gsub(/^\s*['"]use strict['"];?\s*$/, ''). # remove all file-specific strict mode flags - gsub(/'USE STRICT'/, "'use strict'") # rename the placeholder in angular.prefix - - f.write(content) - f.write(gen_css('css/angular.css', true)) - end - - %x(java -jar lib/closure-compiler/compiler.jar \ - --compilation_level SIMPLE_OPTIMIZATIONS \ - --language_in ECMASCRIPT5_STRICT \ - --js #{path_to('angular.js')} \ - --js_output_file #{path_to('angular.min.js')}) + concatFile('angular.js', [ + 'src/angular.prefix', + files['angularSrc'], + 'src/angular.suffix', + ], gen_css('css/angular.css', true)) FileUtils.cp_r 'src/ngLocale', path_to('i18n') - File.open(path_to('angular-loader.js'), 'w') do |f| - concat = 'cat ' + [ + concatFile('angular-loader.js', [ 'src/loader.prefix', 'src/loader.js', - 'src/loader.suffix'].flatten.join(' ') - - content = %x{#{concat}}. - gsub('"NG_VERSION_FULL"', NG_VERSION.full). - gsub(/^\s*['"]use strict['"];?\s*$/, '') # remove all file-specific strict mode flags - - f.write(content) - end - - %x(java -jar lib/closure-compiler/compiler.jar \ - --compilation_level SIMPLE_OPTIMIZATIONS \ - --language_in ECMASCRIPT5_STRICT \ - --js #{path_to('angular-loader.js')} \ - --js_output_file #{path_to('angular-loader.min.js')}) + 'src/loader.suffix']) + + FileUtils.cp 'src/ngMock/angular-mocks.js', path_to('angular-mocks.js') + + closureCompile('angular.js') + closureCompile('angular-loader.js') end @@ -153,11 +108,11 @@ task :package => [:clean, :compile, :docs] do FileUtils.rm_r(path_to('pkg'), :force => true) FileUtils.mkdir_p(pkg_dir) - ['src/ngMock/angular-mocks.js', - path_to('angular.js'), - path_to('angular-loader.js'), + [ path_to('angular.js'), path_to('angular.min.js'), + path_to('angular-loader.js'), path_to('angular-loader.min.js'), + path_to('angular-mocks.js'), path_to('angular-scenario.js'), path_to('jstd-scenario-adapter.js'), path_to('jstd-scenario-adapter-config.js'), @@ -336,3 +291,32 @@ end def path_to(filename) return File.join(BUILD_DIR, *filename) end + +def closureCompile(filename) + puts "Compiling #{filename} ..." + %x(java -jar lib/closure-compiler/compiler.jar \ + --compilation_level SIMPLE_OPTIMIZATIONS \ + --language_in ECMASCRIPT5_STRICT \ + --js #{path_to(filename)} \ + --js_output_file #{path_to(filename.gsub(/\.js$/, '.min.js'))}) +end + +def concatFile(filename, deps, footer='') + puts "Building #{filename} ..." + File.open(path_to(filename), 'w') do |f| + concat = 'cat ' + deps.flatten.join(' ') + + content = %x{#{concat}}. + gsub('"NG_VERSION_FULL"', NG_VERSION.full). + gsub('"NG_VERSION_MAJOR"', NG_VERSION.major). + gsub('"NG_VERSION_MINOR"', NG_VERSION.minor). + gsub('"NG_VERSION_DOT"', NG_VERSION.dot). + gsub('"NG_VERSION_CODENAME"', NG_VERSION.codename). + gsub(/^\s*['"]use strict['"];?\s*$/, ''). # remove all file-specific strict mode flags + gsub(/'USE STRICT'/, "'use strict'") # rename the placeholder in angular.prefix + + f.write(content) + f.write(footer) + end +end + diff --git a/angularFiles.js b/angularFiles.js index 17162296..cb243e8e 100644 --- a/angularFiles.js +++ b/angularFiles.js @@ -66,6 +66,10 @@ angularFiles = { 'src/ng/directive/style.js' ], + 'angularSrcModules': [ + 'src/ngMock/angular-mocks.js' + ], + 'angularScenario': [ 'src/ngScenario/Scenario.js', 'src/ngScenario/Application.js', @@ -83,20 +87,9 @@ angularFiles = { 'src/ngScenario/output/Object.js' ], - 'jstd': [ - 'lib/jasmine/jasmine.js', - 'lib/jasmine-jstd-adapter/JasmineAdapter.js', - 'lib/jquery/jquery.js', - 'test/jquery_remove.js', - '@angularSrc', - 'example/personalLog/*.js', + 'angularTest': [ 'test/testabilityPatch.js', 'test/matchers.js', - 'src/ngScenario/Scenario.js', - 'src/ngScenario/output/*.js', - 'src/ngScenario/jstd-scenario-adapter/*.js', - 'src/ngScenario/*.js', - 'src/ngMock/angular-mocks.js', 'test/ngScenario/*.js', 'test/ngScenario/output/*.js', 'test/ngScenario/jstd-scenario-adapter/*.js', @@ -105,7 +98,21 @@ angularFiles = { 'test/ng/*.js', 'test/ng/directive/*.js', 'test/ng/filter/*.js', - 'test/ngMock/*.js', + 'test/ngMock/*.js' + ], + + 'jstd': [ + 'lib/jasmine/jasmine.js', + 'lib/jasmine-jstd-adapter/JasmineAdapter.js', + 'lib/jquery/jquery.js', + 'test/jquery_remove.js', + '@angularSrc', + 'src/publishExternalApis.js', + '@angularSrcModules', + '@angularScenario', + 'src/ngScenario/jstd-scenario-adapter/Adapter.js', + '@angularTest', + 'example/personalLog/*.js', 'example/personalLog/test/*.js' ], @@ -122,19 +129,20 @@ angularFiles = { 'build/docs/docs-scenario.js' ], - 'jstdMocks': [ + "jstdModules": [ 'lib/jasmine/jasmine.js', 'lib/jasmine-jstd-adapter/JasmineAdapter.js', 'build/angular.js', 'src/ngMock/angular-mocks.js', 'test/matchers.js', - 'test/ngMock/angular-mocksSpec.js' + 'test/ngMock/*.js', ], 'jstdPerf': [ 'lib/jasmine/jasmine.js', 'lib/jasmine-jstd-adapter/JasmineAdapter.js', - 'angularSrc', + '@angularSrc', + '@angularSrcModules', 'src/ngMock/angular-mocks.js', 'perf/data/*.js', 'perf/testUtils.js', @@ -152,23 +160,12 @@ angularFiles = { 'lib/jquery/jquery.js', 'test/jquery_alias.js', '@angularSrc', + 'src/publishExternalApis.js', + '@angularSrcModules', + '@angularScenario', + 'src/ngScenario/jstd-scenario-adapter/Adapter.js', + '@angularTest', 'example/personalLog/*.js', - 'test/testabilityPatch.js', - 'test/matchers.js', - 'src/ngScenario/Scenario.js', - 'src/ngScenario/output/*.js', - 'src/ngScenario/jstd-scenario-adapter/*.js', - 'src/ngScenario/*.js', - 'src/ngMock/angular-mocks.js', - 'test/ngScenario/*.js', - 'test/ngScenario/output/*.js', - 'test/ngScenario/jstd-scenario-adapter/*.js', - 'test/*.js', - 'test/auto/*.js', - 'test/ng/*.js', - 'test/ng/directive/*.js', - 'test/ng/filter/*.js', - 'test/ngMock/*.js', 'example/personalLog/test/*.js' ], @@ -181,15 +178,30 @@ angularFiles = { // Execute only in slim-jim if (typeof JASMINE_ADAPTER !== 'undefined') { - // SlimJim config + // Testacular config + var mergedFiles = []; + angularFiles.jstd.forEach(function(file) { + // replace @ref + var match = file.match(/^\@(.*)/); + if (match) { + var deps = angularFiles[match[1]]; + if (!deps) { + console.log('No dependency:' + file) + } + mergedFiles = mergedFiles.concat(deps); + } else { + mergedFiles.push(file); + } + }); + files = [JASMINE, JASMINE_ADAPTER]; - angularFiles.jstd.forEach(function(pattern) { - // replace angular source - if (pattern === '@angularSrc') files = files.concat(angularFiles.angularSrc); - // ignore jstd and jasmine files - else if (!/jstd|jasmine/.test(pattern)) files.push(pattern); + + mergedFiles.forEach(function(file){ + if (/jstd|jasmine/.test(file)) return; + files.push(file); }); + exclude = angularFiles.jstdExclude; autoWatch = true; diff --git a/gen_jstd_configs.js b/gen_jstd_configs.js index 7165d202..842cfe21 100755 --- a/gen_jstd_configs.js +++ b/gen_jstd_configs.js @@ -16,7 +16,7 @@ fs.readFile('angularFiles.js', function(err, data) { fs.writeFile('./jsTestDriver.conf', prefix + combine(angularFiles.jstd, angularFiles.jstdExclude)); - fs.writeFile('./jsTestDriver-mocks.conf', prefix + combine(angularFiles.jstdMocks)); + fs.writeFile('./jsTestDriver-modules.conf', prefix + combine(angularFiles.jstdModules)); fs.writeFile('./jsTestDriver-scenario.conf', prefixScenario + combine(angularFiles.jstdScenario) + @@ -40,6 +40,8 @@ function combine(load, exclude) { if (exclude) fileList += ('\n\nexclude:\n- ' + exclude.join('\n- ')); //Replace placeholders for src list before returning - return fileList.replace(/@angularSrc/g, angularSrc); + return fileList.replace(/@(.*)/g, function(all, alias) { + return angularFiles[alias].join('\n- '); + }); } diff --git a/src/AngularPublic.js b/src/AngularPublic.js index 9a0e1977..ec307962 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -122,7 +122,6 @@ function publishExternalAPI(angular){ $location: $LocationProvider, $log: $LogProvider, $parse: $ParseProvider, - $resource: $ResourceProvider, $route: $RouteProvider, $routeParams: $RouteParamsProvider, $rootScope: $RootScopeProvider, diff --git a/src/ng/http.js b/src/ng/http.js index c2cbd161..e67dd496 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -152,7 +152,7 @@ function $HttpProvider() { * For unit testing applications that use `$http` service, see * {@link angular.module.ngMock.$httpBackend $httpBackend mock}. * - * For a higher level of abstraction, please check out the {@link angular.module.ng.$resource + * For a higher level of abstraction, please check out the {@link angular.module.ngResource.$resource * $resource} service. * * The $http API is based on the {@link angular.module.ng.$q deferred/promise APIs} exposed by diff --git a/src/ng/httpBackend.js b/src/ng/httpBackend.js index 201d1a87..abe1d8f5 100644 --- a/src/ng/httpBackend.js +++ b/src/ng/httpBackend.js @@ -18,7 +18,7 @@ var XHR = window.XMLHttpRequest || function() { * XMLHttpRequest object or JSONP and deals with browser incompatibilities. * * You should never need to use this service directly, instead use the higher-level abstractions: - * {@link angular.module.ng.$http $http} or {@link angular.module.ng.$resource $resource}. + * {@link angular.module.ng.$http $http} or {@link angular.module.ngResource.$resource $resource}. * * During testing this implementation is swapped with {@link angular.module.ngMock.$httpBackend mock * $httpBackend} which can be trained with responses. diff --git a/src/ng/resource.js b/src/ng/resource.js deleted file mode 100644 index 3aa48e74..00000000 --- a/src/ng/resource.js +++ /dev/null @@ -1,368 +0,0 @@ -'use strict'; - -/** - * @ngdoc object - * @name angular.module.ng.$resource - * @requires $http - * - * @description - * A factory which creates a resource object that lets you interact with - * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. - * - * The returned resource object has action methods which provide high-level behaviors without - * the need to interact with the low level {@link angular.module.ng.$http $http} service. - * - * @param {string} url A parameterized URL template with parameters prefixed by `:` as in - * `/user/:username`. - * - * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in - * `actions` methods. - * - * Each key value in the parameter object is first bound to url template if present and then any - * excess keys are appended to the url search query after the `?`. - * - * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in - * URL `/path/greet?salutation=Hello`. - * - * If the parameter value is prefixed with `@` then the value of that parameter is extracted from - * the data object (useful for non-GET operations). - * - * @param {Object.=} actions Hash with declaration of custom action that should extend the - * default set of resource actions. The declaration should be created in the following format: - * - * {action1: {method:?, params:?, isArray:?}, - * action2: {method:?, params:?, isArray:?}, - * ...} - * - * Where: - * - * - `action` – {string} – The name of action. This name becomes the name of the method on your - * resource object. - * - `method` – {string} – HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`, - * and `JSONP` - * - `params` – {object=} – Optional set of pre-bound parameters for this action. - * - isArray – {boolean=} – If true then the returned object for this action is an array, see - * `returns` section. - * - * @returns {Object} A resource "class" object with methods for the default set of resource actions - * optionally extended with custom `actions`. The default set contains these actions: - * - * { 'get': {method:'GET'}, - * 'save': {method:'POST'}, - * 'query': {method:'GET', isArray:true}, - * 'remove': {method:'DELETE'}, - * 'delete': {method:'DELETE'} }; - * - * Calling these methods invoke an {@link angular.module.ng.$http} with the specified http method, - * destination and parameters. When the data is returned from the server then the object is an - * instance of the resource class `save`, `remove` and `delete` actions are available on it as - * methods with the `$` prefix. This allows you to easily perform CRUD operations (create, read, - * update, delete) on server-side data like this: - *
-        var User = $resource('/user/:userId', {userId:'@id'});
-        var user = User.get({userId:123}, function() {
-          user.abc = true;
-          user.$save();
-        });
-     
- * - * It is important to realize that invoking a $resource object method immediately returns an - * empty reference (object or array depending on `isArray`). Once the data is returned from the - * server the existing reference is populated with the actual data. This is a useful trick since - * usually the resource is assigned to a model which is then rendered by the view. Having an empty - * object results in no rendering, once the data arrives from the server then the object is - * populated with the data and the view automatically re-renders itself showing the new data. This - * means that in most case one never has to write a callback function for the action methods. - * - * The action methods on the class object or instance object can be invoked with the following - * parameters: - * - * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` - * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` - * - non-GET instance actions: `instance.$action([parameters], [success], [error])` - * - * - * @example - * - * # Credit card resource - * - *
-     // Define CreditCard class
-     var CreditCard = $resource('/user/:userId/card/:cardId',
-      {userId:123, cardId:'@id'}, {
-       charge: {method:'POST', params:{charge:true}}
-      });
-
-     // We can retrieve a collection from the server
-     var cards = CreditCard.query();
-     // GET: /user/123/card
-     // server returns: [ {id:456, number:'1234', name:'Smith'} ];
-
-     var card = cards[0];
-     // each item is an instance of CreditCard
-     expect(card instanceof CreditCard).toEqual(true);
-     card.name = "J. Smith";
-     // non GET methods are mapped onto the instances
-     card.$save();
-     // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'}
-     // server returns: {id:456, number:'1234', name: 'J. Smith'};
-
-     // our custom method is mapped as well.
-     card.$charge({amount:9.99});
-     // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'}
-     // server returns: {id:456, number:'1234', name: 'J. Smith'};
-
-     // we can create an instance as well
-     var newCard = new CreditCard({number:'0123'});
-     newCard.name = "Mike Smith";
-     newCard.$save();
-     // POST: /user/123/card {number:'0123', name:'Mike Smith'}
-     // server returns: {id:789, number:'01234', name: 'Mike Smith'};
-     expect(newCard.id).toEqual(789);
- * 
- * - * The object returned from this function execution is a resource "class" which has "static" method - * for each action in the definition. - * - * Calling these methods invoke `$http` on the `url` template with the given `method` and `params`. - * When the data is returned from the server then the object is an instance of the resource type and - * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD - * operations (create, read, update, delete) on server-side data. - -
-     var User = $resource('/user/:userId', {userId:'@id'});
-     var user = User.get({userId:123}, function() {
-       user.abc = true;
-       user.$save();
-     });
-   
- * - * It's worth noting that the success callback for `get`, `query` and other method gets passed - * in the response that came from the server as well as $http header getter function, so one - * could rewrite the above example and get access to http headers as: - * -
-     var User = $resource('/user/:userId', {userId:'@id'});
-     User.get({userId:123}, function(u, getResponseHeaders){
-       u.abc = true;
-       u.$save(function(u, putResponseHeaders) {
-         //u => saved user object
-         //putResponseHeaders => $http header getter
-       });
-     });
-   
- - * # Buzz client - - Let's look at what a buzz client created with the `$resource` service looks like: - - - - -
- - -
-
-

- - {{item.actor.name}} - Expand replies: {{item.links.replies[0].count}} -

- {{item.object.content | html}} -
- - {{reply.actor.name}}: {{reply.content | html}} -
-
-
-
- - -
- */ -function $ResourceProvider() { - this.$get = ['$http', function($http) { - var DEFAULT_ACTIONS = { - 'get': {method:'GET'}, - 'save': {method:'POST'}, - 'query': {method:'GET', isArray:true}, - 'remove': {method:'DELETE'}, - 'delete': {method:'DELETE'} - }; - - - function Route(template, defaults) { - this.template = template = template + '#'; - this.defaults = defaults || {}; - var urlParams = this.urlParams = {}; - forEach(template.split(/\W/), function(param){ - if (param && template.match(new RegExp("[^\\\\]:" + param + "\\W"))) { - urlParams[param] = true; - } - }); - this.template = template.replace(/\\:/g, ':'); - } - - Route.prototype = { - url: function(params) { - var self = this, - url = this.template, - encodedVal; - - params = params || {}; - forEach(this.urlParams, function(_, urlParam){ - encodedVal = encodeUriSegment(params[urlParam] || self.defaults[urlParam] || ""); - url = url.replace(new RegExp(":" + urlParam + "(\\W)"), encodedVal + "$1"); - }); - url = url.replace(/\/?#$/, ''); - var query = []; - forEachSorted(params, function(value, key){ - if (!self.urlParams[key]) { - query.push(encodeUriQuery(key) + '=' + encodeUriQuery(value)); - } - }); - url = url.replace(/\/*$/, ''); - return url + (query.length ? '?' + query.join('&') : ''); - } - }; - - - function ResourceFactory(url, paramDefaults, actions) { - var route = new Route(url); - - actions = extend({}, DEFAULT_ACTIONS, actions); - - function extractParams(data){ - var ids = {}; - forEach(paramDefaults || {}, function(value, key){ - ids[key] = value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value; - }); - return ids; - } - - function Resource(value){ - copy(value || {}, this); - } - - forEach(actions, function(action, name) { - var isPostOrPut = action.method == 'POST' || action.method == 'PUT'; - Resource[name] = function(a1, a2, a3, a4) { - var params = {}; - var data; - var success = noop; - var error = null; - switch(arguments.length) { - case 4: - error = a4; - success = a3; - //fallthrough - case 3: - case 2: - if (isFunction(a2)) { - if (isFunction(a1)) { - success = a1; - error = a2; - break; - } - - success = a2; - error = a3; - //fallthrough - } else { - params = a1; - data = a2; - success = a3; - break; - } - case 1: - if (isFunction(a1)) success = a1; - else if (isPostOrPut) data = a1; - else params = a1; - break; - case 0: break; - default: - throw "Expected between 0-4 arguments [params, data, success, error], got " + - arguments.length + " arguments."; - } - - var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data)); - $http({ - method: action.method, - url: route.url(extend({}, extractParams(data), action.params || {}, params)), - data: data - }).then(function(response) { - var data = response.data; - - if (data) { - if (action.isArray) { - value.length = 0; - forEach(data, function(item) { - value.push(new Resource(item)); - }); - } else { - copy(data, value); - } - } - (success||noop)(value, response.headers); - }, error); - - return value; - }; - - - Resource.bind = function(additionalParamDefaults){ - return ResourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); - }; - - - Resource.prototype['$' + name] = function(a1, a2, a3) { - var params = extractParams(this), - success = noop, - error; - - switch(arguments.length) { - case 3: params = a1; success = a2; error = a3; break; - case 2: - case 1: - if (isFunction(a1)) { - success = a1; - error = a2; - } else { - params = a1; - success = a2 || noop; - } - case 0: break; - default: - throw "Expected between 1-3 arguments [params, success, error], got " + - arguments.length + " arguments."; - } - var data = isPostOrPut ? this : undefined; - Resource[name].call(this, params, data, success, error); - }; - }); - return Resource; - } - - return ResourceFactory; - }]; -} diff --git a/src/ngResource/resource.js b/src/ngResource/resource.js new file mode 100644 index 00000000..fe111b47 --- /dev/null +++ b/src/ngResource/resource.js @@ -0,0 +1,419 @@ +'use strict'; + +/** + * @ngdoc overview + * @name angular.module.ngResource + * @description + */ + + /** + * @ngdoc object + * @name angular.module.ngResource.$resource + * @requires $http + * + * @description + * A factory which creates a resource object that lets you interact with + * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. + * + * The returned resource object has action methods which provide high-level behaviors without + * the need to interact with the low level {@link angular.module.ng.$http $http} service. + * + * @param {string} url A parameterized URL template with parameters prefixed by `:` as in + * `/user/:username`. + * + * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in + * `actions` methods. + * + * Each key value in the parameter object is first bound to url template if present and then any + * excess keys are appended to the url search query after the `?`. + * + * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in + * URL `/path/greet?salutation=Hello`. + * + * If the parameter value is prefixed with `@` then the value of that parameter is extracted from + * the data object (useful for non-GET operations). + * + * @param {Object.=} actions Hash with declaration of custom action that should extend the + * default set of resource actions. The declaration should be created in the following format: + * + * {action1: {method:?, params:?, isArray:?}, + * action2: {method:?, params:?, isArray:?}, + * ...} + * + * Where: + * + * - `action` – {string} – The name of action. This name becomes the name of the method on your + * resource object. + * - `method` – {string} – HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`, + * and `JSONP` + * - `params` – {object=} – Optional set of pre-bound parameters for this action. + * - isArray – {boolean=} – If true then the returned object for this action is an array, see + * `returns` section. + * + * @returns {Object} A resource "class" object with methods for the default set of resource actions + * optionally extended with custom `actions`. The default set contains these actions: + * + * { 'get': {method:'GET'}, + * 'save': {method:'POST'}, + * 'query': {method:'GET', isArray:true}, + * 'remove': {method:'DELETE'}, + * 'delete': {method:'DELETE'} }; + * + * Calling these methods invoke an {@link angular.module.ng.$http} with the specified http method, + * destination and parameters. When the data is returned from the server then the object is an + * instance of the resource class `save`, `remove` and `delete` actions are available on it as + * methods with the `$` prefix. This allows you to easily perform CRUD operations (create, read, + * update, delete) on server-side data like this: + *
+        var User = $resource('/user/:userId', {userId:'@id'});
+        var user = User.get({userId:123}, function() {
+          user.abc = true;
+          user.$save();
+        });
+     
+ * + * It is important to realize that invoking a $resource object method immediately returns an + * empty reference (object or array depending on `isArray`). Once the data is returned from the + * server the existing reference is populated with the actual data. This is a useful trick since + * usually the resource is assigned to a model which is then rendered by the view. Having an empty + * object results in no rendering, once the data arrives from the server then the object is + * populated with the data and the view automatically re-renders itself showing the new data. This + * means that in most case one never has to write a callback function for the action methods. + * + * The action methods on the class object or instance object can be invoked with the following + * parameters: + * + * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` + * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` + * - non-GET instance actions: `instance.$action([parameters], [success], [error])` + * + * + * @example + * + * # Credit card resource + * + *
+     // Define CreditCard class
+     var CreditCard = $resource('/user/:userId/card/:cardId',
+      {userId:123, cardId:'@id'}, {
+       charge: {method:'POST', params:{charge:true}}
+      });
+
+     // We can retrieve a collection from the server
+     var cards = CreditCard.query();
+     // GET: /user/123/card
+     // server returns: [ {id:456, number:'1234', name:'Smith'} ];
+
+     var card = cards[0];
+     // each item is an instance of CreditCard
+     expect(card instanceof CreditCard).toEqual(true);
+     card.name = "J. Smith";
+     // non GET methods are mapped onto the instances
+     card.$save();
+     // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'}
+     // server returns: {id:456, number:'1234', name: 'J. Smith'};
+
+     // our custom method is mapped as well.
+     card.$charge({amount:9.99});
+     // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'}
+     // server returns: {id:456, number:'1234', name: 'J. Smith'};
+
+     // we can create an instance as well
+     var newCard = new CreditCard({number:'0123'});
+     newCard.name = "Mike Smith";
+     newCard.$save();
+     // POST: /user/123/card {number:'0123', name:'Mike Smith'}
+     // server returns: {id:789, number:'01234', name: 'Mike Smith'};
+     expect(newCard.id).toEqual(789);
+ * 
+ * + * The object returned from this function execution is a resource "class" which has "static" method + * for each action in the definition. + * + * Calling these methods invoke `$http` on the `url` template with the given `method` and `params`. + * When the data is returned from the server then the object is an instance of the resource type and + * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD + * operations (create, read, update, delete) on server-side data. + +
+     var User = $resource('/user/:userId', {userId:'@id'});
+     var user = User.get({userId:123}, function() {
+       user.abc = true;
+       user.$save();
+     });
+   
+ * + * It's worth noting that the success callback for `get`, `query` and other method gets passed + * in the response that came from the server as well as $http header getter function, so one + * could rewrite the above example and get access to http headers as: + * +
+     var User = $resource('/user/:userId', {userId:'@id'});
+     User.get({userId:123}, function(u, getResponseHeaders){
+       u.abc = true;
+       u.$save(function(u, putResponseHeaders) {
+         //u => saved user object
+         //putResponseHeaders => $http header getter
+       });
+     });
+   
+ + * # Buzz client + + Let's look at what a buzz client created with the `$resource` service looks like: + + + + +
+ + +
+
+

+ + {{item.actor.name}} + Expand replies: {{item.links.replies[0].count}} +

+ {{item.object.content | html}} +
+ + {{reply.actor.name}}: {{reply.content | html}} +
+
+
+
+ + +
+ */ +angular.module('ngResource', ['ng']). + factory('$resource', ['$http', '$parse', function($http, $parse) { + var DEFAULT_ACTIONS = { + 'get': {method:'GET'}, + 'save': {method:'POST'}, + 'query': {method:'GET', isArray:true}, + 'remove': {method:'DELETE'}, + 'delete': {method:'DELETE'} + }; + var forEach = angular.forEach, + extend = angular.extend, + copy = angular.copy, + isFunction = angular.isFunction, + getter = function(obj, path) { + return $parse(path)(obj); + }; + + /** + * We need our custom mehtod because encodeURIComponent is too agressive and doesn't follow + * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path + * segments: + * segment = *pchar + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + * pct-encoded = "%" HEXDIG HEXDIG + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + */ + function encodeUriSegment(val) { + return encodeUriQuery(val, true). + replace(/%26/gi, '&'). + replace(/%3D/gi, '='). + replace(/%2B/gi, '+'); + } + + + /** + * This method is intended for encoding *key* or *value* parts of query component. We need a custom + * method becuase encodeURIComponent is too agressive and encodes stuff that doesn't have to be + * encoded per http://tools.ietf.org/html/rfc3986: + * query = *( pchar / "/" / "?" ) + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * pct-encoded = "%" HEXDIG HEXDIG + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + */ + function encodeUriQuery(val, pctEncodeSpaces) { + return encodeURIComponent(val). + replace(/%40/gi, '@'). + replace(/%3A/gi, ':'). + replace(/%24/g, '$'). + replace(/%2C/gi, ','). + replace((pctEncodeSpaces ? null : /%20/g), '+'); + } + + function Route(template, defaults) { + this.template = template = template + '#'; + this.defaults = defaults || {}; + var urlParams = this.urlParams = {}; + forEach(template.split(/\W/), function(param){ + if (param && template.match(new RegExp("[^\\\\]:" + param + "\\W"))) { + urlParams[param] = true; + } + }); + this.template = template.replace(/\\:/g, ':'); + } + + Route.prototype = { + url: function(params) { + var self = this, + url = this.template, + encodedVal; + + params = params || {}; + forEach(this.urlParams, function(_, urlParam){ + encodedVal = encodeUriSegment(params[urlParam] || self.defaults[urlParam] || ""); + url = url.replace(new RegExp(":" + urlParam + "(\\W)"), encodedVal + "$1"); + }); + url = url.replace(/\/?#$/, ''); + var query = []; + forEach(params, function(value, key){ + if (!self.urlParams[key]) { + query.push(encodeUriQuery(key) + '=' + encodeUriQuery(value)); + } + }); + query.sort(); + url = url.replace(/\/*$/, ''); + return url + (query.length ? '?' + query.join('&') : ''); + } + }; + + + function ResourceFactory(url, paramDefaults, actions) { + var route = new Route(url); + + actions = extend({}, DEFAULT_ACTIONS, actions); + + function extractParams(data){ + var ids = {}; + forEach(paramDefaults || {}, function(value, key){ + ids[key] = value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value; + }); + return ids; + } + + function Resource(value){ + copy(value || {}, this); + } + + forEach(actions, function(action, name) { + var isPostOrPut = action.method == 'POST' || action.method == 'PUT'; + Resource[name] = function(a1, a2, a3, a4) { + var params = {}; + var data; + var success = noop; + var error = null; + switch(arguments.length) { + case 4: + error = a4; + success = a3; + //fallthrough + case 3: + case 2: + if (isFunction(a2)) { + if (isFunction(a1)) { + success = a1; + error = a2; + break; + } + + success = a2; + error = a3; + //fallthrough + } else { + params = a1; + data = a2; + success = a3; + break; + } + case 1: + if (isFunction(a1)) success = a1; + else if (isPostOrPut) data = a1; + else params = a1; + break; + case 0: break; + default: + throw "Expected between 0-4 arguments [params, data, success, error], got " + + arguments.length + " arguments."; + } + + var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data)); + $http({ + method: action.method, + url: route.url(extend({}, extractParams(data), action.params || {}, params)), + data: data + }).then(function(response) { + var data = response.data; + + if (data) { + if (action.isArray) { + value.length = 0; + forEach(data, function(item) { + value.push(new Resource(item)); + }); + } else { + copy(data, value); + } + } + (success||noop)(value, response.headers); + }, error); + + return value; + }; + + + Resource.bind = function(additionalParamDefaults){ + return ResourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); + }; + + + Resource.prototype['$' + name] = function(a1, a2, a3) { + var params = extractParams(this), + success = noop, + error; + + switch(arguments.length) { + case 3: params = a1; success = a2; error = a3; break; + case 2: + case 1: + if (isFunction(a1)) { + success = a1; + error = a2; + } else { + params = a1; + success = a2 || noop; + } + case 0: break; + default: + throw "Expected between 1-3 arguments [params, success, error], got " + + arguments.length + " arguments."; + } + var data = isPostOrPut ? this : undefined; + Resource[name].call(this, params, data, success, error); + }; + }); + return Resource; + } + + return ResourceFactory; + }]); diff --git a/src/publishExternalApis.js b/src/publishExternalApis.js new file mode 100644 index 00000000..6a6acd13 --- /dev/null +++ b/src/publishExternalApis.js @@ -0,0 +1,3 @@ +'use strict'; + +publishExternalAPI(angular); diff --git a/test-mocks.sh b/test-mocks.sh deleted file mode 100755 index b91fc901..00000000 --- a/test-mocks.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -if [ ! -e test.dissable ]; then - java -jar lib/jstestdriver/JsTestDriver.jar --tests all --config jsTestDriver-mocks.conf $@ -fi diff --git a/test-modules.sh b/test-modules.sh new file mode 100755 index 00000000..58693336 --- /dev/null +++ b/test-modules.sh @@ -0,0 +1,4 @@ +#!/bin/bash +if [ ! -e test.dissable ]; then + java -jar lib/jstestdriver/JsTestDriver.jar --tests all --config jsTestDriver-modules.conf $@ +fi diff --git a/test/ng/resourceSpec.js b/test/ng/resourceSpec.js deleted file mode 100644 index e0049761..00000000 --- a/test/ng/resourceSpec.js +++ /dev/null @@ -1,325 +0,0 @@ -'use strict'; - -describe("resource", function() { - var $resource, CreditCard, callback, $httpBackend; - - beforeEach(inject(function($injector) { - $httpBackend = $injector.get('$httpBackend'); - $resource = $injector.get('$resource'); - CreditCard = $resource('/CreditCard/:id:verb', {id:'@id.key'}, { - charge:{ - method:'POST', - params:{verb:'!charge'} - } - }); - callback = jasmine.createSpy(); - })); - - - afterEach(function() { - $httpBackend.verifyNoOutstandingExpectation(); - }); - - - it("should build resource", function() { - expect(typeof CreditCard).toBe('function'); - expect(typeof CreditCard.get).toBe('function'); - expect(typeof CreditCard.save).toBe('function'); - expect(typeof CreditCard.remove).toBe('function'); - expect(typeof CreditCard['delete']).toBe('function'); - expect(typeof CreditCard.query).toBe('function'); - }); - - - it('should default to empty parameters', function() { - $httpBackend.expect('GET', 'URL').respond({}); - $resource('URL').query(); - }); - - - it('should ignore slashes of undefinend parameters', function() { - var R = $resource('/Path/:a/:b/:c'); - - $httpBackend.when('GET').respond('{}'); - $httpBackend.expect('GET', '/Path'); - $httpBackend.expect('GET', '/Path/1'); - $httpBackend.expect('GET', '/Path/2/3'); - $httpBackend.expect('GET', '/Path/4/5/6'); - - R.get({}); - R.get({a:1}); - R.get({a:2, b:3}); - R.get({a:4, b:5, c:6}); - }); - - - it('should support escaping colons in url template', function() { - var R = $resource('http://localhost\\:8080/Path/:a/\\:stillPath/:b'); - - $httpBackend.expect('GET', 'http://localhost:8080/Path/foo/:stillPath/bar').respond(); - R.get({a: 'foo', b: 'bar'}); - }); - - - it('should correctly encode url params', function() { - var R = $resource('/Path/:a'); - - $httpBackend.expect('GET', '/Path/foo%231').respond('{}'); - $httpBackend.expect('GET', '/Path/doh!@foo?bar=baz%231').respond('{}'); - - R.get({a: 'foo#1'}); - R.get({a: 'doh!@foo', bar: 'baz#1'}); - }); - - - it('should not encode @ in url params', function() { - //encodeURIComponent is too agressive and doesn't follow http://www.ietf.org/rfc/rfc3986.txt - //with regards to the character set (pchar) allowed in path segments - //so we need this test to make sure that we don't over-encode the params and break stuff like - //buzz api which uses @self - - var R = $resource('/Path/:a'); - $httpBackend.expect('GET', '/Path/doh@fo%20o?!do%26h=g%3Da+h&:bar=$baz@1').respond('{}'); - R.get({a: 'doh@fo o', ':bar': '$baz@1', '!do&h': 'g=a h'}); - }); - - - it('should encode & in url params', function() { - var R = $resource('/Path/:a'); - $httpBackend.expect('GET', '/Path/doh&foo?bar=baz%261').respond('{}'); - R.get({a: 'doh&foo', bar: 'baz&1'}); - }); - - - it('should build resource with default param', function() { - $httpBackend.expect('GET', '/Order/123/Line/456.visa?minimum=0.05').respond({id: 'abc'}); - var LineItem = $resource('/Order/:orderId/Line/:id:verb', - {orderId: '123', id: '@id.key', verb:'.visa', minimum: 0.05}); - var item = LineItem.get({id: 456}); - $httpBackend.flush(); - expect(item).toEqualData({id:'abc'}); - }); - - - it("should build resource with action default param overriding default param", function() { - $httpBackend.expect('GET', '/Customer/123').respond({id: 'abc'}); - var TypeItem = $resource('/:type/:typeId', {type: 'Order'}, - {get: {method: 'GET', params: {type: 'Customer'}}}); - var item = TypeItem.get({typeId: 123}); - - $httpBackend.flush(); - expect(item).toEqualData({id: 'abc'}); - }); - - - it("should create resource", function() { - $httpBackend.expect('POST', '/CreditCard', '{"name":"misko"}').respond({id: 123, name: 'misko'}); - - var cc = CreditCard.save({name: 'misko'}, callback); - expect(cc).toEqualData({name: 'misko'}); - expect(callback).not.toHaveBeenCalled(); - - $httpBackend.flush(); - expect(cc).toEqualData({id: 123, name: 'misko'}); - expect(callback).toHaveBeenCalledOnce(); - expect(callback.mostRecentCall.args[0]).toEqual(cc); - expect(callback.mostRecentCall.args[1]()).toEqual({}); - }); - - - it("should read resource", function() { - $httpBackend.expect('GET', '/CreditCard/123').respond({id: 123, number: '9876'}); - var cc = CreditCard.get({id: 123}, callback); - - expect(cc instanceof CreditCard).toBeTruthy(); - expect(cc).toEqualData({}); - expect(callback).not.toHaveBeenCalled(); - - $httpBackend.flush(); - expect(cc).toEqualData({id: 123, number: '9876'}); - expect(callback.mostRecentCall.args[0]).toEqual(cc); - expect(callback.mostRecentCall.args[1]()).toEqual({}); - }); - - - it("should read partial resource", function() { - $httpBackend.expect('GET', '/CreditCard').respond([{id:{key:123}}]); - var ccs = CreditCard.query(); - - $httpBackend.flush(); - expect(ccs.length).toEqual(1); - - var cc = ccs[0]; - expect(cc instanceof CreditCard).toBe(true); - expect(cc.number).toBeUndefined(); - - $httpBackend.expect('GET', '/CreditCard/123').respond({id: {key: 123}, number: '9876'}); - cc.$get(callback); - $httpBackend.flush(); - expect(callback.mostRecentCall.args[0]).toEqual(cc); - expect(callback.mostRecentCall.args[1]()).toEqual({}); - expect(cc.number).toEqual('9876'); - }); - - - it("should update resource", function() { - $httpBackend.expect('POST', '/CreditCard/123', '{"id":{"key":123},"name":"misko"}'). - respond({id: {key: 123}, name: 'rama'}); - - var cc = CreditCard.save({id: {key: 123}, name: 'misko'}, callback); - expect(cc).toEqualData({id:{key:123}, name:'misko'}); - expect(callback).not.toHaveBeenCalled(); - $httpBackend.flush(); - }); - - - it("should query resource", function() { - $httpBackend.expect('GET', '/CreditCard?key=value').respond([{id: 1}, {id: 2}]); - - var ccs = CreditCard.query({key: 'value'}, callback); - expect(ccs).toEqual([]); - expect(callback).not.toHaveBeenCalled(); - - $httpBackend.flush(); - expect(ccs).toEqualData([{id:1}, {id:2}]); - expect(callback.mostRecentCall.args[0]).toEqual(ccs); - expect(callback.mostRecentCall.args[1]()).toEqual({}); - }); - - - it("should have all arguments optional", function() { - $httpBackend.expect('GET', '/CreditCard').respond([{id:1}]); - - var log = ''; - var ccs = CreditCard.query(function() { log += 'cb;'; }); - - $httpBackend.flush(); - expect(ccs).toEqualData([{id:1}]); - expect(log).toEqual('cb;'); - }); - - - it('should delete resource and call callback', function() { - $httpBackend.expect('DELETE', '/CreditCard/123').respond({}); - CreditCard.remove({id:123}, callback); - expect(callback).not.toHaveBeenCalled(); - - $httpBackend.flush(); - expect(callback.mostRecentCall.args[0]).toEqualData({}); - expect(callback.mostRecentCall.args[1]()).toEqual({}); - - callback.reset(); - $httpBackend.expect('DELETE', '/CreditCard/333').respond(204, null); - CreditCard.remove({id:333}, callback); - expect(callback).not.toHaveBeenCalled(); - - $httpBackend.flush(); - expect(callback.mostRecentCall.args[0]).toEqualData({}); - expect(callback.mostRecentCall.args[1]()).toEqual({}); - }); - - - it('should post charge verb', function() { - $httpBackend.expect('POST', '/CreditCard/123!charge?amount=10', '{"auth":"abc"}').respond({success: 'ok'}); - CreditCard.charge({id:123, amount:10}, {auth:'abc'}, callback); - }); - - - it('should post charge verb on instance', function() { - $httpBackend.expect('POST', '/CreditCard/123!charge?amount=10', - '{"id":{"key":123},"name":"misko"}').respond({success: 'ok'}); - - var card = new CreditCard({id:{key:123}, name:'misko'}); - card.$charge({amount:10}, callback); - }); - - - it('should create on save', function() { - $httpBackend.expect('POST', '/CreditCard', '{"name":"misko"}').respond({id: 123}, {header1: 'a'}); - - var cc = new CreditCard(); - expect(cc.$get).toBeDefined(); - expect(cc.$query).toBeDefined(); - expect(cc.$remove).toBeDefined(); - expect(cc.$save).toBeDefined(); - - cc.name = 'misko'; - cc.$save(callback); - expect(cc).toEqualData({name:'misko'}); - - $httpBackend.flush(); - expect(cc).toEqualData({id:123}); - expect(callback.mostRecentCall.args[0]).toEqual(cc); - expect(callback.mostRecentCall.args[1]()).toEqual({header1: 'a'}); - }); - - - it('should not mutate the resource object if response contains no body', function() { - var data = {id:{key:123}, number:'9876'}; - $httpBackend.expect('GET', '/CreditCard/123').respond(data); - - var cc = CreditCard.get({id:123}); - $httpBackend.flush(); - expect(cc instanceof CreditCard).toBe(true); - - $httpBackend.expect('POST', '/CreditCard/123', toJson(data)).respond(''); - var idBefore = cc.id; - - cc.$save(); - $httpBackend.flush(); - expect(idBefore).toEqual(cc.id); - }); - - - it('should bind default parameters', function() { - $httpBackend.expect('GET', '/CreditCard/123.visa?minimum=0.05').respond({id: 123}); - var Visa = CreditCard.bind({verb:'.visa', minimum:0.05}); - var visa = Visa.get({id:123}); - $httpBackend.flush(); - expect(visa).toEqualData({id:123}); - }); - - - it('should exercise full stack', function() { - var Person = $resource('/Person/:id'); - - $httpBackend.expect('GET', '/Person/123').respond('\n{\n"name":\n"misko"\n}\n'); - var person = Person.get({id:123}); - $httpBackend.flush(); - expect(person.name).toEqual('misko'); - }); - - - describe('failure mode', function() { - var ERROR_CODE = 500, - ERROR_RESPONSE = 'Server Error', - errorCB; - - beforeEach(function() { - errorCB = jasmine.createSpy('error').andCallFake(function(response) { - expect(response.data).toBe(ERROR_RESPONSE); - expect(response.status).toBe(ERROR_CODE); - }); - }); - - - it('should call the error callback if provided on non 2xx response', function() { - $httpBackend.expect('GET', '/CreditCard/123').respond(ERROR_CODE, ERROR_RESPONSE); - - CreditCard.get({id:123}, callback, errorCB); - $httpBackend.flush(); - expect(errorCB).toHaveBeenCalledOnce(); - expect(callback).not.toHaveBeenCalled(); - }); - - - it('should call the error callback if provided on non 2xx response', function() { - $httpBackend.expect('GET', '/CreditCard').respond(ERROR_CODE, ERROR_RESPONSE); - - CreditCard.get(callback, errorCB); - $httpBackend.flush(); - expect(errorCB).toHaveBeenCalledOnce(); - expect(callback).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/test/ngMock/angular-mocksSpec.js b/test/ngMock/angular-mocksSpec.js index 2527ca48..d80b1976 100644 --- a/test/ngMock/angular-mocksSpec.js +++ b/test/ngMock/angular-mocksSpec.js @@ -1,6 +1,7 @@ 'use strict'; describe('ngMock', function() { + var noop = angular.noop; describe('$browser', function() { @@ -8,7 +9,7 @@ describe('ngMock', function() { it('should store url, done', inject(function($browser) { var url = 'some.js', - done = noop; + done = angular.noop; $browser.addJs(url, done); diff --git a/test/ngResource/resourceSpec.js b/test/ngResource/resourceSpec.js new file mode 100644 index 00000000..3235a6d0 --- /dev/null +++ b/test/ngResource/resourceSpec.js @@ -0,0 +1,326 @@ +'use strict'; + +describe("resource", function() { + var $resource, CreditCard, callback, $httpBackend; + + beforeEach(module('ngResource')); + beforeEach(inject(function($injector) { + $httpBackend = $injector.get('$httpBackend'); + $resource = $injector.get('$resource'); + CreditCard = $resource('/CreditCard/:id:verb', {id:'@id.key'}, { + charge:{ + method:'POST', + params:{verb:'!charge'} + } + }); + callback = jasmine.createSpy(); + })); + + + afterEach(function() { + $httpBackend.verifyNoOutstandingExpectation(); + }); + + + it("should build resource", function() { + expect(typeof CreditCard).toBe('function'); + expect(typeof CreditCard.get).toBe('function'); + expect(typeof CreditCard.save).toBe('function'); + expect(typeof CreditCard.remove).toBe('function'); + expect(typeof CreditCard['delete']).toBe('function'); + expect(typeof CreditCard.query).toBe('function'); + }); + + + it('should default to empty parameters', function() { + $httpBackend.expect('GET', 'URL').respond({}); + $resource('URL').query(); + }); + + + it('should ignore slashes of undefinend parameters', function() { + var R = $resource('/Path/:a/:b/:c'); + + $httpBackend.when('GET').respond('{}'); + $httpBackend.expect('GET', '/Path'); + $httpBackend.expect('GET', '/Path/1'); + $httpBackend.expect('GET', '/Path/2/3'); + $httpBackend.expect('GET', '/Path/4/5/6'); + + R.get({}); + R.get({a:1}); + R.get({a:2, b:3}); + R.get({a:4, b:5, c:6}); + }); + + + it('should support escaping colons in url template', function() { + var R = $resource('http://localhost\\:8080/Path/:a/\\:stillPath/:b'); + + $httpBackend.expect('GET', 'http://localhost:8080/Path/foo/:stillPath/bar').respond(); + R.get({a: 'foo', b: 'bar'}); + }); + + + it('should correctly encode url params', function() { + var R = $resource('/Path/:a'); + + $httpBackend.expect('GET', '/Path/foo%231').respond('{}'); + $httpBackend.expect('GET', '/Path/doh!@foo?bar=baz%231').respond('{}'); + + R.get({a: 'foo#1'}); + R.get({a: 'doh!@foo', bar: 'baz#1'}); + }); + + + it('should not encode @ in url params', function() { + //encodeURIComponent is too agressive and doesn't follow http://www.ietf.org/rfc/rfc3986.txt + //with regards to the character set (pchar) allowed in path segments + //so we need this test to make sure that we don't over-encode the params and break stuff like + //buzz api which uses @self + + var R = $resource('/Path/:a'); + $httpBackend.expect('GET', '/Path/doh@fo%20o?!do%26h=g%3Da+h&:bar=$baz@1').respond('{}'); + R.get({a: 'doh@fo o', ':bar': '$baz@1', '!do&h': 'g=a h'}); + }); + + + it('should encode & in url params', function() { + var R = $resource('/Path/:a'); + $httpBackend.expect('GET', '/Path/doh&foo?bar=baz%261').respond('{}'); + R.get({a: 'doh&foo', bar: 'baz&1'}); + }); + + + it('should build resource with default param', function() { + $httpBackend.expect('GET', '/Order/123/Line/456.visa?minimum=0.05').respond({id: 'abc'}); + var LineItem = $resource('/Order/:orderId/Line/:id:verb', + {orderId: '123', id: '@id.key', verb:'.visa', minimum: 0.05}); + var item = LineItem.get({id: 456}); + $httpBackend.flush(); + expect(item).toEqualData({id:'abc'}); + }); + + + it("should build resource with action default param overriding default param", function() { + $httpBackend.expect('GET', '/Customer/123').respond({id: 'abc'}); + var TypeItem = $resource('/:type/:typeId', {type: 'Order'}, + {get: {method: 'GET', params: {type: 'Customer'}}}); + var item = TypeItem.get({typeId: 123}); + + $httpBackend.flush(); + expect(item).toEqualData({id: 'abc'}); + }); + + + it("should create resource", function() { + $httpBackend.expect('POST', '/CreditCard', '{"name":"misko"}').respond({id: 123, name: 'misko'}); + + var cc = CreditCard.save({name: 'misko'}, callback); + expect(cc).toEqualData({name: 'misko'}); + expect(callback).not.toHaveBeenCalled(); + + $httpBackend.flush(); + expect(cc).toEqualData({id: 123, name: 'misko'}); + expect(callback).toHaveBeenCalledOnce(); + expect(callback.mostRecentCall.args[0]).toEqual(cc); + expect(callback.mostRecentCall.args[1]()).toEqual({}); + }); + + + it("should read resource", function() { + $httpBackend.expect('GET', '/CreditCard/123').respond({id: 123, number: '9876'}); + var cc = CreditCard.get({id: 123}, callback); + + expect(cc instanceof CreditCard).toBeTruthy(); + expect(cc).toEqualData({}); + expect(callback).not.toHaveBeenCalled(); + + $httpBackend.flush(); + expect(cc).toEqualData({id: 123, number: '9876'}); + expect(callback.mostRecentCall.args[0]).toEqual(cc); + expect(callback.mostRecentCall.args[1]()).toEqual({}); + }); + + + it("should read partial resource", function() { + $httpBackend.expect('GET', '/CreditCard').respond([{id:{key:123}}]); + var ccs = CreditCard.query(); + + $httpBackend.flush(); + expect(ccs.length).toEqual(1); + + var cc = ccs[0]; + expect(cc instanceof CreditCard).toBe(true); + expect(cc.number).toBeUndefined(); + + $httpBackend.expect('GET', '/CreditCard/123').respond({id: {key: 123}, number: '9876'}); + cc.$get(callback); + $httpBackend.flush(); + expect(callback.mostRecentCall.args[0]).toEqual(cc); + expect(callback.mostRecentCall.args[1]()).toEqual({}); + expect(cc.number).toEqual('9876'); + }); + + + it("should update resource", function() { + $httpBackend.expect('POST', '/CreditCard/123', '{"id":{"key":123},"name":"misko"}'). + respond({id: {key: 123}, name: 'rama'}); + + var cc = CreditCard.save({id: {key: 123}, name: 'misko'}, callback); + expect(cc).toEqualData({id:{key:123}, name:'misko'}); + expect(callback).not.toHaveBeenCalled(); + $httpBackend.flush(); + }); + + + it("should query resource", function() { + $httpBackend.expect('GET', '/CreditCard?key=value').respond([{id: 1}, {id: 2}]); + + var ccs = CreditCard.query({key: 'value'}, callback); + expect(ccs).toEqual([]); + expect(callback).not.toHaveBeenCalled(); + + $httpBackend.flush(); + expect(ccs).toEqualData([{id:1}, {id:2}]); + expect(callback.mostRecentCall.args[0]).toEqual(ccs); + expect(callback.mostRecentCall.args[1]()).toEqual({}); + }); + + + it("should have all arguments optional", function() { + $httpBackend.expect('GET', '/CreditCard').respond([{id:1}]); + + var log = ''; + var ccs = CreditCard.query(function() { log += 'cb;'; }); + + $httpBackend.flush(); + expect(ccs).toEqualData([{id:1}]); + expect(log).toEqual('cb;'); + }); + + + it('should delete resource and call callback', function() { + $httpBackend.expect('DELETE', '/CreditCard/123').respond({}); + CreditCard.remove({id:123}, callback); + expect(callback).not.toHaveBeenCalled(); + + $httpBackend.flush(); + expect(callback.mostRecentCall.args[0]).toEqualData({}); + expect(callback.mostRecentCall.args[1]()).toEqual({}); + + callback.reset(); + $httpBackend.expect('DELETE', '/CreditCard/333').respond(204, null); + CreditCard.remove({id:333}, callback); + expect(callback).not.toHaveBeenCalled(); + + $httpBackend.flush(); + expect(callback.mostRecentCall.args[0]).toEqualData({}); + expect(callback.mostRecentCall.args[1]()).toEqual({}); + }); + + + it('should post charge verb', function() { + $httpBackend.expect('POST', '/CreditCard/123!charge?amount=10', '{"auth":"abc"}').respond({success: 'ok'}); + CreditCard.charge({id:123, amount:10}, {auth:'abc'}, callback); + }); + + + it('should post charge verb on instance', function() { + $httpBackend.expect('POST', '/CreditCard/123!charge?amount=10', + '{"id":{"key":123},"name":"misko"}').respond({success: 'ok'}); + + var card = new CreditCard({id:{key:123}, name:'misko'}); + card.$charge({amount:10}, callback); + }); + + + it('should create on save', function() { + $httpBackend.expect('POST', '/CreditCard', '{"name":"misko"}').respond({id: 123}, {header1: 'a'}); + + var cc = new CreditCard(); + expect(cc.$get).toBeDefined(); + expect(cc.$query).toBeDefined(); + expect(cc.$remove).toBeDefined(); + expect(cc.$save).toBeDefined(); + + cc.name = 'misko'; + cc.$save(callback); + expect(cc).toEqualData({name:'misko'}); + + $httpBackend.flush(); + expect(cc).toEqualData({id:123}); + expect(callback.mostRecentCall.args[0]).toEqual(cc); + expect(callback.mostRecentCall.args[1]()).toEqual({header1: 'a'}); + }); + + + it('should not mutate the resource object if response contains no body', function() { + var data = {id:{key:123}, number:'9876'}; + $httpBackend.expect('GET', '/CreditCard/123').respond(data); + + var cc = CreditCard.get({id:123}); + $httpBackend.flush(); + expect(cc instanceof CreditCard).toBe(true); + + $httpBackend.expect('POST', '/CreditCard/123', angular.toJson(data)).respond(''); + var idBefore = cc.id; + + cc.$save(); + $httpBackend.flush(); + expect(idBefore).toEqual(cc.id); + }); + + + it('should bind default parameters', function() { + $httpBackend.expect('GET', '/CreditCard/123.visa?minimum=0.05').respond({id: 123}); + var Visa = CreditCard.bind({verb:'.visa', minimum:0.05}); + var visa = Visa.get({id:123}); + $httpBackend.flush(); + expect(visa).toEqualData({id:123}); + }); + + + it('should exercise full stack', function() { + var Person = $resource('/Person/:id'); + + $httpBackend.expect('GET', '/Person/123').respond('\n{\n"name":\n"misko"\n}\n'); + var person = Person.get({id:123}); + $httpBackend.flush(); + expect(person.name).toEqual('misko'); + }); + + + describe('failure mode', function() { + var ERROR_CODE = 500, + ERROR_RESPONSE = 'Server Error', + errorCB; + + beforeEach(function() { + errorCB = jasmine.createSpy('error').andCallFake(function(response) { + expect(response.data).toBe(ERROR_RESPONSE); + expect(response.status).toBe(ERROR_CODE); + }); + }); + + + it('should call the error callback if provided on non 2xx response', function() { + $httpBackend.expect('GET', '/CreditCard/123').respond(ERROR_CODE, ERROR_RESPONSE); + + CreditCard.get({id:123}, callback, errorCB); + $httpBackend.flush(); + expect(errorCB).toHaveBeenCalledOnce(); + expect(callback).not.toHaveBeenCalled(); + }); + + + it('should call the error callback if provided on non 2xx response', function() { + $httpBackend.expect('GET', '/CreditCard').respond(ERROR_CODE, ERROR_RESPONSE); + + CreditCard.get(callback, errorCB); + $httpBackend.flush(); + expect(errorCB).toHaveBeenCalledOnce(); + expect(callback).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/test/testabilityPatch.js b/test/testabilityPatch.js index b0618178..a60bc1df 100644 --- a/test/testabilityPatch.js +++ b/test/testabilityPatch.js @@ -8,8 +8,6 @@ */ _jQuery.event.special.change = undefined; - -publishExternalAPI(angular); bindJQuery(); beforeEach(function() { publishExternalAPI(angular); -- cgit v1.2.3