Merge pull request #91 from greena13/dev_ref_on
[Database] Make Reference.on() more consistent with Web API
This commit is contained in:
commit
da2a7ec3a3
|
@ -76,6 +76,12 @@ export class ReferenceBase extends Base {
|
||||||
this.path = path || '/';
|
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 {
|
get key(): string|null {
|
||||||
return this.path === '/' ? null : this.path.substring(this.path.lastIndexOf('/') + 1);
|
return this.path === '/' ? null : this.path.substring(this.path.lastIndexOf('/') + 1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,44 @@ const FirebaseDatabase = NativeModules.RNFirebaseDatabase;
|
||||||
let refId = 1;
|
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
|
* @link https://firebase.google.com/docs/reference/js/firebase.database.Reference
|
||||||
* @class Reference
|
* @class Reference
|
||||||
|
* @extends ReferenceBase
|
||||||
*/
|
*/
|
||||||
export default class Reference extends ReferenceBase {
|
export default class Reference extends ReferenceBase {
|
||||||
|
|
||||||
|
@ -99,23 +135,152 @@ export default class Reference extends ReferenceBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Called once with the initial data at the specified location and then once each
|
||||||
|
* time the data changes.
|
||||||
*
|
*
|
||||||
* @param eventName
|
* @callback onValueCallback
|
||||||
* @param successCallback
|
* @param {!DataSnapshot} dataSnapshot - Snapshot representing data at the location
|
||||||
* @param failureCallback
|
* specified by the current ref. It won't trigger (.val() won't return a value)
|
||||||
* @param context TODO
|
* until the entire contents have been synchronized. If location has no data, .val()
|
||||||
* @returns {*}
|
* 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');
|
* Called once for each initial child at the specified location and then again
|
||||||
this.log.debug('adding reference.on', this.refId, eventName);
|
* 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 = {
|
const listener = {
|
||||||
listenerId: Object.keys(this.listeners).length + 1,
|
listenerId: Object.keys(this.listeners).length + 1,
|
||||||
eventName,
|
eventName: eventType,
|
||||||
successCallback,
|
successCallback: _successCallback,
|
||||||
failureCallback,
|
failureCallback: _failureCallback,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.listeners[listener.listenerId] = listener;
|
this.listeners[listener.listenerId] = listener;
|
||||||
this.database.on(this, listener);
|
this.database.on(this, listener);
|
||||||
return successCallback;
|
return successCallback;
|
||||||
|
@ -144,29 +309,49 @@ export default class Reference extends ReferenceBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Detaches a callback attached with on().
|
||||||
*
|
*
|
||||||
* @param eventName
|
* Calling off() on a parent listener will not automatically remove listeners
|
||||||
* @param origCB
|
* registered on child nodes.
|
||||||
* @returns {*}
|
*
|
||||||
|
* 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) {
|
off(eventType?: string = '', originalCallback?: () => any) {
|
||||||
this.log.debug('ref.off(): ', this.refId, eventName);
|
this.log.debug('ref.off(): ', this.refId, eventType);
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
const listeners: Array<DatabaseListener> = Object.values(this.listeners);
|
const listeners: Array<DatabaseListener> = Object.values(this.listeners);
|
||||||
let listenersToRemove;
|
let listenersToRemove;
|
||||||
if (eventName && origCB) {
|
if (eventType && originalCallback) {
|
||||||
listenersToRemove = listeners.filter((listener) => {
|
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
|
// Only remove a single listener as per the web spec
|
||||||
if (listenersToRemove.length > 1) listenersToRemove = [listenersToRemove[0]];
|
if (listenersToRemove.length > 1) listenersToRemove = [listenersToRemove[0]];
|
||||||
} else if (eventName) {
|
} else if (eventType) {
|
||||||
listenersToRemove = listeners.filter((listener) => {
|
listenersToRemove = listeners.filter((listener) => {
|
||||||
return listener.eventName === eventName;
|
return listener.eventName === eventType;
|
||||||
});
|
});
|
||||||
} else if (origCB) {
|
} else if (originalCallback) {
|
||||||
listenersToRemove = listeners.filter((listener) => {
|
listenersToRemove = listeners.filter((listener) => {
|
||||||
return listener.successCallback === origCB;
|
return listener.successCallback === originalCallback;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
listenersToRemove = listeners;
|
listenersToRemove = listeners;
|
||||||
|
@ -349,9 +534,12 @@ export default class Reference extends ReferenceBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a specified child
|
* Creates a Reference to a child of the current Reference, using a relative path.
|
||||||
* @param path
|
* No validation is performed on the path to ensure it has a valid format.
|
||||||
* @returns {Reference}
|
* @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) {
|
child(path: string) {
|
||||||
return new Reference(this.database, `${this.path}/${path}`);
|
return new Reference(this.database, `${this.path}/${path}`);
|
||||||
|
@ -370,6 +558,7 @@ export default class Reference extends ReferenceBase {
|
||||||
* same instance of firebase.app.App - multiple firebase apps not currently supported.
|
* same instance of firebase.app.App - multiple firebase apps not currently supported.
|
||||||
* @param {Reference} otherRef - Other reference to compare to this one
|
* @param {Reference} otherRef - Other reference to compare to this one
|
||||||
* @return {Boolean} Whether otherReference is equal to this one
|
* @return {Boolean} Whether otherReference is equal to this one
|
||||||
|
*
|
||||||
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#isEqual}
|
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#isEqual}
|
||||||
*/
|
*/
|
||||||
isEqual(otherRef: Reference): boolean {
|
isEqual(otherRef: Reference): boolean {
|
||||||
|
@ -381,8 +570,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'
|
* The parent location of a Reference, or null for the root Reference.
|
||||||
* @returns {*}
|
* @type {Reference}
|
||||||
|
*
|
||||||
|
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#parent}
|
||||||
*/
|
*/
|
||||||
get parent(): Reference|null {
|
get parent(): Reference|null {
|
||||||
if (this.path === '/') return null;
|
if (this.path === '/') return null;
|
||||||
|
@ -392,6 +583,7 @@ export default class Reference extends ReferenceBase {
|
||||||
/**
|
/**
|
||||||
* A reference to itself
|
* A reference to itself
|
||||||
* @type {!Reference}
|
* @type {!Reference}
|
||||||
|
*
|
||||||
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#ref}
|
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#ref}
|
||||||
*/
|
*/
|
||||||
get ref(): Reference {
|
get ref(): Reference {
|
||||||
|
@ -399,8 +591,10 @@ export default class Reference extends ReferenceBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a ref to the root of db - '/'
|
* Reference to the root of the database: '/'
|
||||||
* @returns {Reference}
|
* @type {!Reference}
|
||||||
|
*
|
||||||
|
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#root}
|
||||||
*/
|
*/
|
||||||
get root(): Reference {
|
get root(): Reference {
|
||||||
return new Reference(this.database, '/');
|
return new Reference(this.database, '/');
|
||||||
|
|
|
@ -5,6 +5,7 @@ import Reference from './reference.js';
|
||||||
import { isObject, deepGet, deepExists } from './../../utils';
|
import { isObject, deepGet, deepExists } from './../../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @class DataSnapshot
|
||||||
* @link https://firebase.google.com/docs/reference/js/firebase.database.DataSnapshot
|
* @link https://firebase.google.com/docs/reference/js/firebase.database.DataSnapshot
|
||||||
*/
|
*/
|
||||||
export default class Snapshot {
|
export default class Snapshot {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import onTests from './onTests';
|
import onTests from './on/onTests';
|
||||||
|
import onValueTests from './on/onValueTests';
|
||||||
import offTests from './offTests';
|
import offTests from './offTests';
|
||||||
import onceTests from './onceTests';
|
import onceTests from './onceTests';
|
||||||
import setTests from './setTests';
|
import setTests from './setTests';
|
||||||
|
@ -19,8 +20,8 @@ import DatabaseContents from '../../support/DatabaseContents';
|
||||||
|
|
||||||
const testGroups = [
|
const testGroups = [
|
||||||
factoryTests, keyTests, parentTests, childTests, rootTests,
|
factoryTests, keyTests, parentTests, childTests, rootTests,
|
||||||
pushTests, onTests, offTests, onceTests, updateTests, removeTests, setTests,
|
pushTests, onTests, onValueTests, offTests, onceTests, updateTests,
|
||||||
transactionTests, queryTests, refTests, isEqualTests,
|
removeTests, setTests, transactionTests, queryTests, refTests, isEqualTests,
|
||||||
];
|
];
|
||||||
|
|
||||||
function registerTestSuite(testSuite) {
|
function registerTestSuite(testSuite) {
|
||||||
|
|
|
@ -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;
|
|
@ -0,0 +1,362 @@
|
||||||
|
import sinon from 'sinon';
|
||||||
|
import 'should-sinon';
|
||||||
|
import Promise from 'bluebird';
|
||||||
|
|
||||||
|
import DatabaseContents from '../../../support/DatabaseContents';
|
||||||
|
|
||||||
|
function onTests({ describe, context, it, xit, firebase, tryCatch }) {
|
||||||
|
describe('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);
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pending until behaviour of push is possibly fixed. Currently, causes
|
||||||
|
* an array to be converted to an object - don't think this is the intended
|
||||||
|
* behaviour
|
||||||
|
*/
|
||||||
|
xit('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);
|
||||||
|
|
||||||
|
await ref.push(37);
|
||||||
|
|
||||||
|
// Assertions
|
||||||
|
callback.should.be.calledWith([
|
||||||
|
...currentDataValue,
|
||||||
|
37,
|
||||||
|
]);
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
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);
|
||||||
|
context.callCount.should.eql(2);
|
||||||
|
|
||||||
|
// Tear down
|
||||||
|
|
||||||
|
ref.off();
|
||||||
|
await ref.set(currentDataValue);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default onTests;
|
|
@ -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;
|
|
Loading…
Reference in New Issue