const { test2DocRef, COL2_DOC_1, COL2_DOC_1_ID, COL2_DOC_1_PATH, TEST2_COLLECTION_NAME, resetTestCollectionDoc, } = TestHelpers.firestore; 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(); }); it('should support GetOptions source=`default`', async () => { await resetTestCollectionDoc(COL2_DOC_1_PATH, COL2_DOC_1()); const snapshot = await test2DocRef(COL2_DOC_1_ID).get({ source: 'default', }); snapshot.id.should.equal(COL2_DOC_1_ID); snapshot.metadata.should.be.an.Object(); should.equal(snapshot.metadata.fromCache, false); }); it('should support GetOptions source=`server`', async () => { await resetTestCollectionDoc(COL2_DOC_1_PATH, COL2_DOC_1()); const snapshot = await test2DocRef(COL2_DOC_1_ID).get({ source: 'server', }); snapshot.id.should.equal(COL2_DOC_1_ID); snapshot.metadata.should.be.an.Object(); should.equal(snapshot.metadata.fromCache, false); }); // TODO: For some reason when using `cache` it's not seeing the data as available, even if // first requesting it from the server, although interestingly it works fine in the old // tests app xit('should support GetOptions source=`cache`', async () => { await resetTestCollectionDoc(COL2_DOC_1_PATH, COL2_DOC_1()); const ref = test2DocRef(COL2_DOC_1_ID); // Make sure the reference data is populated in the cache await ref.get({ source: 'server' }); // Retrieve the cached version const snapshot = await ref.get({ source: 'cache' }); snapshot.id.should.equal(COL2_DOC_1_ID); snapshot.metadata.should.be.an.Object(); should.equal(snapshot.metadata.fromCache, true); }); it('should error with invalid GetOptions source option', async () => { const docRef = test2DocRef(COL2_DOC_1_ID); try { await docRef.get(() => {}); return Promise.reject( new Error('get() did not reject with invalid argument.') ); } catch (e) { // do nothing } try { await docRef.get({ source: 'invalid' }); return Promise.reject( new Error('get() did not reject with invalid source property.') ); } catch (e) { // do nothing } return Promise.resolve(); }); }); 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'); }); }); describe('update()', () => { beforeEach(async () => { await resetTestCollectionDoc(COL2_DOC_1_PATH, { name: 'doc1' }); }); it('should update Document using object', async () => { await test2DocRef(COL2_DOC_1_ID).update({ name: 'updated' }); const doc = await test2DocRef(COL2_DOC_1_ID).get(); doc.data().name.should.equal('updated'); }); it('should update Document using key/value pairs', async () => { await test2DocRef(COL2_DOC_1_ID).update('name', 'updated'); const doc = await test2DocRef(COL2_DOC_1_ID).get(); doc.data().name.should.equal('updated'); }); it('should update Document using FieldPath/value pair', async () => { await test2DocRef(COL2_DOC_1_ID).update( new firebase.firestore.FieldPath('name'), 'Name' ); 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', async () => { await test2DocRef(COL2_DOC_1_ID).update( new firebase.firestore.FieldPath('nested', 'name'), 'Nested Name' ); 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', async () => { await test2DocRef(COL2_DOC_1_ID).update( new firebase.firestore.FieldPath('nested', 'firstname'), 'First Name', new firebase.firestore.FieldPath('nested', 'lastname'), 'Last Name' ); 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); }); }); }); });