From ccf043caa25de5e60f8482ca80f94f976da559fb Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Mon, 27 Oct 2014 09:22:58 -0600 Subject: [PATCH] Added a fallback to using $http if js-data-http isn't loaded. Finished $http fallback. Changed CI command. --- .travis.yml | 2 +- CHANGELOG.md | 1 + Gruntfile.js | 50 ++- bower.json | 21 +- dist/js-data-angular.js | 350 ++++++++++++++-- dist/js-data-angular.min.js | 4 +- karma.conf.js | 63 +++ karma.start.js | 341 +++++++++++++++ package.json | 20 +- src/index.js | 348 ++++++++++++++-- test/adapters/http/create.test.js | 36 ++ test/adapters/http/destroy.test.js | 30 ++ test/adapters/http/destroyAll.test.js | 36 ++ test/adapters/http/find.test.js | 52 +++ test/adapters/http/findAll.test.js | 36 ++ test/adapters/http/update.test.js | 36 ++ test/adapters/http/updateAll.test.js | 36 ++ test/datastore/async_methods/create.test.js | 337 +++++++++++++++ test/datastore/async_methods/destroy.test.js | 148 +++++++ .../async_methods/destroyAll.test.js | 152 +++++++ test/datastore/async_methods/find.test.js | 242 +++++++++++ test/datastore/async_methods/findAll.test.js | 387 ++++++++++++++++++ .../async_methods/loadRelations.test.js | 230 +++++++++++ test/datastore/async_methods/refresh.test.js | 58 +++ test/datastore/async_methods/save.test.js | 237 +++++++++++ test/datastore/async_methods/update.test.js | 229 +++++++++++ .../datastore/async_methods/updateAll.test.js | 204 +++++++++ test/datastore/sync_methods/bindAll.test.js | 93 +++++ test/datastore/sync_methods/bindOne.test.js | 93 +++++ 29 files changed, 3772 insertions(+), 100 deletions(-) create mode 100644 karma.conf.js create mode 100644 karma.start.js create mode 100644 test/adapters/http/create.test.js create mode 100644 test/adapters/http/destroy.test.js create mode 100644 test/adapters/http/destroyAll.test.js create mode 100644 test/adapters/http/find.test.js create mode 100644 test/adapters/http/findAll.test.js create mode 100644 test/adapters/http/update.test.js create mode 100644 test/adapters/http/updateAll.test.js create mode 100644 test/datastore/async_methods/create.test.js create mode 100644 test/datastore/async_methods/destroy.test.js create mode 100644 test/datastore/async_methods/destroyAll.test.js create mode 100644 test/datastore/async_methods/find.test.js create mode 100644 test/datastore/async_methods/findAll.test.js create mode 100644 test/datastore/async_methods/loadRelations.test.js create mode 100644 test/datastore/async_methods/refresh.test.js create mode 100644 test/datastore/async_methods/save.test.js create mode 100644 test/datastore/async_methods/update.test.js create mode 100644 test/datastore/async_methods/updateAll.test.js create mode 100644 test/datastore/sync_methods/bindAll.test.js create mode 100644 test/datastore/sync_methods/bindOne.test.js diff --git a/.travis.yml b/.travis.yml index 7636d01..3f10973 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,5 +9,5 @@ before_script: - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start script: - - grunt build + - grunt test - grunt coveralls || true diff --git a/CHANGELOG.md b/CHANGELOG.md index cf0dd8b..f1fb983 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ ###### Other - #199 - Re-implement bindOne & bindAll in js-data-angular (they're missing from js-data) - #200 - Need to properly trigger digest where angular-data would have before +- Added DSHttpAdapter fallback that uses $http if js-data-http isn't loaded ##### 1.0.0 - 04 October 2014 diff --git a/Gruntfile.js b/Gruntfile.js index 4bab2a5..e271a95 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -35,14 +35,14 @@ module.exports = function (grunt) { main: { options: { banner: '/**\n' + - '* @author Jason Dobry \n' + - '* @file js-data-angular.min.js\n' + - '* @version <%= pkg.version %> - Homepage \n' + - '* @copyright (c) 2014 Jason Dobry \n' + - '* @license MIT \n' + - '*\n' + - '* @overview Angular wrapper for js-data.\n' + - '*/\n' + '* @author Jason Dobry \n' + + '* @file js-data-angular.min.js\n' + + '* @version <%= pkg.version %> - Homepage \n' + + '* @copyright (c) 2014 Jason Dobry \n' + + '* @license MIT \n' + + '*\n' + + '* @overview Angular wrapper for js-data.\n' + + '*/\n' }, files: { 'dist/js-data-angular.min.js': ['dist/js-data-angular.js'] @@ -63,6 +63,39 @@ module.exports = function (grunt) { 'dist/js-data-angular.js': ['src/index.js'] } } + }, + karma: { + options: { + configFile: './karma.conf.js' + }, + dev: { + browsers: ['Chrome'], + autoWatch: true, + singleRun: false, + reporters: ['spec'], + preprocessors: {} + }, + min: { + browsers: ['Firefox', 'PhantomJS'], + options: { + files: [ + 'bower_components/angular-1.3.2/angular.js', + 'bower_components/angular-mocks-1.3.2/angular-mocks.js', + 'bower_components/js-data/dist/js-data.js', + 'dist/js-data-angular.min.js', + 'karma.start.js', + 'test/**/*.js' + ] + } + }, + ci: { + browsers: ['Firefox', 'PhantomJS'] + } + }, + coveralls: { + options: { + coverage_dir: 'coverage' + } } }); @@ -101,4 +134,5 @@ module.exports = function (grunt) { ]); grunt.registerTask('go', ['build', 'watch:dist']); grunt.registerTask('default', ['build']); + grunt.registerTask('test', ['build', 'karma:min']); }; diff --git a/bower.json b/bower.json index cbf971e..5649a63 100644 --- a/bower.json +++ b/bower.json @@ -1,12 +1,12 @@ { "author": "Jason Dobry", "name": "js-data-angular", - "description": "Angular wrapper for js-data.", - "version": "2.0.0-alpha.1-0", + "description": "Angular wrapper for js-data (originally angular-data).", + "version": "2.0.0-alpha.2-0", "homepage": "http://www.js-data.io/js-data-angular", "repository": { "type": "git", - "url": "git://github.com/js-data/js-data-angular.git" + "url": "https://github.com/js-data/js-data-angular.git" }, "main": "./dist/js-data-angular.min.js", "ignore": [ @@ -18,7 +18,20 @@ "node_modules/", "package.json" ], + "devDependencies": { + "angular-1.1.5": "angular-unstable#1.1.5", + "angular-1.2.16": "angular#1.2.16", + "angular-1.2.25": "angular#1.2.25", + "angular-1.3.2": "angular#1.3.2", + "angular-mocks-1.1.5": "angular-mocks-unstable#1.1.5", + "angular-mocks-1.2.16": "angular-mocks#1.2.16", + "angular-mocks-1.2.25": "angular-mocks#1.2.25", + "angular-mocks-1.3.2": "angular-mocks#1.3.2" + }, "dependencies": { - "js-data": "~0.4.x" + "js-data": "~1.0.x" + }, + "resolutions": { + "angular": "1.3.2" } } diff --git a/dist/js-data-angular.js b/dist/js-data-angular.js index 36f4837..0b57bdc 100644 --- a/dist/js-data-angular.js +++ b/dist/js-data-angular.js @@ -1,7 +1,7 @@ /** * @author Jason Dobry * @file js-data-angular.js -* @version 2.0.0-alpha.1-0 - Homepage +* @version 2.0.0-alpha.2-0 - Homepage * @copyright (c) 2014 Jason Dobry * @license MIT * @@ -12,6 +12,26 @@ (function (window, angular, undefined) { 'use strict'; + var JSData; + + try { + JSData = require('js-data'); + } catch (e) { + + } + + if (!JSData) { + JSData = window.JSData; + } + + if (!JSData) { + throw new Error('js-data must be loaded!'); + } + + var makePath = JSData.DSUtils.makePath; + var deepMixIn = JSData.DSUtils.deepMixIn; + var httpLoaded = false; + var adapters = [ { project: 'js-data-http', @@ -35,6 +55,26 @@ } ]; + var functionsToWrap = [ + 'compute', + 'digest', + 'eject', + 'inject', + 'link', + 'linkAll', + 'linkInverse', + 'unlinkInverse' + ]; + + function Defaults() { + + } + + function DSHttpAdapter(options) { + this.defaults = new Defaults(); + deepMixIn(this.defaults, options); + } + function registerAdapter(adapter) { var Adapter; @@ -49,6 +89,9 @@ } if (Adapter) { + if (adapter.name === 'http') { + httpLoaded = true; + } adapter.loaded = true; angular.module(adapter.project, ['ng']).provider(adapter.class, function () { var _this = this; @@ -64,33 +107,6 @@ registerAdapter(adapters[i]); } - var JSData; - - try { - JSData = require('js-data'); - } catch (e) { - - } - - if (!JSData) { - JSData = window.JSData; - } - - if (!JSData) { - throw new Error('js-data must be loaded!'); - } - - var functionsToWrap = [ - 'compute', - 'digest', - 'eject', - 'inject', - 'link', - 'linkAll', - 'linkInverse', - 'unlinkInverse' - ]; - angular.module('js-data', ['ng']) .value('DSUtils', JSData.DSUtils) .value('DSErrors', JSData.DSErrors) @@ -109,19 +125,19 @@ _this.defaults = {}; - JSData.DS.prototype.bindAll = function (scope, expr, resourceName, params, cb) { + JSData.DS.prototype.bindAll = function (resourceName, params, scope, expr, cb) { var _this = this; params = params || {}; - if (!DSUtils.isObject(scope)) { - throw new DSErrors.IA('"scope" must be an object!'); - } else if (!DSUtils.isString(expr)) { - throw new DSErrors.IA('"expr" must be a string!'); - } else if (!_this.definitions[resourceName]) { + if (!_this.definitions[resourceName]) { throw new DSErrors.NER(resourceName); } else if (!DSUtils.isObject(params)) { throw new DSErrors.IA('"params" must be an object!'); + } else if (!DSUtils.isObject(scope)) { + throw new DSErrors.IA('"scope" must be an object!'); + } else if (!DSUtils.isString(expr)) { + throw new DSErrors.IA('"expr" must be a string!'); } try { @@ -143,18 +159,18 @@ } }; - JSData.DS.prototype.bindAll = function (scope, expr, resourceName, id, cb) { + JSData.DS.prototype.bindOne = function (resourceName, id, scope, expr, cb) { var _this = this; id = DSUtils.resolveId(_this.definitions[resourceName], id); - if (!DSUtils.isObject(scope)) { - throw new DSErrors.IA('"scope" must be an object!'); - } else if (!DSUtils.isString(expr)) { - throw new DSErrors.IA('"expr" must be a string!'); - } else if (!DS.definitions[resourceName]) { + if (!DS.definitions[resourceName]) { throw new DSErrors.NER(resourceName); } else if (!DSUtils.isString(id) && !DSUtils.isNumber(id)) { throw new DSErrors.IA('"id" must be a string or a number!'); + } else if (!DSUtils.isObject(scope)) { + throw new DSErrors.IA('"scope" must be an object!'); + } else if (!DSUtils.isString(expr)) { + throw new DSErrors.IA('"expr" must be a string!'); } try { @@ -178,10 +194,44 @@ function load() { var args = Array.prototype.slice.call(arguments); - var $rootScope = args[args.length - 1]; + var $rootScope = args[args.length - 2]; + var $q = args[args.length - 1]; var store = new JSData.DS(_this.defaults); var originals = {}; + function QPromise(cb) { + var deferred = $q.defer(); + try { + cb(function (val) { + if (!$rootScope.$$phase) { + $rootScope.$apply(function () { + deferred.resolve(val); + }); + } else { + deferred.resolve(val); + } + }, function (err) { + console.log(err); + if (!$rootScope.$$phase) { + $rootScope.$apply(function () { + deferred.reject(err); + }); + } else { + deferred.reject(err); + } + }); + } catch (err) { + deferred.reject(err); + } + return deferred.promise; + } + + //QPromise.all = $q.all; + //QPromise.when = $q.when; + //QPromise.reject = $q.reject; + // + //DSUtils.Promise = QPromise; + // Register any adapters that have been loaded for (var i = 0; i < adapters.length; i++) { if (adapters[i].loaded) { @@ -209,6 +259,7 @@ if (typeof Object.observe !== 'function' || typeof Array.observe !== 'function') { $rootScope.$watch(function () { + // TODO: observe.Platform.performMicrotaskCheckpoint(); // Throttle angular-data's digest loop to tenths of a second return new Date().getTime() / 100 | 0; }, function () { @@ -220,11 +271,228 @@ } deps.push('$rootScope'); + deps.push('$q'); deps.push(load); _this.$get = deps; }); + if (!httpLoaded) { + var defaultsPrototype = Defaults.prototype; + + defaultsPrototype.queryTransform = function (resourceName, params) { + return params; + }; + + defaultsPrototype.basePath = ''; + + defaultsPrototype.forceTrailingSlash = ''; + + defaultsPrototype.httpConfig = {}; + + defaultsPrototype.log = console ? function (a, b) { + console[typeof console.info === 'function' ? 'info' : 'log'](a, b); + } : function () { + }; + + defaultsPrototype.error = console ? function (a, b) { + console[typeof console.error === 'function' ? 'error' : 'log'](a, b); + } : function () { + }; + + defaultsPrototype.deserialize = function (resourceName, data) { + return data ? ('data' in data ? data.data : data) : data; + }; + + defaultsPrototype.serialize = function (resourceName, data) { + return data; + }; + + var dsHttpAdapterPrototype = DSHttpAdapter.prototype; + + dsHttpAdapterPrototype.getIdPath = function (resourceConfig, options, id) { + return makePath(options.basePath || this.defaults.basePath || resourceConfig.basePath, resourceConfig.getEndpoint(id, options), id); + }; + + dsHttpAdapterPrototype.getAllPath = function (resourceConfig, options) { + return makePath(options.basePath || this.defaults.basePath || resourceConfig.basePath, resourceConfig.getEndpoint(null, options)); + }; + + dsHttpAdapterPrototype.GET = function (url, config) { + config = config || {}; + if (!('method' in config)) { + config.method = 'get'; + } + return this.HTTP(deepMixIn(config, { + url: url + })); + }; + + dsHttpAdapterPrototype.POST = function (url, attrs, config) { + config = config || {}; + if (!('method' in config)) { + config.method = 'post'; + } + return this.HTTP(deepMixIn(config, { + url: url, + data: attrs + })); + }; + + dsHttpAdapterPrototype.PUT = function (url, attrs, config) { + config = config || {}; + if (!('method' in config)) { + config.method = 'put'; + } + return this.HTTP(deepMixIn(config, { + url: url, + data: attrs || {} + })); + }; + + dsHttpAdapterPrototype.DEL = function (url, config) { + config = config || {}; + if (!('method' in config)) { + config.method = 'delete'; + } + return this.HTTP(deepMixIn(config, { + url: url + })); + }; + + dsHttpAdapterPrototype.find = function (resourceConfig, id, options) { + var _this = this; + options = options || {}; + return _this.GET( + _this.getIdPath(resourceConfig, options, id), + options + ).then(function (data) { + return (options.deserialize ? options.deserialize : _this.defaults.deserialize)(resourceConfig.name, data); + }); + }; + + dsHttpAdapterPrototype.findAll = function (resourceConfig, params, options) { + var _this = this; + options = options || {}; + options.params = options.params || {}; + if (params) { + params = _this.defaults.queryTransform(resourceConfig.name, params); + deepMixIn(options.params, params); + } + return _this.GET( + _this.getAllPath(resourceConfig, options), + options + ).then(function (data) { + return (options.deserialize ? options.deserialize : _this.defaults.deserialize)(resourceConfig.name, data); + }); + }; + + dsHttpAdapterPrototype.create = function (resourceConfig, attrs, options) { + var _this = this; + options = options || {}; + return _this.POST( + makePath(options.basePath || this.defaults.basePath || resourceConfig.basePath, resourceConfig.getEndpoint(attrs, options)), + options.serialize ? options.serialize(resourceConfig.name, attrs) : _this.defaults.serialize(resourceConfig.name, attrs), + options + ).then(function (data) { + return (options.deserialize ? options.deserialize : _this.defaults.deserialize)(resourceConfig.name, data); + }); + }; + + dsHttpAdapterPrototype.update = function (resourceConfig, id, attrs, options) { + var _this = this; + options = options || {}; + return _this.PUT( + _this.getIdPath(resourceConfig, options, id), + options.serialize ? options.serialize(resourceConfig.name, attrs) : _this.defaults.serialize(resourceConfig.name, attrs), + options + ).then(function (data) { + return (options.deserialize ? options.deserialize : _this.defaults.deserialize)(resourceConfig.name, data); + }); + }; + + dsHttpAdapterPrototype.updateAll = function (resourceConfig, attrs, params, options) { + var _this = this; + options = options || {}; + options.params = options.params || {}; + if (params) { + params = _this.defaults.queryTransform(resourceConfig.name, params); + deepMixIn(options.params, params); + } + return this.PUT( + _this.getAllPath(resourceConfig, options), + options.serialize ? options.serialize(resourceConfig.name, attrs) : _this.defaults.serialize(resourceConfig.name, attrs), + options + ).then(function (data) { + return (options.deserialize ? options.deserialize : _this.defaults.deserialize)(resourceConfig.name, data); + }); + }; + + dsHttpAdapterPrototype.destroy = function (resourceConfig, id, options) { + var _this = this; + options = options || {}; + return _this.DEL( + _this.getIdPath(resourceConfig, options, id), + options + ).then(function (data) { + return (options.deserialize ? options.deserialize : _this.defaults.deserialize)(resourceConfig.name, data); + }); + }; + + dsHttpAdapterPrototype.destroyAll = function (resourceConfig, params, options) { + var _this = this; + options = options || {}; + options.params = options.params || {}; + if (params) { + params = _this.defaults.queryTransform(resourceConfig.name, params); + deepMixIn(options.params, params); + } + return this.DEL( + _this.getAllPath(resourceConfig, options), + options + ).then(function (data) { + return (options.deserialize ? options.deserialize : _this.defaults.deserialize)(resourceConfig.name, data); + }); + }; + + angular.module('js-data').provider('DSHttpAdapter', function () { + var _this = this; + _this.defaults = {}; + _this.$get = ['$http', 'DS', '$q', function ($http, DS, $q) { + dsHttpAdapterPrototype.HTTP = function (config) { + var _this = this; + var start = new Date(); + config = deepMixIn(config, _this.defaults.httpConfig); + if (_this.defaults.forceTrailingSlash && config.url[config.url.length] !== '/') { + config.url += '/'; + } + config.method = config.method.toUpperCase(); + + function logResponse(data) { + var str = start.toUTCString() + ' - ' + data.config.method.toUpperCase() + ' ' + data.config.url + ' - ' + data.status + ' ' + (new Date().getTime() - start.getTime()) + 'ms'; + if (data.status >= 200 && data.status < 300) { + if (_this.defaults.log) { + _this.defaults.log(str, data); + } + return data; + } else { + if (_this.defaults.error) { + _this.defaults.error('FAILED: ' + str, data); + } + return $q.reject(data); + } + } + + return $http(config).then(logResponse, logResponse); + }; + + var adapter = new DSHttpAdapter(_this.defaults); + DS.registerAdapter('http', adapter, { default: true }); + return adapter; + }]; + }); + } + })(window, window.angular); },{"js-data":"js-data"}]},{},[1]); diff --git a/dist/js-data-angular.min.js b/dist/js-data-angular.min.js index ed3c36b..771c2f8 100644 --- a/dist/js-data-angular.min.js +++ b/dist/js-data-angular.min.js @@ -1,10 +1,10 @@ /** * @author Jason Dobry * @file js-data-angular.min.js -* @version 2.0.0-alpha.1-0 - Homepage +* @version 2.0.0-alpha.2-0 - Homepage * @copyright (c) 2014 Jason Dobry * @license MIT * * @overview Angular wrapper for js-data. */ -!function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c?c:a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g=200&&a.status<300?(e.defaults.log&&e.defaults.log(b,a),a):(e.defaults.error&&e.defaults.error("FAILED: "+b,a),d.reject(a))}var e=this,f=new Date;return a=j(a,e.defaults.httpConfig),e.defaults.forceTrailingSlash&&"/"!==a.url[a.url.length]&&(a.url+="/"),a.method=a.method.toUpperCase(),b(a).then(c,c)};var f=new e(a.defaults);return c.registerAdapter("http",f,{"default":!0}),f}]})}}(window,window.angular)},{"js-data":"js-data"}]},{},[1]); \ No newline at end of file diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..6e57fb6 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,63 @@ +// an example karma.conf.js +module.exports = function (config) { + config.set({ + // base path, that will be used to resolve files and exclude + basePath: './', + frameworks: ['sinon', 'chai', 'mocha'], + plugins: [ + // these plugins will be require() by Karma + 'karma-sinon', + 'karma-mocha', + 'karma-chai', + 'karma-chrome-launcher', + 'karma-phantomjs-launcher', + 'karma-firefox-launcher', + 'karma-coverage', + 'karma-spec-reporter' + ], + autoWatch: false, + browserNoActivityTimeout: 30000, + browsers: ['Chrome'], + + // list of files / patterns to load in the browser + files: [ + 'bower_components/angular-1.3.2/angular.js', + 'bower_components/angular-mocks-1.3.2/angular-mocks.js', + 'bower_components/js-data/dist/js-data.js', + 'dist/js-data-angular.js', + 'karma.start.js', + 'test/**/*.js' + ], + + reporters: ['spec', 'coverage'], + + preprocessors: { + 'dist/js-data-angular.js': ['coverage'] + }, + + // optionally, configure the reporter + coverageReporter: { + type: 'lcov', + dir: 'coverage/' + }, + + // web server port + port: 9876, + + // cli runner port + runnerPort: 9100, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + logLevel: config.LOG_INFO, + + // If browser does not capture in given timeout [ms], kill it + captureTimeout: 30000, + + // Continuous Integration mode + // if true, it capture browsers, run tests and exit + singleRun: true + }); +}; diff --git a/karma.start.js b/karma.start.js new file mode 100644 index 0000000..474ce32 --- /dev/null +++ b/karma.start.js @@ -0,0 +1,341 @@ +// Setup global test variables +var $rootScope, $q, $log, $timeout, DSHttpAdapterProvider, DSProvider, DS, DSUtils, DSHttpAdapter, app, $httpBackend, p1, p2, p3, p4, p5; + +var Post, User, Organization, Comment, Profile; +var user1, organization2, comment3, profile4; +var comment11, comment12, comment13, organization14, profile15, user10, user16, user17, user18, organization15, user19, user20, comment19, user22, profile21; + +var lifecycle = {}; + +// Helper globals +var fail = function (msg) { + if (msg instanceof Error) { + console.log(msg.stack); + } else { + assert.equal('should not reach this!: ' + msg, 'failure'); + } + }, + TYPES_EXCEPT_STRING = [123, 123.123, null, undefined, {}, [], true, false, function () { + }], + TYPES_EXCEPT_STRING_OR_ARRAY = [123, 123.123, null, undefined, {}, true, false, function () { + }], + TYPES_EXCEPT_STRING_OR_OBJECT = [123, 123.123, null, undefined, [], true, false, function () { + }], + TYPES_EXCEPT_STRING_OR_NUMBER_OBJECT = [null, undefined, [], true, false, function () { + }], + TYPES_EXCEPT_ARRAY = ['string', 123, 123.123, null, undefined, {}, true, false, function () { + }], + TYPES_EXCEPT_STRING_OR_NUMBER = [null, undefined, {}, [], true, false, function () { + }], + TYPES_EXCEPT_STRING_OR_ARRAY_OR_NUMBER = [null, undefined, {}, true, false, function () { + }], + TYPES_EXCEPT_NUMBER = ['string', null, undefined, {}, [], true, false, function () { + }], + TYPES_EXCEPT_OBJECT = ['string', 123, 123.123, null, undefined, true, false, function () { + }], + TYPES_EXCEPT_BOOLEAN = ['string', 123, 123.123, null, undefined, {}, [], function () { + }], + TYPES_EXCEPT_FUNCTION = ['string', 123, 123.123, null, undefined, {}, [], true, false]; + +angular.module('app', ['ng', 'js-data']); + +// Setup before each test +beforeEach(function () { + lifecycle.beforeValidate = function (resourceName, attrs, cb) { + lifecycle.beforeValidate.callCount += 1; + cb(null, attrs); + }; + lifecycle.validate = function (resourceName, attrs, cb) { + lifecycle.validate.callCount += 1; + cb(null, attrs); + }; + lifecycle.afterValidate = function (resourceName, attrs, cb) { + lifecycle.afterValidate.callCount += 1; + cb(null, attrs); + }; + lifecycle.beforeCreate = function (resourceName, attrs, cb) { + lifecycle.beforeCreate.callCount += 1; + cb(null, attrs); + }; + lifecycle.afterCreate = function (resourceName, attrs, cb) { + lifecycle.afterCreate.callCount += 1; + cb(null, attrs); + }; + lifecycle.beforeUpdate = function (resourceName, attrs, cb) { + lifecycle.beforeUpdate.callCount += 1; + cb(null, attrs); + }; + lifecycle.afterUpdate = function (resourceName, attrs, cb) { + lifecycle.afterUpdate.callCount += 1; + cb(null, attrs); + }; + lifecycle.beforeDestroy = function (resourceName, attrs, cb) { + lifecycle.beforeDestroy.callCount += 1; + cb(null, attrs); + }; + lifecycle.afterDestroy = function (resourceName, attrs, cb) { + lifecycle.afterDestroy.callCount += 1; + cb(null, attrs); + }; + lifecycle.beforeInject = function () { + lifecycle.beforeInject.callCount += 1; + }; + lifecycle.afterInject = function () { + lifecycle.afterInject.callCount += 1; + }; + lifecycle.serialize = function (resourceName, data) { + lifecycle.serialize.callCount += 1; + return data; + }; + lifecycle.deserialize = function (resourceName, data) { + lifecycle.deserialize.callCount += 1; + return data ? (data.data ? data.data : data) : data;; + }; + lifecycle.queryTransform = function (resourceName, query) { + lifecycle.queryTransform.callCount += 1; + return query; + }; + module('app', function (_DSProvider_, _DSHttpAdapterProvider_) { + DSProvider = _DSProvider_; + DSProvider.defaults.basePath = 'http://test.angular-cache.com'; + DSProvider.defaults.beforeValidate = lifecycle.beforeValidate; + DSProvider.defaults.validate = lifecycle.validate; + DSProvider.defaults.afterValidate = lifecycle.afterValidate; + DSProvider.defaults.beforeCreate = lifecycle.beforeCreate; + DSProvider.defaults.afterCreate = lifecycle.afterCreate; + DSProvider.defaults.beforeUpdate = lifecycle.beforeUpdate; + DSProvider.defaults.afterUpdate = lifecycle.afterUpdate; + DSProvider.defaults.beforeDestroy = lifecycle.beforeDestroy; + DSProvider.defaults.afterDestroy = lifecycle.afterDestroy; + DSProvider.defaults.beforeInject = lifecycle.beforeInject; + DSProvider.defaults.afterInject = lifecycle.afterInject; + DSProvider.defaults.serialize = lifecycle.serialize; + DSProvider.defaults.deserialize = lifecycle.deserialize; + DSHttpAdapterProvider = _DSHttpAdapterProvider_; + DSHttpAdapterProvider.defaults.queryTransform = lifecycle.queryTransform; + DSHttpAdapterProvider.defaults.log = false; + }); +}); + +function startInjector() { + inject(function (_$rootScope_, _$q_, _$timeout_, _$httpBackend_, _DS_, _$log_, _DSUtils_, _DSHttpAdapter_) { + // Setup global mocks + + localStorage.clear(); + $q = _$q_; + $rootScope = _$rootScope_; + DS = _DS_; + $timeout = _$timeout_; + DSUtils = _DSUtils_; + DSHttpAdapter = _DSHttpAdapter_; + $httpBackend = _$httpBackend_; + Post = DS.defineResource({ + name: 'post', + keepChangeHistory: true, + endpoint: '/posts' + }); + User = DS.defineResource({ + name: 'user', + relations: { + hasMany: { + comment: { + localField: 'comments', + foreignKey: 'approvedBy' + } + }, + hasOne: { + profile: { + localField: 'profile', + foreignKey: 'userId' + } + }, + belongsTo: { + organization: { + parent: true, + localKey: 'organizationId', + localField: 'organization' + } + } + } + }); + + Organization = DS.defineResource({ + name: 'organization', + relations: { + hasMany: { + user: { + localField: 'users', + foreignKey: 'organizationId' + } + } + } + }); + + Profile = DS.defineResource({ + name: 'profile', + relations: { + belongsTo: { + user: { + localField: 'user', + localKey: 'userId' + } + } + } + }); + + Comment = DS.defineResource({ + name: 'comment', + relations: { + belongsTo: { + user: [ + { + localField: 'user', + localKey: 'userId' + }, + { + parent: true, + localField: 'approvedByUser', + localKey: 'approvedBy' + } + ] + } + } + }); + $log = _$log_; + + lifecycle.beforeValidate.callCount = 0; + lifecycle.validate.callCount = 0; + lifecycle.afterValidate.callCount = 0; + lifecycle.beforeCreate.callCount = 0; + lifecycle.afterCreate.callCount = 0; + lifecycle.beforeUpdate.callCount = 0; + lifecycle.afterUpdate.callCount = 0; + lifecycle.beforeDestroy.callCount = 0; + lifecycle.afterDestroy.callCount = 0; + lifecycle.beforeInject.callCount = 0; + lifecycle.afterInject.callCount = 0; + lifecycle.serialize.callCount = 0; + lifecycle.deserialize.callCount = 0; + lifecycle.queryTransform.callCount = 0; + + p1 = { author: 'John', age: 30, id: 5 }; + p2 = { author: 'Sally', age: 31, id: 6 }; + p3 = { author: 'Mike', age: 32, id: 7 }; + p4 = { author: 'Adam', age: 33, id: 8 }; + p5 = { author: 'Adam', age: 33, id: 9 }; + + user1 = { + name: 'John Anderson', + id: 1, + organizationId: 2 + }; + organization2 = { + name: 'Test Corp 2', + id: 2 + }; + comment3 = { + content: 'test comment 3', + id: 3, + userId: 1 + }; + profile4 = { + content: 'test profile 4', + id: 4, + userId: 1 + }; + + comment11 = { + id: 11, + userId: 10, + content: 'test comment 11' + }; + comment12 = { + id: 12, + userId: 10, + content: 'test comment 12' + }; + comment13 = { + id: 13, + userId: 10, + content: 'test comment 13' + }; + organization14 = { + id: 14, + name: 'Test Corp' + }; + profile15 = { + id: 15, + userId: 10, + email: 'john.anderson@test.com' + }; + user10 = { + name: 'John Anderson', + id: 10, + organizationId: 14, + comments: [ + comment11, + comment12, + comment13 + ], + organization: organization14, + profile: profile15 + }; + user16 = { + id: 16, + organizationId: 15, + name: 'test user 16' + }; + user17 = { + id: 17, + organizationId: 15, + name: 'test user 17' + }; + user18 = { + id: 18, + organizationId: 15, + name: 'test user 18' + }; + organization15 = { + name: 'Another Test Corp', + id: 15, + users: [ + user16, + user17, + user18 + ] + }; + user19 = { + id: 19, + name: 'test user 19' + }; + user20 = { + id: 20, + name: 'test user 20' + }; + comment19 = { + content: 'test comment 19', + id: 19, + approvedBy: 19, + approvedByUser: user19, + userId: 20, + user: user20 + }; + user22 = { + id: 22, + name: 'test user 22' + }; + profile21 = { + content: 'test profile 21', + id: 21, + userId: 22, + user: user22 + }; + }); +} + +// Clean up after each test +afterEach(function () { + $httpBackend.verifyNoOutstandingExpectation(); + $httpBackend.verifyNoOutstandingRequest(); + $log.reset(); +}); diff --git a/package.json b/package.json index 3f0aab2..80a7a42 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "js-data-angular", "description": "Angular wrapper for js-data.", - "version": "2.0.0-alpha.1-0", + "version": "2.0.0-alpha.2-0", "homepage": "http://js-data-angular.pseudobry.com", "repository": { "type": "git", @@ -20,19 +20,31 @@ ], "devDependencies": { "grunt": "0.4.5", - "grunt-browserify": "3.0.1", + "grunt-browserify": "3.2.0", "grunt-contrib-clean": "0.6.0", "grunt-contrib-jshint": "0.10.0", "grunt-contrib-uglify": "0.6.0", "grunt-contrib-watch": "0.6.1", + "grunt-karma": "0.9.0", + "grunt-karma-coveralls": "2.5.2", + "karma": "0.12.24", + "karma-chai": "0.1.0", + "karma-chrome-launcher": "0.1.5", + "karma-coverage": "0.2.6", + "karma-script-launcher": "0.1.0", + "karma-firefox-launcher": "0.1.3", + "karma-phantomjs-launcher": "0.1.4", + "karma-mocha": "0.1.9", + "karma-sinon": "1.0.3", + "karma-spec-reporter": "0.0.13", "time-grunt": "1.0.0", - "jit-grunt": "0.8.0" + "jit-grunt": "0.9.0" }, "scripts": { "test": "grunt test" }, "dependencies": { "mout": "0.10.0", - "js-data": "~0.4.x" + "js-data": "~1.0.x" } } diff --git a/src/index.js b/src/index.js index 5d72959..4f0c36b 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,26 @@ (function (window, angular, undefined) { 'use strict'; + var JSData; + + try { + JSData = require('js-data'); + } catch (e) { + + } + + if (!JSData) { + JSData = window.JSData; + } + + if (!JSData) { + throw new Error('js-data must be loaded!'); + } + + var makePath = JSData.DSUtils.makePath; + var deepMixIn = JSData.DSUtils.deepMixIn; + var httpLoaded = false; + var adapters = [ { project: 'js-data-http', @@ -25,6 +45,26 @@ } ]; + var functionsToWrap = [ + 'compute', + 'digest', + 'eject', + 'inject', + 'link', + 'linkAll', + 'linkInverse', + 'unlinkInverse' + ]; + + function Defaults() { + + } + + function DSHttpAdapter(options) { + this.defaults = new Defaults(); + deepMixIn(this.defaults, options); + } + function registerAdapter(adapter) { var Adapter; @@ -39,6 +79,9 @@ } if (Adapter) { + if (adapter.name === 'http') { + httpLoaded = true; + } adapter.loaded = true; angular.module(adapter.project, ['ng']).provider(adapter.class, function () { var _this = this; @@ -54,33 +97,6 @@ registerAdapter(adapters[i]); } - var JSData; - - try { - JSData = require('js-data'); - } catch (e) { - - } - - if (!JSData) { - JSData = window.JSData; - } - - if (!JSData) { - throw new Error('js-data must be loaded!'); - } - - var functionsToWrap = [ - 'compute', - 'digest', - 'eject', - 'inject', - 'link', - 'linkAll', - 'linkInverse', - 'unlinkInverse' - ]; - angular.module('js-data', ['ng']) .value('DSUtils', JSData.DSUtils) .value('DSErrors', JSData.DSErrors) @@ -99,19 +115,19 @@ _this.defaults = {}; - JSData.DS.prototype.bindAll = function (scope, expr, resourceName, params, cb) { + JSData.DS.prototype.bindAll = function (resourceName, params, scope, expr, cb) { var _this = this; params = params || {}; - if (!DSUtils.isObject(scope)) { - throw new DSErrors.IA('"scope" must be an object!'); - } else if (!DSUtils.isString(expr)) { - throw new DSErrors.IA('"expr" must be a string!'); - } else if (!_this.definitions[resourceName]) { + if (!_this.definitions[resourceName]) { throw new DSErrors.NER(resourceName); } else if (!DSUtils.isObject(params)) { throw new DSErrors.IA('"params" must be an object!'); + } else if (!DSUtils.isObject(scope)) { + throw new DSErrors.IA('"scope" must be an object!'); + } else if (!DSUtils.isString(expr)) { + throw new DSErrors.IA('"expr" must be a string!'); } try { @@ -133,18 +149,18 @@ } }; - JSData.DS.prototype.bindAll = function (scope, expr, resourceName, id, cb) { + JSData.DS.prototype.bindOne = function (resourceName, id, scope, expr, cb) { var _this = this; id = DSUtils.resolveId(_this.definitions[resourceName], id); - if (!DSUtils.isObject(scope)) { - throw new DSErrors.IA('"scope" must be an object!'); - } else if (!DSUtils.isString(expr)) { - throw new DSErrors.IA('"expr" must be a string!'); - } else if (!DS.definitions[resourceName]) { + if (!DS.definitions[resourceName]) { throw new DSErrors.NER(resourceName); } else if (!DSUtils.isString(id) && !DSUtils.isNumber(id)) { throw new DSErrors.IA('"id" must be a string or a number!'); + } else if (!DSUtils.isObject(scope)) { + throw new DSErrors.IA('"scope" must be an object!'); + } else if (!DSUtils.isString(expr)) { + throw new DSErrors.IA('"expr" must be a string!'); } try { @@ -168,10 +184,44 @@ function load() { var args = Array.prototype.slice.call(arguments); - var $rootScope = args[args.length - 1]; + var $rootScope = args[args.length - 2]; + var $q = args[args.length - 1]; var store = new JSData.DS(_this.defaults); var originals = {}; + function QPromise(cb) { + var deferred = $q.defer(); + try { + cb(function (val) { + if (!$rootScope.$$phase) { + $rootScope.$apply(function () { + deferred.resolve(val); + }); + } else { + deferred.resolve(val); + } + }, function (err) { + console.log(err); + if (!$rootScope.$$phase) { + $rootScope.$apply(function () { + deferred.reject(err); + }); + } else { + deferred.reject(err); + } + }); + } catch (err) { + deferred.reject(err); + } + return deferred.promise; + } + + //QPromise.all = $q.all; + //QPromise.when = $q.when; + //QPromise.reject = $q.reject; + // + //DSUtils.Promise = QPromise; + // Register any adapters that have been loaded for (var i = 0; i < adapters.length; i++) { if (adapters[i].loaded) { @@ -199,6 +249,7 @@ if (typeof Object.observe !== 'function' || typeof Array.observe !== 'function') { $rootScope.$watch(function () { + // TODO: observe.Platform.performMicrotaskCheckpoint(); // Throttle angular-data's digest loop to tenths of a second return new Date().getTime() / 100 | 0; }, function () { @@ -210,9 +261,226 @@ } deps.push('$rootScope'); + deps.push('$q'); deps.push(load); _this.$get = deps; }); + if (!httpLoaded) { + var defaultsPrototype = Defaults.prototype; + + defaultsPrototype.queryTransform = function (resourceName, params) { + return params; + }; + + defaultsPrototype.basePath = ''; + + defaultsPrototype.forceTrailingSlash = ''; + + defaultsPrototype.httpConfig = {}; + + defaultsPrototype.log = console ? function (a, b) { + console[typeof console.info === 'function' ? 'info' : 'log'](a, b); + } : function () { + }; + + defaultsPrototype.error = console ? function (a, b) { + console[typeof console.error === 'function' ? 'error' : 'log'](a, b); + } : function () { + }; + + defaultsPrototype.deserialize = function (resourceName, data) { + return data ? ('data' in data ? data.data : data) : data; + }; + + defaultsPrototype.serialize = function (resourceName, data) { + return data; + }; + + var dsHttpAdapterPrototype = DSHttpAdapter.prototype; + + dsHttpAdapterPrototype.getIdPath = function (resourceConfig, options, id) { + return makePath(options.basePath || this.defaults.basePath || resourceConfig.basePath, resourceConfig.getEndpoint(id, options), id); + }; + + dsHttpAdapterPrototype.getAllPath = function (resourceConfig, options) { + return makePath(options.basePath || this.defaults.basePath || resourceConfig.basePath, resourceConfig.getEndpoint(null, options)); + }; + + dsHttpAdapterPrototype.GET = function (url, config) { + config = config || {}; + if (!('method' in config)) { + config.method = 'get'; + } + return this.HTTP(deepMixIn(config, { + url: url + })); + }; + + dsHttpAdapterPrototype.POST = function (url, attrs, config) { + config = config || {}; + if (!('method' in config)) { + config.method = 'post'; + } + return this.HTTP(deepMixIn(config, { + url: url, + data: attrs + })); + }; + + dsHttpAdapterPrototype.PUT = function (url, attrs, config) { + config = config || {}; + if (!('method' in config)) { + config.method = 'put'; + } + return this.HTTP(deepMixIn(config, { + url: url, + data: attrs || {} + })); + }; + + dsHttpAdapterPrototype.DEL = function (url, config) { + config = config || {}; + if (!('method' in config)) { + config.method = 'delete'; + } + return this.HTTP(deepMixIn(config, { + url: url + })); + }; + + dsHttpAdapterPrototype.find = function (resourceConfig, id, options) { + var _this = this; + options = options || {}; + return _this.GET( + _this.getIdPath(resourceConfig, options, id), + options + ).then(function (data) { + return (options.deserialize ? options.deserialize : _this.defaults.deserialize)(resourceConfig.name, data); + }); + }; + + dsHttpAdapterPrototype.findAll = function (resourceConfig, params, options) { + var _this = this; + options = options || {}; + options.params = options.params || {}; + if (params) { + params = _this.defaults.queryTransform(resourceConfig.name, params); + deepMixIn(options.params, params); + } + return _this.GET( + _this.getAllPath(resourceConfig, options), + options + ).then(function (data) { + return (options.deserialize ? options.deserialize : _this.defaults.deserialize)(resourceConfig.name, data); + }); + }; + + dsHttpAdapterPrototype.create = function (resourceConfig, attrs, options) { + var _this = this; + options = options || {}; + return _this.POST( + makePath(options.basePath || this.defaults.basePath || resourceConfig.basePath, resourceConfig.getEndpoint(attrs, options)), + options.serialize ? options.serialize(resourceConfig.name, attrs) : _this.defaults.serialize(resourceConfig.name, attrs), + options + ).then(function (data) { + return (options.deserialize ? options.deserialize : _this.defaults.deserialize)(resourceConfig.name, data); + }); + }; + + dsHttpAdapterPrototype.update = function (resourceConfig, id, attrs, options) { + var _this = this; + options = options || {}; + return _this.PUT( + _this.getIdPath(resourceConfig, options, id), + options.serialize ? options.serialize(resourceConfig.name, attrs) : _this.defaults.serialize(resourceConfig.name, attrs), + options + ).then(function (data) { + return (options.deserialize ? options.deserialize : _this.defaults.deserialize)(resourceConfig.name, data); + }); + }; + + dsHttpAdapterPrototype.updateAll = function (resourceConfig, attrs, params, options) { + var _this = this; + options = options || {}; + options.params = options.params || {}; + if (params) { + params = _this.defaults.queryTransform(resourceConfig.name, params); + deepMixIn(options.params, params); + } + return this.PUT( + _this.getAllPath(resourceConfig, options), + options.serialize ? options.serialize(resourceConfig.name, attrs) : _this.defaults.serialize(resourceConfig.name, attrs), + options + ).then(function (data) { + return (options.deserialize ? options.deserialize : _this.defaults.deserialize)(resourceConfig.name, data); + }); + }; + + dsHttpAdapterPrototype.destroy = function (resourceConfig, id, options) { + var _this = this; + options = options || {}; + return _this.DEL( + _this.getIdPath(resourceConfig, options, id), + options + ).then(function (data) { + return (options.deserialize ? options.deserialize : _this.defaults.deserialize)(resourceConfig.name, data); + }); + }; + + dsHttpAdapterPrototype.destroyAll = function (resourceConfig, params, options) { + var _this = this; + options = options || {}; + options.params = options.params || {}; + if (params) { + params = _this.defaults.queryTransform(resourceConfig.name, params); + deepMixIn(options.params, params); + } + return this.DEL( + _this.getAllPath(resourceConfig, options), + options + ).then(function (data) { + return (options.deserialize ? options.deserialize : _this.defaults.deserialize)(resourceConfig.name, data); + }); + }; + + angular.module('js-data').provider('DSHttpAdapter', function () { + var _this = this; + _this.defaults = {}; + _this.$get = ['$http', 'DS', '$q', function ($http, DS, $q) { + dsHttpAdapterPrototype.HTTP = function (config) { + var _this = this; + var start = new Date(); + config = deepMixIn(config, _this.defaults.httpConfig); + if (_this.defaults.forceTrailingSlash && config.url[config.url.length] !== '/') { + config.url += '/'; + } + config.method = config.method.toUpperCase(); + + function logResponse(data) { + var str = start.toUTCString() + ' - ' + data.config.method.toUpperCase() + ' ' + data.config.url + ' - ' + data.status + ' ' + (new Date().getTime() - start.getTime()) + 'ms'; + if (data.status >= 200 && data.status < 300) { + if (_this.defaults.log) { + _this.defaults.log(str, data); + } + return data; + } else { + if (_this.defaults.error) { + _this.defaults.error('FAILED: ' + str, data); + } + return $q.reject(data); + } + } + + return $http(config).then(logResponse, logResponse); + }; + + var adapter = new DSHttpAdapter(_this.defaults); + DS.registerAdapter('http', adapter, { default: true }); + return adapter; + }]; + }); + } + })(window, window.angular); diff --git a/test/adapters/http/create.test.js b/test/adapters/http/create.test.js new file mode 100644 index 0000000..35dff6a --- /dev/null +++ b/test/adapters/http/create.test.js @@ -0,0 +1,36 @@ +describe('DSHttpAdapter.create', function () { + + beforeEach(startInjector); + + it('should make a POST request', function () { + $httpBackend.expectPOST('http://test.angular-cache.com/posts', { + author: 'John', + age: 30 + }).respond(200, p1); + + DSHttpAdapter.create(Post, { author: 'John', age: 30 }).then(function (data) { + assert.deepEqual(data, p1, 'post should have been created'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + $httpBackend.flush(); + + $httpBackend.expectPOST('api2/posts', { + author: 'John', + age: 30 + }).respond(200, p1); + + DSHttpAdapter.create(Post, { author: 'John', age: 30 }, { basePath: 'api2' }).then(function (data) { + assert.deepEqual(data, p1, 'post should have been created'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + $httpBackend.flush(); + + assert.equal(lifecycle.queryTransform.callCount, 0, 'queryTransform should not have been called'); + }); +}); diff --git a/test/adapters/http/destroy.test.js b/test/adapters/http/destroy.test.js new file mode 100644 index 0000000..dc25160 --- /dev/null +++ b/test/adapters/http/destroy.test.js @@ -0,0 +1,30 @@ +describe('DSHttpAdapter.destroy', function () { + + beforeEach(startInjector); + + it('should make a DELETE request', function () { + $httpBackend.expectDELETE('http://test.angular-cache.com/posts/1').respond(200, 1); + + DSHttpAdapter.destroy(Post, 1).then(function (data) { + assert.deepEqual(data, 1, 'post should have been deleted'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + $httpBackend.flush(); + + $httpBackend.expectDELETE('api2/posts/1').respond(200, 1); + + DSHttpAdapter.destroy(Post, 1, { basePath: 'api2' }).then(function (data) { + assert.deepEqual(data, 1, 'post should have been deleted'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + $httpBackend.flush(); + + assert.equal(lifecycle.queryTransform.callCount, 0, 'queryTransform should not have been called'); + }); +}); diff --git a/test/adapters/http/destroyAll.test.js b/test/adapters/http/destroyAll.test.js new file mode 100644 index 0000000..a9bd7a3 --- /dev/null +++ b/test/adapters/http/destroyAll.test.js @@ -0,0 +1,36 @@ +describe('DSHttpAdapter.destroyAll', function () { + + beforeEach(startInjector); + + it('should make a DELETE request', function () { + $httpBackend.expectDELETE('http://test.angular-cache.com/posts').respond(204); + + DSHttpAdapter.destroyAll(Post, {}).then(function (data) { + assert.isUndefined(data, 'posts should have been found'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + $httpBackend.flush(); + + $httpBackend.expectDELETE('api2/posts?where=%7B%22author%22:%7B%22%3D%3D%22:%22John%22%7D%7D').respond(204); + + DSHttpAdapter.destroyAll(Post, { + where: { + author: { + '==': 'John' + } + } + }, { basePath: 'api2' }).then(function (data) { + assert.isUndefined(data, 'posts should have been destroyed'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + $httpBackend.flush(); + + assert.equal(lifecycle.queryTransform.callCount, 2, 'queryTransform should have been called'); + }); +}); diff --git a/test/adapters/http/find.test.js b/test/adapters/http/find.test.js new file mode 100644 index 0000000..37b8c90 --- /dev/null +++ b/test/adapters/http/find.test.js @@ -0,0 +1,52 @@ +describe('DSHttpAdapter.find', function () { + + beforeEach(startInjector); + + it('should make a GET request', function () { + $httpBackend.expectGET('http://test.angular-cache.com/posts/1').respond(200, p1); + + DSHttpAdapter.find(Post, 1).then(function (data) { + assert.deepEqual(data, p1, 'post should have been found'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + $httpBackend.flush(); + + $httpBackend.expectGET('api2/posts/1').respond(200, p1); + + DSHttpAdapter.find(Post, 1, { basePath: 'api2' }).then(function (data) { + assert.deepEqual(data, p1, 'post should have been found'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + $httpBackend.flush(); + + assert.equal(lifecycle.queryTransform.callCount, 0, 'queryTransform should not have been called'); + }); + + it('should use default configs', function () { + $httpBackend.expectGET('http://test.angular-cache.com/posts/1?test=test', { + Authorization: 'test', + Accept: 'application/json, text/plain, */*' + }).respond(200, p1); + + DSHttpAdapter.defaults.httpConfig.params = { test: 'test' }; + DSHttpAdapter.defaults.httpConfig.headers = { Authorization: 'test' }; + + DSHttpAdapter.find(Post, 1).then(function (data) { + assert.deepEqual(data, p1, 'post should have been found'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + $httpBackend.flush(); + + delete DSHttpAdapter.defaults.httpConfig.params; + delete DSHttpAdapter.defaults.httpConfig.headers; + }); +}); diff --git a/test/adapters/http/findAll.test.js b/test/adapters/http/findAll.test.js new file mode 100644 index 0000000..cac0300 --- /dev/null +++ b/test/adapters/http/findAll.test.js @@ -0,0 +1,36 @@ +describe('DSHttpAdapter.findAll', function () { + + beforeEach(startInjector); + + it('should make a GET request', function () { + $httpBackend.expectGET('http://test.angular-cache.com/posts').respond(200, [p1]); + + DSHttpAdapter.findAll(Post, {}).then(function (data) { + assert.deepEqual(data, [p1], 'posts should have been found'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + $httpBackend.flush(); + + $httpBackend.expectGET('api2/posts?where=%7B%22author%22:%7B%22%3D%3D%22:%22John%22%7D%7D').respond(200, [p1]); + + DSHttpAdapter.findAll(Post, { + where: { + author: { + '==': 'John' + } + } + }, { basePath: 'api2' }).then(function (data) { + assert.deepEqual(data, [p1], 'posts should have been found'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + $httpBackend.flush(); + + assert.equal(lifecycle.queryTransform.callCount, 2, 'queryTransform should have been called'); + }); +}); diff --git a/test/adapters/http/update.test.js b/test/adapters/http/update.test.js new file mode 100644 index 0000000..060132e --- /dev/null +++ b/test/adapters/http/update.test.js @@ -0,0 +1,36 @@ +describe('DSHttpAdapter.update', function () { + + beforeEach(startInjector); + + it('should make a PUT request', function () { + $httpBackend.expectPUT('http://test.angular-cache.com/posts/1', { + author: 'John', + age: 30 + }).respond(200, p1); + + DSHttpAdapter.update(Post, 1, { author: 'John', age: 30 }).then(function (data) { + assert.deepEqual(data, p1, 'post 5 should have been updated'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + $httpBackend.flush(); + + $httpBackend.expectPUT('api2/posts/1', { + author: 'John', + age: 30 + }).respond(200, p1); + + DSHttpAdapter.update(Post, 1, { author: 'John', age: 30 }, { basePath: 'api2' }).then(function (data) { + assert.deepEqual(data, p1, 'post 5 should have been updated'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + $httpBackend.flush(); + + assert.equal(lifecycle.queryTransform.callCount, 0, 'queryTransform should not have been called'); + }); +}); diff --git a/test/adapters/http/updateAll.test.js b/test/adapters/http/updateAll.test.js new file mode 100644 index 0000000..e3c6bed --- /dev/null +++ b/test/adapters/http/updateAll.test.js @@ -0,0 +1,36 @@ +describe('DSHttpAdapter.updateAll', function () { + + beforeEach(startInjector); + + it('should make a PUT request', function () { + $httpBackend.expectPUT('http://test.angular-cache.com/posts').respond(200, [p1]); + + DSHttpAdapter.updateAll(Post, { author: 'John', age: 30 }).then(function (data) { + assert.deepEqual(data, [p1], 'posts should have been updated'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + $httpBackend.flush(); + + $httpBackend.expectPUT('api2/posts?where=%7B%22author%22:%7B%22%3D%3D%22:%22John%22%7D%7D').respond(200, [p1]); + + DSHttpAdapter.updateAll(Post, { author: 'John', age: 30 }, { + where: { + author: { + '==': 'John' + } + } + }, { basePath: 'api2' }).then(function (data) { + assert.deepEqual(data, [p1], 'posts should have been updated'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + $httpBackend.flush(); + + assert.equal(lifecycle.queryTransform.callCount, 1, 'queryTransform should have been called'); + }); +}); diff --git a/test/datastore/async_methods/create.test.js b/test/datastore/async_methods/create.test.js new file mode 100644 index 0000000..4fb0c2a --- /dev/null +++ b/test/datastore/async_methods/create.test.js @@ -0,0 +1,337 @@ +describe('DS.create', function () { + beforeEach(startInjector); + + it('should create an item and save it to the server', function (done) { + $httpBackend.expectPOST('http://test.angular-cache.com/posts').respond(200, p1); + + DS.create('post', { author: 'John', age: 30 }).then(function (post) { + try { + assert.deepEqual(angular.toJson(post), angular.toJson(p1), 'post 5 should have been created'); + + assert.equal(lifecycle.beforeCreate.callCount, 1, 'beforeCreate should have been called'); + assert.equal(lifecycle.afterCreate.callCount, 1, 'afterCreate should have been called'); + assert.equal(lifecycle.beforeInject.callCount, 1, 'beforeInject should have been called'); + assert.equal(lifecycle.afterInject.callCount, 1, 'afterInject should have been called'); + assert.equal(lifecycle.serialize.callCount, 1, 'serialize should have been called'); + assert.equal(lifecycle.deserialize.callCount, 1, 'deserialize should have been called'); + assert.deepEqual(angular.toJson(DS.get('post', 5)), angular.toJson(p1)); + + done(); + } catch (e) { + done(e); + } + }, function (err) { + console.error(err.stack); + done('should not have rejected'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + } catch (e) { + done(e); + } + }, 30); + }); + it('should create an item and save it to the server but not inject the result', function (done) { + DSHttpAdapter.defaults.forceTrailingSlash = true; + $httpBackend.expectPOST('http://test.angular-cache.com/posts/').respond(200, p1); + + DS.create('post', { author: 'John', age: 30 }, { cacheResponse: false }).then(function (post) { + assert.deepEqual(angular.toJson(post), angular.toJson(p1), 'post 5 should have been created'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + DSHttpAdapter.defaults.forceTrailingSlash = false; + + assert.equal(lifecycle.beforeCreate.callCount, 1, 'beforeCreate should have been called'); + assert.equal(lifecycle.afterCreate.callCount, 1, 'afterCreate should have been called'); + assert.equal(lifecycle.beforeInject.callCount, 0, 'beforeInject should not have been called'); + assert.equal(lifecycle.afterInject.callCount, 0, 'afterInject should not have been called'); + assert.equal(lifecycle.serialize.callCount, 1, 'serialize should have been called'); + assert.equal(lifecycle.deserialize.callCount, 1, 'deserialize should have been called'); + assert.isUndefined(DS.get('post', 5)); + + done(); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }, 30); + }); + it('should work with the upsert option', function (done) { + $httpBackend.expectPUT('http://test.angular-cache.com/posts/5').respond(200, p1); + + DS.create('post', { author: 'John', age: 30, id: 5 }).then(function (post) { + assert.deepEqual(angular.toJson(post), angular.toJson(p1), 'post 5 should have been created'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + $httpBackend.expectPOST('http://test.angular-cache.com/posts').respond(200, p2); + + setTimeout(function () { + try { + DS.create('post', { author: 'Sue', age: 70, id: 6 }, { upsert: false }).then(function (post) { + assert.deepEqual(angular.toJson(post), angular.toJson(p2), 'post 6 should have been created'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + assert.equal(lifecycle.beforeUpdate.callCount, 1, 'beforeUpdate should have been called'); + assert.equal(lifecycle.afterUpdate.callCount, 1, 'afterUpdate should have been called'); + assert.equal(lifecycle.beforeCreate.callCount, 1, 'beforeCreate should have been called'); + assert.equal(lifecycle.afterCreate.callCount, 1, 'afterCreate should have been called'); + assert.equal(lifecycle.beforeInject.callCount, 2, 'beforeInject should have been called twice'); + assert.equal(lifecycle.afterInject.callCount, 2, 'afterInject should have been called twice'); + assert.equal(lifecycle.serialize.callCount, 2, 'serialize should have been called twice'); + assert.equal(lifecycle.deserialize.callCount, 2, 'deserialize should have been called twice'); + assert.isDefined(DS.get('post', 5)); + assert.isDefined(DS.get('post', 6)); + + done(); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }, 30); + }); + it('should create an item that includes relations, save them to the server and inject the results', function (done) { + var payload = { + id: 99, + name: 'Sally', + profile: { + id: 999, + userId: 99, + email: 'sally@test.com' + } + }; + + $httpBackend.expectPOST('http://test.angular-cache.com/user').respond(200, payload); + + DS.create('user', { + name: 'Sally', + profile: { + email: 'sally@test.com' + } + }, { + findBelongsTo: true + }).then(function (user) { + assert.deepEqual(user.id, payload.id, 'user should have been created'); + + DS.find('user', 99); // should not trigger another http request + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + assert.equal(lifecycle.beforeCreate.callCount, 1, 'beforeCreate should have been called twice'); + assert.equal(lifecycle.afterCreate.callCount, 1, 'afterCreate should have been called twice'); + assert.equal(lifecycle.beforeInject.callCount, 2, 'beforeInject should have been called twice'); + assert.equal(lifecycle.afterInject.callCount, 2, 'afterInject should have been called twice'); + assert.equal(lifecycle.serialize.callCount, 1, 'serialize should have been called'); + assert.equal(lifecycle.deserialize.callCount, 1, 'deserialize should have been called'); + assert.deepEqual(DS.get('user', 99).id, payload.id); + assert.isObject(DS.get('user', 99).profile); + assert.deepEqual(DS.get('profile', 999).id, 999); + assert.isObject(DS.get('profile', 999).user); + + done(); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }, 30); + }); + it('should handle nested resources', function (done) { + var testComment = { + id: 5, + content: 'test', + approvedBy: 4 + }; + var testComment2 = { + id: 6, + content: 'test', + approvedBy: 4 + }; + $httpBackend.expectPOST('http://test.angular-cache.com/user/4/comment').respond(200, testComment); + + DS.create('comment', { + content: 'test', + approvedBy: 4 + }).then(function (comment) { + assert.deepEqual(angular.toJson(comment), angular.toJson(testComment)); + assert.deepEqual(angular.toJson(comment), angular.toJson(DS.get('comment', 5))); + }, function () { + fail('Should not have failed!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + $httpBackend.expectPOST('http://test.angular-cache.com/user/4/comment').respond(200, testComment2); + + setTimeout(function () { + try { + DS.create('comment', { + content: 'test' + }, { + params: { + approvedBy: 4 + } + }).then(function (comment) { + assert.deepEqual(angular.toJson(comment), angular.toJson(testComment2)); + assert.deepEqual(angular.toJson(comment), angular.toJson(DS.get('comment', 6))); + }, function () { + fail('Should not have failed!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + $httpBackend.expectPOST('http://test.angular-cache.com/comment').respond(200, testComment2); + + setTimeout(function () { + try { + DS.create('comment', { + content: 'test', + approvedBy: 4 + }, { + params: { + approvedBy: false + } + }).then(function (comment) { + assert.deepEqual(angular.toJson(comment), angular.toJson(testComment2)); + assert.deepEqual(comment, DS.get('comment', 6)); + }, function () { + fail('Should not have failed!'); + }); + setTimeout(function () { + try { + $httpBackend.flush(); + done(); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }, 30); + }); + it('should find inverse links', function (done) { + DS.inject('organization', { + id: 77 + }); + + $httpBackend.expectPOST('http://test.angular-cache.com/organization/77/user').respond(200, { + organizationId: 77, + id: 88 + }); + + DS.create('user', { + organizationId: 77, + id: 88 + }, { upsert: false, findBelongsTo: true }).then(function (user) { + var organization = DS.link('organization', 77, ['user']); + assert.isArray(organization.users); + assert.equal(1, organization.users.length); + assert.isObject(user.organization); + assert.isTrue(user.organization === organization); + assert.isTrue(user === organization.users[0]); + done(); + }, function () { + done('Should not have succeeded!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + } catch (e) { + done(e); + } + }, 30); + }); + // Not yet implemented in js-data + //it('should eager inject', function () { + // $httpBackend.expectPOST('http://test.angular-cache.com/organization/77/user').respond(200, { + // organizationId: 77, + // id: 88 + // }); + // + // var eagerUser; + // + // DS.create('user', { + // organizationId: 77 + // }, { eagerInject: true }).then(function (user) { + // assert.equal(user.id, 88); + // assert.isTrue(eagerUser === user); + // assert.isTrue(DS.filter('user')[0] === user); + // }, function () { + // fail('Should not have succeeded!'); + // }); + // + // $rootScope.$apply(); + // + // eagerUser = DS.filter('user')[0]; + // assert.isDefined(eagerUser); + // assert.equal(eagerUser.organizationId, 77); + // assert.notEqual(eagerUser.id, 88); + // + // $httpBackend.flush(); + //}); +}); diff --git a/test/datastore/async_methods/destroy.test.js b/test/datastore/async_methods/destroy.test.js new file mode 100644 index 0000000..3b30e44 --- /dev/null +++ b/test/datastore/async_methods/destroy.test.js @@ -0,0 +1,148 @@ +describe('DS.destroy', function () { + beforeEach(startInjector); + + it('should delete an item from the data store', function (done) { + $httpBackend.expectDELETE('http://test.angular-cache.com/posts/5').respond(200, 5); + + DS.inject('post', p1); + + DS.destroy('post', 5).then(function (id) { + try { + assert.equal(id, 5, 'post 5 should have been deleted'); + assert.equal(lifecycle.beforeDestroy.callCount, 1, 'beforeDestroy should have been called'); + assert.equal(lifecycle.afterDestroy.callCount, 1, 'afterDestroy should have been called'); + assert.isUndefined(DS.get('post', 5)); + assert.equal(DS.lastModified('post', 5), 0); + assert.equal(DS.lastSaved('post', 5), 0); + done(); + } catch (err) { + done(err); + } + }, function (err) { + console.error(err.stack); + done('should not have rejected'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + } catch (e) { + done(e); + } + }, 30); + }); + it('should handle nested resources', function (done) { + var testComment = { + id: 5, + content: 'test' + }; + var testComment2 = { + id: 6, + content: 'test', + approvedBy: 4 + }; + + DS.inject('comment', testComment); + + $httpBackend.expectDELETE('http://test.angular-cache.com/user/4/comment/5').respond(204); + + DS.destroy('comment', 5, { + params: { + approvedBy: 4 + } + }).then(null, function () { + done('Should not have failed!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + $httpBackend.expectDELETE('http://test.angular-cache.com/user/4/comment/6').respond(204); + + DS.inject('comment', testComment2); + + DS.destroy('comment', 6, { + bypassCache: true + }).then(null, function () { + done('Should not have failed!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + $httpBackend.expectDELETE('http://test.angular-cache.com/comment/6').respond(204); + DS.inject('comment', testComment2); + DS.destroy('comment', 6, { + params: { + approvedBy: false + } + }).then(null, function (err) { + console.log(err.stack); + done('Should not have failed!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + done(); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }, 30); + }); + // not yet implemented in js-data + //it('should eager eject', function (done) { + // $httpBackend.expectDELETE('http://test.angular-cache.com/posts/5').respond(200, 5); + // + // DS.inject('post', p1); + // + // DS.destroy('post', 5, { eagerEject: true }).then(function (id) { + // assert.equal(id, 5, 'post 5 should have been deleted'); + // }, function (err) { + // console.error(err.stack); + // fail('should not have rejected'); + // }); + // + // $rootScope.$apply(); + // + // assert.isUndefined(DS.get('post', 5)); + // + // setTimeout(function () { + // try { + // $httpBackend.flush(); + // + // setTimeout(function () { + // try { + // assert.equal(lifecycle.beforeDestroy.callCount, 1, 'beforeDestroy should have been called'); + // assert.equal(lifecycle.afterDestroy.callCount, 1, 'afterDestroy should have been called'); + // assert.isUndefined(DS.get('post', 5)); + // assert.equal(DS.lastModified('post', 5), 0); + // assert.equal(DS.lastSaved('post', 5), 0); + // + // done(); + // } catch (e) { + // done(e); + // } + // }); + // } catch (e) { + // done(e); + // } + // }, 30); + //}); +}); diff --git a/test/datastore/async_methods/destroyAll.test.js b/test/datastore/async_methods/destroyAll.test.js new file mode 100644 index 0000000..22ce4ff --- /dev/null +++ b/test/datastore/async_methods/destroyAll.test.js @@ -0,0 +1,152 @@ +describe('DS.destroyAll', function () { + beforeEach(startInjector); + + it('should query the server for a collection', function (done) { + $httpBackend.expectDELETE('http://test.angular-cache.com/posts?where=%7B%22age%22:33%7D').respond(200); + + DS.inject('post', p1); + DS.inject('post', p2); + DS.inject('post', p3); + DS.inject('post', p4); + DS.inject('post', p5); + + DS.destroyAll('post', { where: { age: 33 } }).then(null, function (err) { + console.error(err.stack); + done('Should not have rejected!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + assert.isDefined(DS.get('post', 5)); + assert.isDefined(DS.get('post', 6)); + assert.isDefined(DS.get('post', 7)); + assert.isUndefined(DS.get('post', 8)); + assert.isUndefined(DS.get('post', 9)); + + $httpBackend.expectDELETE('http://test.angular-cache.com/posts').respond(200); + + DS.inject('post', p1); + DS.inject('post', p2); + DS.inject('post', p3); + DS.inject('post', p4); + DS.inject('post', p5); + + DS.destroyAll('post', {}).then(null, function (err) { + console.error(err.stack); + done('Should not have rejected!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + assert.deepEqual(DS.filter('post', {}), [], 'The posts should not be in the store yet'); + + done(); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + }); + it('should handle nested resources', function (done) { + $httpBackend.expectDELETE('http://test.angular-cache.com/user/4/comment?content=test').respond(204); + + DS.destroyAll('comment', { + content: 'test' + }, { + params: { + approvedBy: 4 + } + }).then(function () { + }, function (err) { + console.log(err); + fail('Should not have failed!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + $httpBackend.expectDELETE('http://test.angular-cache.com/comment?content=test').respond(204); + + DS.destroyAll('comment', { + content: 'test' + }).then(function () { + }, function (err) { + console.log(err); + fail('Should not have failed!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + $httpBackend.expectDELETE('http://test.angular-cache.com/comment?content=test').respond(204); + + DS.destroyAll('comment', { + content: 'test' + }, { + params: { + approvedBy: false + } + }).then(function () { + }, function (err) { + console.log(err); + fail('Should not have failed!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + done(); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + }); +}); diff --git a/test/datastore/async_methods/find.test.js b/test/datastore/async_methods/find.test.js new file mode 100644 index 0000000..d4bb658 --- /dev/null +++ b/test/datastore/async_methods/find.test.js @@ -0,0 +1,242 @@ +describe('DS.find', function () { + beforeEach(startInjector); + + it('should get an item from the server', function (done) { + $httpBackend.expectGET('http://test.angular-cache.com/posts/5').respond(200, p1); + + DS.find('post', 5).then(function (post) { + assert.deepEqual(angular.toJson(post), angular.toJson(p1)); + }, function (err) { + console.error(err.stack); + fail('Should not have rejected!'); + }); + + assert.isUndefined(DS.get('post', 5), 'The post should not be in the store yet'); + + // Should have no effect because there is already a pending query + DS.find('post', 5).then(function (post) { + assert.deepEqual(angular.toJson(post), angular.toJson(p1)); + }, function (err) { + console.error(err.stack); + fail('Should not have rejected!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + assert.deepEqual(angular.toJson(DS.get('post', 5)), angular.toJson(p1), 'The post is now in the store'); + assert.isNumber(DS.lastModified('post', 5)); + assert.isNumber(DS.lastSaved('post', 5)); + + // Should not make a request because the request was already completed + DS.find('post', 5).then(function (post) { + assert.deepEqual(angular.toJson(post), angular.toJson(p1)); + }, function (err) { + console.error(err.stack); + fail('Should not have rejected!'); + }); + + $httpBackend.expectGET('http://test.angular-cache.com/posts/5').respond(200, p1); + + // Should make a request because bypassCache is set to true + DS.find('post', 5, { bypassCache: true }).then(function (post) { + assert.deepEqual(angular.toJson(post), angular.toJson(p1)); + }, function (err) { + console.error(err.stack); + fail('Should not have rejected!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + assert.equal(lifecycle.beforeInject.callCount, 2, 'beforeInject should have been called'); + assert.equal(lifecycle.afterInject.callCount, 2, 'afterInject should have been called'); + assert.equal(lifecycle.serialize.callCount, 0, 'serialize should have been called'); + assert.equal(lifecycle.deserialize.callCount, 2, 'deserialize should have been called'); + + done(); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + }); + it('should get an item from the server but not store it if cacheResponse is false', function (done) { + $httpBackend.expectGET('http://test.angular-cache.com/posts/5').respond(200, p1); + + DS.find('post', 5, { cacheResponse: false }).then(function (post) { + assert.deepEqual(angular.toJson(post), angular.toJson(p1)); + }, function (err) { + console.error(err.stack); + fail('Should not have rejected!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + assert.isUndefined(DS.get('post', 5), 'The post should not have been injected into the store'); + assert.equal(lifecycle.beforeInject.callCount, 0, 'beforeInject should have been called'); + assert.equal(lifecycle.afterInject.callCount, 0, 'afterInject should have been called'); + assert.equal(lifecycle.serialize.callCount, 0, 'serialize should have been called'); + assert.equal(lifecycle.deserialize.callCount, 1, 'deserialize should have been called'); + + done(); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + }); + it('should correctly propagate errors', function (done) { + $httpBackend.expectGET('http://test.angular-cache.com/posts/5').respond(404, 'Not Found'); + + DS.find('post', 5).then(function () { + done('Should not have succeeded!'); + }, function (err) { + assert.equal(err.data, 'Not Found'); + done(); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + } catch (e) { + console.log(e); + done(e); + } + }, 30); + }); + it('should handle nested resources', function (done) { + var testComment = { + id: 5, + content: 'test', + approvedBy: 4 + }; + $httpBackend.expectGET('http://test.angular-cache.com/user/4/comment/5').respond(200, testComment); + + DS.find('comment', 5, { + params: { + approvedBy: 4 + } + }).then(function (comment) { + assert.deepEqual(angular.toJson(comment), angular.toJson(testComment)); + assert.deepEqual(angular.toJson(comment), angular.toJson(DS.get('comment', 5))); + }, function () { + fail('Should not have failed!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + $httpBackend.expectGET('http://test.angular-cache.com/user/4/comment/5').respond(200, testComment); + + DS.find('comment', 5, { + bypassCache: true + }).then(function (comment) { + assert.deepEqual(angular.toJson(comment), angular.toJson(testComment)); + assert.deepEqual(angular.toJson(comment), angular.toJson(DS.get('comment', 5))); + }, function () { + fail('Should not have failed!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + $httpBackend.expectGET('http://test.angular-cache.com/comment/5').respond(200, testComment); + + DS.find('comment', 5, { + bypassCache: true, + params: { + approvedBy: false + } + }).then(function (comment) { + assert.deepEqual(angular.toJson(comment), angular.toJson(testComment)); + assert.deepEqual(angular.toJson(comment), angular.toJson(DS.get('comment', 5))); + }, function () { + fail('Should not have failed!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + $httpBackend.expectGET('http://test.angular-cache.com/organization/14/user/19/comment/19').respond(200, comment19); + + DS.find('comment', 19, { + bypassCache: true, + params: { + approvedBy: 19, + organizationId: 14 + } + }).then(function (comment) { + assert.equal(comment.id, comment19.id); + assert.equal(comment.id, DS.get('comment', 19).id); + }, function () { + fail('Should not have failed!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + done(); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + }); +}); diff --git a/test/datastore/async_methods/findAll.test.js b/test/datastore/async_methods/findAll.test.js new file mode 100644 index 0000000..aa41b2a --- /dev/null +++ b/test/datastore/async_methods/findAll.test.js @@ -0,0 +1,387 @@ +describe('DS.findAll', function () { + + beforeEach(startInjector); + + it('should query the server for a collection', function (done) { + $httpBackend.expectGET(/http:\/\/test\.angular-cache\.com\/posts\??/).respond(200, [p1, p2, p3, p4]); + + DS.findAll('post', {}).then(function (data) { + assert.deepEqual(angular.toJson(data), angular.toJson([p1, p2, p3, p4])); + }, function (err) { + console.error(err.stack); + fail('Should not have rejected!'); + }); + + assert.deepEqual(DS.filter('post', {}), [], 'The posts should not be in the store yet'); + + // Should have no effect because there is already a pending query + DS.findAll('post', {}).then(function (data) { + assert.deepEqual(angular.toJson(data), angular.toJson([p1, p2, p3, p4])); + }, function (err) { + console.error(err.stack); + fail('Should not have rejected!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + assert.deepEqual(angular.toJson(DS.filter('post', {})), angular.toJson([p1, p2, p3, p4]), 'The posts are now in the store'); + assert.isNumber(DS.lastModified('post', 5)); + assert.isNumber(DS.lastSaved('post', 5)); + DS.find('post', p1.id); // should not trigger another XHR + + + // Should not make a request because the request was already completed + DS.findAll('post', {}).then(function (data) { + assert.deepEqual(angular.toJson(data), angular.toJson([p1, p2, p3, p4])); + }, function (err) { + console.error(err.stack); + fail('Should not have rejected!'); + }); + + $httpBackend.expectGET(/http:\/\/test\.angular-cache\.com\/posts\??/).respond(200, [p1, p2, p3, p4]); + + // Should make a request because bypassCache is set to true + DS.findAll('post', {}, { bypassCache: true }).then(function (data) { + assert.deepEqual(angular.toJson(data), angular.toJson([p1, p2, p3, p4])); + }, function (err) { + console.error(err.stack); + fail('Should not have rejected!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + assert.equal(lifecycle.beforeInject.callCount, 2, 'beforeInject should have been called'); + assert.equal(lifecycle.afterInject.callCount, 2, 'afterInject should have been called'); + assert.equal(lifecycle.serialize.callCount, 0, 'serialize should have been called'); + assert.equal(lifecycle.deserialize.callCount, 2, 'deserialize should have been called'); + done(); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + }); + it('should query the server for a collection but not store the data if cacheResponse is false', function (done) { + $httpBackend.expectGET(/http:\/\/test\.angular-cache\.com\/posts\??/).respond(200, [p1, p2, p3, p4]); + + DS.findAll('post', {}, { cacheResponse: false }).then(function (data) { + assert.deepEqual(angular.toJson(data), angular.toJson([p1, p2, p3, p4])); + }, function (err) { + console.error(err.stack); + fail('Should not have rejected!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + assert.deepEqual(angular.toJson(DS.filter('post', {})), angular.toJson([]), 'The posts should not have been injected into the store'); + + assert.equal(lifecycle.beforeInject.callCount, 0, 'beforeInject should have been called'); + assert.equal(lifecycle.afterInject.callCount, 0, 'afterInject should have been called'); + assert.equal(lifecycle.serialize.callCount, 0, 'serialize should have been called'); + assert.equal(lifecycle.deserialize.callCount, 1, 'deserialize should have been called'); + + done(); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + }); + it('should correctly propagate errors', function (done) { + $httpBackend.expectGET(/http:\/\/test\.angular-cache\.com\/posts\??/).respond(404, 'Not Found'); + + DS.findAll('post', {}).then(function () { + done('Should not have succeeded!'); + }, function (err) { + assert.equal(err.data, 'Not Found'); + done(); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + } catch (e) { + done(e); + } + }, 30); + }); + it('"params" argument is optional', function (done) { + $httpBackend.expectGET(/http:\/\/test\.angular-cache\.com\/posts\??/).respond(200, [p1, p2, p3, p4]); + + DS.findAll('post').then(function (data) { + assert.deepEqual(angular.toJson(data), angular.toJson([p1, p2, p3, p4])); + }, function (err) { + console.error(err.message); + fail('Should not have rejected!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + assert.deepEqual(angular.toJson(DS.filter('post', {})), angular.toJson([p1, p2, p3, p4]), 'The posts are now in the store'); + + assert.equal(lifecycle.beforeInject.callCount, 1 , 'beforeInject should have been called'); + assert.equal(lifecycle.afterInject.callCount, 1, 'afterInject should have been called'); + assert.equal(lifecycle.serialize.callCount, 0, 'serialize should have been called'); + assert.equal(lifecycle.deserialize.callCount, 1, 'deserialize should have been called'); + + done(); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + }); + it('"params"', function (done) { + $httpBackend.expectGET('http://test.angular-cache.com/posts?where=%7B%22author%22:%22Adam%22%7D').respond(200, [p4, p5]); + + var params = { + where: { + author: 'Adam' + } + }; + DS.findAll('post', params).then(function (data) { + assert.deepEqual(angular.toJson(data), angular.toJson([p4, p5])); + }, function (err) { + console.error(err.message); + fail('Should not have rejected!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + assert.deepEqual(angular.toJson(DS.filter('post', params)), angular.toJson([p4, p5]), 'The posts are now in the store'); + assert.deepEqual(angular.toJson(DS.filter('post', { + where: { + id: { + '>': 8 + } + } + })), angular.toJson([p5]), 'The posts are now in the store'); + + assert.equal(lifecycle.beforeInject.callCount, 1, 'beforeInject should have been called'); + assert.equal(lifecycle.afterInject.callCount, 1, 'afterInject should have been called'); + assert.equal(lifecycle.serialize.callCount, 0, 'serialize should have been called'); + assert.equal(lifecycle.deserialize.callCount, 1, 'deserialize should have been called'); + + done(); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + }); + it('should return already injected items', function (done) { + var u1 = { + id: 1, + name: 'John' + }, + u2 = { + id: 2, + name: 'Sally' + }; + + DS.defineResource({ + name: 'person', + endpoint: 'users', + methods: { + fullName: function () { + return this.first + ' ' + this.last; + } + } + }); + + $httpBackend.expectGET(/http:\/\/test\.angular-cache\.com\/users\??/).respond(200, [u1, u2]); + + DS.findAll('person').then(function (data) { + assert.deepEqual(angular.toJson(data), angular.toJson([ + DSUtils.deepMixIn(new DS.definitions.person[DS.definitions.person.class](), u1), + DSUtils.deepMixIn(new DS.definitions.person[DS.definitions.person.class](), u2) + ])); + angular.forEach(data, function (person) { + assert.isTrue(person instanceof DS.definitions.person[DS.definitions.person.class], 'should be an instance of User'); + }); + }, function (err) { + console.error(err.message); + fail('Should not have rejected!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + assert.deepEqual(angular.toJson(DS.filter('person')), angular.toJson([ + DSUtils.deepMixIn(new DS.definitions.person[DS.definitions.person.class](), u1), + DSUtils.deepMixIn(new DS.definitions.person[DS.definitions.person.class](), u2) + ]), 'The users are now in the store'); + + assert.equal(lifecycle.beforeInject.callCount, 1, 'beforeInject should have been called'); + assert.equal(lifecycle.afterInject.callCount, 1, 'afterInject should have been called'); + assert.equal(lifecycle.serialize.callCount, 0, 'serialize should have been called'); + assert.equal(lifecycle.deserialize.callCount, 1, 'deserialize should have been called'); + + done(); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + }); + it('should handle nested resources', function (done) { + var testComment = { + id: 5, + content: 'test', + approvedBy: 4 + }; + var testComment2 = { + id: 6, + content: 'test', + approvedBy: 4 + }; + $httpBackend.expectGET('http://test.angular-cache.com/user/4/comment?content=test').respond(200, [testComment, testComment2]); + + DS.findAll('comment', { + content: 'test' + }, { + params: { + approvedBy: 4 + } + }).then(function (comments) { + assert.deepEqual(angular.toJson(comments), angular.toJson([testComment, testComment2])); + assert.deepEqual(angular.toJson(comments), angular.toJson(DS.filter('comment', { + content: 'test' + }))); + done(); + }, function () { + done('Should not have failed!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + } catch (e) { + done(e); + } + }, 30); + }); + it('should handle nested resources 2', function (done) { + var testComment = { + id: 5, + content: 'test', + approvedBy: 4 + }; + var testComment2 = { + id: 6, + content: 'test', + approvedBy: 4 + }; + + $httpBackend.expectGET('http://test.angular-cache.com/comment?content=test').respond(200, [testComment, testComment2]); + + DS.findAll('comment', { + content: 'test' + }, { + bypassCache: true + }).then(function (comments) { + assert.deepEqual(angular.toJson(comments), angular.toJson([testComment, testComment2])); + assert.deepEqual(angular.toJson(comments), angular.toJson(DS.filter('comment', { + content: 'test' + }))); + done(); + }, function () { + done('Should not have failed!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + } catch (e) { + done(e); + } + }, 30); + }); + it('should handle nested resources 3', function (done) { + var testComment = { + id: 5, + content: 'test', + approvedBy: 4 + }; + var testComment2 = { + id: 6, + content: 'test', + approvedBy: 4 + }; + + DS.ejectAll('comment'); + + $httpBackend.expectGET('http://test.angular-cache.com/comment?content=test').respond(200, [testComment, testComment2]); + + DS.findAll('comment', { + content: 'test' + }, { + bypassCache: true, + params: { + approvedBy: false + } + }).then(function (comments) { + assert.deepEqual(angular.toJson(comments), angular.toJson([testComment, testComment2])); + assert.deepEqual(angular.toJson(comments), angular.toJson(DS.filter('comment', { + content: 'test' + }))); + done(); + }, function () { + done('Should not have failed!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + } catch (e) { + done(e); + } + }, 30); + }); +}); diff --git a/test/datastore/async_methods/loadRelations.test.js b/test/datastore/async_methods/loadRelations.test.js new file mode 100644 index 0000000..71e25cc --- /dev/null +++ b/test/datastore/async_methods/loadRelations.test.js @@ -0,0 +1,230 @@ +describe('DS.loadRelations', function () { + beforeEach(startInjector); + + it('should get an item from the server', function (done) { + DS.inject('user', user10); + + $httpBackend.expectGET('http://test.angular-cache.com/user/10/comment?userId=10').respond(200, [ + comment11, + comment12, + comment13 + ]); + $httpBackend.expectGET('http://test.angular-cache.com/profile?userId=10').respond(200, [profile15]); + $httpBackend.expectGET('http://test.angular-cache.com/organization/14?userId=10').respond(200, organization14); + + DS.loadRelations('user', 10, ['comment', 'profile', 'organization'], { params: { approvedBy: 10 } }).then(function (user) { + try { + assert.deepEqual(user.comments[0].id, DS.get('comment', user.comments[0].id).id); + assert.deepEqual(user.comments[0].user, DS.get('comment', user.comments[0].id).user); + assert.deepEqual(user.comments[1].id, DS.get('comment', user.comments[1].id).id); + assert.deepEqual(user.comments[1].user, DS.get('comment', user.comments[1].id).user); + assert.deepEqual(user.comments[2].id, DS.get('comment', user.comments[2].id).id); + assert.deepEqual(user.comments[2].user, DS.get('comment', user.comments[2].id).user); + assert.deepEqual(user.organization.id, DS.get('organization', 14).id); + assert.deepEqual(user.profile.id, DS.get('profile', 15).id); + // try a comment that has a belongsTo relationship to multiple users: + DS.inject('comment', comment19); + $httpBackend.expectGET('http://test.angular-cache.com/user/20').respond(200, user20); + $httpBackend.expectGET('http://test.angular-cache.com/user/19').respond(200, user19); + DS.loadRelations('comment', 19, ['user']).then(function (comment) { + try { + assert.isObject(comment.user); + assert.equal(comment.user.id, user20.id); + assert.isObject(comment.approvedByUser); + assert.equal(comment.approvedByUser.id, user19.id); + done(); + } catch (err) { + console.log(err, err.stack); + done(err); + } + }, function (err) { + console.log(err, err.stack); + done(err); + }); + setTimeout(function () { + try { + $httpBackend.flush(); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + console.log(e, e.stack); + done(e); + } + }, function (err) { + console.log(err, err.stack); + done(err); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + } catch (e) { + done(e); + } + }, 30); + }); + it('should get an item from the server but not store it if cacheResponse is false', function (done) { + DS.inject('user', { + name: 'John Anderson', + id: 10, + organizationId: 14 + }); + + $httpBackend.expectGET('http://test.angular-cache.com/user/10/comment?userId=10').respond(200, [ + comment11, + comment12, + comment13 + ]); + $httpBackend.expectGET('http://test.angular-cache.com/profile?userId=10').respond(200, [profile15]); + $httpBackend.expectGET('http://test.angular-cache.com/organization/14?userId=10').respond(200, organization14); + + DS.loadRelations('user', 10, ['comment', 'profile', 'organization'], { cacheResponse: false }).then(function (user) { + assert.deepEqual(angular.toJson(user.comments), angular.toJson([ + comment11, + comment12, + comment13 + ])); + assert.deepEqual(angular.toJson(user.organization), angular.toJson(organization14)); + assert.deepEqual(angular.toJson(user.profile), angular.toJson(profile15)); + + assert.isUndefined(DS.get('comment', 11)); + assert.isUndefined(DS.get('comment', 12)); + assert.isUndefined(DS.get('comment', 13)); + assert.isUndefined(DS.get('organization', 14)); + assert.isUndefined(DS.get('profile', 15)); + done(); + }, function () { + done('should not have failed!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + } catch (e) { + done(e); + } + }, 30); + }); + it('should correctly propagate errors', function (done) { + DS.inject('user', { + name: 'John Anderson', + id: 10, + organizationId: 14 + }); + + $httpBackend.expectGET('http://test.angular-cache.com/user/10/comment?userId=10').respond(404, 'Not Found'); + $httpBackend.expectGET('http://test.angular-cache.com/profile?userId=10').respond(404, 'Not Found'); + $httpBackend.expectGET('http://test.angular-cache.com/organization/14?userId=10').respond(404, 'Not Found'); + + DS.loadRelations('user', 10, ['comment', 'profile', 'organization']).then(function () { + done('Should not have succeeded!'); + }, function (err) { + assert.equal(err.data, 'Not Found'); + done(); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + } catch (e) { + done(e); + } + }, 30); + }); + it('should handle multiple belongsTo levels', function (done) { + var organization = DS.inject('organization', organization14); + + var copy = angular.extend({}, user10); + delete copy.organization; + delete copy.comments; + delete copy.profile; + + $httpBackend.expectGET('http://test.angular-cache.com/organization/14/user').respond(200, [copy]); + + DS.loadRelations('organization', organization, ['user']).then(function (organization) { + assert.equal(organization.users[0].id, 10); + + $httpBackend.expectGET('http://test.angular-cache.com/user/10/comment').respond(200, [comment11, comment12]); + + var user = DS.get('user', 10); + + DS.loadRelations('user', user, ['comment']).then(function (user) { + assert.isArray(user.comments); + done(); + }, function () { + done('Should not have succeeded!'); + }); + setTimeout(function () { + $httpBackend.flush(); + }, 30); + }, function (err) { + console.log(err.stack); + done('Should not have succeeded!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + } catch (e) { + done(e); + } + }, 30); + }); + it('should handle multiple belongsTo levels when the response includes nested resources', function (done) { + var organization = DS.inject('organization', { + id: 1 + }); + + $httpBackend.expectGET('http://test.angular-cache.com/organization/1/user').respond(200, [ + { + organizationId: 1, + id: 1 + } + ]); + + DS.loadRelations('organization', organization, ['user']).then(function (organization) { + assert.equal(organization.users[0].id, 1); + + $httpBackend.expectGET('http://test.angular-cache.com/user/1/comment').respond(200, [ + { + id: 1, + userId: 1, + user: { + id: 1 + } + }, + { + id: 2, + userId: 1, + user: { + id: 1 + } + } + ]); + + var user = DS.get('user', 1); + + DS.loadRelations('user', user, ['comment']).then(function (user) { + assert.isArray(user.comments); + done(); + }, function () { + done('Should not have succeeded!'); + }); + setTimeout(function () { + $httpBackend.flush(); + }, 30); + }, function () { + fail('Should not have succeeded!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + } catch (e) { + done(e); + } + }, 30); + }); +}); diff --git a/test/datastore/async_methods/refresh.test.js b/test/datastore/async_methods/refresh.test.js new file mode 100644 index 0000000..5417b3b --- /dev/null +++ b/test/datastore/async_methods/refresh.test.js @@ -0,0 +1,58 @@ +describe('DS.refresh', function () { + beforeEach(startInjector); + it('should get an item from the server', function (done) { + + // Should do nothing because the data isn't in the store + DS.refresh('post', 5).then(function (post) { + assert.isUndefined(post); + }); + + assert.isUndefined(DS.get('post', 5), 'The post should not be in the store yet'); + + DS.inject('post', p1); + assert.deepEqual(angular.toJson(DS.get('post', 5)), angular.toJson(p1), 'The post is now in the store'); + + var initialLastModified = DS.lastModified('post', 5); + + $httpBackend.expectGET('http://test.angular-cache.com/posts/5').respond(200, { author: 'John', age: 31, id: 5 }); + + // Should refresh the item that's in the store + DS.refresh('post', 5).then(function (post) { + assert.deepEqual(angular.toJson(post), angular.toJson({ author: 'John', age: 31, id: 5 })); + }, function (err) { + console.error(err.stack); + fail('Should not have rejected!'); + }); + + // Should have no effect because the request is already pending + DS.refresh('post', 5).then(function (post) { + assert.deepEqual(angular.toJson(post), angular.toJson({ author: 'John', age: 31, id: 5 })); + }, function (err) { + console.error(err.stack); + fail('Should not have rejected!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + assert.deepEqual(angular.toJson(DS.get('post', 5)), angular.toJson({ + author: 'John', + age: 31, + id: 5 + }), 'The post has been refreshed'); + assert.notEqual(DS.lastModified('post', 5), initialLastModified); + + done(); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + }); +}); diff --git a/test/datastore/async_methods/save.test.js b/test/datastore/async_methods/save.test.js new file mode 100644 index 0000000..94fa3c0 --- /dev/null +++ b/test/datastore/async_methods/save.test.js @@ -0,0 +1,237 @@ +describe('DS.save', function () { + beforeEach(startInjector); + + it('should save an item to the server and inject the result', function (done) { + $httpBackend.expectPUT('http://test.angular-cache.com/posts/5').respond(200, { + author: 'Jake', + id: 5, + age: 30 + }); + + DS.inject('post', p1); + + var initialModified = DS.lastModified('post', 5); + var initialSaved = DS.lastSaved('post', 5); + + DS.get('post', 5).author = 'Jake'; + + DS.save('post', 5).then(function (post) { + assert.deepEqual(post, DS.get('post', 5), 'post 5 should have been saved'); + assert.equal(post.author, 'Jake'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + assert.equal(lifecycle.beforeUpdate.callCount, 1, 'beforeUpdate should have been called'); + assert.equal(lifecycle.afterUpdate.callCount, 1, 'afterUpdate should have been called'); + assert.deepEqual(angular.toJson(DS.get('post', 5)), angular.toJson({ + author: 'Jake', + age: 30, + id: 5 + })); + DS.digest(); + assert.notEqual(DS.lastModified('post', 5), initialModified); + assert.notEqual(DS.lastSaved('post', 5), initialSaved); + + DS.save('post', 6).then(function () { + fail('should not have succeeded'); + }, function (err) { + assert.isTrue(err instanceof DS.errors.RuntimeError); + assert.equal(err.message, errorPrefix('post', 6) + 'id: "6" not found!'); + }); + + assert.equal(lifecycle.beforeInject.callCount, 2, 'beforeInject should have been called'); + assert.equal(lifecycle.afterInject.callCount, 2, 'afterInject should have been called'); + assert.equal(lifecycle.serialize.callCount, 1, 'serialize should have been called'); + assert.equal(lifecycle.deserialize.callCount, 1, 'deserialize should have been called'); + done(); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + }); + it('should save an item to the server and inject the result when using a custom http method', function (done) { + $httpBackend.expectPATCH('http://test.angular-cache.com/posts/5').respond(200, { + author: 'Jake', + id: 5, + age: 30 + }); + + DS.inject('post', p1); + + DS.get('post', 5).author = 'Jake'; + + DS.save('post', 5, { method: 'PATCH' }).then(function (post) { + assert.deepEqual(post, DS.get('post', 5), 'post 5 should have been saved'); + assert.equal(post.author, 'Jake'); + done(); + }, function (err) { + console.error(err.stack); + done('should not have rejected'); + }); + + setTimeout(function () { + $httpBackend.flush(); + }, 30); + }); + it('should save an item to the server and inject the result via the instance method', function (done) { + $httpBackend.expectPUT('http://test.angular-cache.com/posts/5').respond(200, { + author: 'Jake', + id: 5, + age: 30 + }); + + DS.inject('post', p1); + + var initialModified = DS.lastModified('post', 5); + var initialSaved = DS.lastSaved('post', 5); + + DS.get('post', 5).author = 'Jake'; + + DS.get('post', 5).DSSave().then(function (post) { + assert.deepEqual(post, DS.get('post', 5), 'post 5 should have been saved'); + assert.equal(post.author, 'Jake'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + setTimeout(function () { + $httpBackend.flush(); + + setTimeout(function () { + try { + assert.equal(lifecycle.beforeUpdate.callCount, 1, 'beforeUpdate should have been called'); + assert.equal(lifecycle.afterUpdate.callCount, 1, 'afterUpdate should have been called'); + assert.deepEqual(angular.toJson(DS.get('post', 5)), angular.toJson({ + author: 'Jake', + age: 30, + id: 5 + })); + DS.digest(); + assert.notEqual(DS.lastModified('post', 5), initialModified); + assert.notEqual(DS.lastSaved('post', 5), initialSaved); + done(); + } catch (err) { + done(err); + } + }, 30); + }, 30); + }); + it('should save an item to the server but not inject the result', function (done) { + $httpBackend.expectPUT('http://test.angular-cache.com/posts/5').respond(200, { + random: 'stuff' + }); + + DS.inject('post', p1); + + var initialSaved = DS.lastSaved('post', 5); + + DS.get('post', 5).author = 'Jake'; + + $rootScope.$apply(); + + var initialModified = 0; + + DS.save('post', 5, { cacheResponse: false }).then(function (post) { + try { + assert.deepEqual(post, { + random: 'stuff' + }, 'should have the right response'); + assert.equal(lifecycle.beforeUpdate.callCount, 1, 'beforeUpdate should have been called'); + assert.equal(lifecycle.afterUpdate.callCount, 1, 'afterUpdate should have been called'); + assert.equal(lifecycle.beforeInject.callCount, 1, 'beforeInject should have been called only once'); + assert.equal(lifecycle.afterInject.callCount, 1, 'afterInject should have been called only once'); + assert.deepEqual(angular.toJson(DS.get('post', 5)), angular.toJson({ + author: 'Jake', + age: 30, + id: 5 + })); + assert.equal(DS.lastSaved('post', 5), initialSaved); + done(); + } catch (err) { + done(err); + } + }, function (err) { + console.error(err.stack); + done('should not have rejected'); + }); + + setTimeout(function () { + try { + initialModified = DS.lastModified('post', 5); + $httpBackend.flush(); + } catch (e) { + done(e); + } + }, 30); + }); + it('should save changes of an item to the server', function (done) { + $httpBackend.expectPUT('http://test.angular-cache.com/posts/5', { author: 'Jake' }).respond(200, { + author: 'Jake', + id: 5, + age: 30 + }); + + DS.inject('post', p1); + + var initialModified = DS.lastModified('post', 5); + var initialSaved = DS.lastSaved('post', 5); + var post1 = DS.get('post', 5); + + post1.author = 'Jake'; + + DS.save('post', 5, { changesOnly: true }).then(function (post) { + assert.deepEqual(angular.toJson(post), angular.toJson(post1), 'post 5 should have been saved'); + assert.equal(post.author, 'Jake'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + assert.equal(lifecycle.beforeUpdate.callCount, 1, 'beforeUpdate should have been called'); + assert.equal(lifecycle.afterUpdate.callCount, 1, 'afterUpdate should have been called'); + assert.deepEqual(angular.toJson(DS.get('post', 5)), angular.toJson(post1)); + assert.notEqual(DS.lastModified('post', 5), initialModified); + assert.notEqual(DS.lastSaved('post', 5), initialSaved); + + DS.save('post', 6).then(function () { + fail('should not have succeeded'); + }, function (err) { + assert.isTrue(err instanceof DS.errors.RuntimeError); + assert.equal(err.message, 'id: "6" not found!'); + }); + + assert.equal(lifecycle.beforeInject.callCount, 2, 'beforeInject should have been called'); + assert.equal(lifecycle.afterInject.callCount, 2, 'afterInject should have been called'); + assert.equal(lifecycle.serialize.callCount, 1, 'serialize should have been called'); + assert.equal(lifecycle.deserialize.callCount, 1, 'deserialize should have been called'); + + done(); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }, 30); + }); +}); diff --git a/test/datastore/async_methods/update.test.js b/test/datastore/async_methods/update.test.js new file mode 100644 index 0000000..99edf03 --- /dev/null +++ b/test/datastore/async_methods/update.test.js @@ -0,0 +1,229 @@ +describe('DS.update', function () { + beforeEach(startInjector); + + it('should update an item', function (done) { + $httpBackend.expectPUT('http://test.angular-cache.com/posts/5').respond(200, { author: 'Jake', age: 30, id: 5 }); + + var post = DS.inject('post', p1); + + var initialModified = DS.lastModified('post', 5); + var initialSaved = DS.lastSaved('post', 5); + + DS.update('post', 5, { author: 'Jake' }).then(function (p) { + assert.deepEqual(p, post, 'post 5 should have been updated'); + assert.equal(p.author, 'Jake'); + assert.equal(post.author, 'Jake'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + $httpBackend.expectPUT('http://test.angular-cache.com/posts/6').respond(200, { author: 'Jane', age: 31, id: 6 }); + + assert.equal(lifecycle.beforeUpdate.callCount, 1, 'beforeUpdate should have been called'); + assert.equal(lifecycle.afterUpdate.callCount, 1, 'afterUpdate should have been called'); + assert.equal(lifecycle.beforeInject.callCount, 2, 'beforeInject should have been called'); + assert.equal(lifecycle.afterInject.callCount, 2, 'afterInject should have been called'); + assert.equal(lifecycle.serialize.callCount, 1, 'serialize should have been called'); + assert.equal(lifecycle.deserialize.callCount, 1, 'deserialize should have been called'); + assert.deepEqual(DS.get('post', 5), post); + assert.notEqual(DS.lastModified('post', 5), initialModified); + assert.notEqual(DS.lastSaved('post', 5), initialSaved); + + DS.update('post', 6, { author: 'Jane' }).then(function (p) { + assert.deepEqual(angular.toJson(p), angular.toJson(DS.get('post', 6))); + assert.deepEqual(angular.toJson(p), angular.toJson({ author: 'Jane', age: 31, id: 6 })); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + assert.equal(lifecycle.beforeInject.callCount, 3, 'beforeInject should have been called'); + assert.equal(lifecycle.afterInject.callCount, 3, 'afterInject should have been called'); + assert.equal(lifecycle.serialize.callCount, 2, 'serialize should have been called'); + assert.equal(lifecycle.deserialize.callCount, 2, 'deserialize should have been called'); + + done(); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + }); + it('should update an item via the instance method', function (done) { + $httpBackend.expectPUT('http://test.angular-cache.com/posts/5').respond(200, { author: 'Jake', age: 30, id: 5 }); + + var post = DS.inject('post', p1); + + var initialModified = DS.lastModified('post', 5); + var initialSaved = DS.lastSaved('post', 5); + + post.DSUpdate({ author: 'Jake' }).then(function (p) { + assert.deepEqual(angular.toJson(p), angular.toJson(post), 'post 5 should have been updated'); + assert.equal(p.author, 'Jake'); + assert.equal(post.author, 'Jake'); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + assert.equal(lifecycle.beforeUpdate.callCount, 1, 'beforeUpdate should have been called'); + assert.equal(lifecycle.afterUpdate.callCount, 1, 'afterUpdate should have been called'); + assert.equal(lifecycle.beforeInject.callCount, 2, 'beforeInject should have been called'); + assert.equal(lifecycle.afterInject.callCount, 2, 'afterInject should have been called'); + assert.equal(lifecycle.serialize.callCount, 1, 'serialize should have been called'); + assert.equal(lifecycle.deserialize.callCount, 1, 'deserialize should have been called'); + assert.deepEqual(DS.get('post', 5), post); + assert.notEqual(DS.lastModified('post', 5), initialModified); + assert.notEqual(DS.lastSaved('post', 5), initialSaved); + + done(); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + }); + it('should handle nested resources', function (done) { + var testComment = { + id: 5, + content: 'stuff', + approvedBy: 4 + }; + var testComment2 = { + id: 6, + content: 'stuff', + approvedBy: 4 + }; + $httpBackend.expectPUT('http://test.angular-cache.com/user/4/comment/5').respond(200, testComment); + + DS.inject('comment', testComment); + + DS.update('comment', 5, { + content: 'stuff' + }).then(function (comment) { + assert.deepEqual(angular.toJson(comment), angular.toJson(testComment)); + assert.deepEqual(angular.toJson(comment), angular.toJson(DS.get('comment', 5))); + }, function () { + fail('Should not have failed!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + $httpBackend.expectPUT('http://test.angular-cache.com/user/4/comment/6', { content: 'stuff' }).respond(200, testComment2); + + var comment = DS.inject('comment', testComment2); + + function onBeforeUpdate(resourceName, attrs) { + attrs.other = 'stuff'; + assert.equal(resourceName, 'comment'); + assert.deepEqual(angular.toJson(attrs), angular.toJson({ content: 'stuff', other: 'stuff' })); + } + + function onAfterUpdate(resourceName, attrs) { + assert.equal(resourceName, 'comment'); + assert.deepEqual(angular.toJson(attrs), angular.toJson(testComment2)); + assert.isFalse(testComment2 === attrs); + } + + Comment.on('DS.beforeUpdate', onBeforeUpdate); + Comment.on('DS.afterUpdate', onAfterUpdate); + + Comment.update(comment, { + content: 'stuff' + }, { + params: { + approvedBy: 4 + } + }).then(function (comment) { + assert.deepEqual(angular.toJson(comment), angular.toJson(testComment2)); + assert.deepEqual(angular.toJson(comment), angular.toJson(DS.get('comment', 6))); + }, function () { + fail('Should not have failed!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + $httpBackend.expectPUT('http://test.angular-cache.com/comment/6').respond(200, testComment2); + + DS.inject('comment', testComment2); + + DS.update('comment', 6, { + content: 'stuff' + }, { + params: { + approvedBy: false + } + }).then(function (comment) { + assert.deepEqual(angular.toJson(comment), angular.toJson(testComment2)); + assert.deepEqual(angular.toJson(comment), angular.toJson(DS.get('comment', 6))); + }, function () { + fail('Should not have failed!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + done(); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + }); +}); diff --git a/test/datastore/async_methods/updateAll.test.js b/test/datastore/async_methods/updateAll.test.js new file mode 100644 index 0000000..c209466 --- /dev/null +++ b/test/datastore/async_methods/updateAll.test.js @@ -0,0 +1,204 @@ +describe('DS.updateAll', function () { + beforeEach(startInjector); + + it('should update a collection of items', function (done) { + $httpBackend.expectPUT('http://test.angular-cache.com/posts?where=%7B%22age%22:%7B%22%3D%3D%22:33%7D%7D').respond(200, [ + { author: 'Adam', age: 27, id: 8 }, + { author: 'Adam', age: 27, id: 9 } + ]); + + var post4 = DS.inject('post', p4); + var post5 = DS.inject('post', p5); + var posts = DS.filter('post', { where: { age: { '==': 33 } } }); + + var initialModified = DS.lastModified('post', 8); + var initialSaved = DS.lastSaved('post', 8); + + DS.updateAll('post', { age: 27 }, { where: { age: { '==': 33 } } }).then(function (ps) { + assert.deepEqual(angular.toJson(ps), angular.toJson(posts), '2 posts should have been updated'); + assert.equal(posts[0].age, 27); + assert.equal(posts[1].age, 27); + assert.equal(post4.age, 27); + assert.equal(post5.age, 27); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + $httpBackend.expectPUT('http://test.angular-cache.com/posts?where=%7B%22age%22:%7B%22%3D%3D%22:31%7D%7D').respond(200, [ + { author: 'Jane', age: 5, id: 6 } + ]); + + assert.equal(lifecycle.beforeUpdate.callCount, 1, 'beforeUpdate should have been called'); + assert.equal(lifecycle.afterUpdate.callCount, 1, 'afterUpdate should have been called'); + assert.deepEqual(angular.toJson(DS.filter('post', { where: { age: { '==': 27 } } })), angular.toJson(posts)); + assert.notEqual(DS.lastModified('post', 8), initialModified); + assert.notEqual(DS.lastSaved('post', 8), initialSaved); + + DS.updateAll('post', { age: 5 }, { where: { age: { '==': 31 } } }).then(function (ps) { + assert.deepEqual(angular.toJson(ps), angular.toJson(DS.filter('post', { where: { age: { '==': 5 } } }))); + assert.deepEqual(angular.toJson(ps[0]), angular.toJson({ author: 'Jane', age: 5, id: 6 })); + }, function (err) { + console.error(err.stack); + fail('should not have rejected'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + + setTimeout(function () { + try { + assert.equal(lifecycle.beforeInject.callCount, 4, 'beforeInject should have been called'); + assert.equal(lifecycle.afterInject.callCount, 4, 'afterInject should have been called'); + assert.equal(lifecycle.serialize.callCount, 2, 'serialize should have been called'); + assert.equal(lifecycle.deserialize.callCount, 2, 'deserialize should have been called'); + + done(); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + } catch (e) { + done(e); + } + }); + } catch (e) { + done(e); + } + }, 30); + }); + it('should handle nested resources', function (done) { + var testComment = { + id: 5, + content: 'stuff', + approvedBy: 4 + }; + var testComment2 = { + id: 6, + content: 'stuff', + approvedBy: 4 + }; + $httpBackend.expectPUT('http://test.angular-cache.com/user/4/comment?content=test').respond(200, [testComment, testComment2]); + + DS.inject('comment', testComment); + + DS.updateAll('comment', { + content: 'stuff' + }, { + content: 'test' + }, { + params: { + approvedBy: 4 + } + }).then(function (comments) { + assert.deepEqual(angular.toJson(comments), angular.toJson([testComment, testComment2])); + assert.deepEqual(angular.toJson(comments), angular.toJson(DS.filter('comment', { + content: 'stuff' + }))); + done(); + }, function () { + done('Should not have failed!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + } catch (e) { + done(e); + } + }, 30); + }); + it('should handle nested resources 2', function (done) { + var testComment = { + id: 5, + content: 'stuff', + approvedBy: 4 + }; + var testComment2 = { + id: 6, + content: 'stuff', + approvedBy: 4 + }; + + $httpBackend.expectPUT('http://test.angular-cache.com/comment?content=test').respond(200, [testComment, testComment2]); + + DS.inject('comment', testComment2); + + DS.updateAll('comment', { + content: 'stuff' + }, { + content: 'test' + }).then(function (comments) { + assert.deepEqual(angular.toJson(comments), angular.toJson([testComment, testComment2])); + assert.deepEqual(angular.toJson(comments), angular.toJson(DS.filter('comment', { + content: 'stuff', + sort: 'id' + }))); + done(); + }, function () { + done('Should not have failed!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + } catch (e) { + done(e); + } + }, 30); + }); + it('should handle nested resources 3', function (done) { + var testComment = { + id: 5, + content: 'stuff', + approvedBy: 4 + }; + var testComment2 = { + id: 6, + content: 'stuff', + approvedBy: 4 + }; + + $httpBackend.expectPUT('http://test.angular-cache.com/comment?content=test').respond(200, [testComment, testComment2]); + + DS.inject('comment', testComment2); + + DS.updateAll('comment', { + content: 'stuff' + }, { + content: 'test' + }, { + params: { + approvedBy: false + } + }).then(function (comments) { + assert.deepEqual(angular.toJson(comments), angular.toJson([testComment, testComment2])); + assert.deepEqual(angular.toJson(comments), angular.toJson(DS.filter('comment', { + content: 'stuff', + sort: 'id' + }))); + done(); + }, function () { + done('Should not have failed!'); + }); + + setTimeout(function () { + try { + $httpBackend.flush(); + } catch (e) { + done(e); + } + }, 30); + }); +}); diff --git a/test/datastore/sync_methods/bindAll.test.js b/test/datastore/sync_methods/bindAll.test.js new file mode 100644 index 0000000..aea4692 --- /dev/null +++ b/test/datastore/sync_methods/bindAll.test.js @@ -0,0 +1,93 @@ +describe('DS.bindAll', function () { + var $rootScope, $scope; + + beforeEach(startInjector); + + beforeEach(function () { + inject(function (_$rootScope_) { + $rootScope = _$rootScope_; + $scope = $rootScope.$new(); + }); + }); + + it('should throw an error when method pre-conditions are not met', function () { + assert.throws(function () { + DS.bindAll('does not exist'); + }, DS.errors.NonexistentResourceError, 'does not exist is not a registered resource!'); + + angular.forEach(TYPES_EXCEPT_OBJECT, function (key) { + if (key) { + assert.throws(function () { + DS.bindAll('post', key); + }, DS.errors.IllegalArgumentError, '"params" must be an object!'); + } + }); + + angular.forEach(TYPES_EXCEPT_OBJECT, function (key) { + assert.throws(function () { + DS.bindAll('post', {}, key); + }, DS.errors.IllegalArgumentError, '"scope" must be an object!'); + }); + + angular.forEach(TYPES_EXCEPT_STRING, function (key) { + assert.throws(function () { + DS.bindAll('post', {}, $scope, key); + }, DS.errors.IllegalArgumentError, '"expr" must be a string!'); + }); + }); + it('should bind an item in the data store to the scope', function () { + + DS.inject('post', p1); + DS.inject('post', p2); + DS.inject('post', p3); + DS.inject('post', p4); + DS.inject('post', p5); + + DS.bindAll('post', { + where: { + age: { + '>': 31 + } + } + }, $scope, 'posts'); + + $rootScope.$apply(); + + assert.deepEqual(angular.toJson($scope.posts), angular.toJson([p3, p4, p5])); + + DS.eject('post', 8); + + $rootScope.$apply(); + + assert.deepEqual(angular.toJson($scope.posts), angular.toJson([p3, p5])); + }); + it('should execute a callback if given', function () { + + var cb = sinon.spy(); + DS.inject('post', p1); + DS.inject('post', p2); + DS.inject('post', p3); + DS.inject('post', p4); + DS.inject('post', p5); + + DS.bindAll('post', { + where: { + age: { + '>': 31 + } + } + }, $scope, 'posts', cb); + + $rootScope.$apply(); + + assert.deepEqual(angular.toJson($scope.posts), angular.toJson([p3, p4, p5])); + assert.equal(cb.callCount, 1); + + DS.eject('post', 8); + + $rootScope.$apply(function () { + assert.deepEqual(angular.toJson($scope.posts), angular.toJson([p3, p5])); + assert.equal(cb.callCount, 2); + }); + }); +}); diff --git a/test/datastore/sync_methods/bindOne.test.js b/test/datastore/sync_methods/bindOne.test.js new file mode 100644 index 0000000..42a9564 --- /dev/null +++ b/test/datastore/sync_methods/bindOne.test.js @@ -0,0 +1,93 @@ +describe('DS.bindOne', function () { + var $rootScope, $scope; + + beforeEach(startInjector); + + beforeEach(function () { + inject(function (_$rootScope_) { + $rootScope = _$rootScope_; + $scope = $rootScope.$new(); + }); + }); + + it('should throw an error when method pre-conditions are not met', function () { + assert.throws(function () { + DS.bindOne('does not exist'); + }, DS.errors.NonexistentResourceError, 'does not exist is not a registered resource!'); + + angular.forEach(TYPES_EXCEPT_STRING_OR_NUMBER, function (key) { + assert.throws(function () { + DS.bindOne('post', key); + }, DS.errors.IllegalArgumentError, '"id" must be a string or a number!'); + }); + + angular.forEach(TYPES_EXCEPT_OBJECT, function (key) { + assert.throws(function () { + DS.bindOne('post', 5, key); + }, DS.errors.IllegalArgumentError, '"scope" must be an object!'); + }); + + angular.forEach(TYPES_EXCEPT_STRING, function (key) { + assert.throws(function () { + DS.bindOne('post', 5, $scope, key); + }, DS.errors.IllegalArgumentError, '"expr" must be a string!'); + }); + }); + it('should bind an item in the data store to the scope', function () { + + DS.inject('post', p1); + DS.inject('post', p2); + + var post = DS.get('post', 5); + var post2 = DS.get('post', 6); + + DS.bindOne('post', 5, $scope, 'post'); + DS.bindOne('post', 6, $scope, 'other.post'); + DS.bindOne('post', 7, $scope, 'post3'); + + $rootScope.$apply(); + + assert.deepEqual($scope.post, post); + assert.deepEqual($scope.other.post, post2); + assert.isUndefined($scope.post2); + + post.author = 'Jason'; + + $rootScope.$apply(); + + assert.equal($scope.post.author, 'Jason'); + assert.deepEqual($scope.post, post); + assert.deepEqual($scope.other.post, post2); + assert.isUndefined($scope.post2); + }); + it('should execute a callback if given', function (done) { + + var Post = DS.definitions.post; + var cb = sinon.spy(); + Post.inject(p1); + + var post = Post.get(5); + + Post.bindOne(5, $scope, 'post', cb); + + $rootScope.$apply(); + + assert.equal(cb.callCount, 1); + assert.deepEqual($scope.post, post); + + post.author = 'Jason'; + + DS.digest(); + $rootScope.$apply(); + + setTimeout(function () { + $rootScope.$apply(); + + assert.equal(cb.callCount, 2); + assert.equal($scope.post.author, 'Jason'); + assert.deepEqual($scope.post, post); + + done(); + }, 50); + }); +});