Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions packages/firestore/__tests__/firestore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import FirebaseModule from '../../app/lib/internal/FirebaseModule';
import Query from '../lib/FirestoreQuery';
// @ts-ignore test
import FirestoreDocumentSnapshot from '../lib/FirestoreDocumentSnapshot';
import { parseSnapshotArgs } from '../lib/utils';
// @ts-ignore test
import * as nativeModule from '@react-native-firebase/app/dist/module/internal/nativeModuleAndroidIos';

Expand Down Expand Up @@ -498,6 +499,35 @@ describe('Firestore', function () {
});
});
});

describe('onSnapshot()', function () {
it("accepts { source: 'cache' } listener options", function () {
const parsed = parseSnapshotArgs([{ source: 'cache' }, () => {}]);

expect(parsed.snapshotListenOptions).toEqual({
includeMetadataChanges: false,
source: 'cache',
});
});

it("accepts { source: 'default', includeMetadataChanges: true } listener options", function () {
const parsed = parseSnapshotArgs([
{ source: 'default', includeMetadataChanges: true },
() => {},
]);

expect(parsed.snapshotListenOptions).toEqual({
includeMetadataChanges: true,
source: 'default',
});
});

it("throws for unsupported listener source value 'server'", function () {
expect(() =>
parseSnapshotArgs([{ source: 'server' as 'default' | 'cache' }, () => {}]),
).toThrow("'options' SnapshotOptions.source must be one of 'default' or 'cache'.");
});
});
});

