[firestore] Support all `onSnapshot` parameter options

This commit is contained in:
Chris Bianca 2017-10-06 12:00:40 +01:00
parent 5f11da9d7e
commit 2575fb4956
13 changed files with 807 additions and 444 deletions

View File

@ -57,9 +57,10 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule {
@ReactMethod
public void collectionOnSnapshot(String appName, String path, ReadableArray filters,
ReadableArray orders, ReadableMap options, String listenerId) {
ReadableArray orders, ReadableMap options, String listenerId,
ReadableMap queryListenOptions) {
RNFirebaseFirestoreCollectionReference ref = getCollectionForAppPath(appName, path, filters, orders, options);
ref.onSnapshot(listenerId);
ref.onSnapshot(listenerId, queryListenOptions);
}
@ -145,9 +146,10 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule {
}
@ReactMethod
public void documentOnSnapshot(String appName, String path, String listenerId) {
public void documentOnSnapshot(String appName, String path, String listenerId,
ReadableMap docListenOptions) {
RNFirebaseFirestoreDocumentReference ref = getDocumentForAppPath(appName, path);
ref.onSnapshot(listenerId);
ref.onSnapshot(listenerId, docListenOptions);
}
@ReactMethod

View File

@ -12,10 +12,12 @@ import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.firestore.DocumentListenOptions;
import com.google.firebase.firestore.EventListener;
import com.google.firebase.firestore.FirebaseFirestoreException;
import com.google.firebase.firestore.ListenerRegistration;
import com.google.firebase.firestore.Query;
import com.google.firebase.firestore.QueryListenOptions;
import com.google.firebase.firestore.QuerySnapshot;
import java.util.HashMap;
@ -71,7 +73,7 @@ public class RNFirebaseFirestoreCollectionReference {
}
}
public void onSnapshot(final String listenerId) {
public void onSnapshot(final String listenerId, final ReadableMap queryListenOptions) {
if (!collectionSnapshotListeners.containsKey(listenerId)) {
final EventListener<QuerySnapshot> listener = new EventListener<QuerySnapshot>() {
@Override
@ -87,7 +89,19 @@ public class RNFirebaseFirestoreCollectionReference {
}
}
};
ListenerRegistration listenerRegistration = this.query.addSnapshotListener(listener);
QueryListenOptions options = new QueryListenOptions();
if (queryListenOptions != null) {
if (queryListenOptions.hasKey("includeDocumentMetadataChanges")
&& queryListenOptions.getBoolean("includeDocumentMetadataChanges")) {
options.includeDocumentMetadataChanges();
}
if (queryListenOptions.hasKey("includeQueryMetadataChanges")
&& queryListenOptions.getBoolean("includeQueryMetadataChanges")) {
options.includeQueryMetadataChanges();
}
}
ListenerRegistration listenerRegistration = this.query.addSnapshotListener(options, listener);
collectionSnapshotListeners.put(listenerId, listenerRegistration);
}
}

View File

@ -10,6 +10,7 @@ import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.firestore.DocumentListenOptions;
import com.google.firebase.firestore.DocumentReference;
import com.google.firebase.firestore.DocumentSnapshot;
import com.google.firebase.firestore.EventListener;
@ -85,7 +86,7 @@ public class RNFirebaseFirestoreDocumentReference {
}
}
public void onSnapshot(final String listenerId) {
public void onSnapshot(final String listenerId, final ReadableMap docListenOptions) {
if (!documentSnapshotListeners.containsKey(listenerId)) {
final EventListener<DocumentSnapshot> listener = new EventListener<DocumentSnapshot>() {
@Override
@ -101,7 +102,11 @@ public class RNFirebaseFirestoreDocumentReference {
}
}
};
ListenerRegistration listenerRegistration = this.ref.addSnapshotListener(listener);
DocumentListenOptions options = new DocumentListenOptions();
if (docListenOptions != null && docListenOptions.hasKey("includeMetadataChanges") && docListenOptions.getBoolean("includeMetadataChanges")) {
options.includeMetadataChanges();
}
ListenerRegistration listenerRegistration = this.ref.addSnapshotListener(options, listener);
documentSnapshotListeners.put(listenerId, listenerRegistration);
}
}

View File

