Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
node_modules
coverage
browser
.vscode
132 changes: 66 additions & 66 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions src/firestore-document-snapshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@

var _ = require('./lodash');

function MockFirestoreDocumentSnapshot (id, ref, data) {
function MockFirestoreDocumentSnapshot(id, ref, data) {
this.id = id;
this.ref = ref;
this._snapshotdata = _.cloneDeep(data) || null;
this.data = function() {
return _.cloneDeep(this._snapshotdata);
};
this.exists = this._snapshotdata !== null;
this.metadata = {
fromCache: true,
hasPendingWrites: false
};
}

MockFirestoreDocumentSnapshot.prototype.get = function (path) {
MockFirestoreDocumentSnapshot.prototype.get = function(path) {
if (!path || !this.exists) return undefined;

var parts = path.split('.');
Expand Down
39 changes: 39 additions & 0 deletions src/firestore-document.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,45 @@ MockFirestoreDocument.prototype.delete = function (callback) {
});
};

MockFirestoreDocument.prototype.onSnapshot = function (optionsOrObserverOrOnNext, observerOrOnNextOrOnError, onErrorArg) {
var err = this._nextErr('onSnapshot');
var self = this;
var onNext = optionsOrObserverOrOnNext;
var onError = observerOrOnNextOrOnError;
var includeMetadataChanges = optionsOrObserverOrOnNext.includeMetadataChanges;

if (includeMetadataChanges) {
// Note this doesn't truly mimic the firestore metadata changes behavior, however
// since everything is syncronous, there isn't any difference in behavior.
onNext = observerOrOnNextOrOnError;
onError = onErrorArg;
}
var context = {
data: self._getData(),
};
var onSnapshot = function (forceTrigger) {
// compare the current state to the one from when this function was created
// and send the data to the callback if different.
if (err === null) {
if (!_.isEqual(self.data, context.data) || includeMetadataChanges || forceTrigger) {
onNext(new DocumentSnapshot(self.id, self.ref, self._getData()));
context.data = self._getData();
}
} else {
onError(err);
}
};

// onSnapshot should always return when initially called, then
// every time data changes.
onSnapshot(true);
var unsubscribe = this.queue.onPostFlush(onSnapshot);

// return the unsubscribe function
return unsubscribe;
};


/**
* Fetches the subcollections that are direct children of the document.
* @see https://cloud.google.com/nodejs/docs/reference/firestore/0.15.x/DocumentReference#getCollections
Expand Down
150 changes: 103 additions & 47 deletions src/firestore-query.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
'use strict';

var _ = require('./lodash');
var assert = require('assert');
var Stream = require('stream');
var Promise = require('rsvp').Promise;
var autoId = require('firebase-auto-ids');
var DocumentSnapshot = require('./firestore-document-snapshot');
var QuerySnapshot = require('./firestore-query-snapshot');
var DocumentSnapshot = require('./firestore-document-snapshot');
var Queue = require('./queue').Queue;
var utils = require('./utils');
var validate = require('./validators');

function MockFirestoreQuery(path, data, parent, name) {
this.errs = {};
Expand Down Expand Up @@ -69,51 +66,9 @@ MockFirestoreQuery.prototype.get = function () {
var self = this;
return new Promise(function (resolve, reject) {
self._defer('get', _.toArray(arguments), function () {
var results = {};
var limit = 0;
var atStart = false;
var atEnd = false;
var startFinder = this.buildStartFinder();

var inRange = function(data, key) {
if (atEnd) {
return false;
} else if (atStart) {
return true;
} else {
atStart = startFinder(data, key);
return atStart;
}
};

var results = self._results();
if (err === null) {
if (_.size(self.data) !== 0) {
if (self.orderedProperties.length === 0) {
_.forEach(self.data, function(data, key) {
if (inRange(data, key) && (self.limited <= 0 || limit < self.limited)) {
results[key] = _.cloneDeep(data);
limit++;
}
});
} else {
var queryable = [];
_.forEach(self.data, function(data, key) {
queryable.push({
data: data,
key: key
});
});

queryable = _.orderBy(queryable, _.map(self.orderedProperties, function(p) { return 'data.' + p; }), self.orderedDirections);

queryable.forEach(function(q) {
if (inRange(q.data, q.key) && (self.limited <= 0 || limit < self.limited)) {
results[q.key] = _.cloneDeep(q.data);
limit++;
}
});
}

resolve(new QuerySnapshot(self.parent === null ? self : self.parent.collection(self.id), results));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these two branches are equivalent now.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you're referring to here..

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 71 and line 73 do the same thing, which means the if-else is meaningless.

} else {
resolve(new QuerySnapshot(self.parent === null ? self : self.parent.collection(self.id)));
Expand Down Expand Up @@ -233,6 +188,107 @@ MockFirestoreQuery.prototype.clone = function () {
return query;
};

MockFirestoreQuery.prototype.onSnapshot = function (optionsOrObserverOrOnNext, observerOrOnNextOrOnError, onErrorArg) {
var err = this._nextErr('onSnapshot');
var self = this;
var onNext = optionsOrObserverOrOnNext;
var onError = observerOrOnNextOrOnError;
var includeMetadataChanges = optionsOrObserverOrOnNext.includeMetadataChanges;

if (includeMetadataChanges) {
// Note this doesn't truly mimic the firestore metadata changes behavior, however
// since everything is syncronous, there isn't any difference in behavior.
onNext = observerOrOnNextOrOnError;
onError = onErrorArg;
}
var context = {
data: self._results(),
};
var onSnapshot = function (forceTrigger) {
// compare the current state to the one from when this function was created
// and send the data to the callback if different.
if (err === null) {
if (forceTrigger) {
const results = self._results();
if (_.size(self.data) !== 0) {
onNext(new QuerySnapshot(self.parent === null ? self : self.parent.collection(self.id), results));
} else {
onNext(new QuerySnapshot(self.parent === null ? self : self.parent.collection(self.id)));
}
} else {
self.get().then(function (querySnapshot) {
var results = self._results();
if (!_.isEqual(results, context.data) || includeMetadataChanges) {
onNext(new QuerySnapshot(self.parent === null ? self : self.parent.collection(self.id), results));
context.data = results;
}
});
}
} else {
onError(err);
}
};

// onSnapshot should always return when initially called, then
// every time data changes.
onSnapshot(true);
var unsubscribe = this.queue.onPostFlush(onSnapshot);

// return the unsubscribe function
return unsubscribe;
};

MockFirestoreQuery.prototype._results = function () {
var results = {};
var limit = 0;
var atStart = false;
var atEnd = false;
var startFinder = this.buildStartFinder();

var inRange = function(data, key) {
if (atEnd) {
return false;
} else if (atStart) {
return true;
} else {
atStart = startFinder(data, key);
return atStart;
}
};
if (_.size(this.data) === 0) {
return results;
}

var self = this;
if (this.orderedProperties.length === 0) {
_.forEach(this.data, function(data, key) {
if (inRange(data, key) && (self.limited <= 0 || limit < self.limited)) {
results[key] = _.cloneDeep(data);
limit++;
}
});
} else {
var queryable = [];
_.forEach(self.data, function(data, key) {
queryable.push({
data: data,
key: key
});
});

queryable = _.orderBy(queryable, _.map(self.orderedProperties, function(p) { return 'data.' + p; }), self.orderedDirections);

queryable.forEach(function(q) {
if (inRange(q.data, q.key) && (self.limited <= 0 || limit < self.limited)) {
results[q.key] = _.cloneDeep(q.data);
limit++;
}
});
}

return results;
};

MockFirestoreQuery.prototype._defer = function (sourceMethod, sourceArgs, callback) {
this.queue.push({
fn: callback,
Expand Down
12 changes: 12 additions & 0 deletions src/queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ var EventEmitter = require('events').EventEmitter;

function FlushQueue () {
this.events = [];
this.postFlushListeners = [];
}

FlushQueue.prototype.push = function () {
Expand All @@ -23,6 +24,14 @@ FlushQueue.prototype.push = function () {
}));
};

FlushQueue.prototype.onPostFlush = function(subscriber) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing tests for this change. Flush triggers subscribed listener, flush does not trigger unsubscribed listener, flush does not trigger subscribed listener during flush, unsubscribe is either idempotent or throws error on second call (not currently true!)... anything I forgot?

this.postFlushListeners.push(subscriber);
var self = this;
return function() {
self.postFlushListeners.pop(subscriber);
};
};

FlushQueue.prototype.flushing = false;

FlushQueue.prototype.flush = function (delay) {
Expand All @@ -33,6 +42,9 @@ FlushQueue.prototype.flush = function (delay) {
}
function process () {
self.flushing = true;
_.forEach(self.postFlushListeners, function (subscriber) {
self.push(subscriber);
});
while (self.events.length) {
self.events[0].run();
}
Expand Down
95 changes: 95 additions & 0 deletions test/unit/firestore-collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -419,4 +419,99 @@ describe('MockFirestoreCollection', function () {
]);
});
});

describe('#onSnapshot', function () {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to fully test the allowed API usages here (unfortunately, there are lots of those).

The API doc says we can call onSnapshot with:

  • individual onNext and onError callbacks (tested)
  • a single observer object with next and error callbacks
  • with or without snapshot listen options
  • an onCompletion callback can be provided

it('returns value after collection is updated', function (done) {
var callCount = 0;
collection.onSnapshot(function(snap) {
callCount += 1;
var names = [];
snap.docs.forEach(function(doc) {
names.push(doc.data().name);
});

if (callCount === 2) {
expect(names).to.contain('A');
expect(names).not.to.contain('a');
done();
}
});
collection.doc('a').update({name: 'A'}, {setMerge: true});
collection.flush();
});

it('calls callback after multiple updates', function (done) {
var callCount = 0;
collection.onSnapshot(function(snap) {
callCount += 1;
var names = [];
snap.docs.forEach(function(doc) {
names.push(doc.data().name);
});

if (callCount === 2) {
expect(names).to.contain('A');
expect(names).not.to.contain('a');
}

if (callCount === 3) {
expect(names).to.contain('AA');
expect(names).not.to.contain('A');
done();
}
});

collection.doc('a').update({name: 'A'}, {setMerge: true});
collection.flush();
collection.doc('a').update({name: 'AA'}, {setMerge: true});
collection.flush();
});

it('should unsubscribe', function (done) {
var callCount = 0;
var unsubscribe = collection.onSnapshot(function(snap) {
callCount += 1;
});

collection.doc('a').update({name: 'A'}, {setMerge: true});
collection.flush();

process.nextTick(function() {
expect(callCount).to.equal(2);

collection.doc('a').update({name: 'AA'}, {setMerge: true});
unsubscribe();

collection.flush();

process.nextTick(function() {
expect(callCount).to.equal(2);
done();
});
});


});

it('Calls onError if error', function (done) {
var error = new Error("An error occured.");
collection.errs.onSnapshot = error;
var callCount = 0;
collection.onSnapshot(function(snap) {
throw new Error("This should not be called.");
}, function(err) {
// onSnapshot always returns when first called and then
// after data changes so we get 2 calls here.
if (callCount == 0) {
callCount++;
return;
}
expect(err).to.equal(error);
done();
});
collection.doc('a').update({name: 'A'}, {setMerge: true});
collection.flush();
});

});
});
Loading