voluptasmollitia
Version:
Monorepo for the Firebase JavaScript SDK
349 lines (300 loc) • 11.6 kB
text/typescript
/**
* @license
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/no-floating-promises */
import { expect } from 'chai';
// app is used as namespaces to access types
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import firebase from 'firebase/app';
import 'firebase/firestore';
import {
collection,
collectionChanges,
sortedChanges,
auditTrail,
docData,
collectionData
} from '../firestore';
import { map, take, skip } from 'rxjs/operators';
// eslint-disable-next-line @typescript-eslint/no-require-imports
export const TEST_PROJECT = require('../../../config/project.json');
const createId = (): string => Math.random().toString(36).substring(5);
/**
* Create a collection with a random name. This helps sandbox offline tests and
* makes sure tests don't interfere with each other as they run.
*/
const createRandomCol = (
firestore: firebase.firestore.Firestore
): firebase.firestore.CollectionReference => firestore.collection(createId());
/**
* Unwrap a snapshot but add the type property to the data object.
*/
const unwrapChange = map((changes: firebase.firestore.DocumentChange[]) => {
return changes.map(c => ({ type: c.type, ...c.doc.data() }));
});
/**
* Create an environment for the tests to run in. The information is returned
* from the function for use within the test.
*/
const seedTest = (firestore: firebase.firestore.Firestore): any => {
const colRef = createRandomCol(firestore);
const davidDoc = colRef.doc('david');
davidDoc.set({ name: 'David' });
const shannonDoc = colRef.doc('shannon');
shannonDoc.set({ name: 'Shannon' });
const expectedNames = ['David', 'Shannon'];
const expectedEvents = [
{ name: 'David', type: 'added' },
{ name: 'Shannon', type: 'added' }
];
return { colRef, davidDoc, shannonDoc, expectedNames, expectedEvents };
};
describe('RxFire Firestore', () => {
let app: firebase.app.App;
let firestore: firebase.firestore.Firestore;
/**
* Each test runs inside it's own app instance and the app
* is deleted after the test runs.
*
* Firestore tests run "offline" to reduce "flakeyness".
*
* Each test is responsible for seeding and removing data. Helper
* functions are useful if the process becomes brittle or tedious.
* Note that removing is less necessary since the tests are run
* offline.
*/
beforeEach(() => {
app = firebase.initializeApp({ projectId: TEST_PROJECT.projectId });
firestore = app.firestore();
firestore.disableNetwork();
});
afterEach((done: MochaDone) => {
app.delete().then(() => done());
});
describe('collection', () => {
/**
* This is a simple test to see if the collection() method
* correctly emits snapshots.
*
* The test seeds two "people" into the collection. RxFire
* creats an observable with the `collection()` method and
* asserts that the two "people" are in the array.
*/
it('should emit snapshots', (done: MochaDone) => {
const { colRef, expectedNames } = seedTest(firestore);
collection(colRef)
.pipe(map(docs => docs.map(doc => doc.data().name)))
.subscribe(names => {
expect(names).to.eql(expectedNames);
done();
});
});
});
describe('collectionChanges', () => {
/**
* The `stateChanges()` method emits a stream of events as they
* occur rather than in sorted order.
*
* This test adds a "person" and then modifies it. This should
* result in an array item of "added" and then "modified".
*/
it('should emit events as they occur', (done: MochaDone) => {
const { colRef, davidDoc } = seedTest(firestore);
davidDoc.set({ name: 'David' });
const firstChange = collectionChanges(colRef).pipe(take(1));
const secondChange = collectionChanges(colRef).pipe(skip(1));
firstChange.subscribe(change => {
expect(change[0].type).to.eq('added');
davidDoc.update({ name: 'David!' });
});
secondChange.subscribe(change => {
expect(change[0].type).to.eq('modified');
done();
});
});
});
describe('sortedChanges', () => {
/**
* The `sortedChanges()` method reduces the stream of `collectionChanges()` to
* a sorted array. This test seeds two "people" and checks to make sure
* the 'added' change type exists. Afterwards, one "person" is modified.
* The test then checks that the person is modified and in the proper sorted
* order.
*/
it('should emit an array of sorted snapshots', (done: MochaDone) => {
const { colRef, davidDoc } = seedTest(firestore);
const addedChanges = sortedChanges(colRef, ['added']).pipe(unwrapChange);
const modifiedChanges = sortedChanges(colRef).pipe(
unwrapChange,
skip(1),
take(1)
);
let previousData: Array<{}>;
addedChanges.subscribe(data => {
const expectedNames = [
{ name: 'David', type: 'added' },
{ name: 'Shannon', type: 'added' }
];
expect(data).to.eql(expectedNames);
previousData = data;
davidDoc.update({ name: 'David!' });
});
modifiedChanges.subscribe(data => {
const expectedNames = [
{ name: 'David!', type: 'modified' },
{ name: 'Shannon', type: 'added' }
];
expect(data).to.eql(expectedNames);
expect(data === previousData).to.eql(false);
done();
});
});
/**
* The events parameter in `sortedChanges()` filters out events by change
* type. This test seeds two "people" and creates two observables to test
* the filtering. The first observable filters to 'added' and the second
* filters to 'modified'.
*/
it('should filter by event type', (done: MochaDone) => {
const { colRef, davidDoc, expectedEvents } = seedTest(firestore);
const addedChanges = sortedChanges(colRef, ['added']).pipe(unwrapChange);
const modifiedChanges = sortedChanges(colRef, ['modified']).pipe(
unwrapChange
);
addedChanges.subscribe(data => {
// kick off the modifiedChanges observable
expect(data).to.eql(expectedEvents);
davidDoc.update({ name: 'David!' });
});
modifiedChanges.subscribe(data => {
const expectedModifiedEvent = [{ name: 'David!', type: 'modified' }];
expect(data).to.eql(expectedModifiedEvent);
done();
});
});
});
describe('auditTrail', () => {
/**
* The `auditTrail()` method returns an array of every change that has
* occurred in the application. This test seeds two "people" into the
* collection and checks that the two added events are there. It then
* modifies a "person" and makes sure that event is on the array as well.
*/
it('should keep create a list of all changes', (done: MochaDone) => {
const { colRef, expectedEvents, davidDoc } = seedTest(firestore);
const firstAudit = auditTrail(colRef).pipe(unwrapChange, take(1));
const secondAudit = auditTrail(colRef).pipe(unwrapChange, skip(1));
firstAudit.subscribe(list => {
expect(list).to.eql(expectedEvents);
davidDoc.update({ name: 'David!' });
});
secondAudit.subscribe(list => {
const modifiedList = [
...expectedEvents,
{ name: 'David!', type: 'modified' }
];
expect(list).to.eql(modifiedList);
done();
});
});
/**
* This test seeds two "people" into the collection. It then creates an
* auditList observable that is filtered to 'modified' events. It modifies
* a "person" document and ensures that list contains only the 'modified'
* event.
*/
it('should filter the trail of events by event type', (done: MochaDone) => {
const { colRef, davidDoc } = seedTest(firestore);
const modifiedAudit = auditTrail(colRef, ['modified']).pipe(unwrapChange);
modifiedAudit.subscribe(updateList => {
const expectedEvents = [{ type: 'modified', name: 'David!' }];
expect(updateList).to.eql(expectedEvents);
done();
});
davidDoc.update({ name: 'David!' });
});
});
describe('auditTrail', () => {
/**
* The `auditTrail()` method returns an array of every change that has
* occurred in the application. This test seeds two "people" into the
* collection and checks that the two added events are there. It then
* modifies a "person" and makes sure that event is on the array as well.
*/
it('should keep create a list of all changes', (done: MochaDone) => {
const { colRef, expectedEvents, davidDoc } = seedTest(firestore);
const firstAudit = auditTrail(colRef).pipe(unwrapChange, take(1));
const secondAudit = auditTrail(colRef).pipe(unwrapChange, skip(1));
firstAudit.subscribe(list => {
expect(list).to.eql(expectedEvents);
davidDoc.update({ name: 'David!' });
});
secondAudit.subscribe(list => {
const modifiedList = [
...expectedEvents,
{ name: 'David!', type: 'modified' }
];
expect(list).to.eql(modifiedList);
done();
});
});
/**
* This test seeds two "people" into the collection. The wrap operator then converts
*/
it('should filter the trail of events by event type', (done: MochaDone) => {
const { colRef, davidDoc } = seedTest(firestore);
const modifiedAudit = auditTrail(colRef, ['modified']).pipe(unwrapChange);
modifiedAudit.subscribe(updateList => {
const expectedEvents = [{ type: 'modified', name: 'David!' }];
expect(updateList).to.eql(expectedEvents);
done();
});
davidDoc.update({ name: 'David!' });
});
});
describe('Data Mapping Functions', () => {
/**
* The `unwrap(id)` method will map a collection to its data payload and map the doc ID to a the specificed key.
*/
it('collectionData should map a QueryDocumentSnapshot[] to an array of plain objects', (done: MochaDone) => {
const { colRef } = seedTest(firestore);
// const unwrapped = collection(colRef).pipe(unwrap('userId'));
const unwrapped = collectionData(colRef, 'userId');
unwrapped.subscribe(val => {
const expectedDoc = {
name: 'David',
userId: 'david'
};
expect(val).to.be.instanceof(Array);
expect(val[0]).to.eql(expectedDoc);
done();
});
});
it('docData should map a QueryDocumentSnapshot to a plain object', (done: MochaDone) => {
const { davidDoc } = seedTest(firestore);
// const unwrapped = doc(davidDoc).pipe(unwrap('UID'));
const unwrapped = docData(davidDoc, 'UID');
unwrapped.subscribe(val => {
const expectedDoc = {
name: 'David',
UID: 'david'
};
expect(val).to.eql(expectedDoc);
done();
});
});
});
});