# Conflicts:
#	tests/ios/Podfile.lock
This commit is contained in:
Salakar 2017-05-09 13:53:50 +01:00
commit c88894b2a3
17 changed files with 861 additions and 360 deletions

View File

@ -4,6 +4,7 @@
[![Gitter](https://badges.gitter.im/invertase/react-native-firebase.svg)](https://gitter.im/invertase/react-native-firebase?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![npm version](https://img.shields.io/npm/v/react-native-firebase.svg)](https://www.npmjs.com/package/react-native-firebase)
[![License](https://img.shields.io/npm/l/react-native-firebase.svg)](/LICENSE)
[![Donate](https://img.shields.io/badge/Donate-Patreon-green.svg)](https://www.patreon.com/invertase)
**RNFirebase** makes using [Firebase](http://firebase.com) with React Native simple.

View File

@ -28,6 +28,7 @@ import com.google.firebase.database.OnDisconnect;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.DatabaseException;
import com.google.firebase.database.Transaction;
import io.invertase.firebase.Utils;
@ -55,8 +56,8 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule {
final Callback callback) {
try {
mFirebaseDatabase.setPersistenceEnabled(enable);
} catch (Throwable t) {
Log.e(TAG, "FirebaseDatabase setPersistenceEnabled exception", t);
} catch (DatabaseException t) {
}
WritableMap res = Arguments.createMap();

View File

@ -76,6 +76,12 @@ export class ReferenceBase extends Base {
this.path = path || '/';
}
/**
* The last part of a Reference's path (after the last '/')
* The key of a root Reference is null.
* @type {String}
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#key}
*/
get key(): string|null {
return this.path === '/' ? null : this.path.substring(this.path.lastIndexOf('/') + 1);
}

View File

@ -14,8 +14,44 @@ const FirebaseDatabase = NativeModules.RNFirebaseDatabase;
let refId = 1;
/**
* Enum for event types
* @readonly
* @enum {String}
*/
const ReferenceEventTypes = {
value: 'value',
child_added: 'child_added',
child_removed: 'child_removed',
child_changed: 'child_changed',
child_moved: 'child_moved',
};
/**
* @typedef {String} ReferenceLocation - Path to location in the database, relative
* to the root reference. Consists of a path where segments are separated by a
* forward slash (/) and ends in a ReferenceKey - except the root location, which
* has no ReferenceKey.
*
* @example
* // root reference location: '/'
* // non-root reference: '/path/to/referenceKey'
*/
/**
* @typedef {String} ReferenceKey - Identifier for each location that is unique to that
* location, within the scope of its parent. The last part of a ReferenceLocation.
*/
/**
* Represents a specific location in your Database that can be used for
* reading or writing data.
*
* You can reference the root using firebase.database().ref() or a child location
* by calling firebase.database().ref("child/path").
*
* @link https://firebase.google.com/docs/reference/js/firebase.database.Reference
* @class Reference
* @extends ReferenceBase
*/
export default class Reference extends ReferenceBase {
@ -99,23 +135,157 @@ export default class Reference extends ReferenceBase {
}
/**
* iOS: Called once with the initial data at the specified location and then once each
* time the data changes. It won't trigger until the entire contents have been
* synchronized.
*
* @param eventName
* @param successCallback
* @param failureCallback
* @param context TODO
* @returns {*}
* Android: (@link https://github.com/invertase/react-native-firebase/issues/92)
* - Array & number values: Called once with the initial data at the specified
* location and then twice each time the value changes.
* - Other data types: Called once with the initial data at the specified location
* and once each time the data type changes.
*
* @callback onValueCallback
* @param {!DataSnapshot} dataSnapshot - Snapshot representing data at the location
* specified by the current ref. If location has no data, .val() will return null.
*/
on(eventName: string, successCallback: () => any, failureCallback: () => any) {
if (!isFunction(successCallback)) throw new Error('The specified callback must be a function');
if (failureCallback && !isFunction(failureCallback)) throw new Error('The specified error callback must be a function');
this.log.debug('adding reference.on', this.refId, eventName);
/**
* Called once for each initial child at the specified location and then again
* every time a new child is added.
*
* @callback onChildAddedCallback
* @param {!DataSnapshot} dataSnapshot - Snapshot reflecting the data for the
* relevant child.
* @param {?ReferenceKey} previousChildKey - For ordering purposes, the key
* of the previous sibling child by sort order, or null if it is the first child.
*/
/**
* Called once every time a child is removed.
*
* A child will get removed when either:
* - remove() is explicitly called on a child or one of its ancestors
* - set(null) is called on that child or one of its ancestors
* - a child has all of its children removed
* - there is a query in effect which now filters out the child (because it's sort
* order changed or the max limit was hit)
*
* @callback onChildRemovedCallback
* @param {!DataSnapshot} dataSnapshot - Snapshot reflecting the old data for
* the child that was removed.
*/
/**
* Called when a child (or any of its descendants) changes.
*
* A single child_changed event may represent multiple changes to the child.
*
* @callback onChildChangedCallback
* @param {!DataSnapshot} dataSnapshot - Snapshot reflecting new child contents.
* @param {?ReferenceKey} previousChildKey - For ordering purposes, the key
* of the previous sibling child by sort order, or null if it is the first child.
*/
/**
* Called when a child's sort order changes, i.e. its position relative to its
* siblings changes.
*
* @callback onChildMovedCallback
* @param {!DataSnapshot} dataSnapshot - Snapshot reflecting the data of the moved
* child.
* @param {?ReferenceKey} previousChildKey - For ordering purposes, the key
* of the previous sibling child by sort order, or null if it is the first child.
*/
/**
* @typedef (onValueCallback|onChildAddedCallback|onChildRemovedCallback|onChildChangedCallback|onChildMovedCallback) ReferenceEventCallback
*/
/**
* Called if the event subscription is cancelled because the client does
* not have permission to read this data (or has lost the permission to do so).
*
* @callback onFailureCallback
* @param {Error} error - Object indicating why the failure occurred
*/
/**
* Binds callback handlers to when data changes at the current ref's location.
* The primary method of reading data from a Database.
*
* Callbacks can be unbound using {@link off}.
*
* Event Types:
*
* - value: {@link onValueCallback}.
* - child_added: {@link onChildAddedCallback}
* - child_removed: {@link onChildRemovedCallback}
* - child_changed: {@link onChildChangedCallback}
* - child_moved: {@link onChildMovedCallback}
*
* @param {ReferenceEventType} eventType - Type of event to attach a callback for.
* @param {ReferenceEventCallback} successCallback - Function that will be called
* when the event occurs with the new data.
* @param {onFailureCallback=} failureCallbackOrContext - Optional callback that is called
* if the event subscription fails. {@link onFailureCallback}
* @param {*=} context - Optional object to bind the callbacks to when calling them.
* @returns {ReferenceEventCallback} callback function, unmodified (unbound), for
* convenience if you want to pass an inline function to on() and store it later for
* removing using off().
*
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#on}
*/
on(eventType: string, successCallback: () => any, failureCallbackOrContext: () => any, context: any) {
if (!eventType) throw new Error('Error: Query on failed: Was called with 0 arguments. Expects at least 2');
if (!ReferenceEventTypes[eventType]) throw new Error('Query.on failed: First argument must be a valid event type: "value", "child_added", "child_removed", "child_changed", or "child_moved".');
if (!successCallback) throw new Error('Query.on failed: Was called with 1 argument. Expects at least 2.');
if (!isFunction(successCallback)) throw new Error('Query.on failed: Second argument must be a valid function.');
if (arguments.length > 2 && !failureCallbackOrContext) throw new Error('Query.on failed: third argument must either be a cancel callback or a context object.');
this.log.debug('adding reference.on', this.refId, eventType);
let _failureCallback;
let _context;
if (context) {
_context = context;
_failureCallback = failureCallbackOrContext;
} else {
if (isFunction(failureCallbackOrContext)) {
_failureCallback = failureCallbackOrContext;
} else {
_context = failureCallbackOrContext;
}
}
if (_failureCallback) {
_failureCallback = (error) => {
if (error.message.startsWith('FirebaseError: permission_denied')) {
error.message = `permission_denied at /${this.path}: Client doesn\'t have permission to access the desired data.`
}
failureCallbackOrContext(error);
}
}
let _successCallback;
if (_context) {
_successCallback = successCallback.bind(_context);
} else {
_successCallback = successCallback;
}
const listener = {
listenerId: Object.keys(this.listeners).length + 1,
eventName,
successCallback,
failureCallback,
eventName: eventType,
successCallback: _successCallback,
failureCallback: _failureCallback,
};
this.listeners[listener.listenerId] = listener;
this.database.on(this, listener);
return successCallback;
@ -144,29 +314,49 @@ export default class Reference extends ReferenceBase {
}
/**
* Detaches a callback attached with on().
*
* @param eventName
* @param origCB
* @returns {*}
* Calling off() on a parent listener will not automatically remove listeners
* registered on child nodes.
*
* If on() was called multiple times with the same eventType off() must be
* called multiple times to completely remove it.
*
* If a callback is not specified, all callbacks for the specified eventType
* will be removed. If no eventType or callback is specified, all callbacks
* for the Reference will be removed.
*
* If a context is specified, it too is used as a filter parameter: a callback
* will only be detached if, when it was attached with on(), the same event type,
* callback function and context were provided.
*
* If no callbacks matching the parameters provided are found, no callbacks are
* detached.
*
* @param {('value'|'child_added'|'child_changed'|'child_removed'|'child_moved')=} eventType - Type of event to detach callback for.
* @param {Function=} originalCallback - Original callback passed to on()
* @param {*=} context - The context passed to on() when the callback was bound
*
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#off}
*/
off(eventName?: string = '', origCB?: () => any) {
this.log.debug('ref.off(): ', this.refId, eventName);
off(eventType?: string = '', originalCallback?: () => any) {
this.log.debug('ref.off(): ', this.refId, eventType);
// $FlowFixMe
const listeners: Array<DatabaseListener> = Object.values(this.listeners);
let listenersToRemove;
if (eventName && origCB) {
if (eventType && originalCallback) {
listenersToRemove = listeners.filter((listener) => {
return listener.eventName === eventName && listener.successCallback === origCB;
return listener.eventName === eventType && listener.successCallback === originalCallback;
});
// Only remove a single listener as per the web spec
if (listenersToRemove.length > 1) listenersToRemove = [listenersToRemove[0]];
} else if (eventName) {
} else if (eventType) {
listenersToRemove = listeners.filter((listener) => {
return listener.eventName === eventName;
return listener.eventName === eventType;
});
} else if (origCB) {
} else if (originalCallback) {
listenersToRemove = listeners.filter((listener) => {
return listener.successCallback === origCB;
return listener.successCallback === originalCallback;
});
} else {
listenersToRemove = listeners;
@ -349,9 +539,12 @@ export default class Reference extends ReferenceBase {
}
/**
* Get a specified child
* @param path
* @returns {Reference}
* Creates a Reference to a child of the current Reference, using a relative path.
* No validation is performed on the path to ensure it has a valid format.
* @param {String} path relative to current ref's location
* @returns {!Reference} A new Reference to the path provided, relative to the current
* Reference
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#child}
*/
child(path: string) {
return new Reference(this.database, `${this.path}/${path}`);
@ -370,6 +563,7 @@ export default class Reference extends ReferenceBase {
* same instance of firebase.app.App - multiple firebase apps not currently supported.
* @param {Reference} otherRef - Other reference to compare to this one
* @return {Boolean} Whether otherReference is equal to this one
*
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#isEqual}
*/
isEqual(otherRef: Reference): boolean {
@ -381,8 +575,10 @@ export default class Reference extends ReferenceBase {
*/
/**
* Returns the parent ref of the current ref i.e. a ref of /foo/bar would return a new ref to '/foo'
* @returns {*}
* The parent location of a Reference, or null for the root Reference.
* @type {Reference}
*
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#parent}
*/
get parent(): Reference|null {
if (this.path === '/') return null;
@ -392,6 +588,7 @@ export default class Reference extends ReferenceBase {
/**
* A reference to itself
* @type {!Reference}
*
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#ref}
*/
get ref(): Reference {
@ -399,8 +596,10 @@ export default class Reference extends ReferenceBase {
}
/**
* Returns a ref to the root of db - '/'
* @returns {Reference}
* Reference to the root of the database: '/'
* @type {!Reference}
*
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#root}
*/
get root(): Reference {
return new Reference(this.database, '/');

View File

@ -5,6 +5,7 @@ import Reference from './reference.js';
import { isObject, deepGet, deepExists } from './../../utils';
/**
* @class DataSnapshot
* @link https://firebase.google.com/docs/reference/js/firebase.database.DataSnapshot
*/
export default class Snapshot {

2
tests/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
__tests__/build/
ios/build/

View File

@ -103,6 +103,84 @@ function pendingTestTests({ it: _it, describe: _describe }) {
otherTest.should.be.called();
});
});
_describe('when an outer context is focused', () => {
_it('a pending test will still not run', async () => {
const pendingTest = sinon.spy();
const otherTest = sinon.spy();
const unfocusedTest = sinon.spy();
const testSuite = new TestSuite('', '', {});
testSuite.addTests(({ fdescribe, it, xit }) => {
fdescribe('', () => {
xit('', pendingTest);
it('', otherTest);
});
it('', unfocusedTest);
});
testSuite.setStore({
getState: () => { return {}; },
});
const testIdsToRun = Object.keys(testSuite.testDefinitions.focusedTestIds).reduce((memo, testId) => {
if (!testSuite.testDefinitions.pendingTestIds[testId]) {
memo.push(testId);
}
return memo;
}, []);
await testSuite.run(testIdsToRun);
pendingTest.should.not.be.called();
otherTest.should.be.called();
unfocusedTest.should.not.be.called();
});
});
_describe('when an outer context is focused', () => {
_it('a pending context will still not run', async () => {
const pendingTest = sinon.spy();
const otherTest = sinon.spy();
const unfocusedTest = sinon.spy();
const testSuite = new TestSuite('', '', {});
testSuite.addTests(({ fdescribe, it, xdescribe }) => {
fdescribe('', () => {
xdescribe('', () => {
it('', pendingTest);
});
it('', otherTest);
});
it('', unfocusedTest);
});
testSuite.setStore({
getState: () => { return {}; },
});
const testIdsToRun = Object.keys(testSuite.testDefinitions.focusedTestIds).reduce((memo, testId) => {
if (!testSuite.testDefinitions.pendingTestIds[testId]) {
memo.push(testId);
}
return memo;
}, []);
await testSuite.run(testIdsToRun);
pendingTest.should.not.be.called();
otherTest.should.be.called();
unfocusedTest.should.not.be.called();
});
});
}
export default pendingTestTests;

View File

@ -1,136 +0,0 @@
PODS:
- Firebase/Analytics (3.14.0):
- Firebase/Core
- Firebase/AppIndexing (3.14.0):
- Firebase/Core
- FirebaseAppIndexing (= 1.2.0)
- Firebase/Auth (3.14.0):
- Firebase/Core
- FirebaseAuth (= 3.1.1)
- Firebase/Core (3.14.0):
- FirebaseAnalytics (= 3.7.0)
- FirebaseCore (= 3.5.1)
- Firebase/Crash (3.14.0):
- Firebase/Core
- FirebaseCrash (= 1.1.6)
- Firebase/Database (3.14.0):
- Firebase/Core
- FirebaseDatabase (= 3.1.2)
- Firebase/DynamicLinks (3.14.0):
- Firebase/Core
- FirebaseDynamicLinks (= 1.3.3)
- Firebase/Messaging (3.14.0):
- Firebase/Core
- FirebaseMessaging (= 1.2.2)
- Firebase/RemoteConfig (3.14.0):
- Firebase/Core
- FirebaseRemoteConfig (= 1.3.4)
- Firebase/Storage (3.14.0):
- Firebase/Core
- FirebaseStorage (= 1.1.0)
- FirebaseAnalytics (3.7.0):
- FirebaseCore (~> 3.5)
- FirebaseInstanceID (~> 1.0)
- GoogleToolboxForMac/NSData+zlib (~> 2.1)
- FirebaseAppIndexing (1.2.0)
- FirebaseAuth (3.1.1):
- FirebaseAnalytics (~> 3.7)
- GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)
- GTMSessionFetcher/Core (~> 1.1)
- FirebaseCore (3.5.1):
- GoogleToolboxForMac/NSData+zlib (~> 2.1)
- FirebaseCrash (1.1.6):
- FirebaseAnalytics (~> 3.7)
- FirebaseInstanceID (~> 1.0)
- GoogleToolboxForMac/Logger (~> 2.1)
- GoogleToolboxForMac/NSData+zlib (~> 2.1)
- Protobuf (~> 3.1)
- FirebaseDatabase (3.1.2):
- FirebaseAnalytics (~> 3.7)
- FirebaseDynamicLinks (1.3.3):
- FirebaseAnalytics (~> 3.7)
- FirebaseInstanceID (1.0.9)
- FirebaseMessaging (1.2.2):
- FirebaseAnalytics (~> 3.7)
- FirebaseInstanceID (~> 1.0)
- GoogleToolboxForMac/Logger (~> 2.1)
- Protobuf (~> 3.1)
- FirebaseRemoteConfig (1.3.4):
- FirebaseAnalytics (~> 3.7)
- FirebaseInstanceID (~> 1.0)
- GoogleToolboxForMac/NSData+zlib (~> 2.1)
- Protobuf (~> 3.1)
- FirebaseStorage (1.1.0):
- FirebaseAnalytics (~> 3.7)
- GTMSessionFetcher/Core (~> 1.1)
- GoogleToolboxForMac/DebugUtils (2.1.1):
- GoogleToolboxForMac/Defines (= 2.1.1)
- GoogleToolboxForMac/Defines (2.1.1)
- GoogleToolboxForMac/Logger (2.1.1):
- GoogleToolboxForMac/Defines (= 2.1.1)
- GoogleToolboxForMac/NSData+zlib (2.1.1):
- GoogleToolboxForMac/Defines (= 2.1.1)
- GoogleToolboxForMac/NSDictionary+URLArguments (2.1.1):
- GoogleToolboxForMac/DebugUtils (= 2.1.1)
- GoogleToolboxForMac/Defines (= 2.1.1)
- GoogleToolboxForMac/NSString+URLArguments (= 2.1.1)
- GoogleToolboxForMac/NSString+URLArguments (2.1.1)
- GTMSessionFetcher/Core (1.1.8)
- Protobuf (3.2.0)
- React (0.44.0):
- React/Core (= 0.44.0)
- React/Core (0.44.0):
- React/cxxreact
- Yoga (= 0.44.0.React)
- React/cxxreact (0.44.0):
- React/jschelpers
- React/jschelpers (0.44.0)
- RNFirebase (1.0.2)
- Yoga (0.44.0.React)
DEPENDENCIES:
- Firebase/Analytics
- Firebase/AppIndexing
- Firebase/Auth
- Firebase/Core
- Firebase/Crash
- Firebase/Database
- Firebase/DynamicLinks
- Firebase/Messaging
- Firebase/RemoteConfig
- Firebase/Storage
- React (from `../node_modules/react-native`)
- RNFirebase (from `./../../`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
EXTERNAL SOURCES:
React:
:path: ../node_modules/react-native
RNFirebase:
:path: ./../../
Yoga:
:path: ../node_modules/react-native/ReactCommon/yoga
SPEC CHECKSUMS:
Firebase: 85a581fb04e44f63ae9f4fbc8d6dabf4a4c18653
FirebaseAnalytics: 0d1b7d81d5021155be37702a94ba1ec16d45365d
FirebaseAppIndexing: d0fa52ce0ad13f4b5b2f09e4b47fb0dc2213f4e9
FirebaseAuth: cc8a1824170adbd351edb7f994490a3fb5c18be6
FirebaseCore: 225d40532489835a034b8f4e2c9c87fbf4f615a2
FirebaseCrash: db4c05d9c75baa050744d31b36357c8f1efba481
FirebaseDatabase: 05c96d7b43a7368dc91c07791adb49683e1738d1
FirebaseDynamicLinks: f0d025dd29a1d70418c003344813b67ab748ffb9
FirebaseInstanceID: 2d0518b1378fe9d685ef40cbdd63d2fdc1125339
FirebaseMessaging: df8267f378580a24174ce7861233aa11d5c90109
FirebaseRemoteConfig: af3003f4e8daa2bd1d5cf90d3cccc1fe224f8ed9
FirebaseStorage: a5c55b23741a49a72af8f30f95b3bb5ddbeda12d
GoogleToolboxForMac: 8e329f1b599f2512c6b10676d45736bcc2cbbeb0
GTMSessionFetcher: 6f8d8b28b7e345549ac471071608170b31cb4977
Protobuf: 745f59e122e5de98d4d7ef291e264a0eef80f58e
React: d2077cc20245ccdc8bfe1fdc002f2003318ae8d8
RNFirebase: 9b9ab212d14243db38bee3637029f1752c2a349f
Yoga: a92a5d8e128905bf9f29c82f870192a6e873dd98
PODFILE CHECKSUM: 62ff31b87feae064d9ea6ae4b9c4375b48a6153e
COCOAPODS: 1.2.0

View File

@ -286,8 +286,7 @@ class TestRun {
suiteId: this.testSuite.id,
status: RunStatus.ERR,
time: Date.now() - this.runStartTime,
message: `Test suite failed: ${error.message}`,
stackTrace: error.stack,
message: `Test suite failed: ${error.message}`
});
});
}
@ -306,7 +305,7 @@ class TestRun {
}
async _safelyRunFunction(func, timeOutDuration, description) {
const syncResultOrPromise = tryCatcher(func);
const syncResultOrPromise = captureThrownErrors(func);
if (syncResultOrPromise.error) {
// Synchronous Error
@ -314,49 +313,59 @@ class TestRun {
}
// Asynchronous Error
return promiseToCallBack(syncResultOrPromise.value, timeOutDuration, description);
return capturePromiseErrors(syncResultOrPromise.result, timeOutDuration, description);
}
}
/**
* Try catch to object
* @returns {{}}
* Call a function and capture any errors that are immediately thrown.
* @returns {Object} Object containing result of executing the function, or the error
* message that was captured
* @private
*/
function tryCatcher(func) {
function captureThrownErrors(func) {
const result = {};
try {
result.value = func();
} catch (e) {
result.error = e;
result.result = func();
} catch (error) {
result.error = error;
}
return result;
}
/**
* Make a promise callback-able to trap errors
* @param promise
* Wraps a promise so that if it's rejected or an error is thrown while it's being
* evaluated, it's captured and thrown no further
* @param {*} target - Target to wrap. If a thenable object, it's wrapped so if it's
* rejected or an error is thrown, it will be captured. If a non-thenable object,
* wrapped in resolved promise and returned.
* @param {Number} timeoutDuration - Number of milliseconds the promise is allowed
* to pend before it's considered timed out
* @param {String} description - Description of the context the promises is defined
* in, used for reporting where a timeout occurred in the resulting error message.
* @private
*/
function promiseToCallBack(promise, timeoutDuration, description) {
function capturePromiseErrors(target, timeoutDuration, description) {
let returnValue = null;
try {
returnValue = Promise.resolve(promise)
returnValue = Promise.resolve(target)
.then(() => {
return null;
}, (error) => {
return Promise.resolve(error);
})
.timeout(timeoutDuration, `${description} took longer than ${timeoutDuration}ms. This can be extended with the timeout option.`)
.catch((error) => {
return Promise.resolve(error);
});
})
.timeout(timeoutDuration,
`${description} took longer than ${timeoutDuration}ms. This can be extended with the timeout option.`
);
} catch (error) {
returnValue = Promise.resolve(error);
}

View File

@ -111,19 +111,19 @@ class TestSuite {
*/
async run(testIds = undefined) {
const testsToRun = (() => {
if (testIds) {
return testIds.map((id) => {
const test = this.testDefinitions.tests[id];
return (testIds || Object.keys(this.testDefinitions.tests)).reduce((memo, id) => {
const test = this.testDefinitions.tests[id];
if (!test) {
throw new RangeError(`ReactNativeFirebaseTests.TestRunError: Test with id ${id} not found in test suite ${this.name}`);
}
if (!test) {
throw new RangeError(`ReactNativeFirebaseTests.TestRunError: Test with id ${id} not found in test suite ${this.name}`);
}
return test;
});
}
if (!this.testDefinitions.pendingTestIds[id]) {
memo.push(test);
}
return Object.values(this.testDefinitions.tests);
return memo;
}, []);
})();
const testRun = new TestRun(this, testsToRun.reverse(), this.testDefinitions);

View File

@ -14,13 +14,14 @@ export function setSuiteStatus({ suiteId, status, time, message, progress }) {
};
}
export function setTestStatus({ testId, status, time = 0, message = null }) {
export function setTestStatus({ testId, status, stackTrace, time = 0, message = null }) {
return {
type: TEST_SET_STATUS,
testId,
status,
message,
stackTrace,
time,
};
}

View File

@ -12,6 +12,7 @@ function testsReducers(state = initState.tests, action: Object): State {
flattened[`${action.testId}.status`] = action.status;
flattened[`${action.testId}.message`] = action.message;
flattened[`${action.testId}.time`] = action.time;
flattened[`${action.testId}.stackTrace`] = action.stackTrace;
return unflatten(flattened);
}

View File

@ -51,35 +51,28 @@ class Test extends React.Component {
setParams({ test });
}
renderError() {
const { test: { message } } = this.props;
if (message) {
return (
<ScrollView>
<Text style={styles.codeHeader}>Test Error</Text>
<Text style={styles.code}>
<Text>{message}</Text>
</Text>
</ScrollView>
);
}
return null;
}
render() {
const { test: { func, status, time } } = this.props;
const { test: { stackTrace, description, func, status, time }, testContextName } = this.props;
return (
<View style={styles.container}>
{Test.renderBanner({ status, time })}
<View style={styles.content}>
{this.renderError()}
<Text style={styles.codeHeader}>Test Code Preview</Text>
<ScrollView>
<Text style={styles.code}>
<View >
<ScrollView style={styles.sectionContainer}>
<Text style={styles.heading}>{testContextName}</Text>
<Text style={styles.description}>{description}</Text>
</ScrollView>
<ScrollView style={styles.sectionContainer}>
<Text style={styles.heading}>Test Error</Text>
<Text style={styles.description}>
<Text>{stackTrace || 'None.'}</Text>
</Text>
</ScrollView>
<Text style={styles.heading}>
Test Code Preview
</Text>
<ScrollView style={styles.sectionContainer}>
<Text style={styles.description}>
{beautify(removeLastLine(removeFirstLine(func.toString())), { indent_size: 4, break_chained_methods: true })}
</Text>
</ScrollView>
@ -93,10 +86,13 @@ Test.propTypes = {
test: PropTypes.shape({
status: PropTypes.string,
time: PropTypes.number,
message: PropTypes.string,
func: PropTypes.function,
stackTrace: PropTypes.function,
description: PropTypes.string,
}).isRequired,
testContextName: PropTypes.string,
navigation: PropTypes.shape({
setParams: PropTypes.func.isRequired,
}).isRequired,
@ -107,27 +103,32 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: '#ffffff',
},
content: {},
code: {
backgroundColor: '#3F373A',
color: '#c3c3c3',
padding: 5,
fontSize: 12,
sectionContainer: {
minHeight: 100,
},
codeHeader: {
fontWeight: '600',
fontSize: 18,
backgroundColor: '#000',
color: '#fff',
heading: {
padding: 5,
backgroundColor: '#0288d1',
fontWeight: '600',
color: '#ffffff',
fontSize: 16,
},
description: {
padding: 5,
fontSize: 14,
},
});
function select({ tests }, { navigation: { state: { params: { testId } } } }) {
function select({ tests, testContexts }, { navigation: { state: { params: { testId } } } }) {
const test = tests[testId];
let testContext = testContexts[test.testContextId];
while(testContext.parentContextId && testContexts[testContext.parentContextId].parentContextId) {
testContext = testContexts[testContext.parentContextId];
}
return {
test,
testContextName: testContext.name,
};
}

View File

@ -1,4 +1,5 @@
import onTests from './onTests';
import onTests from './on/onTests';
import onValueTests from './on/onValueTests';
import offTests from './offTests';
import onceTests from './onceTests';
import setTests from './setTests';
@ -19,8 +20,8 @@ import DatabaseContents from '../../support/DatabaseContents';
const testGroups = [
factoryTests, keyTests, parentTests, childTests, rootTests,
pushTests, onTests, offTests, onceTests, updateTests, removeTests, setTests,
transactionTests, queryTests, refTests, isEqualTests,
pushTests, onTests, onValueTests, offTests, onceTests, updateTests,
removeTests, setTests, transactionTests, queryTests, refTests, isEqualTests,
];
function registerTestSuite(testSuite) {

View File

@ -0,0 +1,52 @@
import 'should-sinon';
function onTests({ describe, it, firebase, context }) {
describe('ref().on()', () => {
// Observed Web API Behaviour
context('when no eventName is provided', () => {
it('then raises an error', () => {
const ref = firebase.native.database().ref('tests/types/number');
(() => { ref.on(); }).should.throw('Error: Query on failed: Was called with 0 arguments. Expects at least 2');
});
});
// Observed Web API Behaviour
context('when no callback function is provided', () => {
it('then raises an error', () => {
const ref = firebase.native.database().ref('tests/types/number');
(() => { ref.on('value'); }).should.throw('Query.on failed: Was called with 1 argument. Expects at least 2.');
});
});
// Observed Web API Behaviour
context('when an invalid eventName is provided', () => {
it('then raises an error', () => {
const ref = firebase.native.database().ref('tests/types/number');
(() => { ref.on('invalid', () => {}); }).should.throw('Query.on failed: First argument must be a valid event type: "value", "child_added", "child_removed", "child_changed", or "child_moved".');
});
});
// Observed Web API Behaviour
context('when an invalid success callback function is provided', () => {
it('then raises an error', () => {
const ref = firebase.native.database().ref('tests/types/number');
(() => { ref.on('value', 1); }).should.throw('Query.on failed: Second argument must be a valid function.');
});
});
// Observed Web API Behaviour
context('when an invalid failure callback function is provided', () => {
it('then raises an error', () => {
const ref = firebase.native.database().ref('tests/types/number');
(() => { ref.on('value', () => {}, null); }).should.throw('Query.on failed: third argument must either be a cancel callback or a context object.');
});
});
});
}
export default onTests;

View File

@ -0,0 +1,409 @@
import { Platform } from 'react-native';
import sinon from 'sinon';
import 'should-sinon';
import Promise from 'bluebird';
import DatabaseContents from '../../../support/DatabaseContents';
/**
* On Android, some data types result in callbacks that get called twice every time
* they are updated. This appears to be behaviour coming from the Android Firebase
* library itself.
*
* See https://github.com/invertase/react-native-firebase/issues/92 for details
*/
const DATATYPES_WITH_DUPLICATE_CALLBACK_CALLS = [
'array',
'number'
];
function onTests({ fdescribe, context, it, firebase, tryCatch }) {
fdescribe('ref().on(\'value\')', () => {
// Documented Web API Behaviour
it('returns the success callback', () => {
// Setup
const successCallback = sinon.spy();
const ref = firebase.native.database().ref('tests/types/array');
// Assertions
ref.on('value', successCallback).should.eql(successCallback);
// Tear down
ref.off();
});
// Documented Web API Behaviour
it('calls callback with null if there is no data at ref', async () => {
// Setup
const ref = firebase.native.database().ref('tests/types/invalid');
const callback = sinon.spy();
// Test
await new Promise((resolve) => {
ref.on('value', (snapshot) => {
callback(snapshot.val());
resolve();
});
});
// Assertions
callback.should.be.calledWith(null);
await ref.set(1);
callback.should.be.calledWith(1);
// Teardown
ref.off();
await ref.set(null);
});
// Documented Web API Behaviour
it('calls callback with the initial data and then when value changes', () => {
return Promise.each(Object.keys(DatabaseContents.DEFAULT), async (dataRef) => {
// Setup
const ref = firebase.native.database().ref(`tests/types/${dataRef}`);
const currentDataValue = DatabaseContents.DEFAULT[dataRef];
const callback = sinon.spy();
// Test
await new Promise((resolve) => {
ref.on('value', (snapshot) => {
callback(snapshot.val());
resolve();
});
});
callback.should.be.calledWith(currentDataValue);
const newDataValue = DatabaseContents.NEW[dataRef];
await ref.set(newDataValue);
// Assertions
callback.should.be.calledWith(newDataValue);
if (Platform.OS === 'android' && DATATYPES_WITH_DUPLICATE_CALLBACK_CALLS.includes(dataRef)) {
callback.should.be.calledThrice();
} else {
callback.should.be.calledTwice();
}
// Tear down
ref.off();
await ref.set(currentDataValue);
});
});
it('calls callback when children of the ref change', async () => {
const ref = firebase.native.database().ref('tests/types/object');
const currentDataValue = DatabaseContents.DEFAULT.object;
const callback = sinon.spy();
// Test
await new Promise((resolve) => {
ref.on('value', (snapshot) => {
callback(snapshot.val());
resolve();
});
});
callback.should.be.calledWith(currentDataValue);
const newDataValue = DatabaseContents.NEW.string;
const childRef = firebase.native.database().ref('tests/types/object/foo2');
await childRef.set(newDataValue);
// Assertions
callback.should.be.calledWith({
...currentDataValue,
foo2: newDataValue,
});
callback.should.be.calledTwice();
// Tear down
ref.off();
await ref.set(currentDataValue);
});
it('calls callback when child of the ref is added', async () => {
const ref = firebase.native.database().ref('tests/types/array');
const currentDataValue = DatabaseContents.DEFAULT.array;
const callback = sinon.spy();
// Test
await new Promise((resolve) => {
ref.on('value', (snapshot) => {
callback(snapshot.val());
resolve();
});
});
callback.should.be.calledWith(currentDataValue);
const newElementRef = await ref.push(37);
const arrayAsObject = currentDataValue.reduce((memo, element, index) => {
memo[index] = element;
return memo;
}, {});
// Assertions
callback.should.be.calledWith({
...arrayAsObject,
[newElementRef.key]: 37,
});
if (Platform.OS === 'android') {
callback.should.be.calledThrice();
} else {
callback.should.be.calledTwice();
}
// Tear down
ref.off();
await ref.set(currentDataValue);
});
it('doesn\'t call callback when the ref is updated with the same value', async () => {
const ref = firebase.native.database().ref('tests/types/object');
const currentDataValue = DatabaseContents.DEFAULT.object;
const callback = sinon.spy();
// Test
await new Promise((resolve) => {
ref.on('value', (snapshot) => {
callback(snapshot.val());
resolve();
});
});
callback.should.be.calledWith(currentDataValue);
await ref.set(currentDataValue);
// Assertions
callback.should.be.calledOnce(); // Callback is not called again
// Tear down
ref.off();
});
// Documented Web API Behaviour
it('allows binding multiple callbacks to the same ref', () => {
return Promise.each(Object.keys(DatabaseContents.DEFAULT), async (dataRef) => {
// Setup
const ref = firebase.native.database().ref(`tests/types/${dataRef}`);
const currentDataValue = DatabaseContents.DEFAULT[dataRef];
const callbackA = sinon.spy();
const callbackB = sinon.spy();
// Test
await new Promise((resolve) => {
ref.on('value', (snapshot) => {
callbackA(snapshot.val());
resolve();
});
});
await new Promise((resolve) => {
ref.on('value', (snapshot) => {
callbackB(snapshot.val());
resolve();
});
});
callbackA.should.be.calledWith(currentDataValue);
callbackA.should.be.calledOnce();
callbackB.should.be.calledWith(currentDataValue);
callbackB.should.be.calledOnce();
const newDataValue = DatabaseContents.NEW[dataRef];
await ref.set(newDataValue);
callbackA.should.be.calledWith(newDataValue);
callbackB.should.be.calledWith(newDataValue);
if (Platform.OS === 'android' && DATATYPES_WITH_DUPLICATE_CALLBACK_CALLS.includes(dataRef)) {
callbackA.should.be.calledThrice();
callbackB.should.be.calledThrice();
} else {
callbackA.should.be.calledTwice();
callbackB.should.be.calledTwice();
}
// Tear down
ref.off();
});
});
context('when no failure callback is provided', () => {
it('then does not call the callback for a ref to un-permitted location', () => {
const invalidRef = firebase.native.database().ref('nope');
const callback = sinon.spy();
invalidRef.on('value', callback);
/**
* As we are testing that a callback is "never" called, we just wait for
* a reasonable time before giving up.
*/
return new Promise((resolve) => {
setTimeout(() => {
callback.should.not.be.called();
invalidRef.off();
resolve();
}, 1000);
});
});
// Documented Web API Behaviour
it('then calls callback bound to the specified context with the initial data and then when value changes', () => {
return Promise.each(Object.keys(DatabaseContents.DEFAULT), async (dataRef) => {
// Setup
const ref = firebase.native.database().ref(`tests/types/${dataRef}`);
const currentDataValue = DatabaseContents.DEFAULT[dataRef];
const context = {
callCount: 0,
};
// Test
await new Promise((resolve) => {
ref.on('value', function(snapshot) {
this.value = snapshot.val();
this.callCount += 1;
resolve();
}, context);
});
context.value.should.eql(currentDataValue);
context.callCount.should.eql(1);
const newDataValue = DatabaseContents.NEW[dataRef];
await ref.set(newDataValue);
// Assertions
context.value.should.eql(newDataValue);
if (Platform.OS === 'android' && DATATYPES_WITH_DUPLICATE_CALLBACK_CALLS.includes(dataRef)) {
context.callCount.should.eql(3);
} else {
context.callCount.should.eql(2);
}
// Tear down
ref.off();
await ref.set(currentDataValue);
});
});
});
// Observed Web API Behaviour
context('when a failure callback is provided', () => {
it('then calls only the failure callback for a ref to un-permitted location', () => {
const invalidRef = firebase.native.database().ref('nope');
const callback = sinon.spy();
return new Promise((resolve, reject) => {
invalidRef.on('value', callback, tryCatch((error) => {
error.message.should.eql(
'permission_denied at /nope: Client doesn\'t have permission to access the desired data.'
);
error.name.should.eql('Error');
callback.should.not.be.called();
invalidRef.off();
resolve();
}, reject));
});
});
// Documented Web API Behaviour
it('then calls callback bound to the specified context with the initial data and then when value changes', () => {
return Promise.each(Object.keys(DatabaseContents.DEFAULT), async (dataRef) => {
// Setup
const ref = firebase.native.database().ref(`tests/types/${dataRef}`);
const currentDataValue = DatabaseContents.DEFAULT[dataRef];
const context = {
callCount: 0,
};
const failureCallback = sinon.spy();
// Test
await new Promise((resolve) => {
ref.on('value', function(snapshot) {
this.value = snapshot.val();
this.callCount += 1;
resolve();
}, failureCallback, context);
});
failureCallback.should.not.be.called();
context.value.should.eql(currentDataValue);
context.callCount.should.eql(1);
const newDataValue = DatabaseContents.NEW[dataRef];
await ref.set(newDataValue);
// Assertions
context.value.should.eql(newDataValue);
if (Platform.OS === 'android' && DATATYPES_WITH_DUPLICATE_CALLBACK_CALLS.includes(dataRef)) {
context.callCount.should.eql(3);
} else {
context.callCount.should.eql(2);
}
// Tear down
ref.off();
await ref.set(currentDataValue);
});
})
});
});
}
export default onTests;

View File

@ -1,125 +0,0 @@
import sinon from 'sinon';
import 'should-sinon';
import Promise from 'bluebird';
import DatabaseContents from '../../support/DatabaseContents';
function onTests({ describe, it, firebase, tryCatch }) {
describe('ref().on()', () => {
it('calls callback when value changes', () => {
return Promise.each(Object.keys(DatabaseContents.DEFAULT), async (dataRef) => {
// Setup
const ref = firebase.native.database().ref(`tests/types/${dataRef}`);
const currentDataValue = DatabaseContents.DEFAULT[dataRef];
const callback = sinon.spy();
// Test
await new Promise((resolve) => {
ref.on('value', (snapshot) => {
callback(snapshot.val());
resolve();
});
});
callback.should.be.calledWith(currentDataValue);
const newDataValue = DatabaseContents.NEW[dataRef];
await ref.set(newDataValue);
// Assertions
callback.should.be.calledWith(newDataValue);
// Tear down
ref.off();
});
});
it('allows binding multiple callbacks to the same ref', () => {
return Promise.each(Object.keys(DatabaseContents.DEFAULT), async (dataRef) => {
// Setup
const ref = firebase.native.database().ref(`tests/types/${dataRef}`);
const currentDataValue = DatabaseContents.DEFAULT[dataRef];
const callbackA = sinon.spy();
const callbackB = sinon.spy();
// Test
await new Promise((resolve) => {
ref.on('value', (snapshot) => {
callbackA(snapshot.val());
resolve();
});
});
await new Promise((resolve) => {
ref.on('value', (snapshot) => {
callbackB(snapshot.val());
resolve();
});
});
callbackA.should.be.calledWith(currentDataValue);
callbackB.should.be.calledWith(currentDataValue);
// Tear down
ref.off();
});
});
it('calls callback with current values', () => {
return Promise.each(Object.keys(DatabaseContents.DEFAULT), (dataRef) => {
// Setup
const dataTypeValue = DatabaseContents.DEFAULT[dataRef];
const ref = firebase.native.database().ref(`tests/types/${dataRef}`);
// Test
return ref.on('value', (snapshot) => {
// Assertion
snapshot.val().should.eql(dataTypeValue);
// Tear down
ref.off();
});
});
});
it('errors if permission denied', () => {
return new Promise((resolve, reject) => {
const successCb = tryCatch(() => {
// Assertion
reject(new Error('No permission denied error'));
}, reject);
const failureCb = tryCatch((error) => {
// Assertion
error.message.includes('permission_denied').should.be.true();
resolve();
}, reject);
// Setup
const invalidRef = firebase.native.database().ref('nope');
// Test
invalidRef.on('value', successCb, failureCb);
});
});
});
}
export default onTests;