firebase-mock
Version:
Firebase mock library for writing unit tests
333 lines (289 loc) • 9.11 kB
JavaScript
'use strict';
var _ = require('./lodash');
var Stream = require('stream');
var Promise = require('rsvp').Promise;
var autoId = require('firebase-auto-ids');
var FieldPath = require('./firestore-field-path');
var QuerySnapshot = require('./firestore-query-snapshot');
var DocumentSnapshot = require('./firestore-document-snapshot');
var Queue = require('./queue').Queue;
var utils = require('./utils');
function MockFirestoreQuery(path, data, parent, name) {
this.errs = {};
this.path = path || 'Mock://';
this.id = parent ? name : extractName(path);
this.flushDelay = parent ? parent.flushDelay : false;
this.queue = parent ? parent.queue : new Queue();
this.parent = parent || null;
this.firestore = parent ? parent.firestore : null;
this.children = {};
this.orderedProperties = [];
this.orderedDirections = [];
this.limited = 0;
this.buildStartFinder = function () { return function () { return true; }; };
this._setData(data);
}
MockFirestoreQuery.prototype.flush = function (delay) {
this.queue.flush(delay);
return this;
};
MockFirestoreQuery.prototype.autoFlush = function (delay) {
if (_.isUndefined(delay)) {
delay = true;
}
if (this.flushDelay !== delay) {
this.flushDelay = delay;
_.forEach(this.children, function (child) {
child.autoFlush(delay);
});
if (this.parent) {
this.parent.autoFlush(delay);
}
}
return this;
};
MockFirestoreQuery.prototype.getFlushQueue = function () {
return this.queue.getEvents();
};
MockFirestoreQuery.prototype._setData = function (data) {
this.data = utils.cleanFirestoreData(_.cloneDeep(data) || null);
};
MockFirestoreQuery.prototype._getData = function () {
return _.cloneDeep(this.data);
};
MockFirestoreQuery.prototype.toString = function () {
return this.path;
};
MockFirestoreQuery.prototype.get = function () {
var err = this._nextErr('get');
var self = this;
return new Promise(function (resolve, reject) {
self._defer('get', _.toArray(arguments), function () {
var results = self._results();
if (err === null) {
if (_.size(self.data) !== 0) {
resolve(new QuerySnapshot(self.parent === null ? self : self.parent.collection(self.id), results));
} else {
resolve(new QuerySnapshot(self.parent === null ? self : self.parent.collection(self.id)));
}
} else {
reject(err);
}
});
});
};
MockFirestoreQuery.prototype.stream = function () {
var stream = new Stream.Transform({
objectMode: true,
transform: function (chunk, encoding, done) {
this.push(chunk);
done();
}
});
this.get().then(function (snapshots) {
snapshots.forEach(function (snapshot) {
stream.write(snapshot);
});
stream.end();
});
return stream;
};
MockFirestoreQuery.prototype.where = function (property, operator, value) {
var query = this.clone();
var path = getPropertyPath(property);
// check if unsupported operator
if (operator !== '==' && operator !== 'array-contains') {
console.warn('Using unsupported where() operator for firebase-mock, returning entire dataset');
} else {
if (_.size(this.data) !== 0) {
var results = {};
_.forEach(this.data, function(data, key) {
var queryable = { data: data, key: key };
switch (operator) {
case '==':
if (_.isEqual(_.get(queryable, path), value)) {
results[key] = _.cloneDeep(data);
}
break;
case 'array-contains':
if (_.includes(_.get(data, property), value)) {
results[key] = _.cloneDeep(data);
}
break;
default:
results[key] = _.cloneDeep(data);
break;
}
});
query._setData(results);
} else {
query._setData(null);
}
}
return query;
};
MockFirestoreQuery.prototype.orderBy = function (property, direction) {
var query = this.clone();
query.orderedProperties.push(property);
query.orderedDirections.push(direction || 'asc');
return query;
};
MockFirestoreQuery.prototype.limit = function (limit) {
var query = this.clone();
query.limited = limit;
return query;
};
MockFirestoreQuery.prototype.startAfter = function (doc) {
if (!(doc instanceof DocumentSnapshot)) {
console.warn('Using unsupported startAfter() parameter for firebase-mock, returning entire dataset');
return this;
}
if (this.orderedProperties.length === 0) {
throw new Error('Query must be ordered to paginate');
}
var query = this.clone();
query.buildStartFinder = function () {
var next = false;
return function (data, key) {
if (next) {
return true;
} else {
next = key === doc.ref.id;
return false;
}
};
};
return query;
};
MockFirestoreQuery.prototype.clone = function () {
var query = new MockFirestoreQuery(this.path, this._getData(), this.parent, this.id);
query.orderedProperties = Array.from(this.orderedProperties);
query.orderedDirections = Array.from(this.orderedDirections);
query.limited = this.limited;
query.buildStartFinder = this.buildStartFinder;
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
});
});
var orderBy = _.map(self.orderedProperties, getPropertyPath);
queryable = _.orderBy(queryable, orderBy, 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,
context: this,
sourceData: {
ref: this,
method: sourceMethod,
args: sourceArgs
}
});
if (this.flushDelay !== false) {
this.flush(this.flushDelay);
}
};
MockFirestoreQuery.prototype._nextErr = function (type) {
var err = this.errs[type];
delete this.errs[type];
return err || null;
};
function extractName(path) {
return ((path || '').match(/\/([^.$\[\]#\/]+)$/) || [null, null])[1];
}
function getPropertyPath(p) {
if (FieldPath.documentId().isEqual(p)) {
return 'key';
} else if (p instanceof FieldPath) {
return 'data.' + p._path.join('.');
} else {
return 'data.' + p;
}
}
module.exports = MockFirestoreQuery;