Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

firestore-jest-mock

Package Overview
Dependencies
Maintainers
1
Versions
28
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

firestore-jest-mock - npm Package Compare versions

Comparing version 0.13.0 to 0.14.0

11

__tests__/full-setup-google-cloud-firestore.test.js

@@ -112,2 +112,3 @@ const { mockGoogleCloudFirestore } = require('firestore-jest-mock');

test('collectionGroup with subcollections', () => {
jest.clearAllMocks();
const firestore = new this.Firestore();

@@ -117,8 +118,8 @@

.collectionGroup('cities')
.where('type', '==', 'museum')
.where('country', '==', 'USA')
.get()
.then(querySnapshot => {
expect(mockCollectionGroup).toHaveBeenCalledWith('cities');
expect(mockGet).toHaveBeenCalled();
expect(mockWhere).toHaveBeenCalledWith('type', '==', 'museum');
expect(mockGet).toHaveBeenCalledTimes(1);
expect(mockWhere).toHaveBeenCalledWith('country', '==', 'USA');

@@ -245,5 +246,5 @@ expect(querySnapshot.forEach).toBeTruthy();

.onSnapshot(querySnapshot => {
expect(querySnapshot).toHaveProperty('forEach');
expect(querySnapshot).toHaveProperty('forEach', expect.any(Function));
expect(querySnapshot).toHaveProperty('docChanges');
expect(querySnapshot).toHaveProperty('docs');
expect(querySnapshot).toHaveProperty('docs', expect.any(Array));

@@ -250,0 +251,0 @@ expect(querySnapshot.forEach).toBeInstanceOf(Function);

@@ -1,52 +0,70 @@

const { mockFirebase } = require('firestore-jest-mock');
const { mockInitializeApp } = require('../mocks/firebase');
describe.each`
filters
${true}
${false}
`('we can start a firebase application (query filters: $filters)', ({ filters }) => {
// We call `require` inside of a parameterized `describe` so we get
// a fresh mocked Firebase to test cases with query filters turned on and off
const flushPromises = () => new Promise(setImmediate);
jest.resetModules();
const { mockFirebase } = require('firestore-jest-mock');
const { mockInitializeApp } = require('../mocks/firebase');
const {
mockGet,
mockAdd,
mockSet,
mockUpdate,
mockWhere,
mockCollectionGroup,
mockBatch,
mockBatchCommit,
mockBatchDelete,
mockBatchUpdate,
mockBatchSet,
mockSettings,
mockOnSnapShot,
mockUseEmulator,
mockDoc,
mockCollection,
mockWithConverter,
FakeFirestore,
} = require('../mocks/firestore');
const flushPromises = () => new Promise(setImmediate);
describe('we can start a firebase application', () => {
mockFirebase({
database: {
users: [
{ id: 'abc123', first: 'Bob', last: 'builder', born: 1998 },
{
id: '123abc',
first: 'Blues',
last: 'builder',
born: 1996,
_collections: {
cities: [{ id: 'LA', name: 'Los Angeles', state: 'CA', country: 'USA', visited: true }],
const {
mockGet,
mockAdd,
mockSet,
mockUpdate,
mockWhere,
mockCollectionGroup,
mockBatch,
mockBatchCommit,
mockBatchDelete,
mockBatchUpdate,
mockBatchSet,
mockSettings,
mockOnSnapShot,
mockUseEmulator,
mockDoc,
mockCollection,
mockWithConverter,
FakeFirestore,
mockQueryOnSnapshot,
} = require('../mocks/firestore');
mockFirebase(
{
database: {
users: [
{ id: 'abc123', first: 'Bob', last: 'builder', born: 1998 },
{
id: '123abc',
first: 'Blues',
last: 'builder',
born: 1996,
_collections: {
cities: [
{ id: 'LA', name: 'Los Angeles', state: 'CA', country: 'USA', visited: true },
{ id: 'Mex', name: 'Mexico City', country: 'Mexico', visited: true },
],
},
},
},
],
cities: [
{ id: 'LA', name: 'Los Angeles', state: 'CA', country: 'USA' },
{ id: 'DC', name: 'Disctric of Columbia', state: 'DC', country: 'USA' },
],
],
cities: [
{ id: 'LA', name: 'Los Angeles', state: 'CA', country: 'USA' },
{ id: 'DC', name: 'Disctric of Columbia', state: 'DC', country: 'USA' },
],
},
},
});
{ simulateQueryFilters: filters },
);
/** @type {import('firebase').default} */
const firebase = require('firebase');
beforeEach(() => {
this.firebase = require('firebase');
this.firebase.initializeApp({
jest.resetAllMocks();
firebase.initializeApp({
apiKey: '### FIREBASE API KEY ###',

@@ -59,3 +77,3 @@ authDomain: '### FIREBASE AUTH DOMAIN ###',

test('We can start an application', async () => {
const db = this.firebase.firestore();
const db = firebase.firestore();
db.settings({ ignoreUndefinedProperties: true });

@@ -67,3 +85,3 @@ expect(mockInitializeApp).toHaveBeenCalled();

test('we can use emulator', async () => {
const db = this.firebase.firestore();
const db = firebase.firestore();
db.useEmulator('localhost', 9000);

@@ -75,3 +93,3 @@ expect(mockUseEmulator).toHaveBeenCalledWith('localhost', 9000);

test('add a user', () => {
const db = this.firebase.firestore();
const db = firebase.firestore();

@@ -91,2 +109,3 @@ // Example from documentation:

expect(docRef).toHaveProperty('id');
expect(docRef).toHaveProperty('path', `users/${docRef.id}`);
});

@@ -96,3 +115,3 @@ });

test('get all users', () => {
const db = this.firebase.firestore();
const db = firebase.firestore();
// Example from documentation:

@@ -109,2 +128,5 @@ // https://firebase.google.com/docs/firestore/quickstart#read_data

const paths = querySnapshot.docs.map(d => d.ref.path).sort();
const expectedPaths = ['users/abc123', 'users/123abc'].sort();
expect(paths).toStrictEqual(expectedPaths);
querySnapshot.forEach(doc => {

@@ -118,7 +140,6 @@ expect(doc.exists).toBe(true);

test('collectionGroup at root', () => {
const db = this.firebase.firestore();
test('collectionGroup with collections only at root', () => {
const db = firebase.firestore();
// Example from documentation:
// https://firebase.google.com/docs/firestore/query-data/queries#collection-group-query
return db

@@ -130,3 +151,3 @@ .collectionGroup('users')

expect(mockCollectionGroup).toHaveBeenCalledWith('users');
expect(mockGet).toHaveBeenCalled();
expect(mockGet).toHaveBeenCalledTimes(1);
expect(mockWhere).toHaveBeenCalledWith('last', '==', 'builder');

@@ -145,15 +166,14 @@

test('collectionGroup with subcollections', () =>
this.firebase
.firestore()
test('collectionGroup with subcollections', () => {
const db = firebase.firestore();
return db
.collectionGroup('cities')
.where('type', '==', 'museum')
.get()
.then(querySnapshot => {
expect(mockCollectionGroup).toHaveBeenCalledWith('cities');
expect(mockGet).toHaveBeenCalled();
expect(mockWhere).toHaveBeenCalledWith('type', '==', 'museum');
expect(mockGet).toHaveBeenCalledTimes(1);
expect(mockWhere).not.toHaveBeenCalled();
expect(querySnapshot.forEach).toBeTruthy();
expect(querySnapshot.docs.length).toBe(3);
expect(querySnapshot.docs.length).toBe(4);
expect(querySnapshot.size).toBe(querySnapshot.docs.length);

@@ -165,6 +185,29 @@

});
}));
});
});
test('collectionGroup with queried subcollections', () => {
const db = firebase.firestore();
return db
.collectionGroup('cities')
.where('country', '==', 'USA')
.get()
.then(querySnapshot => {
expect(mockCollectionGroup).toHaveBeenCalledWith('cities');
expect(mockGet).toHaveBeenCalledTimes(1);
expect(mockWhere).toHaveBeenCalledWith('country', '==', 'USA');
expect(querySnapshot.forEach).toBeTruthy();
expect(querySnapshot.docs.length).toBe(filters ? 3 : 4);
expect(querySnapshot.size).toBe(querySnapshot.docs.length);
querySnapshot.forEach(doc => {
expect(doc.exists).toBe(true);
expect(doc.data()).toBeTruthy();
});
});
});
test('set a city', () => {
const db = this.firebase.firestore();
const db = firebase.firestore();
// Example from documentation:

@@ -191,3 +234,3 @@ // https://firebase.google.com/docs/firestore/manage-data/add-data#set_a_document\

test('updating a city', () => {
const db = this.firebase.firestore();
const db = firebase.firestore();
// Example from documentation:

@@ -208,3 +251,3 @@ // https://firebase.google.com/docs/firestore/manage-data/add-data#update-data

test('batch writes', () => {
const db = this.firebase.firestore();
const db = firebase.firestore();
// Example from documentation:

@@ -240,3 +283,3 @@ // https://cloud.google.com/firestore/docs/manage-data/transactions

test('onSnapshot single doc', async () => {
const db = this.firebase.firestore();
const db = firebase.firestore();

@@ -257,6 +300,7 @@ // Example from documentation:

expect(mockOnSnapShot).toHaveBeenCalled();
expect(mockQueryOnSnapshot).not.toHaveBeenCalled();
});
test('onSnapshot can work with options', async () => {
const db = this.firebase.firestore();
const db = firebase.firestore();

@@ -283,6 +327,7 @@ // Example from documentation:

expect(mockOnSnapShot).toHaveBeenCalled();
expect(mockQueryOnSnapshot).not.toHaveBeenCalled();
});
test('onSnapshot with query', async () => {
const db = this.firebase.firestore();
const db = firebase.firestore();

@@ -296,5 +341,5 @@ // Example from documentation:

.onSnapshot(querySnapshot => {
expect(querySnapshot).toHaveProperty('forEach');
expect(querySnapshot).toHaveProperty('forEach', expect.any(Function));
expect(querySnapshot).toHaveProperty('docChanges');
expect(querySnapshot).toHaveProperty('docs');
expect(querySnapshot).toHaveProperty('docs', expect.any(Array));

@@ -312,3 +357,4 @@ expect(querySnapshot.forEach).toBeInstanceOf(Function);

expect(mockWhere).toHaveBeenCalled();
expect(mockOnSnapShot).toHaveBeenCalled();
expect(mockOnSnapShot).not.toHaveBeenCalled();
expect(mockQueryOnSnapshot).toHaveBeenCalled();
});

@@ -323,3 +369,3 @@

test('single document', async () => {
const db = this.firebase.firestore();
const db = firebase.firestore();

@@ -339,3 +385,3 @@ const recordDoc = db.doc('cities/la').withConverter(converter);

test('single undefined document', async () => {
const db = this.firebase.firestore();
const db = firebase.firestore();

@@ -349,3 +395,3 @@ const recordDoc = db

expect(mockWithConverter).toHaveBeenCalledWith(converter);
expect(mockDoc).toHaveBeenCalledWith('abc123');
expect(mockDoc).toHaveBeenCalled();
expect(recordDoc).toBeInstanceOf(FakeFirestore.DocumentReference);

@@ -360,3 +406,3 @@

test('multiple documents', async () => {
const db = this.firebase.firestore();
const db = firebase.firestore();

@@ -363,0 +409,0 @@ const recordsCol = db.collection('cities').withConverter(converter);

@@ -10,26 +10,30 @@ const { FakeFirestore } = require('firestore-jest-mock');

const db = new FakeFirestore({
characters: [
const db = (simulateQueryFilters = false) =>
new FakeFirestore(
{
id: 'homer',
name: 'Homer',
occupation: 'technician',
address: { street: '742 Evergreen Terrace' },
characters: [
{
id: 'homer',
name: 'Homer',
occupation: 'technician',
address: { street: '742 Evergreen Terrace' },
},
{ id: 'krusty', name: 'Krusty', occupation: 'clown' },
{
id: 'bob',
name: 'Bob',
occupation: 'insurance agent',
_collections: {
family: [
{ id: 'violet', name: 'Violet', relation: 'daughter' },
{ id: 'dash', name: 'Dash', relation: 'son' },
{ id: 'jackjack', name: 'Jackjack', relation: 'son' },
{ id: 'helen', name: 'Helen', relation: 'wife' },
],
},
},
],
},
{ id: 'krusty', name: 'Krusty', occupation: 'clown' },
{
id: 'bob',
name: 'Bob',
occupation: 'insurance agent',
_collections: {
family: [
{ id: 'violet', name: 'Violet', relation: 'daughter' },
{ id: 'dash', name: 'Dash', relation: 'son' },
{ id: 'jackjack', name: 'Jackjack', relation: 'son' },
{ id: 'helen', name: 'Helen', relation: 'wife' },
],
},
},
],
});
{ simulateQueryFilters },
);

@@ -39,3 +43,3 @@ describe('Single records versus queries', () => {

expect.assertions(6);
const record = await db
const record = await db()
.collection('characters')

@@ -55,3 +59,3 @@ .doc('krusty')

expect.assertions(4);
const record = await db
const record = await db()
.collection('animals')

@@ -67,3 +71,3 @@ .doc('monkey')

test('it can fetch a single record with a promise', () =>
db
db()
.collection('characters')

@@ -88,3 +92,3 @@ .doc('homer')

test('it can fetch a single record with a promise without a specified collection', () =>
db
db()
.doc('characters/homer')

@@ -104,3 +108,3 @@ .get()

test('it can fetch multiple records and returns documents', async () => {
const records = await db
const records = await db()
.collection('characters')

@@ -120,31 +124,54 @@ .where('name', '==', 'Homer')

test('it throws an error if the collection path ends at a document', () => {
expect(() => db().collection('')).toThrow(Error);
expect(db().collection('foo')).toBeInstanceOf(FakeFirestore.CollectionReference);
expect(() => db().collection('foo/bar')).toThrow(Error);
expect(db().collection('foo/bar/baz')).toBeInstanceOf(FakeFirestore.CollectionReference);
});
test('it throws an error if the document path ends at a collection', () => {
expect(() => db.doc('characters')).toThrow(Error);
expect(() => db.doc('characters/bob')).not.toThrow();
expect(() => db.doc('characters/bob/family')).toThrow(Error);
expect(() => db().doc('')).toThrow(Error);
expect(() => db().doc('characters')).toThrow(Error);
expect(db().doc('characters/bob')).toBeInstanceOf(FakeFirestore.DocumentReference);
expect(() => db().doc('characters/bob/family')).toThrow(Error);
});
test('it can fetch nonexistent documents from a root collection', async () => {
expect.assertions(2);
const nope = await db.doc('characters/joe').get();
expect(nope.exists).toBe(false);
expect(nope.id).toBe('joe');
const nope = await db()
.doc('characters/joe')
.get();
expect(nope).toHaveProperty('exists', false);
expect(nope).toHaveProperty('id', 'joe');
expect(nope).toHaveProperty('ref');
expect(nope.ref).toHaveProperty('path', 'characters/joe');
});
test('it can fetch nonexistent documents from extant subcollections', async () => {
const nope = await db.doc('characters/bob/family/thing3').get();
expect(nope.exists).toBe(false);
expect(nope.id).toBe('thing3');
const nope = await db()
.doc('characters/bob/family/thing3')
.get();
expect(nope).toHaveProperty('exists', false);
expect(nope).toHaveProperty('id', 'thing3');
expect(nope).toHaveProperty('ref');
expect(nope.ref).toHaveProperty('path', 'characters/bob/family/thing3');
});
test('it can fetch nonexistent documents from nonexistent subcollections', async () => {
const nope = await db.doc('characters/sam/family/phil').get();
expect(nope.exists).toBe(false);
expect(nope.id).toBe('phil');
const nope = await db()
.doc('characters/sam/family/phil')
.get();
expect(nope).toHaveProperty('exists', false);
expect(nope).toHaveProperty('id', 'phil');
expect(nope).toHaveProperty('ref');
expect(nope.ref).toHaveProperty('path', 'characters/sam/family/phil');
});
test('it can fetch nonexistent documents from nonexistent root collections', async () => {
const nope = await db.doc('foo/bar/baz/bin').get();
expect(nope.exists).toBe(false);
expect(nope.id).toBe('bin');
const nope = await db()
.doc('foo/bar/baz/bin')
.get();
expect(nope).toHaveProperty('exists', false);
expect(nope).toHaveProperty('id', 'bin');
expect(nope).toHaveProperty('ref');
expect(nope.ref).toHaveProperty('path', 'foo/bar/baz/bin');
});

@@ -154,11 +181,15 @@

expect.assertions(1);
const records = await db
const records = await db()
.collection('animals')
.where('type', '==', 'mammal')
.get();
expect(records.empty).toBe(true);
expect(records).toHaveProperty('empty', true);
});
test('it can fetch multiple records as a promise', () =>
db
test.each`
simulateQueryFilters | expectedSize
${true} | ${1}
${false} | ${3}
`('it can fetch multiple records as a promise', ({ simulateQueryFilters, expectedSize }) =>
db(simulateQueryFilters)
.collection('characters')

@@ -168,15 +199,21 @@ .where('name', '==', 'Homer')

.then(records => {
expect(records.empty).toBe(false);
expect(records).toHaveProperty('empty', false);
expect(records).toHaveProperty('docs', expect.any(Array));
expect(records).toHaveProperty('size', expectedSize);
expect(records.docs[0]).toHaveProperty('id', 'homer');
expect(records.docs[0]).toHaveProperty('exists', true);
expect(records.docs[0].data()).toHaveProperty('name', 'Homer');
}));
}),
);
test('it can return all root records', async () => {
expect.assertions(4);
const firstRecord = db.collection('characters').doc('homer');
const secondRecord = db.collection('characters').doc('krusty');
const firstRecord = db()
.collection('characters')
.doc('homer');
const secondRecord = db()
.collection('characters')
.doc('krusty');
const records = await db.getAll(firstRecord, secondRecord);
const records = await db().getAll(firstRecord, secondRecord);
expect(records.length).toBe(2);

@@ -190,3 +227,3 @@ expect(records[0]).toHaveProperty('id', 'homer');

expect.assertions(4);
const record = await db
const record = await db()
.collection('characters')

@@ -202,8 +239,8 @@ .doc('bob')

test('it can fetch records from subcollections', async () => {
expect.assertions(7);
const family = db
expect.assertions(8);
const family = db()
.collection('characters')
.doc('bob')
.collection('family');
expect(family.path).toBe('database/characters/bob/family');
expect(family.path).toBe('characters/bob/family');

@@ -215,3 +252,3 @@ const allFamilyMembers = await family.get();

const ref = family.doc('violet');
expect(ref.path).toBe('database/characters/bob/family/violet');
expect(ref).toHaveProperty('path', 'characters/bob/family/violet');

@@ -221,16 +258,24 @@ const record = await ref.get();

expect(record).toHaveProperty('id', 'violet');
expect(record).toHaveProperty('data');
expect(record.data()).toHaveProperty('name', 'Violet');
});
test('it can fetch records from subcollections with query parameters', async () => {
const family = db
.collection('characters')
.doc('bob')
.collection('family')
.where('relation', '==', 'son'); // should still return all
expect(family.path).toBe('database/characters/bob/family');
test.each`
simulateQueryFilters | expectedSize
${true} | ${2}
${false} | ${4}
`(
'it can fetch records from subcollections with query parameters',
async ({ simulateQueryFilters, expectedSize }) => {
const family = db(simulateQueryFilters)
.collection('characters')
.doc('bob')
.collection('family')
.where('relation', '==', 'son'); // should return only sons
expect(family).toHaveProperty('path', 'characters/bob/family');
const docs = await family.get();
expect(docs).toHaveProperty('size', 4);
});
const docs = await family.get();
expect(docs).toHaveProperty('size', expectedSize);
},
);
});

@@ -241,5 +286,7 @@

expect.assertions(4);
const characters = await db.collection('characters').get();
expect(characters.empty).toBe(false);
expect(characters.size).toBe(3);
const characters = await db()
.collection('characters')
.get();
expect(characters).toHaveProperty('empty', false);
expect(characters).toHaveProperty('size', 3);
expect(Array.isArray(characters.docs)).toBe(true);

@@ -251,5 +298,7 @@ expect(characters.forEach).toBeTruthy();

expect.assertions(4);
const nope = await db.collection('foo').get();
expect(nope.empty).toBe(true);
expect(nope.size).toBe(0);
const nope = await db()
.collection('foo')
.get();
expect(nope).toHaveProperty('empty', true);
expect(nope).toHaveProperty('size', 0);
expect(Array.isArray(nope.docs)).toBe(true);

@@ -261,3 +310,3 @@ expect(nope.forEach).toBeTruthy();

expect.assertions(4);
const familyRef = db
const familyRef = db()
.collection('characters')

@@ -267,4 +316,4 @@ .doc('bob')

const family = await familyRef.get();
expect(family.empty).toBe(false);
expect(family.size).toBe(4);
expect(family).toHaveProperty('empty', false);
expect(family).toHaveProperty('size', 4);
expect(Array.isArray(family.docs)).toBe(true);

@@ -276,3 +325,3 @@ expect(family.forEach).toBeTruthy();

expect.assertions(4);
const nope = await db
const nope = await db()
.collection('characters')

@@ -282,4 +331,4 @@ .doc('bob')

.get();
expect(nope.empty).toBe(true);
expect(nope.size).toBe(0);
expect(nope).toHaveProperty('empty', true);
expect(nope).toHaveProperty('size', 0);
expect(Array.isArray(nope.docs)).toBe(true);

@@ -291,3 +340,3 @@ expect(nope.forEach).toBeTruthy();

expect.assertions(4);
const nope = await db
const nope = await db()
.collection('foo')

@@ -297,4 +346,4 @@ .doc('bar')

.get();
expect(nope.empty).toBe(true);
expect(nope.size).toBe(0);
expect(nope).toHaveProperty('empty', true);
expect(nope).toHaveProperty('size', 0);
expect(Array.isArray(nope.docs)).toBe(true);

@@ -306,10 +355,11 @@ expect(nope.forEach).toBeTruthy();

test('New documents with random ID', async () => {
expect.assertions(1);
expect.assertions(2);
// See https://firebase.google.com/docs/reference/js/firebase.firestore.CollectionReference#doc
// "If no path is specified, an automatically-generated unique ID will be used for the returned DocumentReference."
const col = db.collection('characters');
const col = db().collection('foo');
const newDoc = col.doc();
const otherIds = col.records().map(doc => doc.id);
const otherIds = col._records().map(doc => doc.id);
expect(otherIds).not.toContainEqual(newDoc.id);
expect(newDoc).toHaveProperty('path', `foo/${newDoc.id}`);
});
});

@@ -11,14 +11,119 @@ const {

describe('test', () => {
mockFirebase({
database: {
animals: [
{ id: 'monkey', name: 'monkey', type: 'mammal' },
{ id: 'elephant', name: 'elephant', type: 'mammal' },
{ id: 'chicken', name: 'chicken', type: 'bird' },
{ id: 'ant', name: 'ant', type: 'insect' },
],
describe('Queries', () => {
mockFirebase(
{
database: {
animals: [
{
id: 'monkey',
name: 'monkey',
type: 'mammal',
legCount: 2,
food: ['banana', 'mango'],
foodCount: 1,
foodEaten: [500, 20],
},
{
id: 'elephant',
name: 'elephant',
type: 'mammal',
legCount: 4,
food: ['banana', 'peanut'],
foodCount: 0,
foodEaten: [0, 500],
},
{
id: 'chicken',
name: 'chicken',
type: 'bird',
legCount: 2,
food: ['leaf', 'nut', 'ant'],
foodCount: 4,
foodEaten: [80, 20, 16],
_collections: {
foodSchedule: [
{
id: 'nut',
interval: 'whenever',
},
{
id: 'leaf',
interval: 'hourly',
},
],
},
},
{
id: 'ant',
name: 'ant',
type: 'insect',
legCount: 6,
food: ['leaf', 'bread'],
foodCount: 2,
foodEaten: [80, 12],
_collections: {
foodSchedule: [
{
id: 'leaf',
interval: 'daily',
},
{
id: 'peanut',
interval: 'weekly',
},
],
},
},
{
id: 'worm',
name: 'worm',
legCount: null,
},
{
id: 'pogo-stick',
name: 'pogo-stick',
food: false,
},
],
foodSchedule: [
{ id: 'ants', interval: 'daily' },
{ id: 'cows', interval: 'twice daily' },
],
nested: [
{
id: 'collections',
_collections: {
have: [
{
id: 'lots',
_collections: {
of: [
{
id: 'applications',
_collections: {
foodSchedule: [
{
id: 'layer4_a',
interval: 'daily',
},
{
id: 'layer4_b',
interval: 'weekly',
},
],
},
},
],
},
},
],
},
},
],
},
currentUser: { uid: 'homer-user' },
},
currentUser: { uid: 'homer-user' },
});
{ simulateQueryFilters: true },
);
const firebase = require('firebase');

@@ -45,4 +150,28 @@ firebase.initializeApp({

test('it can query null values', async () => {
const noLegs = await db
.collection('animals')
.where('legCount', '==', null)
.get();
expect(noLegs).toHaveProperty('size', 1);
const worm = noLegs.docs[0];
expect(worm).toBeDefined();
expect(worm).toHaveProperty('id', 'worm');
});
test('it can query false values', async () => {
const noFood = await db
.collection('animals')
.where('food', '==', false)
.get();
expect(noFood).toHaveProperty('size', 1);
const pogoStick = noFood.docs[0];
expect(pogoStick).toBeDefined();
expect(pogoStick).toHaveProperty('id', 'pogo-stick');
});
test('it can query multiple documents', async () => {
expect.assertions(10);
expect.assertions(9);
const animals = await db

@@ -54,3 +183,7 @@ .collection('animals')

expect(animals).toHaveProperty('docs', expect.any(Array));
expect(animals.docs.length).toBe(4);
expect(mockCollection).toHaveBeenCalledWith('animals');
// Make sure that the filter behaves appropriately
expect(animals.docs.length).toBe(2);
// Make sure that forEach works properly

@@ -64,6 +197,102 @@ expect(animals).toHaveProperty('forEach', expect.any(Function));

expect(mockWhere).toHaveBeenCalledWith('type', '==', 'mammal');
expect(mockGet).toHaveBeenCalled();
expect(animals).toHaveProperty('size', 2); // Returns 2 of 4 documents
});
test('it can filter firestore equality queries in subcollections', async () => {
const antSchedule = await db
.collection('animals')
.doc('ant')
.collection('foodSchedule')
.where('interval', '==', 'daily')
.get();
expect(mockCollection).toHaveBeenCalledWith('animals');
expect(mockCollection).toHaveBeenCalledWith('foodSchedule');
expect(mockWhere).toHaveBeenCalledWith('interval', '==', 'daily');
expect(mockGet).toHaveBeenCalled();
expect(antSchedule).toHaveProperty('docs', expect.any(Array));
expect(antSchedule).toHaveProperty('size', 1); // Returns 1 of 2 documents
});
test('in a transaction, it can filter firestore equality queries in subcollections', async () => {
mockGet.mockReset();
const antSchedule = db
.collection('animals')
.doc('ant')
.collection('foodSchedule')
.where('interval', '==', 'daily');
expect.assertions(6);
await db.runTransaction(async transaction => {
const scheduleItems = await transaction.get(antSchedule);
expect(mockCollection).toHaveBeenCalledWith('animals');
expect(mockCollection).toHaveBeenCalledWith('foodSchedule');
expect(mockWhere).toHaveBeenCalledWith('interval', '==', 'daily');
expect(mockGet).not.toHaveBeenCalled();
expect(scheduleItems).toHaveProperty('docs', expect.any(Array));
expect(scheduleItems).toHaveProperty('size', 1); // Returns 1 of 2 documents
});
});
test('it can filter firestore comparison queries in subcollections', async () => {
const chickenSchedule = db
.collection('animals')
.doc('chicken')
.collection('foodSchedule')
.where('interval', '<=', 'hourly'); // should have 1 result
const scheduleItems = await chickenSchedule.get();
expect(scheduleItems).toHaveProperty('docs', expect.any(Array));
expect(scheduleItems).toHaveProperty('size', 1); // Returns 1 document
expect(scheduleItems.docs[0]).toHaveProperty(
'ref',
expect.any(FakeFirestore.DocumentReference),
);
expect(scheduleItems.docs[0]).toHaveProperty('id', 'leaf');
expect(scheduleItems.docs[0].data()).toHaveProperty('interval', 'hourly');
expect(scheduleItems.docs[0].ref).toHaveProperty('path', 'animals/chicken/foodSchedule/leaf');
});
test('in a transaction, it can filter firestore comparison queries in subcollections', async () => {
const chickenSchedule = db
.collection('animals')
.doc('chicken')
.collection('foodSchedule')
.where('interval', '<=', 'hourly'); // should have 1 result
expect.assertions(6);
await db.runTransaction(async transaction => {
const scheduleItems = await transaction.get(chickenSchedule);
expect(scheduleItems).toHaveProperty('docs', expect.any(Array));
expect(scheduleItems).toHaveProperty('size', 1); // Returns 1 document
expect(scheduleItems.docs[0]).toHaveProperty(
'ref',
expect.any(FakeFirestore.DocumentReference),
);
expect(scheduleItems.docs[0]).toHaveProperty('id', 'leaf');
expect(scheduleItems.docs[0].data()).toHaveProperty('interval', 'hourly');
expect(scheduleItems.docs[0].ref).toHaveProperty('path', 'animals/chicken/foodSchedule/leaf');
});
});
test('it can query collection groups', async () => {
const allSchedules = await db.collectionGroup('foodSchedule').get();
expect(allSchedules).toHaveProperty('size', 8); // Returns all 8
const paths = allSchedules.docs.map(doc => doc.ref.path).sort();
const expectedPaths = [
'nested/collections/have/lots/of/applications/foodSchedule/layer4_a',
'nested/collections/have/lots/of/applications/foodSchedule/layer4_b',
'animals/ant/foodSchedule/leaf',
'animals/ant/foodSchedule/peanut',
'animals/chicken/foodSchedule/leaf',
'animals/chicken/foodSchedule/nut',
'foodSchedule/ants',
'foodSchedule/cows',
].sort();
expect(paths).toStrictEqual(expectedPaths);
});
test('it returns the same instance from query methods', () => {

@@ -93,2 +322,18 @@ const ref = db.collection('animals');

test('it throws an error when comparing to null', () => {
expect(() => db.collection('animals').where('legCount', '>', null)).toThrow();
expect(() => db.collection('animals').where('legCount', '>=', null)).toThrow();
expect(() => db.collection('animals').where('legCount', '<', null)).toThrow();
expect(() => db.collection('animals').where('legCount', '<=', null)).toThrow();
expect(() => db.collection('animals').where('legCount', 'array-contains', null)).toThrow();
expect(() => db.collection('animals').where('legCount', 'array-contains-any', null)).toThrow();
expect(() => db.collection('animals').where('legCount', 'in', null)).toThrow();
expect(() => db.collection('animals').where('legCount', 'not-in', null)).toThrow();
});
test('it allows equality comparisons with null', () => {
expect(() => db.collection('animals').where('legCount', '==', null)).not.toThrow();
expect(() => db.collection('animals').where('legCount', '!=', null)).not.toThrow();
});
test('it permits mocking the results of a where clause', async () => {

@@ -99,3 +344,3 @@ expect.assertions(2);

let result = await ref.where('type', '==', 'mammal').get();
expect(result.docs.length).toBe(4);
expect(result.docs.length).toBe(2);

@@ -117,3 +362,3 @@ // There's got to be a better way to mock like this, but at least it works.

test('It can offset query', async () => {
test('it can offset query', async () => {
const firstTwoMammals = await db

@@ -131,2 +376,149 @@ .collection('animals')

});
describe('Query Operations', () => {
test.each`
comp | value | count
${'=='} | ${2} | ${2}
${'=='} | ${4} | ${1}
${'=='} | ${6} | ${1}
${'=='} | ${7} | ${0}
${'!='} | ${7} | ${5}
${'!='} | ${4} | ${4}
${'>'} | ${1000} | ${0}
${'>'} | ${1} | ${4}
${'>'} | ${6} | ${0}
${'>='} | ${1000} | ${0}
${'>='} | ${6} | ${1}
${'>='} | ${0} | ${4}
${'<'} | ${-10000} | ${0}
${'<'} | ${10000} | ${4}
${'<'} | ${2} | ${0}
${'<'} | ${6} | ${3}
${'<='} | ${-10000} | ${0}
${'<='} | ${10000} | ${4}
${'<='} | ${2} | ${2}
${'<='} | ${6} | ${4}
${'in'} | ${[6, 2]} | ${3}
${'not-in'} | ${[6, 2]} | ${2}
${'not-in'} | ${[4]} | ${4}
${'not-in'} | ${[7]} | ${5}
`(
// eslint-disable-next-line quotes
"it performs '$comp' queries on number values ($count doc(s) where legCount $comp $value)",
async ({ comp, value, count }) => {
const results = await db
.collection('animals')
.where('legCount', comp, value)
.get();
expect(results.size).toBe(count);
},
);
test.each`
comp | value | count
${'=='} | ${0} | ${1}
${'=='} | ${1} | ${1}
${'=='} | ${2} | ${1}
${'=='} | ${4} | ${1}
${'=='} | ${6} | ${0}
${'>'} | ${-1} | ${4}
${'>'} | ${0} | ${3}
${'>'} | ${1} | ${2}
${'>'} | ${4} | ${0}
${'>='} | ${6} | ${0}
${'>='} | ${4} | ${1}
${'>='} | ${0} | ${4}
${'<'} | ${2} | ${2}
${'<'} | ${6} | ${4}
${'<='} | ${2} | ${3}
${'<='} | ${6} | ${4}
${'in'} | ${[2, 0]} | ${2}
${'not-in'} | ${[2, 0]} | ${2}
`(
// eslint-disable-next-line quotes
"it performs '$comp' queries on number values that may be zero ($count doc(s) where foodCount $comp $value)",
async ({ comp, value, count }) => {
const results = await db
.collection('animals')
.where('foodCount', comp, value)
.get();
expect(results.size).toBe(count);
},
);
test.each`
comp | value | count
${'=='} | ${'mammal'} | ${2}
${'=='} | ${'bird'} | ${1}
${'=='} | ${'fish'} | ${0}
${'!='} | ${'bird'} | ${3}
${'!='} | ${'fish'} | ${4}
${'>'} | ${'insect'} | ${2}
${'>'} | ${'z'} | ${0}
${'>='} | ${'mammal'} | ${2}
${'>='} | ${'insect'} | ${3}
${'<'} | ${'bird'} | ${0}
${'<'} | ${'mammal'} | ${2}
${'<='} | ${'mammal'} | ${4}
${'<='} | ${'bird'} | ${1}
${'<='} | ${'a'} | ${0}
${'in'} | ${['a', 'bird', 'mammal']} | ${3}
${'not-in'} | ${['a', 'bird', 'mammal']} | ${1}
`(
// eslint-disable-next-line quotes
"it performs '$comp' queries on string values ($count doc(s) where type $comp '$value')",
async ({ comp, value, count }) => {
const results = await db
.collection('animals')
.where('type', comp, value)
.get();
expect(results.size).toBe(count);
},
);
test.each`
comp | value | count
${'=='} | ${['banana', 'mango']} | ${1}
${'=='} | ${['mango', 'banana']} | ${0}
${'=='} | ${['banana', 'peanut']} | ${1}
${'!='} | ${['banana', 'peanut']} | ${4}
${'array-contains'} | ${'banana'} | ${2}
${'array-contains'} | ${'leaf'} | ${2}
${'array-contains'} | ${'bread'} | ${1}
${'array-contains-any'} | ${['banana', 'mango', 'peanut']} | ${2}
`(
// eslint-disable-next-line quotes
"it performs '$comp' queries on array values ($count doc(s) where food $comp '$value')",
async ({ comp, value, count }) => {
const results = await db
.collection('animals')
.where('food', comp, value)
.get();
expect(results.size).toBe(count);
},
);
test.each`
comp | value | count
${'=='} | ${[500, 20]} | ${1}
${'=='} | ${[20, 500]} | ${0}
${'=='} | ${[0, 500]} | ${1}
${'!='} | ${[20, 500]} | ${4}
${'array-contains'} | ${500} | ${2}
${'array-contains'} | ${80} | ${2}
${'array-contains'} | ${12} | ${1}
${'array-contains'} | ${0} | ${1}
${'array-contains-any'} | ${[0, 11, 500]} | ${2}
`(
// eslint-disable-next-line quotes
"it performs '$comp' queries on array values that may be zero ($count doc(s) where foodEaten $comp '$value')",
async ({ comp, value, count }) => {
const results = await db
.collection('animals')
.where('foodEaten', comp, value)
.get();
expect(results.size).toBe(count);
},
);
});
});

@@ -52,4 +52,4 @@ const { FakeFirestore } = require('firestore-jest-mock');

expect(collectionRef.firestore).toBe(db);
expect(collectionRef.id).toBe('characters');
expect(collectionRef.path).toBe('database/characters');
expect(collectionRef).toHaveProperty('id', 'characters');
expect(collectionRef).toHaveProperty('path', 'characters');

@@ -94,3 +94,3 @@ const other = db.collection('characters');

expect(homerRef).toBeInstanceOf(FakeFirestore.DocumentReference);
expect(homerRef.parent).toBeInstanceOf(FakeFirestore.CollectionReference);
expect(homerRef).toHaveProperty('parent', expect.any(FakeFirestore.CollectionReference));
expect(mockCollection).toHaveBeenCalledWith('characters');

@@ -109,4 +109,4 @@ expect(mockDoc).toHaveBeenCalledWith('homer');

expect(docRef.firestore).toBe(db);
expect(docRef.id).toBe('homer');
expect(docRef.path).toBe('database/characters/homer');
expect(docRef).toHaveProperty('id', 'homer');
expect(docRef).toHaveProperty('path', 'characters/homer');

@@ -113,0 +113,0 @@ const other = db.collection('characters').doc('homer');

@@ -66,4 +66,4 @@ const { mockFirebase, FakeFirestore } = require('firestore-jest-mock');

// Calling `get` on transaction also calls `get` on `ref`
expect(mockGet).toHaveBeenCalled();
// Calling `get` on transaction no longer calls `get` on `ref`
expect(mockGet).not.toHaveBeenCalled();
expect(doc).toHaveProperty('id', 'body');

@@ -70,0 +70,0 @@ expect(doc).toHaveProperty('exists', false);

export { FakeFirestore } from './mocks/firestore';
export { FakeAuth } from './mocks/auth';
export { mockFirebase } from './mocks/firebase';
export { mockGoogleCloudFirestore } from './mocks/googleCloudFirestore';

@@ -10,2 +10,4 @@ export const mockCreateUserWithEmailAndPassword: jest.Mock;

export const mockSignOut: jest.Mock;
// FIXME: We should decide whether this should be exported from auth or firestore
export const mockUseEmulator: jest.Mock;

@@ -12,0 +14,0 @@

@@ -21,5 +21,4 @@ import type { FirebaseUser, FakeAuth } from './auth';

export interface StubOptions {
includeIdsInData?: boolean;
}
type DefaultOptions = typeof import('./helpers/defaultMockOptions');
export interface StubOptions extends Partial<DefaultOptions> {}

@@ -26,0 +25,0 @@ export interface FirebaseMock {

@@ -114,6 +114,6 @@ import type { FieldValue } from './fieldValue';

*/
private records(): Array<MockedDocument>
private _records(): Array<MockedDocument>
}
// Mocks defined here
// Mocks exported from this module
export const mockBatch: jest.Mock;

@@ -130,2 +130,4 @@ export const mockRunTransaction: jest.Mock;

export const mockSettings: jest.Mock;
// FIXME: We should decide whether this should be exported from auth or firestore
export const mockUseEmulator: jest.Mock;

@@ -132,0 +134,0 @@ export const mockListDocuments: jest.Mock;

@@ -89,10 +89,35 @@ const mockCollectionGroup = jest.fn();

collection(collectionName) {
collection(path) {
// Accept any collection path
// See https://firebase.google.com/docs/reference/js/firebase.firestore.Firestore#collection
mockCollection(...arguments);
return new FakeFirestore.CollectionReference(collectionName, null, this);
if (path === undefined) {
throw new Error(
`FakeFirebaseError: Function Firestore.collection() requires 1 argument, but was called with 0 arguments.`,
);
} else if (!path || typeof path !== 'string') {
throw new Error(
`FakeFirebaseError: Function Firestore.collection() requires its first argument to be of type non-empty string, but it was: ${JSON.stringify(
path,
)}`,
);
}
// Ignore leading slash
const pathArray = path.replace(/^\/+/, '').split('/');
// Must be collection-level, so odd-numbered elements
if (pathArray.length % 2 !== 1) {
throw new Error(
`FakeFirebaseError: Invalid collection reference. Collection references must have an odd number of segments, but ${path} has ${pathArray.length}`,
);
}
const { coll } = this._docAndColForPathArray(pathArray);
return coll;
}
collectionGroup(collectionName) {
collectionGroup(collectionId) {
mockCollectionGroup(...arguments);
return new FakeFirestore.Query(collectionName, this);
return new FakeFirestore.Query(collectionId, this, true);
}

@@ -102,21 +127,48 @@

mockDoc(path);
return this._doc(path);
}
_doc(path) {
// Accept any document path
// See https://firebase.google.com/docs/reference/js/firebase.firestore.Firestore#doc
if (path === undefined) {
throw new Error(
`FakeFirebaseError: Function Firestore.doc() requires 1 argument, but was called with 0 arguments.`,
);
} else if (!path || typeof path !== 'string') {
throw new Error(
`FakeFirebaseError: Function Firestore.doc() requires its first argument to be of type non-empty string, but it was: ${JSON.stringify(
path,
)}`,
);
}
// Ignore leading slash
const pathArray = path.replace(/^\/+/, '').split('/');
// Must be document-level, so even-numbered elements
if (pathArray.length % 2) {
throw new Error('The path array must be document-level');
if (pathArray.length % 2 !== 0) {
throw new Error(`FakeFirebaseError: Invalid document reference. Document references must have an even number of segments, but ${path} has ${pathArray.length}
`);
}
const { doc } = this._docAndColForPathArray(pathArray);
return doc;
}
_docAndColForPathArray(pathArray) {
let doc = null;
for (let index = 0; index < pathArray.length; index++) {
const collectionId = pathArray[index];
const documentId = pathArray[index + 1];
let coll = null;
for (let index = 0; index < pathArray.length; index += 2) {
const collectionId = pathArray[index] || '';
const documentId = pathArray[index + 1] || '';
const collection = new FakeFirestore.CollectionReference(collectionId, doc, this);
doc = new FakeFirestore.DocumentReference(documentId, collection);
coll = new FakeFirestore.CollectionReference(collectionId, doc, this);
if (!documentId) {
break;
}
doc = new FakeFirestore.DocumentReference(documentId, coll);
}
index++; // skip to next collection
}
return doc;
return { doc, coll };
}

@@ -136,9 +188,9 @@

// note: this logic could be deduplicated
const pathArray = path
.replace(/^\/+/, '')
.split('/')
.slice(1);
const pathArray = path.replace(/^\/+/, '').split('/');
// Must be document-level, so even-numbered elements
if (pathArray.length % 2) {
throw new Error('The path array must be document-level');
if (pathArray.length % 2 !== 0) {
throw new Error(
`FakeFirebaseError: Invalid document reference. Document references must have an even number of segments, but ${path} has ${pathArray.length}`,
);
}

@@ -194,3 +246,6 @@

this.firestore = parent.firestore;
this.path = parent.path.concat(`/${id}`);
this.path = parent.path
.split('/')
.concat(id)
.join('/');
}

@@ -223,11 +278,9 @@

this.get()
.then(result => {
callback(result);
})
.catch(e => {
throw e;
});
callback(this._get());
} catch (e) {
errorCallback(e);
if (errorCallback) {
errorCallback(e);
} else {
throw e;
}
}

@@ -291,3 +344,6 @@

pathArray.shift(); // drop 'database'; it's always first
if (pathArray[0] === 'database') {
pathArray.shift(); // drop 'database'; it was included in legacy paths, but we don't need it now
}
let requestedRecords = this.firestore.database[pathArray.shift()];

@@ -299,3 +355,3 @@ let document = null;

} else {
return { exists: false, data: () => undefined, id: this.id };
return { exists: false, data: () => undefined, id: this.id, ref: this };
}

@@ -308,7 +364,7 @@

if (!document || !document._collections) {
return { exists: false, data: () => undefined, id: this.id };
return { exists: false, data: () => undefined, id: this.id, ref: this };
}
requestedRecords = document._collections[collectionId] || [];
if (requestedRecords.length === 0) {
return { exists: false, data: () => undefined, id: this.id };
return { exists: false, data: () => undefined, id: this.id, ref: this };
}

@@ -318,3 +374,3 @@

if (!document) {
return { exists: false, data: () => undefined, id: this.id };
return { exists: false, data: () => undefined, id: this.id, ref: this };
}

@@ -353,3 +409,3 @@

} else {
this.path = `database/${id}`;
this.path = id;
}

@@ -371,3 +427,2 @@ }

/**
* @function records
* A private method, meant mainly to be used by `get` and other internal objects to retrieve

@@ -377,7 +432,6 @@ * the list of database records referenced by this CollectionReference.

*/
records() {
_records() {
// Ignore leading slash
const pathArray = this.path.replace(/^\/+/, '').split('/');
pathArray.shift(); // drop 'database'; it's always first
let requestedRecords = this.firestore.database[pathArray.shift()];

@@ -420,8 +474,13 @@ if (pathArray.length === 0) {

query.mocks.mockGet(...arguments);
return Promise.resolve(this._get());
}
_get() {
// Make sure we have a 'good enough' document reference
const records = this.records();
const records = this._records();
records.forEach(rec => {
rec._ref = this.doc(rec.id);
rec._ref = new FakeFirestore.DocumentReference(rec.id, this, this.firestore);
});
return Promise.resolve(buildQuerySnapShot(records));
const isFilteringEnabled = this.firestore.options.simulateQueryFilters;
return buildQuerySnapShot(records, isFilteringEnabled ? this.filters : undefined);
}

@@ -428,0 +487,0 @@

import type { MockedDocument, DocumentHash } from './buildDocFromHash';
export type Comparator = '<' | '<=' | '==' | '!=' | '>=' | '>' | 'array-contains' | 'in' | 'not-in' | 'array-contains-any';
export interface QueryFilter {
key: string;
comp: Comparator;
value: string;
}
export interface MockedQuerySnapshot<Doc = MockedDocument> {

@@ -15,3 +23,7 @@ empty: boolean;

* @param requestedRecords
* @param filters
*/
export default function buildQuerySnapShot(requestedRecords: Array<DocumentHash>): MockedQuerySnapshot;
export default function buildQuerySnapShot(
requestedRecords: Array<DocumentHash>,
filters?: Array<QueryFilter>
): MockedQuerySnapshot;
const buildDocFromHash = require('./buildDocFromHash');
module.exports = function buildQuerySnapShot(requestedRecords) {
const multipleRecords = requestedRecords.filter(rec => !!rec);
const docs = multipleRecords.map(buildDocFromHash);
module.exports = function buildQuerySnapShot(requestedRecords, filters) {
const definiteRecords = requestedRecords.filter(rec => !!rec);
const results = _filteredDocuments(definiteRecords, filters);
const docs = results.map(buildDocFromHash);
return {
empty: multipleRecords.length < 1,
size: multipleRecords.length,
empty: results.length < 1,
size: results.length,
docs,

@@ -19,1 +20,237 @@ forEach(callback) {

};
/**
* @typedef DocumentHash
* @type {import('./buildDocFromHash').DocumentHash}
*/
/**
* @typedef Comparator
* @type {import('./buildQuerySnapShot').Comparator}
*/
/**
* Applies query filters to an array of mock document data.
*
* @param {Array<DocumentHash>} records The array of records to filter.
* @param {Array<{ key: string; comp: Comparator; value: unknown }>=} filters The filters to apply.
* If no filters are provided, then the records array is returned as-is.
*
* @returns {Array<import('./buildDocFromHash').DocumentHash>} The filtered documents.
*/
function _filteredDocuments(records, filters) {
if (!filters || !Array.isArray(filters) || filters.length === 0) {
return records;
}
filters.forEach(({ key, comp, value }) => {
// https://firebase.google.com/docs/reference/js/firebase.firestore#wherefilterop
// Convert values to string to make Array comparisons work
// See https://jsbin.com/bibawaf/edit?js,console
switch (comp) {
// https://firebase.google.com/docs/firestore/query-data/queries#query_operators
case '<':
records = _recordsLessThanValue(records, key, value);
break;
case '<=':
records = _recordsLessThanOrEqualToValue(records, key, value);
break;
case '==':
records = _recordsEqualToValue(records, key, value);
break;
case '!=':
records = _recordsNotEqualToValue(records, key, value);
break;
case '>=':
records = _recordsGreaterThanOrEqualToValue(records, key, value);
break;
case '>':
records = _recordsGreaterThanValue(records, key, value);
break;
case 'array-contains':
records = _recordsArrayContainsValue(records, key, value);
break;
case 'in':
records = _recordsWithValueInList(records, key, value);
break;
case 'not-in':
records = _recordsWithValueNotInList(records, key, value);
break;
case 'array-contains-any':
records = _recordsWithOneOfValues(records, key, value);
break;
}
});
return records;
}
function _recordsWithKey(records, key) {
return records.filter(record => record && record[key] !== undefined);
}
function _recordsWithNonNullKey(records, key) {
return records.filter(record => record && record[key] !== undefined && record[key] !== null);
}
function _shouldCompareNumerically(a, b) {
return typeof a === 'number' && typeof b === 'number';
}
/**
* @param {Array<DocumentHash>} records
* @param {string} key
* @param {unknown} value
* @returns {Array<DocumentHash>}
*/
function _recordsLessThanValue(records, key, value) {
return _recordsWithNonNullKey(records, key).filter(record => {
if (_shouldCompareNumerically(record[key], value)) {
return record[key] < value;
}
return String(record[key]) < String(value);
});
}
/**
* @param {Array<DocumentHash>} records
* @param {string} key
* @param {unknown} value
* @returns {Array<DocumentHash>}
*/
function _recordsLessThanOrEqualToValue(records, key, value) {
return _recordsWithNonNullKey(records, key).filter(record => {
if (_shouldCompareNumerically(record[key], value)) {
return record[key] <= value;
}
return String(record[key]) <= String(value);
});
}
/**
* @param {Array<DocumentHash>} records
* @param {string} key
* @param {unknown} value
* @returns {Array<DocumentHash>}
*/
function _recordsEqualToValue(records, key, value) {
return _recordsWithKey(records, key).filter(record => String(record[key]) === String(value));
}
/**
* @param {Array<DocumentHash>} records
* @param {string} key
* @param {unknown} value
* @returns {Array<DocumentHash>}
*/
function _recordsNotEqualToValue(records, key, value) {
return _recordsWithKey(records, key).filter(record => String(record[key]) !== String(value));
}
/**
* @param {Array<DocumentHash>} records
* @param {string} key
* @param {unknown} value
* @returns {Array<DocumentHash>}
*/
function _recordsGreaterThanOrEqualToValue(records, key, value) {
return _recordsWithNonNullKey(records, key).filter(record => {
if (_shouldCompareNumerically(record[key], value)) {
return record[key] >= value;
}
return String(record[key]) >= String(value);
});
}
/**
* @param {Array<DocumentHash>} records
* @param {string} key
* @param {unknown} value
* @returns {Array<DocumentHash>}
*/
function _recordsGreaterThanValue(records, key, value) {
return _recordsWithNonNullKey(records, key).filter(record => {
if (_shouldCompareNumerically(record[key], value)) {
return record[key] > value;
}
return String(record[key]) > String(value);
});
}
/**
* @see https://firebase.google.com/docs/firestore/query-data/queries#array_membership
*
* @param {Array<DocumentHash>} records
* @param {string} key
* @param {unknown} value
* @returns {Array<DocumentHash>}
*/
function _recordsArrayContainsValue(records, key, value) {
return records.filter(
record => record && record[key] && Array.isArray(record[key]) && record[key].includes(value),
);
}
/**
* @see https://firebase.google.com/docs/firestore/query-data/queries#in_not-in_and_array-contains-any
*
* @param {Array<DocumentHash>} records
* @param {string} key
* @param {unknown} value
* @returns {Array<DocumentHash>}
*/
function _recordsWithValueInList(records, key, value) {
// TODO: Throw an error when a value is passed that contains more than 10 values
return records.filter(record => {
if (!record || record[key] === undefined) {
return false;
}
return value && Array.isArray(value) && value.includes(record[key]);
});
}
/**
* @see https://firebase.google.com/docs/firestore/query-data/queries#not-in
*
* @param {Array<DocumentHash>} records
* @param {string} key
* @param {unknown} value
* @returns {Array<DocumentHash>}
*/
function _recordsWithValueNotInList(records, key, value) {
// TODO: Throw an error when a value is passed that contains more than 10 values
return _recordsWithKey(records, key).filter(
record => value && Array.isArray(value) && !value.includes(record[key]),
);
}
/**
* @see https://firebase.google.com/docs/firestore/query-data/queries#in_not-in_and_array-contains-any
*
* @param {Array<DocumentHash>} records
* @param {string} key
* @param {unknown} value
* @returns {Array<DocumentHash>}
*/
function _recordsWithOneOfValues(records, key, value) {
// TODO: Throw an error when a value is passed that contains more than 10 values
return records.filter(
record =>
record &&
record[key] &&
Array.isArray(record[key]) &&
value &&
Array.isArray(value) &&
record[key].some(v => value.includes(v)),
);
}
export const includeIdsInData: boolean;
export const mutable: boolean;
export const simulateQueryFilters: boolean;
module.exports = {
includeIdsInData: false,
mutable: false,
simulateQueryFilters: false,
};

@@ -14,5 +14,7 @@ const buildQuerySnapShot = require('./helpers/buildQuerySnapShot');

class Query {
constructor(collectionName, firestore) {
constructor(collectionName, firestore, isGroupQuery = false) {
this.collectionName = collectionName;
this.firestore = firestore;
this.filters = [];
this.isGroupQuery = isGroupQuery;
}

@@ -22,32 +24,72 @@

mockGet(...arguments);
// Use DFS to find all records in collections that match collectionName
return Promise.resolve(this._get());
}
_get() {
// Simulate collectionGroup query
// Get Firestore collections whose name match `this.collectionName`; return their documents
const requestedRecords = [];
const st = [this.firestore.database];
// At each collection list node, get collection in collection list whose id
// matches this.collectionName
while (st.length > 0) {
const subcollections = st.pop();
const documents = subcollections[this.collectionName];
if (documents && Array.isArray(documents)) {
requestedRecords.push(...documents);
}
const queue = [
{
lastParent: '',
collections: this.firestore.database,
},
];
// For each collection in subcollections, get each document's _collections array
// and push onto st.
Object.values(subcollections).forEach(collection => {
const documents = collection.filter(d => !!d._collections);
st.push(...documents.map(d => d._collections));
while (queue.length > 0) {
// Get a collection
const { lastParent, collections } = queue.shift();
Object.entries(collections).forEach(([collectionPath, docs]) => {
const prefix = lastParent ? `${lastParent}/` : '';
const newLastParent = `${prefix}${collectionPath}`;
const lastPathComponent = collectionPath.split('/').pop();
// If this is a matching collection, grep its documents
if (lastPathComponent === this.collectionName) {
const docHashes = docs.map(doc => {
// Fetch the document from the mock db
const path = `${newLastParent}/${doc.id}`;
return {
...doc,
_ref: this.firestore._doc(path),
};
});
requestedRecords.push(...docHashes);
}
// Enqueue adjacent collections for next run
docs.forEach(doc => {
if (doc._collections) {
queue.push({
lastParent: `${prefix}${collectionPath}/${doc.id}`,
collections: doc._collections,
});
}
});
});
}
// Make sure we have a 'good enough' document reference
requestedRecords.forEach(rec => {
rec._ref = this.firestore.doc('database/'.concat(rec.id));
});
return Promise.resolve(buildQuerySnapShot(requestedRecords));
// Return the requested documents
const isFilteringEnabled = this.firestore.options.simulateQueryFilters;
return buildQuerySnapShot(requestedRecords, isFilteringEnabled ? this.filters : undefined);
}
where() {
return mockWhere(...arguments) || this;
where(key, comp, value) {
const result = mockWhere(...arguments);
if (result) {
return result;
}
// Firestore has been tested to throw an error at this point when trying to compare null as a quantity
if (value === null && !['==', '!='].includes(comp)) {
throw new Error(
`FakeFirebaseError: Invalid query. Null only supports '==' and '!=' comparisons.`,
);
}
this.filters.push({ key, comp, value });
return result || this;
}

@@ -83,7 +125,9 @@

try {
this.get().then(result => {
callback(result);
});
callback(this._get());
} catch (e) {
errorCallback(e);
if (errorCallback) {
errorCallback(e);
} else {
throw e;
}
}

@@ -90,0 +134,0 @@

@@ -20,3 +20,3 @@ const mockGetAll = jest.fn();

mockGetTransaction(...arguments);
return ref.get();
return Promise.resolve(ref._get());
}

@@ -23,0 +23,0 @@

{
"name": "firestore-jest-mock",
"version": "0.13.0",
"version": "0.14.0",
"description": "Jest helper for mocking Google Cloud Firestore",

@@ -5,0 +5,0 @@ "author": "",

@@ -32,2 +32,3 @@ # Mock Firestore

- [mutable](#mutable)
- [simulateQueryFilters](#simulateQueryFilters)
- [Functions you can test](#functions-you-can-test)

@@ -88,4 +89,10 @@ - [Firestore](#firestore)

This will populate a fake database with a `users` and `posts` collection.
If you are using TypeScript, you can import `mockFirebase` using ES module syntax:
```TypeScript
import { mockFirebase } from 'firestore-jest-mock';
```
This will populate a fake database with a `users` and `posts` collection. This database is read-only by default, meaning that any Firestore write calls will not actually persist across invocations.
Now you can write queries or requests for data just as you would with Firestore:

@@ -112,2 +119,10 @@

In TypeScript, you would import `mockCollection` using ES module syntax:
```TypeScript
import { mockCollection } from 'firestore-jest-mock/mocks/firestore';
```
The other mock functions may be imported similarly.
### `@google-cloud/firestore` compatibility

@@ -151,3 +166,3 @@

A common case in Firestore is to store data in document [subcollections](https://firebase.google.com/docs/firestore/manage-data/structure-data#subcollections). You can model these in firestore-jest-mock like so:
A common Firestore use case is to store data in document [subcollections](https://firebase.google.com/docs/firestore/manage-data/structure-data#subcollections). You can model these with firestore-jest-mock like so:

@@ -182,3 +197,3 @@ ```js

Similar to how the `id` key models a document object, the `_collections` key models a subcollection. You model each subcollection key in the same way that `database` is modeled above: an object keyed by collection IDs and populated with document arrays.
Similar to how the `id` key defines a document object to firestore-jest-mock, the `_collections` key defines a subcollection. You model each subcollection structure in the same way that `database` is modeled above: an object keyed by collection IDs and populated with document arrays.

@@ -212,3 +227,4 @@ This lets you model and validate more complex document access:

The job of the this library is not to test Firestore, but to allow you to test your code without hitting firebase.
The job of the this library is not to test Firestore, but to allow you to test your code without hitting Firebase servers or booting a local emulator. Since this package simulates most of the Firestore interface in plain JavaScript, unit tests can be quick and easy both to write and to execute.
Take this example:

@@ -313,2 +329,3 @@

mutable: true,
simulateQueryFilters: true,
};

@@ -319,3 +336,3 @@

#### includeIdsInData
#### `includeIdsInData`

@@ -326,3 +343,3 @@ By default, id's are not returned with the document's data.

#### mutable
#### `mutable`

@@ -333,3 +350,3 @@ _Warning: Thar be dragons_

This means it doesn't update, even when you call things like `set` or `add`, as the result isn't typically important for your tests.
If you need your tests to update the mock database, you can set `mutable` to true when calling `mockFirebase`.
If you need your tests to update the mock database, you can set `mutable` to `true` when calling `mockFirebase`.
Calling `.set()` on a document or collection would update the mock database you created.

@@ -340,4 +357,10 @@ This can make your tests less predictable, as they may need to be run in the same order.

_Note: not all APIs that update the database are supported yet. PR's welcome!_
_Note: not all APIs that update the database are supported yet. PRs welcome!_
#### `simulateQueryFilters`
By default, query filters (read: `where` clauses) pass through all mock Firestore data without applying the requested filters.
If you need your tests to perform `where` queries on mock database data, you can set `simulateQueryFilters` to `true` when calling `mockFirebase`.
### Functions you can test

@@ -344,0 +367,0 @@

@@ -12,3 +12,2 @@ {

"alwaysStrict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,

@@ -15,0 +14,0 @@ "declaration": true,

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc