UNPKG

pouchdb-live-find

Version:

Live PouchDB queries that update automatically as changes come in!

497 lines (469 loc) 15.3 kB
'use strict'; /* jshint mocha: true */ var expect, PouchDB, memdown; if(typeof window === 'undefined') { expect = require('chai').expect; PouchDB = require('pouchdb'); memdown = require('memdown'); PouchDB.plugin(require('pouchdb-find')); PouchDB.plugin(require('../lib/index')); } else { expect = window.chai.expect; expect = window.chai.expect; PouchDB = window.PouchDB; } var UpdateWatcher = require('./watcher'); var smashers1 = [ { name: 'Mario', _id: 'mario', series: 'Mario', debut: 1981 }, { name: 'Jigglypuff', _id: 'puff', series: 'Pokemon', debut: 1996 }, { name: 'Link', _id: 'link', series: 'Zelda', debut: 1986 }, { name: 'Donkey Kong', _id: 'dk', series: 'Mario', debut: 1981 }, { name: 'Pikachu', series: 'Pokemon', _id: 'pikachu', debut: 1996 }, { name: 'Captain Falcon', _id: 'falcon', series: 'F-Zero', debut: 1990 } ]; var smashers2 = [ { name: 'Luigi', _id: 'luigi', series: 'Mario', debut: 1983 }, { name: 'Fox', _id: 'fox', series: 'Star Fox', debut: 1993 }, { name: 'Ness', _id: 'ness', series: 'Earthbound', debut: 1994 }, { name: 'Samus', _id: 'samus', series: 'Metroid', debut: 1986 }, { name: 'Yoshi', _id: 'yoshi', series: 'Mario', debut: 1990 }, { name: 'Kirby', _id: 'kirby', series: 'Kirby', debut: 1992 } ]; describe('PouchDB LiveFind', function() { var previous; var db = createDB('liveFindTest'); var feed1, feed2, feed3, feed4, feed5, feed6, feed7; var watcher1, watcher2, watcher3, watcher4, watcher5, watcher6, watcher7; before(function() { previous = db.createIndex({ index: {fields: ['series', 'debut']} }); return previous; }); after(function() { return db.destroy(); }); it('should perform the initial query', function() { var ready = false; function waitForReady(feed) { feed.once('ready', function() { ready = true; expect(watcher1.updateCount).to.equal(2); }); } return previous .then(function() { return db.bulkDocs(smashers1); }) .then(function () { feed1 = db.liveFind({ selector: {series: 'Mario'}, sort: [{series: 'desc'}, {debut: 'desc'}] }); waitForReady(feed1); watcher1 = new UpdateWatcher(feed1); expect(db._activeQueries).to.equal(1); expect(db._changeListener.listenerCount('change')).to.equal(1); return watcher1.awaitUpdates(['ADD:mario', 'ADD:dk']); }) .then(function() { // Test that the ready event fired after initial query expect(ready).to.equal(true); }); }); it('the query should update as matching docs are added', function() { return previous .then(function() { watcher1.reset(); return Promise.all([ watcher1.awaitUpdates(['ADD:luigi', 'ADD:yoshi']), db.bulkDocs(smashers2) ]); }) .then(function() { expect(watcher1.updateCount).to.equal(2); }) .then(function() { return Promise.all( [ watcher1.awaitUpdates(['ADD:wario']), db.put({ name: 'Wario', _id: 'wario', series: 'Mario', debut: 1992 }) ]); }) .then(function() { expect(watcher1.updateCount).to.equal(3); }); }); it('the query should not update if an added doc does not match', function() { return previous .then(function() { watcher1.reset(); return db.put({ name: 'Mega Man', _id: 'megaman', series: 'Mega Man', debut: 1987 }); }) .then(function() { expect(watcher1.updateCount).to.equal(0); }); }); it('should remove an updated doc that no longer matches', function() { return previous .then(function() { expect(watcher1.updateCount).to.equal(0); watcher1.reset(); return db.get('dk'); }) .then(function(doc) { doc.series = 'Donkey Kong'; return Promise.all([ watcher1.awaitUpdates(['REMOVE:dk']), db.put(doc) ]); }) .then(function() { expect(watcher1.updateCount).to.equal(1); }); }); it('should update when a matching doc is changed', function() { return previous .then(function() { watcher1.reset(); return db.get('mario'); }) .then(function(doc) { doc.debut = 1980; return Promise.all([ watcher1.awaitUpdates(['UPDATE:mario']), db.put(doc) ]); }) .then(function() { expect(watcher1.updateCount).to.equal(1); }); }); it('should remove a matching doc that is deleted', function() { return previous .then(function() { watcher1.reset(); return db.get('yoshi'); }) .then(function(doc) { return Promise.all([ watcher1.awaitUpdates(['REMOVE:yoshi']), db.remove(doc) ]); }) .then(function() { expect(watcher1.updateCount).to.equal(1); }); }); it('should add a second query', function() { return previous .then(function() { watcher1.reset(); feed2 = db.liveFind({ selector: {series: {$eq: 'Star Fox'}} }); watcher2 = new UpdateWatcher(feed2); expect(db._activeQueries).to.equal(2); expect(db._changeListener.listenerCount('change')).to.equal(2); return watcher2.awaitUpdates(['ADD:fox']); }) .then(function() { expect(watcher1.updateCount).to.equal(0); expect(watcher2.updateCount).to.equal(1); return Promise.all( [ watcher2.awaitUpdates(['ADD:slippy']), db.put({ name: 'Slippy Toad', _id: 'slippy', series: 'Star Fox', debut: 1990 }) ]); }) .then(function() { expect(watcher1.updateCount).to.equal(0); expect(watcher2.updateCount).to.equal(2); }); }); it('should properly cancel the listeners', function() { var cancelled = false; // Test that the 'cancelled' event is properly fired function waitForCancel(feed) { feed.once('cancelled', function() { cancelled = true; }); } return previous .then(function() { watcher1.reset(); watcher2.reset(); var listeners = db._changeListener.listeners('change'); expect(listeners.length).to.equal(2); expect(listeners[0]).to.not.equal(listeners[1]); waitForCancel(feed1); feed1.cancel(); expect(cancelled).to.equal(true); expect(db._activeQueries).to.equal(1); expect(db._changeListener.listenerCount('change')).to.equal(1); return Promise.all( [ watcher2.awaitUpdates(['ADD:peppy']), db.put({ name: 'Peppy', _id: 'peppy', series: 'Star Fox', debut: 1990 }), db.put({ name: 'Peach', _id: 'peach', series: 'Mario', debut: 1980 }) ]); }) .then(function() { expect(watcher1.updateCount).to.equal(0); expect(watcher2.updateCount).to.equal(1); feed2.cancel(); expect(db._activeQueries).to.equal(0); expect(db._changeListener).to.be.an('undefined'); }); }); it('should return only selected fields', function() { return previous .then(function() { feed3 = db.liveFind({ selector: {series: {$eq: 'Kirby'}}, fields: ['name'] }); watcher3 = new UpdateWatcher(feed3); expect(db._activeQueries).to.equal(1); expect(db._changeListener.listenerCount('change')).to.equal(1); return watcher3.passNextUpdate(); }) .then(function(info) { var doc = info.doc; expect(info.id).to.equal('kirby'); expect(doc._id).to.be.an('undefined'); expect(doc.name).to.be.a('string'); expect(doc.series).to.be.an('undefined'); expect(doc._rev).to.be.an('undefined'); expect(watcher3.updateCount).to.equal(1); return Promise.all([ watcher3.passNextUpdate(), db.put({ name: 'King Dedede', _id: 'dedede', series: 'Kirby', debut: 1992 }) ]); }) .then(function(result) { var doc = result[0].doc; expect(result[0].id).to.equal('dedede'); expect(doc._id).to.be.an('undefined'); expect(doc.name).to.be.a('string'); expect(doc.series).to.be.an('undefined'); expect(watcher3.updateCount).to.equal(2); feed3.cancel(); expect(db._activeQueries).to.equal(0); expect(db._changeListener).to.be.an('undefined'); }); }); it('should create an aggregate list and sort it', function() { return previous .then(function() { return db.destroy(); }) .then(function() { db = createDB('liveFindTest'); return db.createIndex({ index: {fields: ['series', 'name']} }); }) .then(function() { return db.bulkDocs(smashers1); }) .then(function () { feed4 = db.liveFind({ selector: {series: 'Mario'}, sort: [{series: 'desc'}, {name: 'desc'}], aggregate: true }); watcher4 = new UpdateWatcher(feed4); return watcher4.fetchListUpdates(2); }) .then(function(aggregates) { expect(aggregates[1]).to.deep.equal(['mario', 'dk']); return Promise.all([ watcher4.fetchListUpdates(2), db.bulkDocs(smashers2) ]); }) .then(function(result) { // Testing add to list var aggregates = result[0]; expect(aggregates).to.deep.equal([ [ 'mario', 'luigi', 'dk' ], [ 'yoshi', 'mario', 'luigi', 'dk' ] ] ); return db.get('luigi'); }) .then(function(luigi) { // Testing remove return Promise.all([ watcher4.fetchListUpdates(1), db.remove(luigi) ]); }) .then(function(result) { var aggregates = result[0]; expect(aggregates).to.deep.equal([ [ 'yoshi', 'mario', 'dk' ] ]); return db.get('yoshi'); }) .then(function(yoshi) { // Testing update yoshi.name = 'AYoshi'; return Promise.all([ watcher4.fetchListUpdates(1), db.put(yoshi) ]); }) .then(function(result) { var aggregates = result[0]; expect(aggregates).to.deep.equal([ [ 'mario', 'dk', 'yoshi' ] ]); feed4.cancel(); }); }); it('should aggregate a list with no sort', function() { return previous .then(function () { feed5 = db.liveFind({ selector: {series: 'Kirby'}, aggregate: true }); watcher5 = new UpdateWatcher(feed5); return watcher5.fetchListUpdates(1); }) .then(function(aggregates) { expect(aggregates).to.deep.equal([ ['kirby'] ]); return Promise.all([ watcher5.fetchListUpdates(1), db.put({ name: 'King Dedede', _id: 'dedede', series: 'Kirby', debut: 1992 }) ]); }) .then(function(result) { // Testing add to list var aggregates = result[0]; expect(aggregates).to.deep.equal([ [ 'kirby', 'dedede' ] ]); feed5.cancel(); }); }); it('should sort and apply skip and limit', function() { return previous .then(function() { return db.destroy(); }) .then(function() { db = createDB('liveFindTest'); return db.createIndex({ index: {fields: ['series', 'name']} }); }) .then(function() { return db.bulkDocs(smashers1); }) .then(function () { feed6 = db.liveFind({ selector: {series: {$gt: null}}, sort: [{series: 'desc'}, {name: 'desc'}], aggregate: true, skip: 2, limit: 3 }); watcher6 = new UpdateWatcher(feed6); return watcher6.fetchListUpdates(6); }) .then(function(aggregates) { expect(aggregates[5]).to.deep.equal(['puff', 'mario', 'dk']); }); }); it('should have a working custom sort function', function() { return previous .then(function() { var sorted = feed6.sort(smashers1).map(mapById); expect(sorted).to.deep.equal([ 'link', 'pikachu', 'puff', 'mario', 'dk', 'falcon' ]); }); }); it('paginate should set a different sort, skip and limit', function() { return previous .then(function() { var result1 = feed6.paginate({ sort: [{series: 'desc'}, {name: 'desc'}], skip: 0, limit: 0 }).map(mapById); expect(result1).to.deep.equal(['link', 'pikachu', 'puff', 'mario', 'dk', 'falcon']); var result2 = feed6.paginate({ sort: [{series: 'asc'}, {name: 'asc'}], skip: 0, limit: 0 }).map(mapById); expect(result2).to.deep.equal(['falcon', 'dk', 'mario', 'puff', 'pikachu', 'link']); var result3 = feed6.paginate({ skip: 2, limit: 3 }).map(mapById); expect(result3).to.deep.equal(['mario', 'puff', 'pikachu']); // Now we'll make sure that updates keep the new pagination return Promise.all( [ watcher6.fetchListUpdates(1), db.put({ name: 'Bulbasaur', _id: 'bulb', series: 'Pokemon', debut: 1996 }) ]); }) .then(function(results) { var update = results[0][0]; expect(update).to.deep.equal(['mario', 'bulb', 'puff']); feed6.cancel(); }); }); it('should sort fields with mixed directions', function() { var sortDocs = [ {_id: 'apple', letter: 'a', word: 'Apple'}, {_id: 'axel', letter: 'a', word: 'Axel'}, {_id: 'abby', letter: 'a', word: 'Abby'}, {_id: 'bat', letter: 'b', word: 'Bat'}, {_id: 'bone', letter: 'b', word: 'Bone'}, {_id: 'better', letter: 'b', word: 'Better'} ]; return previous .then(function() { return db.destroy(); }) .then(function() { db = createDB('liveFindTest'); return db.createIndex({ index: {fields: ['letter', 'word']} }); }) .then(function() { return db.bulkDocs(sortDocs); }) .then(function() { feed7 = db.liveFind({ selector: {letter: {$gt: null}}, sort: [{letter: 'desc'}, {word: 'asc'}], aggregate: true }); watcher7 = new UpdateWatcher(feed7); return watcher7.fetchListUpdates(6); }) .then(function(results) { expect(results[5]).to.deep.equal(['bat', 'better', 'bone', 'abby', 'apple', 'axel']); feed7.cancel(); }); }); it('should throw an error if pouchdb-find is not loaded', function() { return previous .then(function() { db._oldFind = db.find; db.find = null; var fn = function() { db.liveFind({ selector: {series: 'Mario'}, sort: [{series: 'desc'}, {name: 'desc'}] }); }; expect(fn).to.throw(/requirement/); }); }); }); function createDB(name) { if(typeof window === 'undefined') { return new PouchDB(name, {db: memdown}); } return new PouchDB(name, {adapter: 'memory'}); } function mapById(item) { return item._id; }