[android][database] database improvements (#1619)

- [ANDROID] [BUGFIX] [DATABASE] - Database listeners now correctly tearing down between RN reloads. (Fixes #1498 #1611 #1609)
 - [JS] [BUGFIX] [DATABASE] - Fixed an issue where `Reference.toString()` incorrectly contains `//` instead of `/` when joining the parent and child paths.
 - [JS] [BUGFIX] [DATABASE] - Rework `.push()` behaviour to match WebSDK and correctly return a Reference instance in all scenarios. (Fixes #893 #1464 #1572)
 - [JS] [ENHANCEMENT] [UTILS] - Added a `firebase.utils().database.cleanup()` utility method which removes all database listeners.
This commit is contained in:
Michael Diarmid
2018-10-27 05:34:09 +01:00
committed by GitHub
parent 210c966443
commit d3b9b24cca
22 changed files with 809 additions and 114 deletions

View File

@@ -25,6 +25,7 @@ import com.google.firebase.database.ServerValue;
import com.google.firebase.database.Transaction;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -36,18 +37,18 @@ import io.invertase.firebase.Utils;
public class RNFirebaseDatabase extends ReactContextBaseJavaModule {
private static final String TAG = "RNFirebaseDatabase";
private static boolean enableLogging = false;
private static ReactApplicationContext reactApplicationContext = null;
private static HashMap<String, Boolean> loggingLevelSet = new HashMap<>();
private HashMap<String, RNFirebaseDatabaseReference> references = new HashMap<>();
private SparseArray<RNFirebaseTransactionHandler> transactionHandlers = new SparseArray<>();
private static HashMap<String, RNFirebaseDatabaseReference> references = new HashMap<>();
private static SparseArray<RNFirebaseTransactionHandler> transactionHandlers = new SparseArray<>();
RNFirebaseDatabase(ReactApplicationContext reactContext) {
super(reactContext);
}
/*
* REACT NATIVE METHODS
*/
static ReactApplicationContext getReactApplicationContextInstance() {
return reactApplicationContext;
}
/**
* Resolve null or reject with a js like error if databaseError exists
@@ -68,6 +69,11 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule {
}
}
/*
* REACT NATIVE METHODS
*/
/**
* Get a database instance for a specific firebase app instance
*
@@ -253,6 +259,26 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule {
return errorMap;
}
@Override
public void initialize() {
super.initialize();
Log.d(TAG, "RNFirebaseDatabase:initialized");
reactApplicationContext = getReactApplicationContext();
}
@Override
public void onCatalystInstanceDestroy() {
super.onCatalystInstanceDestroy();
Iterator refIterator = references.entrySet().iterator();
while (refIterator.hasNext()) {
Map.Entry pair = (Map.Entry) refIterator.next();
RNFirebaseDatabaseReference nativeRef = (RNFirebaseDatabaseReference) pair.getValue();
nativeRef.removeAllEventListeners();
refIterator.remove(); // avoids a ConcurrentModificationException
}
}
/**
* @param appName
*/
@@ -792,7 +818,6 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule {
ReadableArray modifiers
) {
return new RNFirebaseDatabaseReference(
getReactApplicationContext(),
appName,
dbURL,
key,

View File

@@ -19,6 +19,7 @@ import com.google.firebase.database.ValueEventListener;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -33,7 +34,6 @@ class RNFirebaseDatabaseReference {
private Query query;
private String appName;
private String dbURL;
private ReactContext reactContext;
private HashMap<String, ChildEventListener> childEventListeners = new HashMap<>();
private HashMap<String, ValueEventListener> valueEventListeners = new HashMap<>();
@@ -41,14 +41,12 @@ class RNFirebaseDatabaseReference {
* RNFirebase wrapper around FirebaseDatabaseReference,
* handles Query generation and event listeners.
*
* @param context
* @param app
* @param refKey
* @param refPath
* @param modifiersArray
*/
RNFirebaseDatabaseReference(
ReactContext context,
String app,
String url,
String refKey,
@@ -59,10 +57,32 @@ class RNFirebaseDatabaseReference {
query = null;
appName = app;
dbURL = url;
reactContext = context;
buildDatabaseQueryAtPathAndModifiers(refPath, modifiersArray);
}
void removeAllEventListeners() {
if (hasListeners()) {
Iterator valueIterator = valueEventListeners.entrySet().iterator();
while (valueIterator.hasNext()) {
Map.Entry pair = (Map.Entry) valueIterator.next();
ValueEventListener valueEventListener = (ValueEventListener) pair.getValue();
query.removeEventListener(valueEventListener);
valueIterator.remove();
}
Iterator childIterator = childEventListeners.entrySet().iterator();
while (childIterator.hasNext()) {
Map.Entry pair = (Map.Entry) childIterator.next();
ChildEventListener childEventListener = (ChildEventListener) pair.getValue();
query.removeEventListener(childEventListener);
childIterator.remove();
}
}
}
/**
* Used outside of class for keepSynced etc.
*
@@ -141,7 +161,6 @@ class RNFirebaseDatabaseReference {
*/
private void addOnceValueEventListener(final Promise promise) {
@SuppressLint("StaticFieldLeak") final DataSnapshotToMapAsyncTask asyncTask = new DataSnapshotToMapAsyncTask(
reactContext,
this
) {
@Override
@@ -338,7 +357,7 @@ class RNFirebaseDatabaseReference {
@Nullable String previousChildName
) {
@SuppressLint("StaticFieldLeak")
DataSnapshotToMapAsyncTask asyncTask = new DataSnapshotToMapAsyncTask(reactContext, this) {
DataSnapshotToMapAsyncTask asyncTask = new DataSnapshotToMapAsyncTask(this) {
@Override
protected void onPostExecute(WritableMap data) {
if (this.isAvailable()) {
@@ -347,7 +366,11 @@ class RNFirebaseDatabaseReference {
event.putString("key", key);
event.putString("eventType", eventType);
event.putMap("registration", Utils.readableMapToWritableMap(registration));
Utils.sendEvent(reactContext, "database_sync_event", event);
Utils.sendEvent(
RNFirebaseDatabase.getReactApplicationContextInstance(),
"database_sync_event",
event
);
}
}
};
@@ -367,7 +390,11 @@ class RNFirebaseDatabaseReference {
event.putMap("error", RNFirebaseDatabase.getJSError(error));
event.putMap("registration", Utils.readableMapToWritableMap(registration));
Utils.sendEvent(reactContext, "database_sync_event", event);
Utils.sendEvent(
RNFirebaseDatabase.getReactApplicationContextInstance(),
"database_sync_event",
event
);
}
/**
@@ -554,13 +581,10 @@ class RNFirebaseDatabaseReference {
* Introduced due to https://github.com/invertase/react-native-firebase/issues/1284
*/
private static class DataSnapshotToMapAsyncTask extends AsyncTask<Object, Void, WritableMap> {
private WeakReference<ReactContext> reactContextWeakReference;
private WeakReference<RNFirebaseDatabaseReference> referenceWeakReference;
DataSnapshotToMapAsyncTask(ReactContext context, RNFirebaseDatabaseReference reference) {
DataSnapshotToMapAsyncTask(RNFirebaseDatabaseReference reference) {
referenceWeakReference = new WeakReference<>(reference);
reactContextWeakReference = new WeakReference<>(context);
}
@Override
@@ -572,8 +596,7 @@ class RNFirebaseDatabaseReference {
return RNFirebaseDatabaseUtils.snapshotToMap(dataSnapshot, previousChildName);
} catch (RuntimeException e) {
if (isAvailable()) {
reactContextWeakReference
.get()
RNFirebaseDatabase.getReactApplicationContextInstance()
.handleException(e);
}
throw e;
@@ -586,7 +609,7 @@ class RNFirebaseDatabaseReference {
}
Boolean isAvailable() {
return reactContextWeakReference.get() != null && referenceWeakReference.get() != null;
return RNFirebaseDatabase.getReactApplicationContextInstance() != null && referenceWeakReference.get() != null;
}
}
}

View File

@@ -77,19 +77,20 @@ type DatabaseListener = {
export default class Reference extends ReferenceBase {
_database: Database;
_promise: ?Promise<*>;
_query: Query;
_refListeners: { [listenerId: number]: DatabaseListener };
then: (a?: any) => Promise<any>;
catch: (a?: any) => Promise<any>;
constructor(
database: Database,
path: string,
existingModifiers?: Array<DatabaseModifier>
) {
super(path);
this._promise = null;
this._refListeners = {};
this._database = database;
this._query = new Query(this, existingModifiers);
@@ -303,34 +304,26 @@ export default class Reference extends ReferenceBase {
* @returns {*}
*/
push(value: any, onComplete?: Function): Reference | Promise<void> {
if (value === null || value === undefined) {
return new Reference(
this._database,
`${this.path}/${generatePushID(this._database._serverTimeOffset)}`
);
const name = generatePushID(this._database._serverTimeOffset);
const pushRef = this.child(name);
const thennablePushRef = this.child(name);
let promise;
if (value != null) {
promise = thennablePushRef.set(value, onComplete).then(() => pushRef);
} else {
promise = Promise.resolve(pushRef);
}
const newRef = new Reference(
this._database,
`${this.path}/${generatePushID(this._database._serverTimeOffset)}`
);
const promise = newRef.set(value);
thennablePushRef.then = promise.then.bind(promise);
thennablePushRef.catch = promise.catch.bind(promise);
// if callback provided then internally call the set promise with value
if (isFunction(onComplete)) {
return (
promise
// $FlowExpectedError: Reports that onComplete can change to null despite the null check: https://github.com/facebook/flow/issues/1655
.then(() => onComplete(null, newRef))
// $FlowExpectedError: Reports that onComplete can change to null despite the null check: https://github.com/facebook/flow/issues/1655
.catch(error => onComplete(error, null))
);
promise.catch(() => {});
}
// otherwise attach promise to 'thenable' reference and return the
// new reference
newRef._setThenable(promise);
return newRef;
return thennablePushRef;
}
/**
@@ -500,7 +493,7 @@ export default class Reference extends ReferenceBase {
* @returns {string}
*/
toString(): string {
return `${this._database.databaseUrl}/${this.path}`;
return `${this._database.databaseUrl}${this.path}`;
}
/**
@@ -566,47 +559,6 @@ export default class Reference extends ReferenceBase {
return new Reference(this._database, '/');
}
/**
* Access then method of promise if set
* @return {*}
*/
then(fnResolve: any => any, fnReject: any => any) {
if (isFunction(fnResolve) && this._promise && this._promise.then) {
return this._promise.then.bind(this._promise)(
result => {
this._promise = null;
return fnResolve(result);
},
possibleErr => {
this._promise = null;
if (isFunction(fnReject)) {
return fnReject(possibleErr);
}
throw possibleErr;
}
);
}
throw new Error("Cannot read property 'then' of undefined.");
}
/**
* Access catch method of promise if set
* @return {*}
*/
catch(fnReject: any => any) {
if (isFunction(fnReject) && this._promise && this._promise.catch) {
return this._promise.catch.bind(this._promise)(possibleErr => {
this._promise = null;
return fnReject(possibleErr);
});
}
throw new Error("Cannot read property 'catch' of undefined.");
}
/**
* INTERNALS
*/
@@ -635,15 +587,6 @@ export default class Reference extends ReferenceBase {
}$${this._query.queryIdentifier()}`;
}
/**
* Set the promise this 'thenable' reference relates to
* @param promise
* @private
*/
_setThenable(promise: Promise<*>) {
this._promise = promise;
}
/**
*
* @param obj
@@ -812,7 +755,7 @@ export default class Reference extends ReferenceBase {
},
});
// increment number of listeners - just s short way of making
// increment number of listeners - just a short way of making
// every registration unique per .on() call
listeners += 1;
@@ -903,12 +846,3 @@ export default class Reference extends ReferenceBase {
return SyncTree.removeListenersForRegistrations(registrations);
}
}
// eslint-disable-next-line no-unused-vars
// class ThenableReference<+R> extends Reference {
// then<U>(
// onFulfill?: (value: R) => Promise<U> | U,
// onReject?: (error: any) => Promise<U> | U
// ): Promise<U>;
// catch<U>(onReject?: (error: any) => Promise<U> | U): Promise<R | U>;
// }

View File

@@ -0,0 +1,12 @@
import SyncTree from '../../utils/SyncTree';
export default {
/**
* Removes all database listeners (JS & Native)
*/
cleanup(): void {
SyncTree.removeListenersForRegistrations(
Object.keys(SyncTree._reverseLookup)
);
},
};

View File

@@ -4,6 +4,7 @@ import INTERNALS from '../../utils/internals';
import { isIOS } from '../../utils';
import ModuleBase from '../../utils/ModuleBase';
import type App from '../core/app';
import DatabaseUtils from './database';
const FirebaseCoreModule = NativeModules.RNFirebase;
@@ -28,6 +29,10 @@ export default class RNFirebaseUtils extends ModuleBase {
});
}
get database(): DatabaseUtils {
return DatabaseUtils;
}
/**
*
*/

View File

@@ -1,6 +1,15 @@
/* eslint-disable import/extensions,import/no-unresolved */
/* eslint-disable import/extensions,import/no-unresolved,import/first */
import React, { Component } from 'react';
import { AppRegistry, Text, View, Image, StyleSheet } from 'react-native';
import {
AppRegistry,
Text,
View,
Image,
StyleSheet,
YellowBox,
} from 'react-native';
YellowBox.ignoreWarnings(['Require cycle:']);
import firebase from 'react-native-firebase';
import jet from 'jet/platform/react-native';

View File

@@ -0,0 +1,279 @@
const { CONTENTS, setDatabaseContents } = TestHelpers.database;
describe('database()', () => {
beforeEach(() => setDatabaseContents());
describe('issue_100', () => {
describe('array-like values should', () => {
it('return null in returned array at positions where a key is missing', async () => {
const ref = firebase.database().ref('tests/issues/100');
const snapshot = await ref.once('value');
snapshot
.val()
.should.eql(
jet.contextify([
null,
jet.contextify(CONTENTS.ISSUES[100][1]),
jet.contextify(CONTENTS.ISSUES[100][2]),
jet.contextify(CONTENTS.ISSUES[100][3]),
])
);
});
});
});
describe('issue_108', () => {
describe('filters using floats', () => {
it('return correct results', async () => {
const ref = firebase.database().ref('tests/issues/108');
const snapshot = await ref
.orderByChild('latitude')
.startAt(34.00867000999119)
.endAt(34.17462960866099)
.once('value');
const val = snapshot.val();
val.foobar.should.eql(jet.contextify(CONTENTS.ISSUES[108].foobar));
should.equal(Object.keys(val).length, 1);
});
it('return correct results when not using float values', async () => {
const ref = firebase.database().ref('tests/issues/108');
const snapshot = await ref
.orderByChild('latitude')
.equalTo(37)
.once('value');
const val = snapshot.val();
val.notAFloat.should.eql(
jet.contextify(CONTENTS.ISSUES[108].notAFloat)
);
should.equal(Object.keys(val).length, 1);
});
});
});
xdescribe('issue_171', () => {
describe('non array-like values should', () => {
it('return as objects', async () => {
const ref = firebase.database().ref('tests/issues/171');
const snapshot = await ref.once('value');
snapshot.val().should.eql(jet.contextify(CONTENTS.ISSUES[171]));
});
});
});
describe('issue_489', () => {
describe('long numbers should', () => {
it('return as longs', async () => {
const long1Ref = firebase.database().ref('tests/issues/489/long1');
const long2Ref = firebase.database().ref('tests/issues/489/long2');
const long2 = 1234567890123456;
let snapshot = await long1Ref.once('value');
snapshot.val().should.eql(CONTENTS.ISSUES[489].long1);
await long2Ref.set(long2);
snapshot = await long2Ref.once('value');
snapshot.val().should.eql(long2);
});
});
});
describe('issue_521', () => {
describe('orderByChild (numerical field) and limitToLast', () => {
it('once() returns correct results', async () => {
const ref = firebase.database().ref('tests/issues/521');
const snapshot = await ref
.orderByChild('number')
.limitToLast(1)
.once('value');
const val = snapshot.val();
val.key3.should.eql(jet.contextify(CONTENTS.ISSUES[521].key3));
should.equal(Object.keys(val).length, 1);
});
it('on() returns correct initial results', async () => {
const ref = firebase
.database()
.ref('tests/issues/521')
.orderByChild('number')
.limitToLast(2);
const callback = sinon.spy();
await new Promise(resolve => {
ref.on('value', snapshot => {
callback(snapshot.val());
resolve();
});
});
callback.should.be.calledWith({
key2: CONTENTS.ISSUES[521].key2,
key3: CONTENTS.ISSUES[521].key3,
});
callback.should.be.calledOnce();
});
it('on() returns correct subsequent results', async () => {
const ref = firebase
.database()
.ref('tests/issues/521')
.orderByChild('number')
.limitToLast(2);
const callback = sinon.spy();
await new Promise(resolve => {
ref.on('value', snapshot => {
callback(snapshot.val());
resolve();
});
});
callback.should.be.calledWith({
key2: CONTENTS.ISSUES[521].key2,
key3: CONTENTS.ISSUES[521].key3,
});
callback.should.be.calledOnce();
const newDataValue = {
name: 'Item 4',
number: 4,
string: 'item4',
};
const newRef = firebase.database().ref('tests/issues/521/key4');
await newRef.set(newDataValue);
await sleep(5);
callback.should.be.calledWith({
key3: CONTENTS.ISSUES[521].key3,
key4: newDataValue,
});
callback.should.be.calledTwice();
});
});
describe('orderByChild (string field) and limitToLast', () => {
it('once() returns correct results', async () => {
const ref = firebase.database().ref('tests/issues/521');
const snapshot = await ref
.orderByChild('string')
.limitToLast(1)
.once('value');
const val = snapshot.val();
val.key3.should.eql(jet.contextify(CONTENTS.ISSUES[521].key3));
should.equal(Object.keys(val).length, 1);
});
it('on() returns correct initial results', async () => {
const ref = firebase
.database()
.ref('tests/issues/521')
.orderByChild('string')
.limitToLast(2);
const callback = sinon.spy();
await new Promise(resolve => {
ref.on('value', snapshot => {
callback(snapshot.val());
resolve();
});
});
callback.should.be.calledWith({
key2: CONTENTS.ISSUES[521].key2,
key3: CONTENTS.ISSUES[521].key3,
});
callback.should.be.calledOnce();
});
it('on() returns correct subsequent results', async () => {
const ref = firebase
.database()
.ref('tests/issues/521')
.orderByChild('string')
.limitToLast(2);
const callback = sinon.spy();
await new Promise(resolve => {
ref.on('value', snapshot => {
callback(snapshot.val());
resolve();
});
});
callback.should.be.calledWith({
key2: CONTENTS.ISSUES[521].key2,
key3: CONTENTS.ISSUES[521].key3,
});
callback.should.be.calledOnce();
const newDataValue = {
name: 'Item 4',
number: 4,
string: 'item4',
};
const newRef = firebase.database().ref('tests/issues/521/key4');
await newRef.set(newDataValue);
await sleep(5);
callback.should.be.calledWith({
key3: CONTENTS.ISSUES[521].key3,
key4: newDataValue,
});
callback.should.be.calledTwice();
});
});
});
describe('issue_679', () => {
describe('path from snapshot reference', () => {
it('should match web SDK', async () => {
const nativeRef = firebase.database().ref('tests/issues/679');
const webRef = firebaseAdmin.database().ref('tests/issues/679');
const nativeRef2 = firebase.database().ref('tests/issues/679/');
const webRef2 = firebaseAdmin.database().ref('tests/issues/679/');
webRef.toString().should.equal(nativeRef.toString());
webRef2.toString().should.equal(nativeRef2.toString());
});
it('should be correct when returned from native', async () => {
const nativeRef = firebase.database().ref('tests/issues/679/');
const webRef = firebaseAdmin.database().ref('tests/issues/679/');
const nativeSnapshot = await nativeRef.once('value');
const webSnapshot = await webRef.once('value');
webSnapshot.ref.toString().should.equal(nativeSnapshot.ref.toString());
});
});
});
});

View File

@@ -0,0 +1,39 @@
const { setDatabaseContents } = TestHelpers.database;
describe('database()', () => {
before(() => setDatabaseContents());
describe('ref().child', () => {
describe('when passed a shallow path', () => {
it('returns correct child ref', () => {
const ref = firebase.database().ref('tests');
const childRef = ref.child('tests');
childRef.key.should.eql('tests');
});
});
describe('when passed a nested path', () => {
it('returns correct child ref', () => {
const ref = firebase.database().ref('tests');
const grandChildRef = ref.child('tests/number');
grandChildRef.key.should.eql('number');
});
});
describe("when passed a path that doesn't exist", () => {
it('creates a reference, anyway', () => {
const ref = firebase.database().ref('tests');
const grandChildRef = ref.child('doesnt/exist');
grandChildRef.key.should.eql('exist');
});
});
describe('when passed an invalid path', () => {
it('creates a reference, anyway', () => {
const ref = firebase.database().ref('tests');
const grandChildRef = ref.child('does$&nt/exist');
grandChildRef.key.should.eql('exist');
});
});
});
});

View File

@@ -0,0 +1,24 @@
const { CONTENTS, setDatabaseContents } = TestHelpers.database;
describe('database()', () => {
before(() => setDatabaseContents());
describe('ref()', () => {
it('returns root reference when provided no path', () => {
const ref = firebase.database().ref();
(ref.key === null).should.be.true();
(ref.parent === null).should.be.true();
});
it('returns reference to data at path', async () => {
const ref = firebase.database().ref('tests/types/number');
let valueAtRef;
await ref.once('value', snapshot => {
valueAtRef = snapshot.val();
});
valueAtRef.should.eql(CONTENTS.DEFAULT.number);
});
});
});

View File

@@ -0,0 +1,32 @@
const { setDatabaseContents } = TestHelpers.database;
describe('database()', () => {
before(() => setDatabaseContents());
describe('ref().isEqual()', () => {
before(() => {
this.ref = firebase.database().ref('tests/types');
});
it('returns true when the reference is for the same location', () => {
const ref = firebase.database().ref();
ref.ref.should.eql(ref);
const ref2 = firebase.database().ref('tests/types');
this.ref.isEqual(ref2).should.eql(true);
});
it('returns false when the reference is for a different location', () => {
const ref2 = firebase.database().ref('tests/types/number');
this.ref.isEqual(ref2).should.eql(false);
});
it('returns false when the reference is null', () => {
this.ref.isEqual(null).should.eql(false);
});
it('returns false when the reference is not a Reference', () => {
this.ref.isEqual(1).should.eql(false);
});
});
});

View File

@@ -0,0 +1,16 @@
describe('database()', () => {
describe('ref().key', () => {
it('returns null for root ref', () => {
const ref = firebase.database().ref();
(ref.key === null).should.be.true();
});
it('returns correct key for path', () => {
const ref = firebase.database().ref('tests/types/number');
const arrayItemRef = firebase.database().ref('tests/types/array/1');
ref.key.should.eql('number');
arrayItemRef.key.should.eql('1');
});
});
});

View File

@@ -0,0 +1,19 @@
describe('database()', () => {
describe('ref().parent', () => {
describe('on the root ref', () => {
it('returns null', () => {
const ref = firebase.database().ref();
(ref.parent === null).should.be.true();
});
});
describe('on a non-root ref', () => {
it('returns correct parent', () => {
const ref = firebase.database().ref('tests/types/number');
const parentRef = firebase.database().ref('tests/types');
ref.parent.key.should.eql(parentRef.key);
});
});
});
});

View File

@@ -0,0 +1,33 @@
const { CONTENTS, setDatabaseContents } = TestHelpers.database;
describe('database()', () => {
before(() => setDatabaseContents());
describe('ref().priority', () => {
it('setPriority() should correctly set a priority for all non-null values', async () => {
await Promise.all(
Object.keys(CONTENTS.DEFAULT).map(async dataRef => {
const ref = firebase.database().ref(`tests/types/${dataRef}`);
await ref.setPriority(1);
await ref.once('value').then(snapshot => {
if (snapshot.val() !== null) {
snapshot.getPriority().should.eql(1);
}
});
})
);
});
it('setWithPriority() should correctly set the priority', async () => {
const ref = firebase.database().ref('tests/types/number');
await ref.setWithPriority(CONTENTS.DEFAULT.number, '2');
await ref.once('value').then(snapshot => {
snapshot.getPriority().should.eql('2');
});
});
});
});

View File

@@ -0,0 +1,126 @@
const { CONTENTS, setDatabaseContents } = TestHelpers.database;
describe('database()', () => {
before(() => setDatabaseContents());
describe('ref().push()', () => {
it('returns a ref that can be used to set value later', async () => {
const ref = firebase.database().ref('tests/types/array');
let originalListValue;
await ref.once('value', snapshot => {
originalListValue = snapshot.val();
});
await sleep(5);
originalListValue.should.eql(jet.contextify(CONTENTS.DEFAULT.array));
const newItemRef = ref.push();
const valueToAddToList = CONTENTS.NEW.number;
await newItemRef.set(valueToAddToList);
let newItemValue;
await newItemRef.once('value', snapshot => {
newItemValue = snapshot.val();
});
await sleep(5);
newItemValue.should.eql(valueToAddToList);
let newListValue;
await ref.once('value', snapshot => {
newListValue = snapshot.val();
});
await sleep(5);
const originalListAsObject = {
...originalListValue,
[newItemRef.key]: valueToAddToList,
};
newListValue.should.eql(jet.contextify(originalListAsObject));
});
it('allows setting value immediately', async () => {
let snapshot;
const ref = firebase.database().ref('tests/types/array');
const valueToAddToList = CONTENTS.NEW.number;
snapshot = await ref.once('value');
const originalListValue = snapshot.val();
const newItemRef = ref.push(valueToAddToList);
snapshot = await newItemRef.once('value');
const newItemValue = snapshot.val();
newItemValue.should.eql(valueToAddToList);
snapshot = await firebase
.database()
.ref('tests/types/array')
.once('value');
const newListValue = snapshot.val();
const originalListAsObject = {
...originalListValue,
[newItemRef.key]: valueToAddToList,
};
newListValue.should.eql(jet.contextify(originalListAsObject));
});
// https://github.com/invertase/react-native-firebase/issues/893
it('correctly returns the reference', async () => {
let result;
const path = 'tests/types/array';
const valueToAddToList = CONTENTS.NEW.number;
const Reference = jet.require('src/modules/database/Reference');
// 1
const ref1 = firebase
.database()
.ref(path)
.push();
should.exist(ref1, 'ref1 did not return a Reference instance');
ref1.key.should.be.a.String();
ref1.should.be.instanceOf(Reference);
result = await ref1.set(valueToAddToList);
should.not.exist(result);
// 2
const ref2 = await firebase
.database()
.ref(path)
.push(valueToAddToList);
should.exist(ref2, 'ref2 did not return a Reference instance');
ref2.key.should.be.a.String();
ref2.should.be.instanceOf(Reference);
// 3
const ref3 = await firebase
.database()
.ref(path)
.push();
should.exist(ref3, 'ref3 did not return a Reference instance');
ref3.key.should.be.a.String();
ref3.should.be.instanceOf(Reference);
result = await ref3.set(valueToAddToList);
should.not.exist(result);
});
it('calls an onComplete callback', async () => {
const callback = sinon.spy();
const ref = firebase.database().ref('tests/types/array');
const valueToAddToList = CONTENTS.NEW.number;
const newItemRef = await ref.push(valueToAddToList, callback);
callback.should.be.calledWith(null);
newItemRef.parent.path.should.equal('tests/types/array');
});
});
});

View File

@@ -0,0 +1,21 @@
const { CONTENTS, setDatabaseContents } = TestHelpers.database;
describe('database()', () => {
before(() => setDatabaseContents());
describe('ref() query', () => {
it('orderByChild().equalTo()', async () => {
const snapshot = await firebase
.database()
.ref('tests/query')
.orderByChild('search')
.equalTo('foo')
.once('value');
const val = snapshot.val();
CONTENTS.QUERY[0].should.eql({ ...val[0] });
});
// TODO more query tests
});
});

View File

@@ -0,0 +1,97 @@
const { CONTENTS, setDatabaseContents } = TestHelpers.database;
describe('database()', () => {
before(() => setDatabaseContents());
describe('ref().once()', () => {
it('same reference path works after React Native reload', async () => {
let ref;
let snapshot;
const path = 'tests/types/number';
const dataTypeValue = CONTENTS.DEFAULT.number;
// before reload
ref = firebase.database().ref(path);
snapshot = await ref.once('value');
snapshot.val().should.eql(dataTypeValue);
// RELOAD
await device.reloadReactNative();
// after reload
ref = firebase.database().ref(path);
snapshot = await ref.once('value');
snapshot.val().should.eql(dataTypeValue);
}).timeout(15000);
it(':android: same reference path works after app backgrounded', async () => {
let ref;
let snapshot;
const path = 'tests/types/number';
const dataTypeValue = CONTENTS.DEFAULT.number;
// before
ref = firebase.database().ref(path);
snapshot = await ref.once('value');
snapshot.val().should.eql(dataTypeValue);
await device.sendToHome();
await sleep(250);
await device.launchApp({ newInstance: false });
await sleep(250);
// after
ref = firebase.database().ref(path);
snapshot = await ref.once('value');
snapshot.val().should.eql(dataTypeValue);
}).timeout(15000);
});
describe('ref().on()', () => {
it('same reference path works after React Native reload', async () => {
let ref;
let snapshot;
const path = 'tests/types/number';
const dataTypeValue = CONTENTS.DEFAULT.number;
// before reload
ref = firebase.database().ref(path);
snapshot = await new Promise(resolve => ref.on('value', resolve));
snapshot.val().should.eql(dataTypeValue);
// RELOAD
await device.reloadReactNative();
// after reload
ref = firebase.database().ref(path);
snapshot = await new Promise(resolve => ref.on('value', resolve));
snapshot.val().should.eql(dataTypeValue);
firebase.utils().database.cleanup();
}).timeout(15000);
it(':android: same reference path works after app backgrounded', async () => {
let ref;
let snapshot;
const path = 'tests/types/number';
const dataTypeValue = CONTENTS.DEFAULT.number;
// before background
ref = firebase.database().ref(path);
snapshot = await new Promise(resolve => ref.on('value', resolve));
snapshot.val().should.eql(dataTypeValue);
await device.sendToHome();
await sleep(250);
await device.launchApp({ newInstance: false });
await sleep(250);
// after background
ref = firebase.database().ref(path);
snapshot = await new Promise(resolve => ref.on('value', resolve));
snapshot.val().should.eql(dataTypeValue);
firebase.utils().database.cleanup();
}).timeout(15000);
});
});

View File

@@ -1,6 +1,5 @@
const { setDatabaseContents } = TestHelpers.database;
// TODO use testRunId in refs to prevent multiple test instances interfering with each other
describe('database()', () => {
describe('Snapshot', () => {
before(() => setDatabaseContents());

View File

@@ -13,6 +13,7 @@ module.exports = {
666
),
database.ref('tests/query').set(CONTENTS.QUERY),
database.ref('tests/issues').set(CONTENTS.ISSUES),
]);
},
};

View File

@@ -62,6 +62,7 @@ console.log = (...args) => {
args[0] &&
typeof args[0] === 'string' &&
(args[0].toLowerCase().includes('deprecated') ||
args[0].toLowerCase().includes('require cycle') ||
args[0].toLowerCase().includes('restrictions in the native sdk'))
) {
return undefined;

View File

@@ -254,7 +254,7 @@ PODS:
- React/Core
- React/fishhook
- React/RCTBlob
- RNFirebase (5.0.0):
- RNFirebase (5.1.0-rc1):
- Firebase/Core
- React
- yoga (0.57.1.React)
@@ -325,7 +325,7 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native"
RNFirebase:
:path: "../../ios/RNFirebase.podspec"
:version: "~> 5.0.0"
:version: "~> 5.1.0-rc1"
yoga:
:path: "../node_modules/react-native/ReactCommon/yoga"

View File

@@ -23,7 +23,7 @@
"detox": "^9.0.4",
"fbjs": "^0.8.16",
"firebase-admin": "^5.12.0",
"jet": "^0.1.0",
"jet": "^0.2.0",
"jsonwebtoken": "^8.2.1",
"mocha": "^5.2.0",
"prop-types": "^15.6.1",