diff --git a/bridge/e2e/firestore/documentReference.e2e.js b/bridge/e2e/firestore/documentReference.e2e.js index e69de29b..5fac2760 100644 --- a/bridge/e2e/firestore/documentReference.e2e.js +++ b/bridge/e2e/firestore/documentReference.e2e.js @@ -0,0 +1,610 @@ +const { + test2DocRef, + COL2_DOC_1, + COL2_DOC_1_ID, + COL2_DOC_1_PATH, + TEST2_COLLECTION_NAME, + resetTestCollectionDoc, +} = TestHelpers.firestore; + +// TODO cleanup promises and replace with async/await +describe('firestore()', () => { + describe('DocumentReference', () => { + before(async () => { + await resetTestCollectionDoc(COL2_DOC_1_PATH, COL2_DOC_1()); + }); + + describe('class', () => { + it('should return instance methods', () => { + const document = test2DocRef(COL2_DOC_1_ID); + document.should.have.property('firestore'); + // TODO: Remaining checks + }); + }); + + describe('id', () => { + it('should return document id', () => { + const document = test2DocRef(COL2_DOC_1_ID); + document.id.should.equal(COL2_DOC_1_ID); + }); + }); + + describe('parent', () => { + it('should return parent collection', () => { + const document = test2DocRef(COL2_DOC_1_ID); + document.parent.id.should.equal(TEST2_COLLECTION_NAME); + }); + }); + + describe('collection()', () => { + it('should return a child collection', () => { + const document = test2DocRef(COL2_DOC_1_ID); + const collection = document.collection('pages'); + collection.id.should.equal('pages'); + }); + + it('should error if invalid collection path supplied', () => { + (() => { + test2DocRef(COL2_DOC_1_ID).collection('pages/page1'); + }).should.throw( + 'Argument "collectionPath" must point to a collection.' + ); + }); + }); + + describe('delete()', () => { + it('should delete Document', async () => { + await resetTestCollectionDoc(COL2_DOC_1_PATH, COL2_DOC_1()); + await test2DocRef(COL2_DOC_1_ID).delete(); + const doc = await test2DocRef(COL2_DOC_1_ID).get(); + should.equal(doc.exists, false); + }); + }); + + describe('get()', () => { + it('DocumentSnapshot should have correct properties', async () => { + await resetTestCollectionDoc(COL2_DOC_1_PATH, COL2_DOC_1()); + const snapshot = await test2DocRef(COL2_DOC_1_ID).get(); + snapshot.id.should.equal(COL2_DOC_1_ID); + snapshot.metadata.should.be.an.Object(); + }); + }); + + describe('onSnapshot()', () => { + it('calls callback with the initial data and then when value changes', async () => { + await resetTestCollectionDoc(COL2_DOC_1_PATH, { name: 'doc1' }); + const docRef = test2DocRef(COL2_DOC_1_ID); + 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(); + }); + }); + + callback.should.be.calledWith(currentDataValue); + + // Update the document + + await docRef.set(newDataValue); + + await sleep(50); + + // Assertions + + callback.should.be.calledWith(newDataValue); + callback.should.be.calledTwice(); + + // Tear down + + unsubscribe(); + }); + + it("doesn't call callback when the ref is updated with the same value", async () => { + await resetTestCollectionDoc(COL2_DOC_1_PATH, { name: 'doc1' }); + const docRef = test2DocRef(COL2_DOC_1_ID); + const currentDataValue = { name: 'doc1' }; + + 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 sleep(50); + + // Assertions + + callback.should.be.calledOnce(); // Callback is not called again + + // Tear down + + unsubscribe(); + }); + + it('allows binding multiple callbacks to the same ref', async () => { + // Setup + await resetTestCollectionDoc(COL2_DOC_1_PATH, { name: 'doc1' }); + const docRef = test2DocRef(COL2_DOC_1_ID); + 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 sleep(50); + + callbackA.should.be.calledWith(newDataValue); + callbackB.should.be.calledWith(newDataValue); + + callbackA.should.be.calledTwice(); + callbackB.should.be.calledTwice(); + + // Tear down + + unsubscribeA(); + unsubscribeB(); + }); + + it('listener stops listening when unsubscribed', async () => { + await resetTestCollectionDoc(COL2_DOC_1_PATH, { name: 'doc1' }); + + // Setup + const docRef = test2DocRef(COL2_DOC_1_ID); + 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 sleep(50); + + 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 sleep(50); + + callbackB.should.be.calledWith(currentDataValue); + + callbackA.should.be.calledTwice(); + callbackB.should.be.calledThrice(); + + // Unsubscribe B + + unsubscribeB(); + + await docRef.set(newDataValue); + + await sleep(50); + + callbackA.should.be.calledTwice(); + callbackB.should.be.calledThrice(); + }); + + it('supports options and callbacks', async () => { + await resetTestCollectionDoc(COL2_DOC_1_PATH, { name: 'doc1' }); + const docRef = test2DocRef(COL2_DOC_1_ID); + const currentDataValue = { name: 'doc1' }; + const newDataValue = { name: 'updated' }; + + const callback = sinon.spy(); + + // Test + + let unsubscribe; + await new Promise(resolve2 => { + unsubscribe = docRef.onSnapshot( + { includeMetadataChanges: true }, + snapshot => { + callback(snapshot.data()); + resolve2(); + } + ); + }); + + callback.should.be.calledWith(currentDataValue); + + // Update the document + + await docRef.set(newDataValue); + + await sleep(50); + + // Assertions + + callback.should.be.calledWith(newDataValue); + + // Tear down + + unsubscribe(); + }); + + it('supports observer', async () => { + await resetTestCollectionDoc(COL2_DOC_1_PATH, { name: 'doc1' }); + const docRef = test2DocRef(COL2_DOC_1_ID); + const currentDataValue = { name: 'doc1' }; + const newDataValue = { name: 'updated' }; + + const callback = sinon.spy(); + + // Test + + let unsubscribe; + await new Promise(resolve2 => { + const observer = { + next: snapshot => { + callback(snapshot.data()); + resolve2(); + }, + }; + unsubscribe = docRef.onSnapshot(observer); + }); + + callback.should.be.calledWith(currentDataValue); + + // Update the document + + await docRef.set(newDataValue); + + await sleep(50); + + // Assertions + + callback.should.be.calledWith(newDataValue); + callback.should.be.calledTwice(); + + // Tear down + + unsubscribe(); + }); + + it('supports options and observer', async () => { + await resetTestCollectionDoc(COL2_DOC_1_PATH, { name: 'doc1' }); + const docRef = test2DocRef(COL2_DOC_1_ID); + const currentDataValue = { name: 'doc1' }; + const newDataValue = { name: 'updated' }; + + const callback = sinon.spy(); + + // Test + + let unsubscribe; + await new Promise(resolve2 => { + const observer = { + next: snapshot => { + callback(snapshot.data()); + resolve2(); + }, + error: () => {}, + }; + unsubscribe = docRef.onSnapshot( + { includeMetadataChanges: true }, + observer + ); + }); + + callback.should.be.calledWith(currentDataValue); + + // Update the document + + await docRef.set(newDataValue); + + await sleep(50); + + // Assertions + + callback.should.be.calledWith(newDataValue); + + // Tear down + + unsubscribe(); + }); + + it('errors when invalid parameters supplied', async () => { + const docRef = test2DocRef(COL2_DOC_1_ID); + (() => { + docRef.onSnapshot(() => {}, 'error'); + }).should.throw( + 'DocumentReference.onSnapshot failed: Second argument must be a valid function.' + ); + (() => { + docRef.onSnapshot({ + next: () => {}, + error: 'error', + }); + }).should.throw( + 'DocumentReference.onSnapshot failed: Observer.error must be a valid function.' + ); + (() => { + docRef.onSnapshot({ + next: 'error', + }); + }).should.throw( + 'DocumentReference.onSnapshot failed: Observer.next must be a valid function.' + ); + (() => { + docRef.onSnapshot( + { + includeMetadataChanges: true, + }, + () => {}, + 'error' + ); + }).should.throw( + 'DocumentReference.onSnapshot failed: Third argument must be a valid function.' + ); + (() => { + docRef.onSnapshot( + { + includeMetadataChanges: true, + }, + { + next: () => {}, + error: 'error', + } + ); + }).should.throw( + 'DocumentReference.onSnapshot failed: Observer.error must be a valid function.' + ); + (() => { + docRef.onSnapshot( + { + includeMetadataChanges: true, + }, + { + next: 'error', + } + ); + }).should.throw( + 'DocumentReference.onSnapshot failed: Observer.next must be a valid function.' + ); + (() => { + docRef.onSnapshot( + { + includeMetadataChanges: true, + }, + 'error' + ); + }).should.throw( + 'DocumentReference.onSnapshot failed: Second argument must be a function or observer.' + ); + (() => { + docRef.onSnapshot({ + error: 'error', + }); + }).should.throw( + 'DocumentReference.onSnapshot failed: First argument must be a function, observer or options.' + ); + (() => { + docRef.onSnapshot(); + }).should.throw( + 'DocumentReference.onSnapshot failed: Called with invalid arguments.' + ); + }); + }); + + describe('set()', () => { + before(async () => { + await resetTestCollectionDoc(COL2_DOC_1_PATH, { name: 'doc1' }); + }); + it('should create Document', async () => { + await test2DocRef('doc2').set({ name: 'doc2', testArray: [] }); + const doc = await test2DocRef('doc2').get(); + doc.data().name.should.equal('doc2'); + }); + + it('should merge Document', async () => { + await test2DocRef(COL2_DOC_1_ID).set( + { merge: 'merge' }, + { merge: true } + ); + + const doc = await test2DocRef(COL2_DOC_1_ID).get(); + doc.data().name.should.equal('doc1'); + doc.data().merge.should.equal('merge'); + }); + + it('should overwrite Document', async () => { + await test2DocRef(COL2_DOC_1_ID).set({ name: 'overwritten' }); + const doc = await test2DocRef(COL2_DOC_1_ID).get(); + doc.data().name.should.equal('overwritten'); + }); + }); + + // TODO async/await these tests + describe('update()', () => { + beforeEach(async () => { + await resetTestCollectionDoc(COL2_DOC_1_PATH, { name: 'doc1' }); + }); + it('should update Document using object', () => + test2DocRef(COL2_DOC_1_ID) + .update({ name: 'updated' }) + .then(async () => { + const doc = await test2DocRef(COL2_DOC_1_ID).get(); + doc.data().name.should.equal('updated'); + })); + + it('should update Document using key/value pairs', () => + test2DocRef(COL2_DOC_1_ID) + .update('name', 'updated') + .then(async () => { + const doc = await test2DocRef(COL2_DOC_1_ID).get(); + doc.data().name.should.equal('updated'); + })); + + it('should update Document using FieldPath/value pair', () => + test2DocRef(COL2_DOC_1_ID) + .update(new firebase.firestore.FieldPath('name'), 'Name') + .then(async () => { + const doc = await test2DocRef(COL2_DOC_1_ID).get(); + doc.data().name.should.equal('Name'); + })); + + it('should update Document using nested FieldPath and value pair', () => + test2DocRef(COL2_DOC_1_ID) + .update( + new firebase.firestore.FieldPath('nested', 'name'), + 'Nested Name' + ) + .then(async () => { + const doc = await test2DocRef(COL2_DOC_1_ID).get(); + doc.data().nested.name.should.equal('Nested Name'); + })); + + it('should update Document using multiple FieldPath/value pairs', () => + test2DocRef(COL2_DOC_1_ID) + .update( + new firebase.firestore.FieldPath('nested', 'firstname'), + 'First Name', + new firebase.firestore.FieldPath('nested', 'lastname'), + 'Last Name' + ) + .then(async () => { + const doc = await test2DocRef(COL2_DOC_1_ID).get(); + doc.data().nested.firstname.should.equal('First Name'); + doc.data().nested.lastname.should.equal('Last Name'); + })); + + it('errors when invalid parameters supplied', async () => { + const docRef = test2DocRef(COL2_DOC_1_ID); + (() => { + docRef.update('error'); + }).should.throw( + 'DocumentReference.update failed: If using a single update argument, it must be an object.' + ); + (() => { + docRef.update('error1', 'error2', 'error3'); + }).should.throw( + 'DocumentReference.update failed: The update arguments must be either a single object argument, or equal numbers of key/value pairs.' + ); + (() => { + docRef.update(0, 'error'); + }).should.throw( + 'DocumentReference.update failed: Argument at index 0 must be a string or FieldPath' + ); + }); + }); + + describe('types', () => { + it('should handle Boolean field', async () => { + const docRef = test2DocRef('reference'); + await docRef.set({ + field: true, + }); + + const doc = await docRef.get(); + should.equal(doc.data().field, true); + }); + + it('should handle Date field', async () => { + const date = new bridge.context.window.Date(); + const docRef = test2DocRef('reference'); + await docRef.set({ + field: date, + }); + + const doc = await docRef.get(); + doc.data().field.should.be.instanceof(bridge.context.window.Date); + should.equal(doc.data().field.toISOString(), date.toISOString()); + should.equal(doc.data().field.getTime(), date.getTime()); + }); + + it('should handle DocumentReference field', async () => { + const docRef = test2DocRef('reference'); + await docRef.set({ + field: firebase.firestore().doc('test/field'), + }); + + const doc = await docRef.get(); + should.equal(doc.data().field.path, 'test/field'); + }); + + it('should handle GeoPoint field', async () => { + const docRef = test2DocRef('reference'); + await docRef.set({ + field: new firebase.firestore.GeoPoint(1.01, 1.02), + }); + + const doc = await docRef.get(); + should.equal(doc.data().field.latitude, 1.01); + should.equal(doc.data().field.longitude, 1.02); + }); + }); + }); +}); diff --git a/bridge/helpers/firestore.js b/bridge/helpers/firestore.js index e866ec35..cdee8f1c 100644 --- a/bridge/helpers/firestore.js +++ b/bridge/helpers/firestore.js @@ -1,4 +1,6 @@ const TEST_COLLECTION_NAME = 'tests'; +const TEST2_COLLECTION_NAME = 'tests2'; +// const TEST3_COLLECTION_NAME = 'tests3'; let shouldCleanup = false; const ONE_HOUR = 60 * 60 * 1000; @@ -8,7 +10,7 @@ module.exports = { if (!shouldCleanup) return Promise.resolve(); await Promise.all([ module.exports.cleanCollection(TEST_COLLECTION_NAME), - module.exports.cleanCollection(`${TEST_COLLECTION_NAME}2`), + module.exports.cleanCollection(TEST2_COLLECTION_NAME), ]); // await module.exports.cleanCollection(`${TEST_COLLECTION_NAME}3`); // await module.exports.cleanCollection(`${TEST_COLLECTION_NAME}4`); @@ -16,6 +18,8 @@ module.exports = { }, TEST_COLLECTION_NAME, + TEST2_COLLECTION_NAME, + // TEST3_COLLECTION_NAME, DOC_1: { name: 'doc1' }, DOC_1_PATH: `tests/doc1${testRunId}`, @@ -40,8 +44,28 @@ module.exports = { }; }, - COL_DOC_1_PATH: `tests/col1${testRunId}`, + // needs to be a fn as firebase may not yet be available + COL2_DOC_1() { + shouldCleanup = true; + return { + baz: true, + daz: 123, + foo: 'bar', + gaz: 12.1234567, + geopoint: new firebase.firestore.GeoPoint(0, 0), + naz: null, + object: { + daz: 123, + }, + timestamp: new Date(2017, 2, 10, 10, 0, 0), + }; + }, + COL_DOC_1_ID: `col1${testRunId}`, + COL_DOC_1_PATH: `${TEST_COLLECTION_NAME}/col1${testRunId}`, + + COL2_DOC_1_ID: `doc1${testRunId}`, + COL2_DOC_1_PATH: `${TEST2_COLLECTION_NAME}/doc1${testRunId}`, /** * Removes all documents on the collection for the current testId or @@ -84,12 +108,28 @@ module.exports = { return firebase .firestore() .collection(TEST_COLLECTION_NAME) - .doc(`${testRunId}${docId}`); + .doc( + docId.startsWith(testRunId) || docId.endsWith(testRunId) + ? docId + : `${testRunId}${docId}` + ); + }, + + test2DocRef(docId) { + shouldCleanup = true; + return firebase + .firestore() + .collection(TEST2_COLLECTION_NAME) + .doc( + docId.startsWith(testRunId) || docId.endsWith(testRunId) + ? docId + : `${testRunId}${docId}` + ); }, testCollection(collection) { shouldCleanup = true; - return firebase.firestore().collection(collection || TEST_COLLECTION_NAME); + return firebase.firestore().collection(collection); }, testCollectionDoc(path) {