diff --git a/src/firestore-query-snapshot.js b/src/firestore-query-snapshot.js index 597d8226..78bf3426 100644 --- a/src/firestore-query-snapshot.js +++ b/src/firestore-query-snapshot.js @@ -3,7 +3,8 @@ var _ = require('./lodash'); var DocumentSnapshot = require('./firestore-document-snapshot'); -function MockFirestoreQuerySnapshot (ref, data, keyOrder) { +function MockFirestoreQuerySnapshot (ref, data, keyOrder, changes = {}) { + this.changes = changes; this._ref = ref; this.data = _.cloneDeep(data) || {}; this.keyOrder = keyOrder; @@ -25,4 +26,30 @@ MockFirestoreQuerySnapshot.prototype.forEach = function (callback, context) { }); }; + +MockFirestoreQuerySnapshot.prototype.docChanges = function () { + var {added = {}, removed = {}, modified = {}} = this.changes || {}; + const result = []; + + _.forEach(added, addChange(result, 'added')); + _.forEach(modified, addChange(result, 'modified')); + _.forEach(removed, addChange(result, 'removed')); + + return result; +}; + +function addChange (result, changeType) { + return function(value, key) { + result.push({ + type: changeType, + doc: { + id: key, + data: function() { + return value; + } + } + }); + }; +} + module.exports = MockFirestoreQuerySnapshot; diff --git a/src/firestore-query.js b/src/firestore-query.js index 527cbeee..916b6910 100644 --- a/src/firestore-query.js +++ b/src/firestore-query.js @@ -160,7 +160,9 @@ MockFirestoreQuery.prototype.onSnapshot = function (optionsOrObserverOrOnNext, o onError = onErrorArg; } var context = { - data: self._results(), + data: { + results: {} + }, }; var onSnapshot = function () { // compare the current state to the one from when this function was created @@ -168,8 +170,31 @@ MockFirestoreQuery.prototype.onSnapshot = function (optionsOrObserverOrOnNext, o if (err === null) { self.get().then(function (querySnapshot) { var results = self._results(); - if (JSON.stringify(results) !== JSON.stringify(context.data) || includeMetadataChanges) { - onNext(new QuerySnapshot(self.parent === null ? self : self.parent.collection(self.id), results.results, results.keyOrder)); + + var added = {}; + var removed = {}; + var modified = {}; + + _.forEach(results.results, function(nextValue, nextKey) { + if(Object.keys(context.data.results || {}).indexOf(nextKey) === -1) { + added[nextKey] = nextValue; + } else if (!_.isEqual(context.data.results[nextKey], nextValue)) { + modified[nextKey] = nextValue; + } + }); + + _.forEach(context.data.results, function(value, key) { + if (Object.keys(results.results).indexOf(key) === -1) { + removed[key] = value; + } + }); + + var hasAdditions = Object.keys(added).length > 0; + var hasRemovals = Object.keys(removed).length > 0; + var hasModififations = Object.keys(modified).length > 0; + + if (hasAdditions || hasRemovals || hasModififations || includeMetadataChanges) { + onNext(new QuerySnapshot(self.parent === null ? self : self.parent.collection(self.id), results.results, results.keyOrder, {added, removed, modified})); // onNext(new QuerySnapshot(self.id, self.ref, results)); context.data = results; } diff --git a/test/.jshintrc b/test/.jshintrc index 6fadeb8f..c687f97c 100644 --- a/test/.jshintrc +++ b/test/.jshintrc @@ -1,5 +1,5 @@ { "node": true, "mocha": true, - "esversion" : "6" + "esversion" : "8" } diff --git a/test/unit/firestore-collection.js b/test/unit/firestore-collection.js index 72ded957..6c8399b9 100644 --- a/test/unit/firestore-collection.js +++ b/test/unit/firestore-collection.js @@ -390,6 +390,97 @@ describe('MockFirestoreCollection', function () { collection.flush(); }); + it('returns value on first call', function(done) { + collection.doc('a').update({name: 'A'}, {setMerge: true}); + db.autoFlush(); + + collection.onSnapshot(function(snap) { + var names = []; + snap.docs.forEach(function(doc) { + names.push(doc.data().name); + }); + expect(names).to.contain('A'); + expect(names).not.to.contain('a'); + done(); + }); + }); + + it('(TODO) returns doc changes as "added" on first call', function (done) { + collection.onSnapshot(function(snap) { + var names = []; + var changes = snap.docChanges(); + // TODO! + expect(changes).to.have.length(6); + const allAdded = _.every(changes, function(change) { + return change.type === 'added'; + }); + expect(allAdded).to.equal(true); + done(); + }); + collection.doc('a').update({name: 'A'}, {setMerge: true}); + collection.flush(); + }); + + it('(TODO) returns doc changes as "added" on document addition', function (done) { + db.autoFlush(); + var call = 0; + collection.onSnapshot(function(snap) { + call += 1; + if (call === 1) { + collection.add({name: 'New'}); + } + if (call === 2) { + var changes = snap.docChanges(); + expect(changes).to.have.length(1); + expect(changes[0]).to.have.property('type', 'added'); + done(); + } + }); + + // collection.add({name: 'New'}); + // collection.flush(); + }); + + it('(TODO) returns doc changes as "modified" on document modification', function (done) { + db.autoFlush(); + var call = 0; + collection.onSnapshot(async function(snap) { + call += 1; + if (call === 1) { + await collection.doc('1').update({name: 'Modified'}); + } + if (call === 2) { + var changes = snap.docChanges(); + expect(changes).to.have.length(1); + expect(changes[0]).to.have.property('type', 'modified'); + done(); + } + }); + + // collection.add({name: 'New'}); + // collection.flush(); + }); + + it('(TODO) returns doc changes as "removed" on document deletion', function (done) { + db.autoFlush(); + var call = 0; + collection.onSnapshot(async function(snap) { + call += 1; + if (call === 1) { + await collection.doc('1').delete(); + } + if (call === 2) { + var changes = snap.docChanges(); + expect(changes).to.have.length(1); + expect(changes[0]).to.have.property('type', 'removed'); + done(); + } + }); + + // collection.add({name: 'New'}); + // collection.flush(); + }); + it('calls callback after multiple updates', function (done) { var callCount = 0; collection.onSnapshot(function(snap) {