@ -42,9 +42,10 @@ RCT_EXPORT_METHOD(collectionOnSnapshot:(NSString *) appName
filters:(NSArray *) filters
orders:(NSArray *) orders
options:(NSDictionary *) options
listenerId:(nonnull NSString *) listenerId) {
listenerId:(nonnull NSString *) listenerId
queryListenOptions:(NSDictionary *) queryListenOptions) {
RNFirebaseFirestoreCollectionReference *ref = [self getCollectionForAppPath:appName path:path filters:filters orders:orders options:options];
[ref onSnapshot:listenerId];
[ref onSnapshot:listenerId queryListenOptions:queryListenOptions];
}
RCT_EXPORT_METHOD(documentBatch:(NSString *) appName
@ -128,9 +129,10 @@ RCT_EXPORT_METHOD(documentOffSnapshot:(NSString *) appName
RCT_EXPORT_METHOD(documentOnSnapshot:(NSString *) appName
path:(NSString *) path
listenerId:(nonnull NSString *) listenerId) {
listenerId:(nonnull NSString *) listenerId
docListenOptions:(NSDictionary *) docListenOptions) {
RNFirebaseFirestoreDocumentReference *ref = [self getDocumentForAppPath:appName path:path];
[ref onSnapshot:listenerId];
[ref onSnapshot:listenerId docListenOptions:docListenOptions];
}
RCT_EXPORT_METHOD(documentSet:(NSString *) appName

View File

@ -22,7 +22,7 @@
- (id)initWithPathAndModifiers:(RCTEventEmitter *)emitter app:(NSString *)app path:(NSString *)path filters:(NSArray *)filters orders:(NSArray *)orders options:(NSDictionary *)options;
- (void)get:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject;
+ (void)offSnapshot:(NSString *)listenerId;
- (void)onSnapshot:(NSString *)listenerId;
- (void)onSnapshot:(NSString *)listenerId queryListenOptions:(NSDictionary *) queryListenOptions;
+ (NSDictionary *)snapshotToDictionary:(FIRQuerySnapshot *)querySnapshot;
@end

View File

@ -49,7 +49,8 @@ static NSMutableDictionary *_listeners;
}
}
- (void)onSnapshot:(NSString *) listenerId {
- (void)onSnapshot:(NSString *) listenerId
queryListenOptions:(NSDictionary *) queryListenOptions {
if (_listeners[listenerId] == nil) {
id listenerBlock = ^(FIRQuerySnapshot * _Nullable snapshot, NSError * _Nullable error) {
if (error) {
@ -63,7 +64,18 @@ static NSMutableDictionary *_listeners;
[self handleQuerySnapshotEvent:listenerId querySnapshot:snapshot];
}
};
id<FIRListenerRegistration> listener = [_query addSnapshotListener:listenerBlock];
FIRQueryListenOptions *options = [[FIRQueryListenOptions alloc] init];
if (queryListenOptions) {
if (queryListenOptions[@"includeDocumentMetadataChanges"]) {
[options includeDocumentMetadataChanges:TRUE];
}
if (queryListenOptions[@"includeQueryMetadataChanges"]) {
[options includeQueryMetadataChanges:TRUE];
}
}
id<FIRListenerRegistration> listener = [_query addSnapshotListenerWithOptions:options listener:listenerBlock];
_listeners[listenerId] = listener;
}
}

View File

@ -22,7 +22,7 @@
- (void)delete:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject;
- (void)get:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject;
+ (void)offSnapshot:(NSString *)listenerId;
- (void)onSnapshot:(NSString *)listenerId;
- (void)onSnapshot:(NSString *)listenerId docListenOptions:(NSDictionary *) docListenOptions;
- (void)set:(NSDictionary *)data options:(NSDictionary *)options resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject;
- (void)update:(NSDictionary *)data resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject;
- (BOOL)hasListeners;

View File

@ -61,7 +61,8 @@ static NSMutableDictionary *_listeners;
}
}
- (void)onSnapshot:(NSString *) listenerId {
- (void)onSnapshot:(NSString *) listenerId
docListenOptions:(NSDictionary *) docListenOptions {
if (_listeners[listenerId] == nil) {
id listenerBlock = ^(FIRDocumentSnapshot * _Nullable snapshot, NSError * _Nullable error) {
if (error) {
@ -75,8 +76,11 @@ static NSMutableDictionary *_listeners;
[self handleDocumentSnapshotEvent:listenerId documentSnapshot:snapshot];
}
};
id<FIRListenerRegistration> listener = [_ref addSnapshotListener:listenerBlock];
FIRDocumentListenOptions *options = [[FIRDocumentListenOptions alloc] init];
if (docListenOptions && docListenOptions[@"includeMetadataChanges"]) {
[options includeMetadataChanges:TRUE];
}
id<FIRListenerRegistration> listener = [_ref addSnapshotListenerWithOptions:options listener:listenerBlock];
_listeners[listenerId] = listener;
}
}

View File

@ -5,12 +5,21 @@
import CollectionReference from './CollectionReference';
import DocumentSnapshot from './DocumentSnapshot';
import Path from './Path';
import { firestoreAutoId } from '../../utils';
import { firestoreAutoId, isFunction, isObject } from '../../utils';
export type WriteOptions = {
merge?: boolean,
}
type DocumentListenOptions = {
includeMetadataChanges: boolean,
}
type Observer = {
next: (DocumentSnapshot) => void,
error?: (Object) => void,
}
/**
* @class DocumentReference
*/
@ -60,13 +69,64 @@ export default class DocumentReference {
.then(result => new DocumentSnapshot(this._firestore, result));
}
onSnapshot(onNext: Function, onError?: Function): () => void {
// TODO: Validation
onSnapshot(
optionsOrObserverOrOnNext: DocumentListenOptions | Observer | (DocumentSnapshot) => void,
observerOrOnNextOrOnError?: Observer | (DocumentSnapshot) => void | (Object) => void,
onError?: (Object) => void
) {
let observer = {};
let docListenOptions = {};
// Called with: onNext, ?onError
if (isFunction(optionsOrObserverOrOnNext)) {
observer.next = optionsOrObserverOrOnNext;
if (observerOrOnNextOrOnError && !isFunction(observerOrOnNextOrOnError)) {
throw new Error('DocumentReference.onSnapshot failed: Second argument must be a valid function.');
}
observer.error = observerOrOnNextOrOnError;
} else if (optionsOrObserverOrOnNext && isObject(optionsOrObserverOrOnNext)) {
// Called with: Observer
if (optionsOrObserverOrOnNext.next) {
if (isFunction(optionsOrObserverOrOnNext.next)) {
if (optionsOrObserverOrOnNext.error && !isFunction(optionsOrObserverOrOnNext.error)) {
throw new Error('DocumentReference.onSnapshot failed: Observer.error must be a valid function.');
}
observer = optionsOrObserverOrOnNext;
} else {
throw new Error('DocumentReference.onSnapshot failed: Observer.next must be a valid function.');
}
} else if (optionsOrObserverOrOnNext.includeMetadataChanges) {
docListenOptions = optionsOrObserverOrOnNext;
// Called with: Options, onNext, ?onError
if (isFunction(observerOrOnNextOrOnError)) {
observer.next = observerOrOnNextOrOnError;
if (onError && !isFunction(onError)) {
throw new Error('DocumentReference.onSnapshot failed: Third argument must be a valid function.');
}
observer.error = onError;
// Called with Options, Observer
} else if (observerOrOnNextOrOnError && isObject(observerOrOnNextOrOnError) && observerOrOnNextOrOnError.next) {
if (isFunction(observerOrOnNextOrOnError.next)) {
if (observerOrOnNextOrOnError.error && !isFunction(observerOrOnNextOrOnError.error)) {
throw new Error('DocumentReference.onSnapshot failed: Observer.error must be a valid function.');
}
observer = observerOrOnNextOrOnError;
} else {
throw new Error('DocumentReference.onSnapshot failed: Observer.next must be a valid function.');
}
} else {
throw new Error('DocumentReference.onSnapshot failed: Second argument must be a function or observer.');
}
} else {
throw new Error('DocumentReference.onSnapshot failed: First argument must be a function, observer or options.');
}
} else {
throw new Error('DocumentReference.onSnapshot failed: Called with invalid arguments.');
}
const listenerId = firestoreAutoId();
const listener = (nativeDocumentSnapshot) => {
const documentSnapshot = new DocumentSnapshot(this, nativeDocumentSnapshot);
onNext(documentSnapshot);
observer.next(documentSnapshot);
};
// Listen to snapshot events
@ -76,16 +136,16 @@ export default class DocumentReference {
);
// Listen for snapshot error events
if (onError) {
if (observer.error) {
this._firestore.on(
this._firestore._getAppEventName(`onDocumentSnapshotError:${listenerId}`),
onError,
observer.error,
);
}
// Add the native listener
this._firestore._native
.documentOnSnapshot(this.path, listenerId);
.documentOnSnapshot(this.path, listenerId, docListenOptions);
// Return an unsubscribe method
return this._offDocumentSnapshot.bind(this, listenerId, listener);

View File

@ -5,8 +5,7 @@
import DocumentSnapshot from './DocumentSnapshot';
import Path from './Path';
import QuerySnapshot from './QuerySnapshot';
import INTERNALS from '../../internals';
import { firestoreAutoId } from '../../utils';
import { firestoreAutoId, isFunction, isObject } from '../../utils';
const DIRECTIONS = {
ASC: 'ASCENDING',
@ -44,6 +43,15 @@ type QueryOptions = {
startAt?: any[],
}
export type Operator = '<' | '<=' | '=' | '==' | '>' | '>=';
type QueryListenOptions = {
includeQueryMetadataChanges: boolean,
includeQueryMetadataChanges: boolean,
}
type Observer = {
next: (DocumentSnapshot) => void,
error?: (Object) => void,
}
/**
* @class Query
@ -116,13 +124,65 @@ export default class Query {
this._fieldOrders, options);
}
onSnapshot(onNext: () => any, onError?: () => any): () => void {
// TODO: Validation
onSnapshot(
optionsOrObserverOrOnNext: QueryListenOptions | Observer | (DocumentSnapshot) => void,
observerOrOnNextOrOnError?: Observer | (DocumentSnapshot) => void | (Object) => void,
onError?: (Object) => void,
) {
let observer = {};
let queryListenOptions = {};
// Called with: onNext, ?onError
if (isFunction(optionsOrObserverOrOnNext)) {
observer.next = optionsOrObserverOrOnNext;
if (observerOrOnNextOrOnError && !isFunction(observerOrOnNextOrOnError)) {
throw new Error('Query.onSnapshot failed: Second argument must be a valid function.');
}
observer.error = observerOrOnNextOrOnError;
} else if (optionsOrObserverOrOnNext && isObject(optionsOrObserverOrOnNext)) {
// Called with: Observer
if (optionsOrObserverOrOnNext.next) {
if (isFunction(optionsOrObserverOrOnNext.next)) {
if (optionsOrObserverOrOnNext.error && !isFunction(optionsOrObserverOrOnNext.error)) {
throw new Error('Query.onSnapshot failed: Observer.error must be a valid function.');
}
observer = optionsOrObserverOrOnNext;
} else {
throw new Error('Query.onSnapshot failed: Observer.next must be a valid function.');
}
} else if (optionsOrObserverOrOnNext.includeDocumentMetadataChanges || optionsOrObserverOrOnNext.includeQueryMetadataChanges) {
queryListenOptions = optionsOrObserverOrOnNext;
// Called with: Options, onNext, ?onError
if (isFunction(observerOrOnNextOrOnError)) {
observer.next = observerOrOnNextOrOnError;
if (onError && !isFunction(onError)) {
throw new Error('Query.onSnapshot failed: Third argument must be a valid function.');
}
observer.error = onError;
// Called with Options, Observer
} else if (observerOrOnNextOrOnError && isObject(observerOrOnNextOrOnError) && observerOrOnNextOrOnError.next) {
if (isFunction(observerOrOnNextOrOnError.next)) {
if (observerOrOnNextOrOnError.error && !isFunction(observerOrOnNextOrOnError.error)) {
throw new Error('Query.onSnapshot failed: Observer.error must be a valid function.');
}
observer = observerOrOnNextOrOnError;
} else {
throw new Error('Query.onSnapshot failed: Observer.next must be a valid function.');
}
} else {
throw new Error('Query.onSnapshot failed: Second argument must be a function or observer.');
}
} else {
throw new Error('Query.onSnapshot failed: First argument must be a function, observer or options.');
}
} else {
throw new Error('Query.onSnapshot failed: Called with invalid arguments.');
}
const listenerId = firestoreAutoId();
const listener = (nativeQuerySnapshot) => {
const querySnapshot = new QuerySnapshot(this._firestore, this, nativeQuerySnapshot);
onNext(querySnapshot);
observer.next(querySnapshot);
};
// Listen to snapshot events
@ -132,10 +192,10 @@ export default class Query {
);
// Listen for snapshot error events
if (onError) {
if (observer.error) {
this._firestore.on(
this._firestore._getAppEventName(`onQuerySnapshotError:${listenerId}`),
onError,
observer.error,
);
}
@ -146,7 +206,8 @@ export default class Query {
this._fieldFilters,
this._fieldOrders,
this._queryOptions,
listenerId
listenerId,
queryListenOptions,
);
// Return an unsubscribe method

View File

@ -129,30 +129,30 @@ PODS:
- nanopb/decode (0.3.8)
- nanopb/encode (0.3.8)
- Protobuf (3.4.0)
- React (0.49.0-rc.6):
- React/Core (= 0.49.0-rc.6)
- React/BatchedBridge (0.49.0-rc.6):
- React (0.49.1):
- React/Core (= 0.49.1)
- React/BatchedBridge (0.49.1):
- React/Core
- React/cxxreact_legacy
- React/Core (0.49.0-rc.6):
- yoga (= 0.49.0-rc.6.React)
- React/cxxreact_legacy (0.49.0-rc.6):
- React/Core (0.49.1):
- yoga (= 0.49.1.React)
- React/cxxreact_legacy (0.49.1):
- React/jschelpers_legacy
- React/fishhook (0.49.0-rc.6)
- React/jschelpers_legacy (0.49.0-rc.6)
- React/RCTBlob (0.49.0-rc.6):
- React/fishhook (0.49.1)
- React/jschelpers_legacy (0.49.1)
- React/RCTBlob (0.49.1):
- React/Core
- React/RCTNetwork (0.49.0-rc.6):
- React/RCTNetwork (0.49.1):
- React/Core
- React/RCTText (0.49.0-rc.6):
- React/RCTText (0.49.1):
- React/Core
- React/RCTWebSocket (0.49.0-rc.6):
- React/RCTWebSocket (0.49.1):
- React/Core
- React/fishhook
- React/RCTBlob
- RNFirebase (3.0.0):
- React
- yoga (0.49.0-rc.6.React)
- yoga (0.49.1.React)
DEPENDENCIES:
- Firebase/AdMob
@ -207,9 +207,9 @@ SPEC CHECKSUMS:
leveldb-library: 10fb39c39e243db4af1828441162405bbcec1404
nanopb: 5601e6bca2dbf1ed831b519092ec110f66982ca3
Protobuf: 03eef2ee0b674770735cf79d9c4d3659cf6908e8
React: e6ef6a41ec6dd1b7941417d60ca582bf5e9c739d
RNFirebase: 2ceda3aef595ea1379bf5bfcc1cd9ee7d6c05d3b
yoga: f9485d2ebf0ca773db2d727ea71b1aa8c9f3e075
React: cf892fb84b7d06bf5fea7f328e554c6dcabe85ee
RNFirebase: 901a473c68fcbaa28125c56a911923f2fbe5d61b
yoga: 3abf02d6d9aeeb139b4c930eb1367feae690a35a
PODFILE CHECKSUM: b5674be55653f5dda937c8b794d0479900643d45

View File

@ -53,262 +53,360 @@ function collectionReferenceTests({ describe, it, context, firebase }) {
});
context('onSnapshot()', () => {
it('calls callback with the initial data and then when document changes', () => {
return new Promise(async (resolve) => {
const collectionRef = firebase.native.firestore().collection('document-tests');
const currentDocValue = { name: 'doc1' };
const newDocValue = { name: 'updated' };
it('calls callback with the initial data and then when document changes', async () => {
const collectionRef = firebase.native.firestore().collection('document-tests');
const currentDocValue = { name: 'doc1' };
const newDocValue = { name: 'updated' };
const callback = sinon.spy();
const callback = sinon.spy();
// Test
// Test
let unsubscribe;
await new Promise((resolve2) => {
unsubscribe = collectionRef.onSnapshot((snapshot) => {
snapshot.forEach(doc => callback(doc.data()));
resolve2();
});
let unsubscribe;
await new Promise((resolve2) => {
unsubscribe = collectionRef.onSnapshot((snapshot) => {
snapshot.forEach(doc => callback(doc.data()));
resolve2();
});
callback.should.be.calledWith(currentDocValue);
const docRef = firebase.native.firestore().doc('document-tests/doc1');
await docRef.set(newDocValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
// Assertions
callback.should.be.calledWith(newDocValue);
callback.should.be.calledTwice();
// Tear down
unsubscribe();
resolve();
});
callback.should.be.calledWith(currentDocValue);
const docRef = firebase.native.firestore().doc('document-tests/doc1');
await docRef.set(newDocValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
// Assertions
callback.should.be.calledWith(newDocValue);
callback.should.be.calledTwice();
// Tear down
unsubscribe();
});
});
context('onSnapshot()', () => {
it('calls callback with the initial data and then when document is added', () => {
return new Promise(async (resolve) => {
const collectionRef = firebase.native.firestore().collection('document-tests');
const currentDocValue = { name: 'doc1' };
const newDocValue = { name: 'updated' };
it('calls callback with the initial data and then when document is added', async () => {
const collectionRef = firebase.native.firestore().collection('document-tests');
const currentDocValue = { name: 'doc1' };
const newDocValue = { name: 'updated' };
const callback = sinon.spy();
const callback = sinon.spy();
// Test
// Test
let unsubscribe;
await new Promise((resolve2) => {
unsubscribe = collectionRef.onSnapshot((snapshot) => {
snapshot.forEach(doc => callback(doc.data()));
resolve2();
});
let unsubscribe;
await new Promise((resolve2) => {
unsubscribe = collectionRef.onSnapshot((snapshot) => {
snapshot.forEach(doc => callback(doc.data()));
resolve2();
});
callback.should.be.calledWith(currentDocValue);
const docRef = firebase.native.firestore().doc('document-tests/doc2');
await docRef.set(newDocValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
// Assertions
callback.should.be.calledWith(currentDocValue);
callback.should.be.calledWith(newDocValue);
callback.should.be.calledThrice();
// Tear down
unsubscribe();
resolve();
});
callback.should.be.calledWith(currentDocValue);
const docRef = firebase.native.firestore().doc('document-tests/doc2');
await docRef.set(newDocValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
// Assertions
callback.should.be.calledWith(currentDocValue);
callback.should.be.calledWith(newDocValue);
callback.should.be.calledThrice();
// Tear down
unsubscribe();
});
});
context('onSnapshot()', () => {
it('doesn\'t call callback when the ref is updated with the same value', async () => {
return new Promise(async (resolve) => {
const collectionRef = firebase.native.firestore().collection('document-tests');
const currentDocValue = { name: 'doc1' };
const collectionRef = firebase.native.firestore().collection('document-tests');
const currentDocValue = { name: 'doc1' };
const callback = sinon.spy();
const callback = sinon.spy();
// Test
// Test
let unsubscribe;
await new Promise((resolve2) => {
unsubscribe = collectionRef.onSnapshot((snapshot) => {
let unsubscribe;
await new Promise((resolve2) => {
unsubscribe = collectionRef.onSnapshot((snapshot) => {
snapshot.forEach(doc => callback(doc.data()));
resolve2();
});
});
callback.should.be.calledWith(currentDocValue);
const docRef = firebase.native.firestore().doc('document-tests/doc1');
await docRef.set(currentDocValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
// Assertions
callback.should.be.calledOnce(); // Callback is not called again
// Tear down
unsubscribe();
});
});
context('onSnapshot()', () => {
it('allows binding multiple callbacks to the same ref', async () => {
// Setup
const collectionRef = firebase.native.firestore().collection('document-tests');
const currentDocValue = { name: 'doc1' };
const newDocValue = { name: 'updated' };
const callbackA = sinon.spy();
const callbackB = sinon.spy();
// Test
let unsubscribeA;
let unsubscribeB;
await new Promise((resolve2) => {
unsubscribeA = collectionRef.onSnapshot((snapshot) => {
snapshot.forEach(doc => callbackA(doc.data()));
resolve2();
});
});
await new Promise((resolve2) => {
unsubscribeB = collectionRef.onSnapshot((snapshot) => {
snapshot.forEach(doc => callbackB(doc.data()));
resolve2();
});
});
callbackA.should.be.calledWith(currentDocValue);
callbackA.should.be.calledOnce();
callbackB.should.be.calledWith(currentDocValue);
callbackB.should.be.calledOnce();
const docRef = firebase.native.firestore().doc('document-tests/doc1');
await docRef.set(newDocValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
callbackA.should.be.calledWith(newDocValue);
callbackB.should.be.calledWith(newDocValue);
callbackA.should.be.calledTwice();
callbackB.should.be.calledTwice();
// Tear down
unsubscribeA();
unsubscribeB();
});
});
context('onSnapshot()', () => {
it('listener stops listening when unsubscribed', async () => {
// Setup
const collectionRef = firebase.native.firestore().collection('document-tests');
const currentDocValue = { name: 'doc1' };
const newDocValue = { name: 'updated' };
const callbackA = sinon.spy();
const callbackB = sinon.spy();
// Test
let unsubscribeA;
let unsubscribeB;
await new Promise((resolve2) => {
unsubscribeA = collectionRef.onSnapshot((snapshot) => {
snapshot.forEach(doc => callbackA(doc.data()));
resolve2();
});
});
await new Promise((resolve2) => {
unsubscribeB = collectionRef.onSnapshot((snapshot) => {
snapshot.forEach(doc => callbackB(doc.data()));
resolve2();
});
});
callbackA.should.be.calledWith(currentDocValue);
callbackA.should.be.calledOnce();
callbackB.should.be.calledWith(currentDocValue);
callbackB.should.be.calledOnce();
const docRef = firebase.native.firestore().doc('document-tests/doc1');
await docRef.set(newDocValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
callbackA.should.be.calledWith(newDocValue);
callbackB.should.be.calledWith(newDocValue);
callbackA.should.be.calledTwice();
callbackB.should.be.calledTwice();
// Unsubscribe A
unsubscribeA();
await docRef.set(currentDocValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
callbackB.should.be.calledWith(currentDocValue);
callbackA.should.be.calledTwice();
callbackB.should.be.calledThrice();
// Unsubscribe B
unsubscribeB();
await docRef.set(newDocValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
callbackA.should.be.calledTwice();
callbackB.should.be.calledThrice();
});
});
context('onSnapshot()', () => {
it('supports options and callback', async () => {
const collectionRef = firebase.native.firestore().collection('document-tests');
const currentDocValue = { name: 'doc1' };
const newDocValue = { name: 'updated' };
const callback = sinon.spy();
// Test
let unsubscribe;
await new Promise((resolve2) => {
unsubscribe = collectionRef.onSnapshot({ includeQueryMetadataChanges: true, includeDocumentMetadataChanges: true }, (snapshot) => {
snapshot.forEach(doc => callback(doc.data()));
resolve2();
});
});
callback.should.be.calledWith(currentDocValue);
const docRef = firebase.native.firestore().doc('document-tests/doc1');
await docRef.set(newDocValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
// Assertions
callback.should.be.calledWith(newDocValue);
// Tear down
unsubscribe();
});
});
context('onSnapshot()', () => {
it('supports observer', async () => {
const collectionRef = firebase.native.firestore().collection('document-tests');
const currentDocValue = { name: 'doc1' };
const newDocValue = { name: 'updated' };
const callback = sinon.spy();
// Test
let unsubscribe;
await new Promise((resolve2) => {
const observer = {
next: (snapshot) => {
snapshot.forEach(doc => callback(doc.data()));
resolve2();
});
});
callback.should.be.calledWith(currentDocValue);
const docRef = firebase.native.firestore().doc('document-tests/doc1');
await docRef.set(currentDocValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
// Assertions
callback.should.be.calledOnce(); // Callback is not called again
// Tear down
unsubscribe();
resolve();
},
};
unsubscribe = collectionRef.onSnapshot(observer);
});
callback.should.be.calledWith(currentDocValue);
const docRef = firebase.native.firestore().doc('document-tests/doc1');
await docRef.set(newDocValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
// Assertions
callback.should.be.calledWith(newDocValue);
callback.should.be.calledTwice();
// Tear down
unsubscribe();
});
});
context('onSnapshot()', () => {
it('allows binding multiple callbacks to the same ref', () => {
return new Promise(async (resolve) => {
// Setup
const collectionRef = firebase.native.firestore().collection('document-tests');
const currentDocValue = { name: 'doc1' };
const newDocValue = { name: 'updated' };
it('supports options and observer', async () => {
const collectionRef = firebase.native.firestore().collection('document-tests');
const currentDocValue = { name: 'doc1' };
const newDocValue = { name: 'updated' };
const callbackA = sinon.spy();
const callbackB = sinon.spy();
const callback = sinon.spy();
// Test
let unsubscribeA;
let unsubscribeB;
await new Promise((resolve2) => {
unsubscribeA = collectionRef.onSnapshot((snapshot) => {
snapshot.forEach(doc => callbackA(doc.data()));
// Test
let unsubscribe;
await new Promise((resolve2) => {
const observer = {
next: (snapshot) => {
snapshot.forEach(doc => callback(doc.data()));
resolve2();
});
});
await new Promise((resolve2) => {
unsubscribeB = collectionRef.onSnapshot((snapshot) => {
snapshot.forEach(doc => callbackB(doc.data()));
resolve2();
});
});
callbackA.should.be.calledWith(currentDocValue);
callbackA.should.be.calledOnce();
callbackB.should.be.calledWith(currentDocValue);
callbackB.should.be.calledOnce();
const docRef = firebase.native.firestore().doc('document-tests/doc1');
await docRef.set(newDocValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
callbackA.should.be.calledWith(newDocValue);
callbackB.should.be.calledWith(newDocValue);
callbackA.should.be.calledTwice();
callbackB.should.be.calledTwice();
// Tear down
unsubscribeA();
unsubscribeB();
resolve();
},
};
unsubscribe = collectionRef.onSnapshot({ includeQueryMetadataChanges: true, includeDocumentMetadataChanges: true }, observer);
});
});
});
context('onSnapshot()', () => {
it('listener stops listening when unsubscribed', () => {
return new Promise(async (resolve) => {
// Setup
const collectionRef = firebase.native.firestore().collection('document-tests');
const currentDocValue = { name: 'doc1' };
const newDocValue = { name: 'updated' };
callback.should.be.calledWith(currentDocValue);
const callbackA = sinon.spy();
const callbackB = sinon.spy();
const docRef = firebase.native.firestore().doc('document-tests/doc1');
await docRef.set(newDocValue);
// Test
let unsubscribeA;
let unsubscribeB;
await new Promise((resolve2) => {
unsubscribeA = collectionRef.onSnapshot((snapshot) => {
snapshot.forEach(doc => callbackA(doc.data()));
resolve2();
});
});
await new Promise((resolve2) => {
unsubscribeB = collectionRef.onSnapshot((snapshot) => {
snapshot.forEach(doc => callbackB(doc.data()));
resolve2();
});
});
callbackA.should.be.calledWith(currentDocValue);
callbackA.should.be.calledOnce();
callbackB.should.be.calledWith(currentDocValue);
callbackB.should.be.calledOnce();
const docRef = firebase.native.firestore().doc('document-tests/doc1');
await docRef.set(newDocValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
callbackA.should.be.calledWith(newDocValue);
callbackB.should.be.calledWith(newDocValue);
callbackA.should.be.calledTwice();
callbackB.should.be.calledTwice();
// Unsubscribe A
unsubscribeA();
await docRef.set(currentDocValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
callbackB.should.be.calledWith(currentDocValue);
callbackA.should.be.calledTwice();
callbackB.should.be.calledThrice();
// Unsubscribe B
unsubscribeB();
await docRef.set(newDocValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
callbackA.should.be.calledTwice();
callbackB.should.be.calledThrice();
resolve();
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
// Assertions
callback.should.be.calledWith(newDocValue);
// Tear down
unsubscribe();
});
});

View File

@ -2,7 +2,7 @@ import sinon from 'sinon';
import 'should-sinon';
import should from 'should';
function collectionReferenceTests({ describe, it, context, firebase }) {
function documentReferenceTests({ describe, it, context, firebase }) {
describe('DocumentReference', () => {
context('class', () => {
it('should return instance methods', () => {
@ -29,219 +29,324 @@ function collectionReferenceTests({ describe, it, context, firebase }) {
});
context('onSnapshot()', () => {
it('calls callback with the initial data and then when value changes', () => {
return new Promise(async (resolve) => {
const docRef = firebase.native.firestore().doc('document-tests/doc1');
const currentDataValue = { name: 'doc1' };
const newDataValue = { name: 'updated' };
it('calls callback with the initial data and then when value changes', async () => {
const docRef = firebase.native.firestore().doc('document-tests/doc1');
const currentDataValue = { name: 'doc1' };
const newDataValue = { name: 'updated' };
const callback = sinon.spy();
const callback = sinon.spy();
// Test
// Test
let unsubscribe;
await new Promise((resolve2) => {
unsubscribe = docRef.onSnapshot((snapshot) => {
callback(snapshot.data());
resolve2();
});
let unsubscribe;
await new Promise((resolve2) => {
unsubscribe = docRef.onSnapshot((snapshot) => {
callback(snapshot.data());
resolve2();
});
callback.should.be.calledWith(currentDataValue);
// Update the document
await docRef.set(newDataValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
// Assertions
callback.should.be.calledWith(newDataValue);
callback.should.be.calledTwice();
// Tear down
unsubscribe();
resolve();
});
callback.should.be.calledWith(currentDataValue);
// Update the document
await docRef.set(newDataValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
// Assertions
callback.should.be.calledWith(newDataValue);
callback.should.be.calledTwice();
// Tear down
unsubscribe();
});
});
context('onSnapshot()', () => {
it('doesn\'t call callback when the ref is updated with the same value', async () => {
return new Promise(async (resolve) => {
const docRef = firebase.native.firestore().doc('document-tests/doc1');
const currentDataValue = { name: 'doc1' };
const docRef = firebase.native.firestore().doc('document-tests/doc1');
const currentDataValue = { name: 'doc1' };
const callback = sinon.spy();
const callback = sinon.spy();
// Test
let unsubscribe;
await new Promise((resolve2) => {
unsubscribe = docRef.onSnapshot((snapshot) => {
callback(snapshot.data());
resolve2();
});
});
callback.should.be.calledWith(currentDataValue);
await docRef.set(currentDataValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
// Assertions
callback.should.be.calledOnce(); // Callback is not called again
// Tear down
unsubscribe();
});
});
context('onSnapshot()', () => {
it('allows binding multiple callbacks to the same ref', async () => {
// Setup
const docRef = firebase.native.firestore().doc('document-tests/doc1');
const currentDataValue = { name: 'doc1' };
const newDataValue = { name: 'updated' };
const callbackA = sinon.spy();
const callbackB = sinon.spy();
// Test
let unsubscribeA;
let unsubscribeB;
await new Promise((resolve2) => {
unsubscribeA = docRef.onSnapshot((snapshot) => {
callbackA(snapshot.data());
resolve2();
});
});
await new Promise((resolve2) => {
unsubscribeB = docRef.onSnapshot((snapshot) => {
callbackB(snapshot.data());
resolve2();
});
});
callbackA.should.be.calledWith(currentDataValue);
callbackA.should.be.calledOnce();
callbackB.should.be.calledWith(currentDataValue);
callbackB.should.be.calledOnce();
await docRef.set(newDataValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
callbackA.should.be.calledWith(newDataValue);
callbackB.should.be.calledWith(newDataValue);
callbackA.should.be.calledTwice();
callbackB.should.be.calledTwice();
// Tear down
unsubscribeA();
unsubscribeB();
});
});
context('onSnapshot()', () => {
it('listener stops listening when unsubscribed', async () => {
// Setup
const docRef = firebase.native.firestore().doc('document-tests/doc1');
const currentDataValue = { name: 'doc1' };
const newDataValue = { name: 'updated' };
const callbackA = sinon.spy();
const callbackB = sinon.spy();
// Test
let unsubscribeA;
let unsubscribeB;
await new Promise((resolve2) => {
unsubscribeA = docRef.onSnapshot((snapshot) => {
callbackA(snapshot.data());
resolve2();
});
});
await new Promise((resolve2) => {
unsubscribeB = docRef.onSnapshot((snapshot) => {
callbackB(snapshot.data());
resolve2();
});
});
callbackA.should.be.calledWith(currentDataValue);
callbackA.should.be.calledOnce();
callbackB.should.be.calledWith(currentDataValue);
callbackB.should.be.calledOnce();
await docRef.set(newDataValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
callbackA.should.be.calledWith(newDataValue);
callbackB.should.be.calledWith(newDataValue);
callbackA.should.be.calledTwice();
callbackB.should.be.calledTwice();
// Unsubscribe A
unsubscribeA();
await docRef.set(currentDataValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
callbackB.should.be.calledWith(currentDataValue);
callbackA.should.be.calledTwice();
callbackB.should.be.calledThrice();
// Unsubscribe B
unsubscribeB();
await docRef.set(newDataValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
callbackA.should.be.calledTwice();
callbackB.should.be.calledThrice();
});
});
context('onSnapshot()', () => {
it('supports options and callbacks', async () => {
const docRef = firebase.native.firestore().doc('document-tests/doc1');
const currentDataValue = { name: 'doc1' };
const newDataValue = { name: 'updated' };
const callback = sinon.spy();
// Test
let unsubscribe;
await new Promise((resolve2) => {
unsubscribe = docRef.onSnapshot((snapshot) => {
callback(snapshot.data());
resolve2();
});
let unsubscribe;
await new Promise((resolve2) => {
unsubscribe = docRef.onSnapshot({ includeMetadataChanges: true }, (snapshot) => {
callback(snapshot.data());
resolve2();
});
});
callback.should.be.calledWith(currentDataValue);
callback.should.be.calledWith(currentDataValue);
await docRef.set(currentDataValue);
// Update the document
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
await docRef.set(newDataValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
// Assertions
callback.should.be.calledOnce(); // Callback is not called again
callback.should.be.calledWith(newDataValue);
// Tear down
unsubscribe();
resolve();
});
unsubscribe();
});
});
context('onSnapshot()', () => {
it('allows binding multiple callbacks to the same ref', () => {
return new Promise(async (resolve) => {
// Setup
const docRef = firebase.native.firestore().doc('document-tests/doc1');
const currentDataValue = { name: 'doc1' };
const newDataValue = { name: 'updated' };
it('supports observer', async () => {
const docRef = firebase.native.firestore().doc('document-tests/doc1');
const currentDataValue = { name: 'doc1' };
const newDataValue = { name: 'updated' };
const callbackA = sinon.spy();
const callbackB = sinon.spy();
const callback = sinon.spy();
// Test
let unsubscribeA;
let unsubscribeB;
await new Promise((resolve2) => {
unsubscribeA = docRef.onSnapshot((snapshot) => {
callbackA(snapshot.data());
// Test
let unsubscribe;
await new Promise((resolve2) => {
const observer = {
next: (snapshot) => {
callback(snapshot.data());
resolve2();
});
});
await new Promise((resolve2) => {
unsubscribeB = docRef.onSnapshot((snapshot) => {
callbackB(snapshot.data());
resolve2();
});
});
callbackA.should.be.calledWith(currentDataValue);
callbackA.should.be.calledOnce();
callbackB.should.be.calledWith(currentDataValue);
callbackB.should.be.calledOnce();
await docRef.set(newDataValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
callbackA.should.be.calledWith(newDataValue);
callbackB.should.be.calledWith(newDataValue);
callbackA.should.be.calledTwice();
callbackB.should.be.calledTwice();
// Tear down
unsubscribeA();
unsubscribeB();
resolve();
},
};
unsubscribe = docRef.onSnapshot(observer);
});
callback.should.be.calledWith(currentDataValue);
// Update the document
await docRef.set(newDataValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
// Assertions
callback.should.be.calledWith(newDataValue);
callback.should.be.calledTwice();
// Tear down
unsubscribe();
});
});
context('onSnapshot()', () => {
it('listener stops listening when unsubscribed', () => {
return new Promise(async (resolve) => {
// Setup
const docRef = firebase.native.firestore().doc('document-tests/doc1');
const currentDataValue = { name: 'doc1' };
const newDataValue = { name: 'updated' };
it('supports options and observer', async () => {
const docRef = firebase.native.firestore().doc('document-tests/doc1');
const currentDataValue = { name: 'doc1' };
const newDataValue = { name: 'updated' };
const callbackA = sinon.spy();
const callbackB = sinon.spy();
const callback = sinon.spy();
// Test
let unsubscribeA;
let unsubscribeB;
await new Promise((resolve2) => {
unsubscribeA = docRef.onSnapshot((snapshot) => {
callbackA(snapshot.data());
// Test
let unsubscribe;
await new Promise((resolve2) => {
const observer = {
next: (snapshot) => {
callback(snapshot.data());
resolve2();
});
});
await new Promise((resolve2) => {
unsubscribeB = docRef.onSnapshot((snapshot) => {
callbackB(snapshot.data());
resolve2();
});
});
callbackA.should.be.calledWith(currentDataValue);
callbackA.should.be.calledOnce();
callbackB.should.be.calledWith(currentDataValue);
callbackB.should.be.calledOnce();
await docRef.set(newDataValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
callbackA.should.be.calledWith(newDataValue);
callbackB.should.be.calledWith(newDataValue);
callbackA.should.be.calledTwice();
callbackB.should.be.calledTwice();
// Unsubscribe A
unsubscribeA();
await docRef.set(currentDataValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
callbackB.should.be.calledWith(currentDataValue);
callbackA.should.be.calledTwice();
callbackB.should.be.calledThrice();
// Unsubscribe B
unsubscribeB();
await docRef.set(newDataValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
callbackA.should.be.calledTwice();
callbackB.should.be.calledThrice();
resolve();
},
};
unsubscribe = docRef.onSnapshot({ includeMetadataChanges: true }, observer);
});
callback.should.be.calledWith(currentDataValue);
// Update the document
await docRef.set(newDataValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
// Assertions
callback.should.be.calledWith(newDataValue);
// Tear down
unsubscribe();
});
});
@ -296,4 +401,4 @@ function collectionReferenceTests({ describe, it, context, firebase }) {
});
}
export default collectionReferenceTests;
export default documentReferenceTests;