describe('modular', function () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ private void handleQueryOnSnapshot(
int listenerId,
ReadableMap listenerOptions) {
MetadataChanges metadataChanges;
SnapshotListenOptions.Builder snapshotListenOptionsBuilder = new SnapshotListenOptions.Builder();

if (listenerOptions != null
&& listenerOptions.hasKey("includeMetadataChanges")
Expand All @@ -342,6 +343,15 @@ private void handleQueryOnSnapshot(
} else {
metadataChanges = MetadataChanges.EXCLUDE;
}
snapshotListenOptionsBuilder.setMetadataChanges(metadataChanges);

if (listenerOptions != null
&& listenerOptions.hasKey("source")
&& "cache".equals(listenerOptions.getString("source"))) {
snapshotListenOptionsBuilder.setSource(ListenSource.CACHE);
} else {
snapshotListenOptionsBuilder.setSource(ListenSource.DEFAULT);
}

final EventListener<QuerySnapshot> listener =
(querySnapshot, exception) -> {
Expand All @@ -358,7 +368,7 @@ private void handleQueryOnSnapshot(
};

ListenerRegistration listenerRegistration =
firestoreQuery.query.addSnapshotListener(metadataChanges, listener);
firestoreQuery.query.addSnapshotListener(snapshotListenOptionsBuilder.build(), listener);

collectionSnapshotListeners.put(listenerId, listenerRegistration);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,26 @@ public void documentOnSnapshot(
}
};

MetadataChanges metadataChanges;
SnapshotListenOptions.Builder snapshotListenOptionsBuilder = new SnapshotListenOptions.Builder();

if (listenerOptions != null
&& listenerOptions.hasKey("includeMetadataChanges")
&& listenerOptions.getBoolean("includeMetadataChanges")) {
metadataChanges = MetadataChanges.INCLUDE;
snapshotListenOptionsBuilder.setMetadataChanges(MetadataChanges.INCLUDE);
} else {
metadataChanges = MetadataChanges.EXCLUDE;
snapshotListenOptionsBuilder.setMetadataChanges(MetadataChanges.EXCLUDE);
}

if (listenerOptions != null
&& listenerOptions.hasKey("source")
&& "cache".equals(listenerOptions.getString("source"))) {
snapshotListenOptionsBuilder.setSource(ListenSource.CACHE);
} else {
snapshotListenOptionsBuilder.setSource(ListenSource.DEFAULT);
}

ListenerRegistration listenerRegistration =
documentReference.addSnapshotListener(metadataChanges, listener);
documentReference.addSnapshotListener(snapshotListenOptionsBuilder.build(), listener);

documentSnapshotListeners.put(listenerId, listenerRegistration);
}
Expand Down
156 changes: 155 additions & 1 deletion packages/firestore/e2e/DocumentReference/onSnapshot.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*/
const COLLECTION = 'firestore';
const NO_RULE_COLLECTION = 'no_rules';
const { wipe } = require('../helpers');
const { wipe, setDocumentOutOfBand } = require('../helpers');

describe('firestore().doc().onSnapshot()', function () {
before(function () {
Expand Down Expand Up @@ -305,6 +305,108 @@ describe('firestore().doc().onSnapshot()', function () {
}
});

it("throws if SnapshotListenerOptions.source is invalid ('server')", function () {
try {
firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot({
source: 'server',
});
return Promise.reject(new Error('Did not throw an Error.'));
} catch (error) {
error.message.should.containEql(
"'options' SnapshotOptions.source must be one of 'default' or 'cache'",
);
return Promise.resolve();
}
});

it('accepts source-only SnapshotListenerOptions', async function () {
if (Platform.other) {
return;
}
const callback = sinon.spy();
const unsub = firebase.firestore().doc(`${COLLECTION}/source-only`).onSnapshot(
{
source: 'cache',
},
callback,
);

await Utils.spyToBeCalledOnceAsync(callback);
unsub();
});

it('accepts source + includeMetadataChanges SnapshotListenerOptions', async function () {
if (Platform.other) {
return;
}
const callback = sinon.spy();
const unsub = firebase.firestore().doc(`${COLLECTION}/source-with-metadata`).onSnapshot(
{
source: 'default',
includeMetadataChanges: true,
},
callback,
);

await Utils.spyToBeCalledOnceAsync(callback);
unsub();
});

it('cache source listeners ignore out-of-band server writes', async function () {
if (Platform.other) {
return;
}

const docPath = `${COLLECTION}/${Utils.randString(12, '#aA')}`;
const docRef = firebase.firestore().doc(docPath);
await docRef.set({ value: 1 });
await docRef.get();

const callback = sinon.spy();
const unsub = docRef.onSnapshot({ source: 'cache' }, callback);
try {
await Utils.spyToBeCalledOnceAsync(callback);

await setDocumentOutOfBand(docPath, { value: 2 });
await Utils.sleep(1500);
callback.should.be.callCount(1);

await docRef.set({ value: 3 });
await Utils.spyToBeCalledTimesAsync(callback, 2);
callback.args[1][0].get('value').should.equal(3);
} finally {
unsub();
}
});

it('default source listeners receive out-of-band server writes', async function () {
if (Platform.other) {
return;
}

const docPath = `${COLLECTION}/${Utils.randString(12, '#aA')}`;
const docRef = firebase.firestore().doc(docPath);
await docRef.set({ value: 1 });
await docRef.get();

const callback = sinon.spy();
const unsub = docRef.onSnapshot(
{ source: 'default', includeMetadataChanges: true },
callback,
);
try {
await Utils.spyToBeCalledOnceAsync(callback);

await setDocumentOutOfBand(docPath, { value: 2 });
await Utils.spyToBeCalledTimesAsync(callback, 2, 8000);

const latestSnapshot = callback.args[callback.callCount - 1][0];
latestSnapshot.get('value').should.equal(2);
} finally {
unsub();
}
});

it('throws if next callback is invalid', function () {
try {
firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot({
Expand Down Expand Up @@ -616,6 +718,58 @@ describe('firestore().doc().onSnapshot()', function () {
}
});

it("throws if SnapshotListenerOptions.source is invalid ('server')", function () {
const { getFirestore, doc, onSnapshot } = firestoreModular;
try {
onSnapshot(doc(getFirestore(), `${NO_RULE_COLLECTION}/nope`), {
source: 'server',
});
return Promise.reject(new Error('Did not throw an Error.'));
} catch (error) {
error.message.should.containEql(
"'options' SnapshotOptions.source must be one of 'default' or 'cache'",
);
return Promise.resolve();
}
});

it('accepts source-only SnapshotListenerOptions', async function () {
if (Platform.other) {
return;
}
const { getFirestore, doc, onSnapshot } = firestoreModular;
const callback = sinon.spy();
const unsub = onSnapshot(
doc(getFirestore(), `${COLLECTION}/mod-source-only`),
{
source: 'cache',
},
callback,
);

await Utils.spyToBeCalledOnceAsync(callback);
unsub();
});

it('accepts source + includeMetadataChanges SnapshotListenerOptions', async function () {
if (Platform.other) {
return;
}
const { getFirestore, doc, onSnapshot } = firestoreModular;
const callback = sinon.spy();
const unsub = onSnapshot(
doc(getFirestore(), `${COLLECTION}/mod-source-with-metadata`),
{
source: 'default',
includeMetadataChanges: true,
},
callback,
);

await Utils.spyToBeCalledOnceAsync(callback);
unsub();
});

it('throws if next callback is invalid', function () {
const { getFirestore, doc, onSnapshot } = firestoreModular;
try {
Expand Down
80 changes: 79 additions & 1 deletion packages/firestore/e2e/Query/onSnapshot.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*
*/
const { wipe } = require('../helpers');
const { wipe, setDocumentOutOfBand } = require('../helpers');
const COLLECTION = 'firestore';
const NO_RULE_COLLECTION = 'no_rules';

Expand Down Expand Up @@ -319,6 +319,69 @@ describe('firestore().collection().onSnapshot()', function () {
}
});

it("throws if SnapshotListenerOptions.source is invalid ('server')", function () {
try {
firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot({
source: 'server',
});
return Promise.reject(new Error('Did not throw an Error.'));
} catch (error) {
error.message.should.containEql(
"'options' SnapshotOptions.source must be one of 'default' or 'cache'",
);
return Promise.resolve();
}
});

it('cache source query listeners ignore out-of-band server writes', async function () {
if (Platform.other) {
return;
}

const collectionPath = `${COLLECTION}/${Utils.randString(12, '#aA')}/cache-source`;
const colRef = firebase.firestore().collection(collectionPath);
await colRef.doc('one').set({ enabled: true });
await colRef.get();

const callback = sinon.spy();
const unsub = colRef.onSnapshot({ source: 'cache' }, callback);
try {
await Utils.spyToBeCalledOnceAsync(callback);
await setDocumentOutOfBand(`${collectionPath}/server-write`, { enabled: false });
await Utils.sleep(1500);
callback.should.be.callCount(1);

await colRef.doc('local-write').set({ enabled: true });
await Utils.spyToBeCalledTimesAsync(callback, 2);
} finally {
unsub();
}
});

it('default source query listeners receive out-of-band server writes', async function () {
if (Platform.other) {
return;
}

const collectionPath = `${COLLECTION}/${Utils.randString(12, '#aA')}/cache-source-meta`;
const colRef = firebase.firestore().collection(collectionPath);
await colRef.doc('one').set({ enabled: true });
await colRef.get();

const callback = sinon.spy();
const unsub = colRef.onSnapshot(
{ source: 'default', includeMetadataChanges: true },
callback,
);
try {
await Utils.spyToBeCalledOnceAsync(callback);
await setDocumentOutOfBand(`${collectionPath}/server-write`, { enabled: false });
await Utils.spyToBeCalledTimesAsync(callback, 2, 8000);
} finally {
unsub();
}
});

it('throws if next callback is invalid', function () {
try {
firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot({
Expand Down Expand Up @@ -637,6 +700,21 @@ describe('firestore().collection().onSnapshot()', function () {
}
});

it("throws if SnapshotListenerOptions.source is invalid ('server')", function () {
const { getFirestore, collection, onSnapshot } = firestoreModular;
try {
onSnapshot(collection(getFirestore(), NO_RULE_COLLECTION), {
source: 'server',
});
return Promise.reject(new Error('Did not throw an Error.'));
} catch (error) {
error.message.should.containEql(
"'options' SnapshotOptions.source must be one of 'default' or 'cache'",
);
return Promise.resolve();
}
});

it('throws if next callback is invalid', function () {
const { getFirestore, collection, onSnapshot } = firestoreModular;
try {
Expand Down
Loading
Loading