From 5464b564b287d42d03a9ae3bf72998712713d95e Mon Sep 17 00:00:00 2001 From: Rhett Sutphin Date: Sun, 7 Jun 2015 22:55:59 -0500 Subject: [PATCH 1/3] Basic CRUD integration tests. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Happy path only. Verifies only that records in PouchDB can be read into / written from ember-data models. Includes manually downloading PhantomJS 2.0 for Travis — the integration tests don't work with PhantomJS 1.9. --- .travis.yml | 11 +- tests/dummy/app/adapters/application.js | 13 ++ tests/dummy/app/models/taco-soup.js | 7 + tests/integration/adapters/pouch-test.js | 165 +++++++++++++++++++++++ 4 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 tests/dummy/app/adapters/application.js create mode 100644 tests/dummy/app/models/taco-soup.js create mode 100644 tests/integration/adapters/pouch-test.js diff --git a/.travis.yml b/.travis.yml index 8197d31..be48ce6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,16 @@ matrix: - env: EMBER_TRY_SCENARIO=ember-canary before_install: - - export PATH=/usr/local/phantomjs-2.0.0/bin:$PATH + # After travis-ci/travis-ci#3225 is resolved, restore this and remove the + # manual download/install of PhantomJS 2.0. + # - export PATH=/usr/local/phantomjs-2.0.0/bin:$PATH + - mkdir 'phantomjs-2.0.0' + - cd 'phantomjs-2.0.0' + - curl -O 'https://s3.amazonaws.com/travis-phantomjs/phantomjs-2.0.0-ubuntu-12.04.tar.bz2' + - tar xjvf 'phantomjs-2.0.0-ubuntu-12.04.tar.bz2' + - cd - + - export PATH=./phantomjs-2.0.0:$PATH + - "npm config set spin false" - "npm install -g npm@^2" diff --git a/tests/dummy/app/adapters/application.js b/tests/dummy/app/adapters/application.js new file mode 100644 index 0000000..b087629 --- /dev/null +++ b/tests/dummy/app/adapters/application.js @@ -0,0 +1,13 @@ +import { Adapter } from 'ember-pouch/index'; +import PouchDB from 'pouchdb'; + +function createDb() { + return new PouchDB('ember-pouch-test'); +} + +export default Adapter.extend({ + init() { + this._super(...arguments); + this.set('db', createDb()); + } +}); diff --git a/tests/dummy/app/models/taco-soup.js b/tests/dummy/app/models/taco-soup.js new file mode 100644 index 0000000..bdf5ac0 --- /dev/null +++ b/tests/dummy/app/models/taco-soup.js @@ -0,0 +1,7 @@ +import DS from 'ember-data'; + +export default DS.Model.extend({ + rev: DS.attr('string'), + + flavor: DS.attr('string') +}); diff --git a/tests/integration/adapters/pouch-test.js b/tests/integration/adapters/pouch-test.js new file mode 100644 index 0000000..7393689 --- /dev/null +++ b/tests/integration/adapters/pouch-test.js @@ -0,0 +1,165 @@ +import { module, test } from 'qunit'; +import startApp from '../../helpers/start-app'; + +import Ember from 'ember'; +/* globals PouchDB */ + +var App; + +/* + * Tests basic CRUD behavior for an app using the ember-pouch adapter. + */ + +module('adapter:pouch [integration]', { + beforeEach: function (assert) { + var done = assert.async(); + + // TODO: do this in a way that doesn't require duplicating the name of the + // test database here and in dummy/app/adapters/application.js. Importing + // the adapter directly doesn't work because of what seems like a resolver + // issue. + (new PouchDB('ember-pouch-test')).destroy().then(() => { + App = startApp(); + done(); + }); + }, + + afterEach: function (assert) { + Ember.run(App, 'destroy'); + } +}); + +function db() { + return adapter().get('db'); +} + +function adapter() { + // the default adapter in the dummy app is an ember-pouch adapter + return App.__container__.lookup('adapter:application'); +} + +function store() { + return App.__container__.lookup('store:main'); +} + +test('can find all', function (assert) { + assert.expect(3); + + var done = assert.async(); + Ember.RSVP.Promise.resolve().then(() => { + return db().bulkDocs([ + { _id: 'tacoSoup_2_A', data: { flavor: 'al pastor' } }, + { _id: 'tacoSoup_2_B', data: { flavor: 'black bean' } }, + { _id: 'burritoShake_2_X', data: { consistency: 'smooth' } } + ]); + }).then(() => { + return store().find('taco-soup'); + }).then((found) => { + assert.equal(found.get('length'), 2, 'should have found the two taco soup items only'); + assert.deepEqual(found.mapBy('id'), ['A', 'B'], + 'should have extracted the IDs correctly'); + assert.deepEqual(found.mapBy('flavor'), ['al pastor', 'black bean'], + 'should have extracted the attributes also'); + done(); + }).catch((error) => { + console.error('error in test', error); + assert.ok(false, 'error in test:' + error); + done(); + }); +}); + +test('can find one', function (assert) { + assert.expect(2); + + var done = assert.async(); + Ember.RSVP.Promise.resolve().then(() => { + return db().bulkDocs([ + { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor' } }, + { _id: 'tacoSoup_2_D', data: { flavor: 'black bean' } }, + ]); + }).then(() => { + return store().find('taco-soup', 'D'); + }).then((found) => { + assert.equal(found.get('id'), 'D', + 'should have found the requested item'); + assert.deepEqual(found.get('flavor'), 'black bean', + 'should have extracted the attributes also'); + done(); + }).catch((error) => { + console.error('error in test', error); + assert.ok(false, 'error in test:' + error); + done(); + }); +}); + +test('create a new record', function (assert) { + assert.expect(1); + + var done = assert.async(); + Ember.RSVP.Promise.resolve().then(() => { + var newSoup = store().createRecord('taco-soup', { id: 'E', flavor: 'balsamic' }); + return newSoup.save(); + }).then((saved) => { + return db().get('tacoSoup_2_E'); + }).then((newDoc) => { + assert.equal(newDoc.data.flavor, 'balsamic', 'should have saved the attribute'); + done(); + }).catch((error) => { + console.error('error in test', error); + assert.ok(false, 'error in test:' + error); + done(); + }); +}); + +test('update an existing record', function (assert) { + assert.expect(1); + + var done = assert.async(); + Ember.RSVP.Promise.resolve().then(() => { + return db().bulkDocs([ + { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor' } }, + { _id: 'tacoSoup_2_D', data: { flavor: 'black bean' } }, + ]); + }).then(() => { + return store().find('taco-soup', 'C'); + }).then((found) => { + found.set('flavor', 'pork'); + return found.save(); + }).then((saved) => { + return db().get('tacoSoup_2_C'); + }).then((updatedDoc) => { + assert.equal(updatedDoc.data.flavor, 'pork', 'should have updated the attribute'); + done(); + }).catch((error) => { + console.error('error in test', error); + assert.ok(false, 'error in test:' + error); + done(); + }); +}); + +test('delete an existing record', function (assert) { + assert.expect(1); + + var done = assert.async(); + Ember.RSVP.Promise.resolve().then(() => { + return db().bulkDocs([ + { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor' } }, + { _id: 'tacoSoup_2_D', data: { flavor: 'black bean' } }, + ]); + }).then(() => { + return store().find('taco-soup', 'C'); + }).then((found) => { + return found.destroyRecord(); + }).then(() => { + return db().get('tacoSoup_2_C'); + }).then((doc) => { + assert.ok(!doc, 'document should no longer exist'); + }, (result) => { + assert.equal(result.status, 404, 'document should no longer exist'); + done(); + }).catch((error) => { + console.error('error in test', error); + assert.ok(false, 'error in test:' + error); + done(); + }); +}); From 8aa35df2cbb50456b3112e0fb82623853123c9f8 Mon Sep 17 00:00:00 2001 From: Rhett Sutphin Date: Sun, 7 Jun 2015 23:28:33 -0500 Subject: [PATCH 2/3] Use modelName instead of typeKey, maintaining compat. This removes scads of deprecation warnings with ember-data 1.0-beta18 and later while preserving access to data created with current versions of ember-pouch. See nolanlawson/ember-pouch#63 for a discussion. --- addon/adapters/pouch.js | 58 +++++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/addon/adapters/pouch.js b/addon/adapters/pouch.js index c938eaf..737dc69 100644 --- a/addon/adapters/pouch.js +++ b/addon/adapters/pouch.js @@ -12,6 +12,7 @@ const { on, String: { pluralize, + camelize, classify } } = Ember; @@ -72,21 +73,22 @@ export default DS.RESTAdapter.extend({ }, _init: function (type) { - var self = this; + var self = this, + recordTypeName = this.getRecordTypeName(type); if (!this.db || typeof this.db !== 'object') { throw new Error('Please set the `db` property on the adapter.'); } if (!Ember.get(type, 'attributes').has('rev')) { - var modelName = classify(type.typeKey); + var modelName = classify(recordTypeName); throw new Error('Please add a `rev` attribute of type `string`' + ' on the ' + modelName + ' model.'); } this._schema = this._schema || []; - var singular = type.typeKey; - var plural = pluralize(type.typeKey); + var singular = recordTypeName; + var plural = pluralize(recordTypeName); // check that we haven't already registered this model for (var i = 0, len = this._schema.length; i < len; i++) { @@ -131,7 +133,8 @@ export default DS.RESTAdapter.extend({ _recordToData: function (store, type, record) { var data = {}; - var serializer = store.serializerFor(type.typeKey); + var recordTypeName = this.getRecordTypeName(type); + var serializer = store.serializerFor(recordTypeName); var recordToStore = record; // In Ember-Data beta.15, we need to take a snapshot. See issue #45. @@ -149,7 +152,7 @@ export default DS.RESTAdapter.extend({ {includeId: true} ); - data = data[type.typeKey]; + data = data[recordTypeName]; // ember sets it to null automatically. don't need it. if (data.rev === null) { @@ -159,15 +162,37 @@ export default DS.RESTAdapter.extend({ return data; }, + /** + * Returns the string to use for the model name part of the PouchDB document + * ID for records of the given ember-data type. + * + * This method uses the camelized version of the model name in order to + * preserve data compatibility with older versions of ember-pouch. See + * nolanlawson/ember-pouch#63 for a discussion. + * + * You can override this to change the behavior. If you do, be aware that you + * need to execute a data migration to ensure that any existing records are + * moved to the new IDs. + */ + getRecordTypeName(type) { + if (type.modelName) { + return camelize(type.modelName); + } else { + // This branch can be removed when the library drops support for + // ember-data 1.0-beta17 and earlier. + return type.typeKey; + } + }, + findAll: function(store, type /*, sinceToken */) { // TODO: use sinceToken this._init(type); - return this.db.rel.find(type.typeKey); + return this.db.rel.find(this.getRecordTypeName(type)); }, findMany: function(store, type, ids) { this._init(type); - return this.db.rel.find(type.typeKey, ids); + return this.db.rel.find(this.getRecordTypeName(type), ids); }, findQuery: function(/* store, type, query */) { @@ -178,18 +203,19 @@ export default DS.RESTAdapter.extend({ find: function (store, type, id) { this._init(type); - return this.db.rel.find(type.typeKey, id).then(function (payload) { + var recordTypeName = this.getRecordTypeName(type); + return this.db.rel.find(recordTypeName, id).then(function (payload) { // Ember Data chokes on empty payload, this function throws // an error when the requested data is not found if (typeof payload === 'object' && payload !== null) { - var singular = type.typeKey; - var plural = pluralize(type.typeKey); + var singular = recordTypeName; + var plural = pluralize(recordTypeName); var results = payload[singular] || payload[plural]; if (results && results.length > 0) { return payload; } } - throw new Error('Not found: type "' + type.typeKey + + throw new Error('Not found: type "' + recordTypeName + '" with id "' + id + '"'); }); }, @@ -197,19 +223,19 @@ export default DS.RESTAdapter.extend({ createRecord: function(store, type, record) { this._init(type); var data = this._recordToData(store, type, record); - return this.db.rel.save(type.typeKey, data); + return this.db.rel.save(this.getRecordTypeName(type), data); }, updateRecord: function (store, type, record) { this._init(type); var data = this._recordToData(store, type, record); - return this.db.rel.save(type.typeKey, data); + return this.db.rel.save(this.getRecordTypeName(type), data); }, deleteRecord: function (store, type, record) { this._init(type); var data = this._recordToData(store, type, record); - return this.db.rel.del(type.typeKey, data) + return this.db.rel.del(this.getRecordTypeName(type), data) .then(extractDeleteRecord); } -}); \ No newline at end of file +}); From 4b33181511804396247d5b8c243693ea9ce22985 Mon Sep 17 00:00:00 2001 From: Rhett Sutphin Date: Mon, 8 Jun 2015 09:34:03 -0500 Subject: [PATCH 3/3] Serializer keys are conceptually distinct from recordTypeName. --- addon/adapters/pouch.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/addon/adapters/pouch.js b/addon/adapters/pouch.js index 737dc69..5f064eb 100644 --- a/addon/adapters/pouch.js +++ b/addon/adapters/pouch.js @@ -133,8 +133,12 @@ export default DS.RESTAdapter.extend({ _recordToData: function (store, type, record) { var data = {}; - var recordTypeName = this.getRecordTypeName(type); - var serializer = store.serializerFor(recordTypeName); + // Though it would work to use the default recordTypeName for modelName & + // serializerKey here, these uses are conceptually distinct and may vary + // independently. + var modelName = type.modelName || type.typeKey; + var serializerKey = camelize(modelName); + var serializer = store.serializerFor(modelName); var recordToStore = record; // In Ember-Data beta.15, we need to take a snapshot. See issue #45. @@ -152,7 +156,7 @@ export default DS.RESTAdapter.extend({ {includeId: true} ); - data = data[recordTypeName]; + data = data[serializerKey]; // ember sets it to null automatically. don't need it. if (data.rev === null) {