Skip to content

Commit 3be3ad8

Browse files
committed
chore(tests): Firestore tests
1 parent 899a066 commit 3be3ad8

File tree

4 files changed

+225
-48
lines changed

4 files changed

+225
-48
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "The official library of Firebase and Angular.",
55
"private": true,
66
"scripts": {
7-
"test": "karma start --single-run",
7+
"test": "npm run build && karma start --single-run",
88
"test:watch": "concurrently \"npm run build:watch\" \"npm run delayed_karma\"",
99
"test:debug": "npm run build && karma start",
1010
"delayed_karma": "sleep 10 && karma start",
@@ -20,7 +20,7 @@
2020
"type": "git",
2121
"url": "git+https://github.com/angular/angularfire2.git"
2222
},
23-
"author": "jeffbcross,davideast",
23+
"author": "davideast",
2424
"license": "MIT",
2525
"bugs": {
2626
"url": "https://github.com/angular/angularfire2/issues"

src/firestore/firestore.spec.ts

Lines changed: 196 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FirebaseApp, FirebaseAppConfig, AngularFireModule} from 'angularfire2';
1+
import { FirebaseApp, FirebaseAppConfig, AngularFireModule } from 'angularfire2';
22
import { AngularFirestore, AngularFirestoreModule, AngularFirestoreDocument, AngularFirestoreCollection } from 'angularfire2/firestore';
33
import * as firebase from 'firebase/app';
44
import * as firestore from 'firestore';
@@ -15,12 +15,30 @@ interface Stock {
1515

1616
const FAKE_STOCK_DATA = { name: 'FAKE', price: 1 };
1717

18-
fdescribe('Firestore', () => {
18+
const randomName = (firestore): string => firestore.collection('a').doc().id;
19+
20+
const createRandomStocks = async (firestore: firestore.Firestore, collectionRef: firestore.CollectionReference, numberOfItems) => {
21+
// Create a batch to update everything at once
22+
const batch = firestore.batch();
23+
// Store the random names to delete them later
24+
let count = 0;
25+
let names: string[] = [];
26+
Array.from(Array(numberOfItems)).forEach((a, i) => {
27+
const name = randomName(firestore);
28+
batch.set(collectionRef.doc(name), FAKE_STOCK_DATA);
29+
names = [...names, name];
30+
});
31+
// Create the batch entries
32+
// Commit!
33+
await batch.commit();
34+
return names;
35+
}
36+
37+
describe('Firestore', () => {
1938
let app: firebase.app.App;
2039
let afs: AngularFirestore;
21-
let stock: AngularFirestoreDocument<Stock>;
2240
let sub: Subscription;
23-
41+
2442
beforeEach(() => {
2543
TestBed.configureTestingModule({
2644
imports: [
@@ -31,16 +49,11 @@ fdescribe('Firestore', () => {
3149
inject([FirebaseApp, AngularFirestore], (_app: firebase.app.App, _afs: AngularFirestore) => {
3250
app = _app;
3351
afs = _afs;
34-
stock = afs.doc('stocks/FAKE');
3552
})();
3653
});
3754

3855
afterEach(async (done) => {
3956
await app.delete();
40-
//await stock.delete();
41-
if(sub) {
42-
sub.unsubscribe();
43-
}
4457
done();
4558
});
4659

@@ -54,42 +67,187 @@ fdescribe('Firestore', () => {
5467
expect(afs.app).toBeDefined();
5568
});
5669

57-
describe('AngularFirestore#document', () => {
70+
it('should create an AngularFirestoreDocument', () => {
71+
const doc = afs.doc('a/doc');
72+
expect(doc instanceof AngularFirestoreDocument).toBe(true);
73+
});
5874

59-
it('should throw on an invalid path', () => {
60-
const singleWrapper = () => afs.doc('collection');
61-
const tripleWrapper = () => afs.doc('collection/doc/subcollection');
62-
expect(singleWrapper).toThrowError();
63-
expect(tripleWrapper).toThrowError();
64-
});
75+
it('should create an AngularFirestoreCollection', () => {
76+
const collection = afs.collection('stuffs');
77+
expect(collection instanceof AngularFirestoreCollection).toBe(true);
78+
});
79+
80+
it('should throw on an invalid document path', () => {
81+
const singleWrapper = () => afs.doc('collection');
82+
const tripleWrapper = () => afs.doc('collection/doc/subcollection');
83+
expect(singleWrapper).toThrowError();
84+
expect(tripleWrapper).toThrowError();
85+
});
6586

66-
it('should get data as an Observable', async(done: any) => {
67-
//await stock.set(FAKE_STOCK_DATA);
68-
const obs$ = Observable.from(stock);
69-
obs$.catch(e => { console.log(e); return e; })
70-
.take(1) // this will unsubscribe after the first
71-
.subscribe(async (data: Stock) => {
72-
debugger;
73-
expect(JSON.stringify(data)).toBe(JSON.stringify(FAKE_STOCK_DATA));
87+
it('should throw on an invalid collection path', () => {
88+
const singleWrapper = () => afs.collection('collection/doc');
89+
const quadWrapper = () => afs.collection('collection/doc/subcollection/doc');
90+
expect(singleWrapper).toThrowError();
91+
expect(quadWrapper).toThrowError();
92+
});
93+
94+
});
95+
96+
describe('AngularFirestoreDocument', () => {
97+
98+
it('should get unwrapped data as an Observable', async (done: any) => {
99+
const randomCollectionName = afs.firestore.collection('a').doc().id;
100+
const ref = afs.firestore.doc(`${randomCollectionName}/FAKE`);
101+
const stock = new AngularFirestoreDocument<Stock>(ref);
102+
await stock.set(FAKE_STOCK_DATA);
103+
const obs$ = Observable.from(stock);
104+
const sub = obs$.catch(e => { console.log(e); return e; })
105+
.take(1) // this will unsubscribe after the first
106+
.subscribe(async (data: Stock) => {
107+
sub.unsubscribe();
108+
expect(JSON.stringify(data)).toBe(JSON.stringify(FAKE_STOCK_DATA));
109+
stock.delete().then(done).catch(done.fail);
110+
});
111+
});
112+
113+
it('should get snapshot updates', async (done: any) => {
114+
const randomCollectionName = randomName(afs.firestore);
115+
const ref = afs.firestore.doc(`${randomCollectionName}/FAKE`);
116+
const stock = new AngularFirestoreDocument<Stock>(ref);
117+
await stock.set(FAKE_STOCK_DATA);
118+
const sub = stock
119+
.snapshotChanges()
120+
.subscribe(async a => {
121+
sub.unsubscribe();
122+
if (a.exists) {
123+
expect(a.data()).toEqual(FAKE_STOCK_DATA);
74124
stock.delete().then(done).catch(done.fail);
75-
});
125+
}
126+
});
127+
});
128+
129+
it('should get unwrapped snapshot', async (done: any) => {
130+
const randomCollectionName = afs.firestore.collection('a').doc().id;
131+
const ref = afs.firestore.doc(`${randomCollectionName}/FAKE`);
132+
const stock = new AngularFirestoreDocument<Stock>(ref);
133+
await stock.set(FAKE_STOCK_DATA);
134+
const obs$ = stock.valueChanges();
135+
const sub = obs$.catch(e => { console.log(e); return e; })
136+
.take(1) // this will unsubscribe after the first
137+
.subscribe(async (data: Stock) => {
138+
sub.unsubscribe();
139+
expect(JSON.stringify(data)).toBe(JSON.stringify(FAKE_STOCK_DATA));
140+
stock.delete().then(done).catch(done.fail);
141+
});
142+
});
143+
144+
145+
});
146+
147+
describe('AngularFirestoreCollection', () => {
148+
149+
it('should get unwrapped snapshot', async (done: any) => {
150+
const randomCollectionName = randomName(afs.firestore);
151+
const ref = afs.firestore.collection(`${randomCollectionName}`);
152+
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
153+
const ITEMS = 4;
154+
155+
const names = await createRandomStocks(afs.firestore, ref, ITEMS)
156+
157+
const sub = stocks.valueChanges().subscribe(data => {
158+
// unsub immediately as we will be deleting data at the bottom
159+
// and that will trigger another subscribe callback and fail
160+
// the test
161+
sub.unsubscribe();
162+
// We added four things. This should be four.
163+
// This could not be four if the batch failed or
164+
// if the collection state is altered during a test run
165+
expect(data.length).toEqual(ITEMS);
166+
data.forEach(stock => {
167+
// We used the same piece of data so they should all equal
168+
expect(stock).toEqual(FAKE_STOCK_DATA);
169+
});
170+
// Delete them all
171+
const promises = names.map(name => ref.doc(name).delete());
172+
Promise.all(promises).then(done).catch(fail);
76173
});
77174

78-
it('should get realtime updates', async(done: Function) => {
79-
// pick a new stock
80-
//await stock.set(FAKE_STOCK_DATA);
81-
sub = stock
82-
.valueChanges()
83-
.subscribe(async a => {
84-
debugger;
85-
if(a.exists) {
86-
expect(a.data()).toEqual(FAKE_STOCK_DATA);
87-
await stock.delete();
88-
done();
89-
}
90-
});
175+
});
176+
177+
it('should get snapshot updates', async (done: any) => {
178+
const randomCollectionName = randomName(afs.firestore);
179+
const ref = afs.firestore.collection(`${randomCollectionName}`);
180+
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
181+
const ITEMS = 10;
182+
183+
const names = await createRandomStocks(afs.firestore, ref, ITEMS);
184+
185+
const sub = stocks.snapshotChanges().subscribe(data => {
186+
// unsub immediately as we will be deleting data at the bottom
187+
// and that will trigger another subscribe callback and fail
188+
// the test
189+
sub.unsubscribe();
190+
// We added ten things. This should be ten.
191+
// This could not be ten if the batch failed or
192+
// if the collection state is altered during a test run
193+
expect(data.docs.length).toEqual(ITEMS);
194+
data.docs.forEach(stock => {
195+
// We used the same piece of data so they should all equal
196+
expect(stock.data()).toEqual(FAKE_STOCK_DATA);
197+
});
198+
// Delete them all
199+
const promises = names.map(name => ref.doc(name).delete());
200+
Promise.all(promises).then(done).catch(fail);
201+
});
202+
203+
});
204+
205+
it('should be able to filter by docChanges', async(done: any) => {
206+
const randomCollectionName = randomName(afs.firestore);
207+
const ref = afs.firestore.collection(`${randomCollectionName}`);
208+
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
209+
210+
const added$ = stocks.snapshotChanges()
211+
.map(snap => snap.docChanges.filter(change => change.type === 'added'))
212+
.filter(snap => snap.length > 0);
213+
214+
const modified$ = stocks.snapshotChanges()
215+
.map(snap => snap.docChanges.filter(change => change.type === 'modified'))
216+
.filter(snap => snap.length > 0);
217+
218+
const removed$ = stocks.snapshotChanges()
219+
.map(snap => snap.docChanges.filter(change => change.type === 'removed'))
220+
.filter(snap => snap.length > 0);
221+
222+
added$.subscribe(added => {
223+
// there should only be one addition
224+
expect(added.length).toEqual(1)
225+
const change = added[0];
226+
expect(change.doc.data()).toEqual(FAKE_STOCK_DATA);
91227
});
92228

229+
modified$.subscribe(added => {
230+
// there should only be one modification
231+
expect(added.length).toEqual(1);
232+
const change = added[0];
233+
expect(change.doc.data()).toEqual({ name: 'FAKE', price: 2 });
234+
});
235+
236+
removed$.subscribe(added => {
237+
// there should only be one removal
238+
expect(added.length).toEqual(1);
239+
const change = added[0];
240+
expect(change.doc.data()).toEqual({ name: 'FAKE', price: 2 });
241+
// delete and done
242+
change.doc.ref.delete().then(done).catch(done.fail);
243+
});
244+
245+
const randomDocName = randomName(afs.firestore);
246+
const addedRef = stocks.doc(randomDocName);
247+
addedRef.set(FAKE_STOCK_DATA);
248+
addedRef.update({ price: 2 });
249+
addedRef.delete();
250+
93251
});
94252

95253
});

src/firestore/firestore.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'firestore';
33
import { Firestore, CollectionReference, DocumentReference, Query, DocumentChangeType, SnapshotMetadata, DocumentSnapshot, QuerySnapshot, DocumentChange } from 'firestore';
44
import { Observable } from 'rxjs/Observable';
55
import { Subscriber } from 'rxjs/Subscriber';
6+
import 'rxjs/add/operator/map';
67

78
import { Injectable } from '@angular/core';
89
import { FirebaseApp } from 'angularfire2';
@@ -231,15 +232,24 @@ export class AngularFirestoreDocument<T> implements ArrayLike<T> {
231232
}
232233

233234
/**
234-
* Listen to data in the document.
235+
* Listen to snapshot updates from the document.
235236
*/
236-
valueChanges(): Observable<DocumentSnapshot> {
237+
snapshotChanges(): Observable<DocumentSnapshot> {
237238
return new Observable<DocumentSnapshot>(subscriber => {
238239
const unsubscribe = this.ref.onSnapshot(subscriber);
239240
return { unsubscribe };
240241
});
241242
}
242243

244+
/**
245+
* Listen to unwrapped snapshot updates from the document.
246+
*/
247+
valueChanges(): Observable<T> {
248+
return this.snapshotChanges().map(snap => {
249+
return (snap.exists ? snap.data() : null) as T;
250+
});
251+
}
252+
243253
/**
244254
* Provide the ability to create an Observable from this reference. The
245255
* underlying reference's onSnapshot() method is compatible with rxjs's
@@ -315,19 +325,27 @@ export class AngularFirestoreCollection<T> implements ArrayLike<T> {
315325
*/
316326
constructor(
317327
public readonly ref: CollectionReference,
318-
public readonly query: Query) { }
328+
private readonly query: Query) { }
319329

320330
/**
321331
* Listen to all documents in the collection and its possible query as an Observable.
322332
* This method returns a stream of DocumentSnapshots which gives the ability to get
323333
* the data set back as array and/or the delta updates in the collection.
324-
*/
325-
valueChanges<T>(): Observable<T[]> {
334+
*/
335+
snapshotChanges(): Observable<QuerySnapshot> {
326336
return new Observable<QuerySnapshot>(subscriber => {
327337
const unsubscribe = this.query.onSnapshot(subscriber);
328338
return { unsubscribe };
329-
})
330-
.map(snap => snap.docs.map(doc => doc.data()) as T[]);
339+
});
340+
}
341+
342+
/**
343+
* Listen to all documents in the collection and its possible query as an Observable.
344+
* This method returns a stream of unwrapped snapshots.
345+
*/
346+
valueChanges(): Observable<T[]> {
347+
return this.snapshotChanges()
348+
.map(snap => snap.docs.map(doc => doc.data()) as T[]);
331349
}
332350

333351
/**
@@ -363,7 +381,7 @@ export class AngularFirestoreCollection<T> implements ArrayLike<T> {
363381
* on a non-existent document. We are converting to null to provide
364382
* flexibility in the view template.
365383
*/
366-
[Symbol.observable]() {
384+
[Symbol.observable] () {
367385
const query = this.query;
368386
return {
369387
subscribe(subscriber: Subscriber<T[]>) {

src/firestore/tsconfig-test.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"extends": "./tsconfig-esm.json",
33
"compilerOptions": {
44
"baseUrl": ".",
5+
"target": "es2015",
56
"paths": {
67
"angularfire2": ["../../dist/packages-dist"],
78
"angularfire2/firestore": ["../../dist/packages-dist/firestore"]

0 commit comments

Comments
 (0)