diff --git a/.gitignore b/.gitignore index c98bd324ccb..7809d954d9f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ npm-debug.log /coverage /.nyc_output -/tests/integration/config +/tests/config /temp -/.vscode \ No newline at end of file +/.vscode +/.ts-node \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index daceaf47f74..57a613d9ba0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,8 +16,6 @@ addons: - g++-4.8 before_script: - "export DISPLAY=:99.0" + - "mkdir -p tests/config && echo \"$PROJECT_CONFIG\" > tests/config/project.json" script: - xvfb-run npm test -branches: - only: - - master \ No newline at end of file diff --git a/gulp/config.js b/gulp/config.js index 1945b879037..85b99dec3f7 100644 --- a/gulp/config.js +++ b/gulp/config.js @@ -17,9 +17,13 @@ const path = require('path'); const cwd = process.cwd(); const karma = require('karma'); -module.exports = { +const configObj = { root: path.resolve(cwd), pkg: require(path.resolve(cwd, 'package.json')), + testConfig: { + timeout: 5000, + retries: 5 + }, tsconfig: require(path.resolve(cwd, 'tsconfig.json')), tsconfigTest: require(path.resolve(cwd, 'tsconfig.test.json')), paths: { @@ -30,6 +34,7 @@ module.exports = { 'tests/**/*.test.ts', '!tests/**/browser/**/*.test.ts', '!tests/**/binary/**/*.test.ts', + '!src/firebase-*.ts', ], binary: [ 'tests/**/binary/**/*.test.ts', @@ -102,7 +107,7 @@ module.exports = { // start these browsers // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher - browsers: ['Chrome', 'Firefox'], + browsers: ['ChromeHeadless', 'Firefox'], // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits @@ -115,6 +120,16 @@ module.exports = { // karma-typescript config karmaTypescriptConfig: { tsconfig: `./tsconfig.test.json` + }, + + // Stub for client config + client: { + mocha: {} } } -}; \ No newline at end of file +}; + +configObj.karma.client.mocha.timeout = configObj.testConfig.timeout; +configObj.karma.client.mocha.retries = configObj.testConfig.retries; + +module.exports = configObj; \ No newline at end of file diff --git a/gulp/tasks/dev.js b/gulp/tasks/dev.js index 0bbc3d7f2e6..96fc824a257 100644 --- a/gulp/tasks/dev.js +++ b/gulp/tasks/dev.js @@ -17,19 +17,18 @@ const gulp = require('gulp'); const config = require('../config'); // Ensure that the test tasks get set up -require('./test'); +const testFxns = require('./test'); function watchDevFiles() { const stream = gulp.watch([ `${config.root}/src/**/*.ts`, - config.paths.test.unit - ], gulp.parallel('test:unit')); + 'tests/**/*.test.ts' + ], testFxns.runBrowserUnitTests(true)); - stream.on('error', () => {}); + stream.on('error', err => {}); return stream; } gulp.task('dev', gulp.parallel([ - 'test:unit', watchDevFiles ])); \ No newline at end of file diff --git a/gulp/tasks/test.js b/gulp/tasks/test.js index 22485e3910a..324f46da759 100644 --- a/gulp/tasks/test.js +++ b/gulp/tasks/test.js @@ -32,7 +32,9 @@ function runNodeUnitTests() { .pipe(envs) .pipe(mocha({ reporter: 'spec', - compilers: 'ts:ts-node/register' + compilers: 'ts:ts-node/register', + timeout: config.testConfig.timeout, + retries: config.testConfig.retries })); } @@ -47,33 +49,41 @@ function runNodeBinaryTests() { .pipe(envs) .pipe(mocha({ reporter: 'spec', - compilers: 'ts:ts-node/register' + compilers: 'ts:ts-node/register', + timeout: config.testConfig.timeout, + retries: config.testConfig.retries })); } /** * Runs all of the browser unit tests in karma */ -function runBrowserUnitTests(done) { - const karmaConfig = Object.assign({}, config.karma, { - // list of files / patterns to load in the browser - files: [ - './+(src|tests)/**/*.ts' - ], - - // list of files to exclude from the included globs above - exclude: [ - // we don't want this file as it references files that only exist once compiled - `./src/firebase.ts`, +function runBrowserUnitTests(dev) { + return (done) => { + const karmaConfig = Object.assign({}, config.karma, { + // list of files / patterns to load in the browser + files: [ + './+(src|tests)/**/*.ts' + ], + + // list of files to exclude from the included globs above + exclude: [ + // we don't want this file as it references files that only exist once compiled + `./src/firebase-*.ts`, - // Don't include node test files - './tests/**/node/**/*.test.ts', + // We don't want to load the node env + `./src/utils/nodePatches.ts`, - // Don't include binary test files - './tests/**/binary/**/*.test.ts', - ], - }); - new karma.Server(karmaConfig, done).start(); + // Don't include node test files + './tests/**/node/**/*.test.ts', + + // Don't include binary test files + './tests/**/binary/**/*.test.ts', + ], + browsers: !!dev ? ['ChromeHeadless'] : config.karma.browsers, + }); + new karma.Server(karmaConfig, done).start(); + }; } /** @@ -111,7 +121,10 @@ function runAllKarmaTests(done) { // list of files to exclude from the included globs above exclude: [ // we don't want this file as it references files that only exist once compiled - `./src/firebase.ts`, + `./src/firebase-*.ts`, + + // We don't want to load the node env + `./src/utils/nodePatches.ts`, // Don't include node test files './tests/**/node/**/*.test.ts', @@ -121,9 +134,9 @@ function runAllKarmaTests(done) { } gulp.task('test:unit:node', runNodeUnitTests); -gulp.task('test:unit:browser', runBrowserUnitTests); +gulp.task('test:unit:browser', runBrowserUnitTests()); -const unitTestSuite = gulp.parallel(runNodeUnitTests, runBrowserUnitTests); +const unitTestSuite = gulp.parallel(runNodeUnitTests, runBrowserUnitTests()); gulp.task('test:unit', unitTestSuite); gulp.task('test:binary:browser', runBrowserBinaryTests); @@ -137,3 +150,6 @@ gulp.task('test', gulp.parallel([ runNodeBinaryTests, runAllKarmaTests ])); + +exports.runNodeUnitTests = runNodeUnitTests; +exports.runBrowserUnitTests = runBrowserUnitTests; \ No newline at end of file diff --git a/package.json b/package.json index b00f2deb0cb..993b65070fc 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "babel-preset-env": "^1.2.1", "chai": "^3.5.0", "child-process-promise": "^2.2.1", + "cross-env": "^5.0.1", "cz-customizable": "^5.0.0", "filesize": "^3.5.6", "git-rev-sync": "^1.9.0", diff --git a/src/database.ts b/src/database.ts index 11edeb11740..f56ccb48258 100644 --- a/src/database.ts +++ b/src/database.ts @@ -58,7 +58,15 @@ declare module './app/firebase_app' { declare module './app/firebase_app' { interface FirebaseNamespace { - database?(app: FirebaseApp): Database + database?: { + (app?: FirebaseApp): Database, + Database, + enableLogging, + INTERNAL, + Query, + Reference, + ServerValue, + } } } diff --git a/src/database/api/Database.ts b/src/database/api/Database.ts index fcb2aadf46e..6701c8980fe 100644 --- a/src/database/api/Database.ts +++ b/src/database/api/Database.ts @@ -51,7 +51,7 @@ export class Database { * @param {string=} pathString * @return {!Firebase} Firebase reference. */ - ref(pathString?): Reference { + ref(pathString?: string): Reference { this.checkDeleted_('ref'); validateArgCount('database.ref', 0, 1, arguments.length); diff --git a/src/database/api/Query.ts b/src/database/api/Query.ts index be0620a3824..6f7218143d9 100644 --- a/src/database/api/Query.ts +++ b/src/database/api/Query.ts @@ -221,8 +221,9 @@ export class Query { * @param context * @return {!firebase.Promise} */ - once(eventType: string, userCallback: SnapshotCallback, - cancelOrContext?, context?: Object) { + once(eventType: string, + userCallback?: SnapshotCallback, + cancelOrContext?, context?: Object): Promise { validateArgCount('Query.once', 1, 4, arguments.length); validateEventType('Query.once', 1, eventType, false); validateCallback('Query.once', 2, userCallback, true); diff --git a/src/database/api/onDisconnect.ts b/src/database/api/onDisconnect.ts index ead95424416..d192817ce68 100644 --- a/src/database/api/onDisconnect.ts +++ b/src/database/api/onDisconnect.ts @@ -26,7 +26,7 @@ export class OnDisconnect { } /** - * @param {function(?Error)=} opt_onComplete + * @param {function(?Error)=} onComplete * @return {!firebase.Promise} */ cancel(onComplete?) { @@ -38,7 +38,7 @@ export class OnDisconnect { } /** - * @param {function(?Error)=} opt_onComplete + * @param {function(?Error)=} onComplete * @return {!firebase.Promise} */ remove(onComplete?) { @@ -52,7 +52,7 @@ export class OnDisconnect { /** * @param {*} value - * @param {function(?Error)=} opt_onComplete + * @param {function(?Error)=} onComplete * @return {!firebase.Promise} */ set(value, onComplete?) { @@ -68,7 +68,7 @@ export class OnDisconnect { /** * @param {*} value * @param {number|string|null} priority - * @param {function(?Error)=} opt_onComplete + * @param {function(?Error)=} onComplete * @return {!firebase.Promise} */ setWithPriority(value, priority, onComplete?) { @@ -86,7 +86,7 @@ export class OnDisconnect { /** * @param {!Object} objectToMerge - * @param {function(?Error)=} opt_onComplete + * @param {function(?Error)=} onComplete * @return {!firebase.Promise} */ update(objectToMerge, onComplete?) { diff --git a/src/database/core/Repo.ts b/src/database/core/Repo.ts index 8c9eb2f55c4..ce8dc96de1f 100644 --- a/src/database/core/Repo.ts +++ b/src/database/core/Repo.ts @@ -286,8 +286,9 @@ export class Repo { * @param {number|string|null} newPriority * @param {?function(?Error, *=)} onComplete */ - setWithPriority(path: Path, newVal: any, newPriority: number | string | null, - onComplete: ((status: Error | null, errorReason?: string) => any) | null) { + setWithPriority(path: Path, newVal: any, + newPriority: number | string | null, + onComplete: ((status: Error | null, errorReason?: string) => any) | null) { this.log_('set', {path: path.toString(), value: newVal, priority: newPriority}); // TODO: Optimize this behavior to either (a) store flag to skip resolving where possible and / or diff --git a/src/database/core/util/SortedMap.ts b/src/database/core/util/SortedMap.ts index 55dbf5b4da8..2cb19374dc8 100644 --- a/src/database/core/util/SortedMap.ts +++ b/src/database/core/util/SortedMap.ts @@ -714,35 +714,35 @@ export class SortedMap { * @param {(function(K, V):T)=} opt_resultGenerator * @return {SortedMapIterator.} The iterator. */ - getIterator(opt_resultGenerator) { + getIterator(resultGenerator?) { return new SortedMapIterator(this.root_, null, this.comparator_, false, - opt_resultGenerator); + resultGenerator); } - getIteratorFrom(key, opt_resultGenerator) { + getIteratorFrom(key, resultGenerator?) { return new SortedMapIterator(this.root_, key, this.comparator_, false, - opt_resultGenerator); + resultGenerator); } - getReverseIteratorFrom(key, opt_resultGenerator) { + getReverseIteratorFrom(key, resultGenerator?) { return new SortedMapIterator(this.root_, key, this.comparator_, true, - opt_resultGenerator); + resultGenerator); } - getReverseIterator(opt_resultGenerator) { + getReverseIterator(resultGenerator?) { return new SortedMapIterator(this.root_, null, this.comparator_, true, - opt_resultGenerator); + resultGenerator); } }; // end SortedMap \ No newline at end of file diff --git a/src/database/core/util/util.ts b/src/database/core/util/util.ts index 2d64004305f..08db8183fdc 100644 --- a/src/database/core/util/util.ts +++ b/src/database/core/util/util.ts @@ -114,7 +114,7 @@ export const buildLogMessage_ = function(var_args) { * Use this for all debug messages in Firebase. * @type {?function(string)} */ -export var logger = console.log.bind(console); +export var logger = null; /** diff --git a/src/database/core/view/filter/RangedFilter.ts b/src/database/core/view/filter/RangedFilter.ts index ea471486445..c9a16137811 100644 --- a/src/database/core/view/filter/RangedFilter.ts +++ b/src/database/core/view/filter/RangedFilter.ts @@ -1,6 +1,6 @@ import { IndexedFilter } from "./IndexedFilter"; import { PRIORITY_INDEX } from "../../../core/snap/indexes/PriorityIndex"; -import { NamedNode } from "../../../core/snap/Node"; +import { Node, NamedNode } from "../../../core/snap/Node"; import { ChildrenNode } from "../../../core/snap/ChildrenNode"; /** * Filters nodes by range and uses an IndexFilter to track any changes after filtering the node diff --git a/src/database/realtime/BrowserPollConnection.ts b/src/database/realtime/BrowserPollConnection.ts index 94082b272a3..4a7d84cb8d1 100644 --- a/src/database/realtime/BrowserPollConnection.ts +++ b/src/database/realtime/BrowserPollConnection.ts @@ -18,19 +18,19 @@ import { Transport } from './Transport'; import { RepoInfo } from '../core/RepoInfo'; // URL query parameters associated with longpolling -const FIREBASE_LONGPOLL_START_PARAM = 'start'; -const FIREBASE_LONGPOLL_CLOSE_COMMAND = 'close'; -const FIREBASE_LONGPOLL_COMMAND_CB_NAME = 'pLPCommand'; -const FIREBASE_LONGPOLL_DATA_CB_NAME = 'pRTLPCB'; -const FIREBASE_LONGPOLL_ID_PARAM = 'id'; -const FIREBASE_LONGPOLL_PW_PARAM = 'pw'; -const FIREBASE_LONGPOLL_SERIAL_PARAM = 'ser'; -const FIREBASE_LONGPOLL_CALLBACK_ID_PARAM = 'cb'; -const FIREBASE_LONGPOLL_SEGMENT_NUM_PARAM = 'seg'; -const FIREBASE_LONGPOLL_SEGMENTS_IN_PACKET = 'ts'; -const FIREBASE_LONGPOLL_DATA_PARAM = 'd'; -const FIREBASE_LONGPOLL_DISCONN_FRAME_PARAM = 'disconn'; -const FIREBASE_LONGPOLL_DISCONN_FRAME_REQUEST_PARAM = 'dframe'; +export const FIREBASE_LONGPOLL_START_PARAM = 'start'; +export const FIREBASE_LONGPOLL_CLOSE_COMMAND = 'close'; +export const FIREBASE_LONGPOLL_COMMAND_CB_NAME = 'pLPCommand'; +export const FIREBASE_LONGPOLL_DATA_CB_NAME = 'pRTLPCB'; +export const FIREBASE_LONGPOLL_ID_PARAM = 'id'; +export const FIREBASE_LONGPOLL_PW_PARAM = 'pw'; +export const FIREBASE_LONGPOLL_SERIAL_PARAM = 'ser'; +export const FIREBASE_LONGPOLL_CALLBACK_ID_PARAM = 'cb'; +export const FIREBASE_LONGPOLL_SEGMENT_NUM_PARAM = 'seg'; +export const FIREBASE_LONGPOLL_SEGMENTS_IN_PACKET = 'ts'; +export const FIREBASE_LONGPOLL_DATA_PARAM = 'd'; +export const FIREBASE_LONGPOLL_DISCONN_FRAME_PARAM = 'disconn'; +export const FIREBASE_LONGPOLL_DISCONN_FRAME_REQUEST_PARAM = 'dframe'; //Data size constants. //TODO: Perf: the maximum length actually differs from browser to browser. @@ -123,8 +123,9 @@ export class BrowserPollConnection implements Transport { return; //Set up a callback that gets triggered once a connection is set up. - this.scriptTagHolder = new FirebaseIFrameScriptHolder((command, arg1, arg2, arg3, arg4) => { - this.incrementIncomingBytes_(arguments); + this.scriptTagHolder = new FirebaseIFrameScriptHolder((...args) => { + const [command, arg1, arg2, arg3, arg4] = args; + this.incrementIncomingBytes_(args); if (!this.scriptTagHolder) return; // we closed the connection. @@ -152,8 +153,9 @@ export class BrowserPollConnection implements Transport { } else { throw new Error('Unrecognized command received: ' + command); } - }, (pN, data) => { - this.incrementIncomingBytes_(arguments); + }, (...args) => { + const [pN, data] = args; + this.incrementIncomingBytes_(args); this.myPacketOrderer.handleResponse(pN, data); }, () => { this.onClosed_(); @@ -338,7 +340,7 @@ export class BrowserPollConnection implements Transport { }; } -interface IFrameElement extends HTMLIFrameElement { +export interface IFrameElement extends HTMLIFrameElement { doc: Document; } @@ -350,7 +352,7 @@ interface IFrameElement extends HTMLIFrameElement { * @param onDisconnect - The callback to be triggered when this tag holder is closed * @param urlFn - A function that provides the URL of the endpoint to send data to. *********************************************************************************************/ -class FirebaseIFrameScriptHolder { +export class FirebaseIFrameScriptHolder { //We maintain a count of all of the outstanding requests, because if we have too many active at once it can cause //problems in some browsers. /** @@ -648,53 +650,3 @@ class FirebaseIFrameScriptHolder { } } } - -if (isNodeSdk()) { - /** - * @type {?function({url: string, forever: boolean}, function(Error, number, string))} - */ - (FirebaseIFrameScriptHolder as any).request = null; - - /** - * @param {{url: string, forever: boolean}} req - * @param {function(string)=} onComplete - */ - (FirebaseIFrameScriptHolder as any).nodeRestRequest = function(req, onComplete) { - if (!(FirebaseIFrameScriptHolder as any).request) - (FirebaseIFrameScriptHolder as any).request = - /** @type {function({url: string, forever: boolean}, function(Error, number, string))} */ (require('request')); - - (FirebaseIFrameScriptHolder as any).request(req, function(error, response, body) { - if (error) - throw 'Rest request for ' + req.url + ' failed.'; - - if (onComplete) - onComplete(body); - }); - }; - - /** - * @param {!string} url - * @param {function()} loadCB - */ - (FirebaseIFrameScriptHolder.prototype).doNodeLongPoll = function(url, loadCB) { - var self = this; - (FirebaseIFrameScriptHolder as any).nodeRestRequest({ url: url, forever: true }, function(body) { - self.evalBody(body); - loadCB(); - }); - }; - - /** - * Evaluates the string contents of a jsonp response. - * @param {!string} body - */ - (FirebaseIFrameScriptHolder.prototype).evalBody = function(body) { - var jsonpCB; - //jsonpCB is externed in firebase-extern.js - eval('jsonpCB = function(' + FIREBASE_LONGPOLL_COMMAND_CB_NAME + ', ' + FIREBASE_LONGPOLL_DATA_CB_NAME + ') {' + - body + - '}'); - jsonpCB(this.commandCB, this.onMessageCB); - }; -} \ No newline at end of file diff --git a/src/database/realtime/Connection.ts b/src/database/realtime/Connection.ts index 1896ed2946a..cffd9aafd02 100644 --- a/src/database/realtime/Connection.ts +++ b/src/database/realtime/Connection.ts @@ -107,7 +107,7 @@ export class Connection { */ private start_() { const conn = this.transportManager_.initialTransport(); - this.conn_ = new conn(this.nextTransportId_(), this.repoInfo_, /*transportSessionId=*/undefined, this.lastSessionId); + this.conn_ = new conn(this.nextTransportId_(), this.repoInfo_, undefined, this.lastSessionId); // For certain transports (WebSockets), we need to send and receive several messages back and forth before we // can consider the transport healthy. @@ -120,37 +120,36 @@ export class Connection { this.secondaryConn_ = null; this.isHealthy_ = false; - const self = this; /* * Firefox doesn't like when code from one iframe tries to create another iframe by way of the parent frame. * This can occur in the case of a redirect, i.e. we guessed wrong on what server to connect to and received a reset. * Somehow, setTimeout seems to make this ok. That doesn't make sense from a security perspective, since you should * still have the context of your originating frame. */ - setTimeout(function () { + setTimeout(() => { // self.conn_ gets set to null in some of the tests. Check to make sure it still exists before using it - self.conn_ && self.conn_.open(onMessageReceived, onConnectionLost); + this.conn_ && this.conn_.open(onMessageReceived, onConnectionLost); }, Math.floor(0)); const healthyTimeout_ms = conn['healthyTimeout'] || 0; if (healthyTimeout_ms > 0) { - this.healthyTimeout_ = setTimeoutNonBlocking(function () { - self.healthyTimeout_ = null; - if (!self.isHealthy_) { - if (self.conn_ && self.conn_.bytesReceived > BYTES_RECEIVED_HEALTHY_OVERRIDE) { - self.log_('Connection exceeded healthy timeout but has received ' + self.conn_.bytesReceived + + this.healthyTimeout_ = setTimeoutNonBlocking(() => { + this.healthyTimeout_ = null; + if (!this.isHealthy_) { + if (this.conn_ && this.conn_.bytesReceived > BYTES_RECEIVED_HEALTHY_OVERRIDE) { + this.log_('Connection exceeded healthy timeout but has received ' + this.conn_.bytesReceived + ' bytes. Marking connection healthy.'); - self.isHealthy_ = true; - self.conn_.markConnectionHealthy(); - } else if (self.conn_ && self.conn_.bytesSent > BYTES_SENT_HEALTHY_OVERRIDE) { - self.log_('Connection exceeded healthy timeout but has sent ' + self.conn_.bytesSent + + this.isHealthy_ = true; + this.conn_.markConnectionHealthy(); + } else if (this.conn_ && this.conn_.bytesSent > BYTES_SENT_HEALTHY_OVERRIDE) { + this.log_('Connection exceeded healthy timeout but has sent ' + this.conn_.bytesSent + ' bytes. Leaving connection alive.'); // NOTE: We don't want to mark it healthy, since we have no guarantee that the bytes have made it to // the server. } else { - self.log_('Closing unhealthy connection after timeout.'); - self.close(); + this.log_('Closing unhealthy connection after timeout.'); + this.close(); } } }, Math.floor(healthyTimeout_ms)); @@ -166,29 +165,27 @@ export class Connection { }; private disconnReceiver_(conn) { - const self = this; - return function (everConnected) { - if (conn === self.conn_) { - self.onConnectionLost_(everConnected); - } else if (conn === self.secondaryConn_) { - self.log_('Secondary connection lost.'); - self.onSecondaryConnectionLost_(); + return everConnected => { + if (conn === this.conn_) { + this.onConnectionLost_(everConnected); + } else if (conn === this.secondaryConn_) { + this.log_('Secondary connection lost.'); + this.onSecondaryConnectionLost_(); } else { - self.log_('closing an old connection'); + this.log_('closing an old connection'); } } }; private connReceiver_(conn) { - const self = this; - return function (message) { - if (self.state_ != REALTIME_STATE_DISCONNECTED) { - if (conn === self.rx_) { - self.onPrimaryMessageReceived_(message); - } else if (conn === self.secondaryConn_) { - self.onSecondaryMessageReceived_(message); + return message => { + if (this.state_ != REALTIME_STATE_DISCONNECTED) { + if (conn === this.rx_) { + this.onPrimaryMessageReceived_(message); + } else if (conn === this.secondaryConn_) { + this.onSecondaryMessageReceived_(message); } else { - self.log_('message on old connection'); + this.log_('message on old connection'); } } }; diff --git a/src/database/realtime/WebSocketConnection.ts b/src/database/realtime/WebSocketConnection.ts index 009c643af65..304432c7a01 100644 --- a/src/database/realtime/WebSocketConnection.ts +++ b/src/database/realtime/WebSocketConnection.ts @@ -16,14 +16,16 @@ const WEBSOCKET_MAX_FRAME_SIZE = 16384; const WEBSOCKET_KEEPALIVE_INTERVAL = 45000; let WebSocketImpl = null; -if (isNodeSdk()) { - WebSocketImpl = require('faye-websocket')['Client']; -} else if (typeof MozWebSocket !== 'undefined') { +if (typeof MozWebSocket !== 'undefined') { WebSocketImpl = MozWebSocket; } else if (typeof WebSocket !== 'undefined') { WebSocketImpl = WebSocket; } +export function setWebSocketImpl(impl) { + WebSocketImpl = impl; +} + /** * Create a new websocket connection with the given callbacks. * @constructor @@ -108,7 +110,7 @@ export class WebSocketConnection implements Transport { // UA Format: Firebase//// const options = { 'headers': { - 'User-Agent': 'Firebase/' + CONSTANTS.PROTOCOL_VERSION + '/' + firebase.SDK_VERSION + '/' + process.platform + '/' + device + 'User-Agent': `Firebase/${CONSTANTS.PROTOCOL_VERSION}/${firebase.SDK_VERSION}/${process.platform}/${device}` }}; // Plumb appropriate http_proxy environment variable into faye-websocket if it exists. @@ -122,11 +124,9 @@ export class WebSocketConnection implements Transport { } this.mySock = new WebSocketImpl(this.connURL, [], options); - } - else { + } else { this.mySock = new WebSocketImpl(this.connURL); } - this.mySock = new WebSocketImpl(this.connURL); } catch (e) { this.log_('Error instantiating WebSocket.'); const error = e.message || e.data; diff --git a/src/firebase-node.ts b/src/firebase-node.ts index d1d6e24088d..c7150a533a0 100644 --- a/src/firebase-node.ts +++ b/src/firebase-node.ts @@ -17,7 +17,7 @@ import firebase from "./app"; import './auth'; import './database'; -import './database/nodePatches'; +import './utils/nodePatches'; var Storage = require('dom-storage'); diff --git a/src/utils/nodePatches.ts b/src/utils/nodePatches.ts index 4ea503703f7..42f1782be8f 100644 --- a/src/utils/nodePatches.ts +++ b/src/utils/nodePatches.ts @@ -1,4 +1,13 @@ import { CONSTANTS } from "./constants"; +import { setWebSocketImpl } from "../database/realtime/WebSocketConnection"; +import { + FirebaseIFrameScriptHolder, + FIREBASE_LONGPOLL_COMMAND_CB_NAME, + FIREBASE_LONGPOLL_DATA_CB_NAME +} from "../database/realtime/BrowserPollConnection"; +import { Client } from "faye-websocket"; + +setWebSocketImpl(Client); // Overriding the constant (we should be the only ones doing this) CONSTANTS.NODE_CLIENT = true; @@ -8,116 +17,164 @@ CONSTANTS.NODE_CLIENT = true; */ (function() { var version = process['version']; - if (version === 'v0.10.22' || version === 'v0.10.23' || version === 'v0.10.24') { - /** - * The following duplicates much of `/lib/_stream_writable.js` at - * b922b5e90d2c14dd332b95827c2533e083df7e55, applying the fix for - * https://github.com/joyent/node/issues/6506. Note that this fix also - * needs to be applied to `Duplex.prototype.write()` (in - * `/lib/_stream_duplex.js`) as well. - */ - var Writable = require('_stream_writable'); - - Writable['prototype']['write'] = function(chunk, encoding, cb) { - var state = this['_writableState']; - var ret = false; - - if (typeof encoding === 'function') { - cb = encoding; - encoding = null; - } - - if (Buffer['isBuffer'](chunk)) - encoding = 'buffer'; - else if (!encoding) - encoding = state['defaultEncoding']; - - if (typeof cb !== 'function') - cb = function() {}; - - if (state['ended']) - writeAfterEnd(this, state, cb); - else if (validChunk(this, state, chunk, cb)) - ret = writeOrBuffer(this, state, chunk, encoding, cb); - - return ret; - }; - - function writeAfterEnd(stream, state, cb) { - var er = new Error('write after end'); - // TODO: defer error events consistently everywhere, not just the cb + if (version !== 'v0.10.22' && version !== 'v0.10.23' && version !== 'v0.10.24') return; + /** + * The following duplicates much of `/lib/_stream_writable.js` at + * b922b5e90d2c14dd332b95827c2533e083df7e55, applying the fix for + * https://github.com/joyent/node/issues/6506. Note that this fix also + * needs to be applied to `Duplex.prototype.write()` (in + * `/lib/_stream_duplex.js`) as well. + */ + var Writable = require('_stream_writable'); + + Writable['prototype']['write'] = function(chunk, encoding, cb) { + var state = this['_writableState']; + var ret = false; + + if (typeof encoding === 'function') { + cb = encoding; + encoding = null; + } + + if (Buffer['isBuffer'](chunk)) + encoding = 'buffer'; + else if (!encoding) + encoding = state['defaultEncoding']; + + if (typeof cb !== 'function') + cb = function() {}; + + if (state['ended']) + writeAfterEnd(this, state, cb); + else if (validChunk(this, state, chunk, cb)) + ret = writeOrBuffer(this, state, chunk, encoding, cb); + + return ret; + }; + + function writeAfterEnd(stream, state, cb) { + var er = new Error('write after end'); + // TODO: defer error events consistently everywhere, not just the cb + stream['emit']('error', er); + process['nextTick'](function() { + cb(er); + }); + } + + function validChunk(stream, state, chunk, cb) { + var valid = true; + if (!Buffer['isBuffer'](chunk) && + 'string' !== typeof chunk && + chunk !== null && + chunk !== undefined && + !state['objectMode']) { + var er = new TypeError('Invalid non-string/buffer chunk'); stream['emit']('error', er); process['nextTick'](function() { cb(er); }); + valid = false; } + return valid; + } - function validChunk(stream, state, chunk, cb) { - var valid = true; - if (!Buffer['isBuffer'](chunk) && - 'string' !== typeof chunk && - chunk !== null && - chunk !== undefined && - !state['objectMode']) { - var er = new TypeError('Invalid non-string/buffer chunk'); - stream['emit']('error', er); - process['nextTick'](function() { - cb(er); - }); - valid = false; - } - return valid; - } - - function writeOrBuffer(stream, state, chunk, encoding, cb) { - chunk = decodeChunk(state, chunk, encoding); - if (Buffer['isBuffer'](chunk)) - encoding = 'buffer'; - var len = state['objectMode'] ? 1 : chunk['length']; - - state['length'] += len; + function writeOrBuffer(stream, state, chunk, encoding, cb) { + chunk = decodeChunk(state, chunk, encoding); + if (Buffer['isBuffer'](chunk)) + encoding = 'buffer'; + var len = state['objectMode'] ? 1 : chunk['length']; - var ret = state['length'] < state['highWaterMark']; - // we must ensure that previous needDrain will not be reset to false. - if (!ret) - state['needDrain'] = true; + state['length'] += len; - if (state['writing']) - state['buffer']['push'](new WriteReq(chunk, encoding, cb)); - else - doWrite(stream, state, len, chunk, encoding, cb); + var ret = state['length'] < state['highWaterMark']; + // we must ensure that previous needDrain will not be reset to false. + if (!ret) + state['needDrain'] = true; - return ret; - } + if (state['writing']) + state['buffer']['push'](new WriteReq(chunk, encoding, cb)); + else + doWrite(stream, state, len, chunk, encoding, cb); - function decodeChunk(state, chunk, encoding) { - if (!state['objectMode'] && - state['decodeStrings'] !== false && - typeof chunk === 'string') { - chunk = new Buffer(chunk, encoding); - } - return chunk; - } + return ret; + } - /** - * @constructor - */ - function WriteReq(chunk, encoding, cb) { - this['chunk'] = chunk; - this['encoding'] = encoding; - this['callback'] = cb; + function decodeChunk(state, chunk, encoding) { + if (!state['objectMode'] && + state['decodeStrings'] !== false && + typeof chunk === 'string') { + chunk = new Buffer(chunk, encoding); } + return chunk; + } - function doWrite(stream, state, len, chunk, encoding, cb) { - state['writelen'] = len; - state['writecb'] = cb; - state['writing'] = true; - state['sync'] = true; - stream['_write'](chunk, encoding, state['onwrite']); - state['sync'] = false; - } + /** + * @constructor + */ + function WriteReq(chunk, encoding, cb) { + this['chunk'] = chunk; + this['encoding'] = encoding; + this['callback'] = cb; + } - var Duplex = require('_stream_duplex'); - Duplex['prototype']['write'] = Writable['prototype']['write']; + function doWrite(stream, state, len, chunk, encoding, cb) { + state['writelen'] = len; + state['writecb'] = cb; + state['writing'] = true; + state['sync'] = true; + stream['_write'](chunk, encoding, state['onwrite']); + state['sync'] = false; } + + var Duplex = require('_stream_duplex'); + Duplex['prototype']['write'] = Writable['prototype']['write']; })(); + +/** + * @type {?function({url: string, forever: boolean}, function(Error, number, string))} + */ +(FirebaseIFrameScriptHolder as any).request = null; + +/** + * @param {{url: string, forever: boolean}} req + * @param {function(string)=} onComplete + */ +(FirebaseIFrameScriptHolder as any).nodeRestRequest = function(req, onComplete) { + if (!(FirebaseIFrameScriptHolder as any).request) + (FirebaseIFrameScriptHolder as any).request = + /** @type {function({url: string, forever: boolean}, function(Error, number, string))} */ (require('request')); + + (FirebaseIFrameScriptHolder as any).request(req, function(error, response, body) { + if (error) + throw 'Rest request for ' + req.url + ' failed.'; + + if (onComplete) + onComplete(body); + }); +}; + +/** + * @param {!string} url + * @param {function()} loadCB + */ +(FirebaseIFrameScriptHolder.prototype).doNodeLongPoll = function(url, loadCB) { + var self = this; + (FirebaseIFrameScriptHolder as any).nodeRestRequest({ url: url, forever: true }, function(body) { + self.evalBody(body); + loadCB(); + }); +}; + +/** + * Evaluates the string contents of a jsonp response. + * @param {!string} body + */ +(FirebaseIFrameScriptHolder.prototype).evalBody = function(body) { + var jsonpCB; + //jsonpCB is externed in firebase-extern.js + eval('jsonpCB = function(' + FIREBASE_LONGPOLL_COMMAND_CB_NAME + ', ' + FIREBASE_LONGPOLL_DATA_CB_NAME + ') {' + + body + + '}'); + jsonpCB(this.commandCB, this.onMessageCB); +}; + diff --git a/tests/database/browser/connection.test.ts b/tests/database/browser/connection.test.ts new file mode 100644 index 00000000000..3f765a9bbf1 --- /dev/null +++ b/tests/database/browser/connection.test.ts @@ -0,0 +1,40 @@ +import { expect } from "chai"; +import { TEST_PROJECT, testRepoInfo } from "../helpers/util"; +import { Connection } from "../../../src/database/realtime/Connection"; + +describe('Connection', function() { + it('return the session id', function(done) { + new Connection('1', + testRepoInfo(TEST_PROJECT.databaseURL), + message => {}, + (timestamp, sessionId) => { + expect(sessionId).not.to.be.null; + expect(sessionId).not.to.equal(''); + done(); + }, + () => {}, + reason => {}); + }); + + // TODO - Flakey Test. When Dev Tools is closed on my Mac, this test + // fails about 20% of the time (open - it never fails). In the failing + // case a long-poll is opened first. + // https://app.asana.com/0/58926111402292/101921715724749 + it.skip('disconnect old session on new connection', function(done) { + const info = testRepoInfo(TEST_PROJECT.databaseURL); + new Connection('1', info, + message => {}, + (timestamp, sessionId) => { + new Connection('2', info, + message => {}, + (timestamp, sessionId) => {}, + () => {}, + reason => {}, + sessionId); + }, + () => { + done(); // first connection was disconnected + }, + reason => {}); + }); +}); diff --git a/tests/database/browser/crawler_support.test.ts b/tests/database/browser/crawler_support.test.ts new file mode 100644 index 00000000000..d98b8ac40c6 --- /dev/null +++ b/tests/database/browser/crawler_support.test.ts @@ -0,0 +1,181 @@ +import { expect } from "chai"; +import { forceRestClient } from "../../../src/database/api/test_access"; + +import { + getRandomNode, + testAuthTokenProvider, + getFreshRepoFromReference +} from "../helpers/util"; + +// Some sanity checks for the ReadonlyRestClient crawler support. +describe('Crawler Support', function() { + var initialData; + var normalRef; + var restRef; + var tokenProvider; + + beforeEach(function(done) { + normalRef = getRandomNode(); + + forceRestClient(true); + restRef = getFreshRepoFromReference(normalRef); + forceRestClient(false); + + tokenProvider = testAuthTokenProvider(restRef.database.app); + + setInitialData(done); + }); + + afterEach(function() { + tokenProvider.setToken(null); + }); + + function setInitialData(done) { + // Set some initial data. + initialData = { + leaf: 42, + securedLeaf: 'secret', + leafWithPriority: { '.value': 42, '.priority': 'pri' }, + obj: { a: 1, b: 2 }, + list: { + 10: { name: 'amy', age: 75, '.priority': 22 }, + 20: { name: 'becky', age: 42, '.priority': 52 }, + 30: { name: 'fred', age: 35, '.priority': 23 }, + 40: { name: 'fred', age: 29, '.priority': 26 }, + 50: { name: 'sally', age: 21, '.priority': 96 }, + 60: { name: 'tom', age: 16, '.priority': 15 }, + 70: { name: 'victor', age: 4, '.priority': 47 } + }, + valueList: { + 10: 'c', + 20: 'b', + 30: 'e', + 40: 'f', + 50: 'a', + 60: 'd', + 70: 'e' + } + }; + + normalRef.set(initialData, function(error) { + expect(error).to.equal(null); + done(); + }); + } + + it('set() is a no-op', function(done) { + normalRef.child('leaf').on('value', function(s) { + expect(s.val()).to.equal(42); + }); + + restRef.child('leaf').set('hello'); + + // We need to wait long enough to be sure that our 'hello' didn't actually get set, but there's + // no good way to do that. So we just do a couple round-trips via the REST client and assume + // that's good enough. + restRef.child('obj').once('value', function(s) { + expect(s.val()).to.deep.equal(initialData.obj); + + restRef.child('obj').once('value', function(s) { + expect(s.val()).to.deep.equal(initialData.obj); + + normalRef.child('leaf').off(); + done(); + }); + }); + }); + + it('set() is a no-op (Promise)', function() { + // This test mostly exists to make sure restRef really is using ReadonlyRestClient + // and we're not accidentally testing a normal Firebase connection. + + normalRef.child('leaf').on('value', function(s) { + expect(s.val()).to.equal(42); + }); + + restRef.child('leaf').set('hello'); + + // We need to wait long enough to be sure that our 'hello' didn't actually get set, but there's + // no good way to do that. So we just do a couple round-trips via the REST client and assume + // that's good enough. + return restRef.child('obj').once('value').then(function(s) { + expect(s.val()).to.deep.equal(initialData.obj); + + return restRef.child('obj').once('value'); + }).then(function(s) { + expect(s.val()).to.deep.equal(initialData.obj); + normalRef.child('leaf').off(); + }, function (reason) { + normalRef.child('leaf').off(); + return Promise.reject(reason); + }); + }); + + it('.info/connected fires with true', function(done) { + restRef.root.child('.info/connected').on('value', function(s) { + if (s.val() == true) { + done(); + } + }); + }); + + it('Leaf read works.', function(done) { + restRef.child('leaf').once('value', function(s) { + expect(s.val()).to.equal(initialData.leaf); + done(); + }); + }); + + it('Leaf read works. (Promise)', function() { + return restRef.child('leaf').once('value').then(function(s) { + expect(s.val()).to.equal(initialData.leaf); + }); + }); + + it('Object read works.', function(done) { + restRef.child('obj').once('value', function(s) { + expect(s.val()).to.deep.equal(initialData.obj); + done(); + }); + }); + + it('Object read works. (Promise)', function() { + return restRef.child('obj').once('value').then(function(s) { + expect(s.val()).to.deep.equal(initialData.obj); + }); + }); + + it('Leaf with priority read works.', function(done) { + restRef.child('leafWithPriority').once('value', function(s) { + expect(s.exportVal()).to.deep.equal(initialData.leafWithPriority); + done(); + }); + }); + + it('Leaf with priority read works. (Promise)', function() { + return restRef.child('leafWithPriority').once('value').then(function(s) { + expect(s.exportVal()).to.deep.equal(initialData.leafWithPriority); + }); + }); + + it('Null read works.', function(done) { + restRef.child('nonexistent').once('value', function(s) { + expect(s.val()).to.equal(null); + done(); + }); + }); + + it('Null read works. (Promise)', function() { + return restRef.child('nonexistent').once('value').then(function(s) { + expect(s.val()).to.equal(null); + }); + }); + + it('on works.', function(done) { + restRef.child('leaf').on('value', function(s) { + expect(s.val()).to.equal(initialData.leaf); + restRef.child('leaf').off(); + done(); + }); + }); +}); diff --git a/tests/database/compound_write.test.ts b/tests/database/compound_write.test.ts new file mode 100644 index 00000000000..df95f55e10d --- /dev/null +++ b/tests/database/compound_write.test.ts @@ -0,0 +1,438 @@ +import { expect } from "chai"; +import { ChildrenNode } from "../../src/database/core/snap/ChildrenNode"; +import { CompoundWrite } from "../../src/database/core/CompoundWrite"; +import { LeafNode } from "../../src/database/core/snap/LeafNode"; +import { NamedNode } from "../../src/database/core/snap/Node"; +import { nodeFromJSON } from "../../src/database/core/snap/nodeFromJSON"; +import { Path } from "../../src/database/core/util/Path"; + +describe('CompoundWrite Tests', function() { + var LEAF_NODE = nodeFromJSON('leaf-node'); + var PRIO_NODE = nodeFromJSON('prio'); + var CHILDREN_NODE = nodeFromJSON({ 'child-1': 'value-1', 'child-2': 'value-2' }); + var EMPTY_NODE = ChildrenNode.EMPTY_NODE; + + function assertNodeGetsCorrectPriority(compoundWrite, node, priority) { + if (node.isEmpty()) { + expect(compoundWrite.apply(node)).to.equal(EMPTY_NODE); + } else { + expect(compoundWrite.apply(node)).to.deep.equal(node.updatePriority(priority)); + } + } + + function assertNodesEqual(expected, actual) { + expect(actual.equals(expected)).to.be.true; + } + + it('Empty merge is empty', function() { + expect(CompoundWrite.Empty.isEmpty()).to.be.true; + }); + + it('CompoundWrite with priority update is not empty.', function() { + expect(CompoundWrite.Empty.addWrite(new Path('.priority'), PRIO_NODE).isEmpty()).to.be.false; + }); + + it('CompoundWrite with update is not empty.', function() { + expect(CompoundWrite.Empty.addWrite(new Path('foo/bar'), LEAF_NODE).isEmpty()).to.be.false; + }); + + it('CompoundWrite with root update is not empty.', function() { + expect(CompoundWrite.Empty.addWrite(Path.Empty, LEAF_NODE).isEmpty()).to.be.false; + }); + + it('CompoundWrite with empty root update is not empty.', function() { + expect(CompoundWrite.Empty.addWrite(Path.Empty, EMPTY_NODE).isEmpty()).to.be.false; + }); + + it('CompoundWrite with root priority update, child write is not empty.', function() { + var compoundWrite = CompoundWrite.Empty.addWrite(new Path('.priority'), PRIO_NODE); + expect(compoundWrite.childCompoundWrite(new Path('.priority')).isEmpty()).to.be.false; + }); + + it('Applies leaf overwrite', function() { + var compoundWrite = CompoundWrite.Empty; + compoundWrite = compoundWrite.addWrite(Path.Empty, LEAF_NODE); + expect(compoundWrite.apply(EMPTY_NODE)).to.equal(LEAF_NODE); + }); + + it('Applies children overwrite', function() { + var compoundWrite = CompoundWrite.Empty; + var childNode = EMPTY_NODE.updateImmediateChild('child', LEAF_NODE); + compoundWrite = compoundWrite.addWrite(Path.Empty, childNode); + expect(compoundWrite.apply(EMPTY_NODE)).to.equal(childNode); + }); + + it('Adds child node', function() { + var compoundWrite = CompoundWrite.Empty; + var expected = EMPTY_NODE.updateImmediateChild('child', LEAF_NODE); + compoundWrite = compoundWrite.addWrite(new Path('child'), LEAF_NODE); + assertNodesEqual(expected, compoundWrite.apply(EMPTY_NODE)); + }); + + it('Adds deep child node', function() { + var compoundWrite = CompoundWrite.Empty; + var path = new Path('deep/deep/node'); + var expected = EMPTY_NODE.updateChild(path, LEAF_NODE); + compoundWrite = compoundWrite.addWrite(path, LEAF_NODE); + expect(compoundWrite.apply(EMPTY_NODE)).to.deep.equal(expected); + }); + + it('shallow update removes deep update', function() { + var compoundWrite = CompoundWrite.Empty; + var updateOne = nodeFromJSON('new-foo-value'); + var updateTwo = nodeFromJSON('baz-value'); + var updateThree = nodeFromJSON({'foo': 'foo-value', 'bar': 'bar-value' }); + compoundWrite = compoundWrite.addWrite(new Path('child-1/foo'), updateOne); + compoundWrite = compoundWrite.addWrite(new Path('child-1/baz'), updateTwo); + compoundWrite = compoundWrite.addWrite(new Path('child-1'), updateThree); + var expectedChildOne = { + 'foo': 'foo-value', + 'bar': 'bar-value' + }; + var expected = CHILDREN_NODE.updateImmediateChild('child-1', + nodeFromJSON(expectedChildOne)); + assertNodesEqual(expected, compoundWrite.apply(CHILDREN_NODE)); + }); + + it('child priority updates empty priority on child write', function() { + var compoundWrite = CompoundWrite.Empty; + compoundWrite = compoundWrite.addWrite(new Path('child-1/.priority'), EMPTY_NODE); + var node = new LeafNode('foo', PRIO_NODE); + assertNodeGetsCorrectPriority(compoundWrite.childCompoundWrite(new Path('child-1')), node, EMPTY_NODE); + }); + + it('deep priority set works on empty node when other set is available', function() { + var compoundWrite = CompoundWrite.Empty; + compoundWrite = compoundWrite.addWrite(new Path('foo/.priority'), PRIO_NODE); + compoundWrite = compoundWrite.addWrite(new Path('foo/child'), LEAF_NODE); + var node = compoundWrite.apply(EMPTY_NODE); + assertNodesEqual(PRIO_NODE, node.getChild(new Path('foo')).getPriority()); + }); + + it('child merge looks into update node', function() { + var compoundWrite = CompoundWrite.Empty; + var update = nodeFromJSON({ 'foo': 'foo-value', 'bar': 'bar-value'}); + compoundWrite = compoundWrite.addWrite(Path.Empty, update); + assertNodesEqual(nodeFromJSON('foo-value'), + compoundWrite.childCompoundWrite(new Path('foo')).apply(EMPTY_NODE)); + }); + + it('child merge removes node on deeper paths', function() { + var compoundWrite = CompoundWrite.Empty; + var update = nodeFromJSON({ 'foo': 'foo-value', 'bar': 'bar-value' }); + compoundWrite = compoundWrite.addWrite(Path.Empty, update); + assertNodesEqual(EMPTY_NODE, compoundWrite.childCompoundWrite(new Path('foo/not/existing')).apply(LEAF_NODE)); + }); + + it('child merge with empty path is same merge', function() { + var compoundWrite = CompoundWrite.Empty; + var update = nodeFromJSON({ 'foo': 'foo-value', 'bar': 'bar-value' }); + compoundWrite = compoundWrite.addWrite(Path.Empty, update); + expect(compoundWrite.childCompoundWrite(Path.Empty)).to.equal(compoundWrite); + }); + + it('root update removes root priority', function() { + var compoundWrite = CompoundWrite.Empty; + compoundWrite = compoundWrite.addWrite(new Path('.priority'), PRIO_NODE); + compoundWrite = compoundWrite.addWrite(Path.Empty, nodeFromJSON('foo')); + assertNodesEqual(nodeFromJSON('foo'), compoundWrite.apply(EMPTY_NODE)); + }); + + it('deep update removes priority there', function() { + var compoundWrite = CompoundWrite.Empty; + compoundWrite = compoundWrite.addWrite(new Path('foo/.priority'), PRIO_NODE); + compoundWrite = compoundWrite.addWrite(new Path('foo'), nodeFromJSON('bar')); + var expected = nodeFromJSON({ 'foo': 'bar' }); + assertNodesEqual(expected, compoundWrite.apply(EMPTY_NODE)); + }); + + it('adding updates at path works', function() { + var compoundWrite = CompoundWrite.Empty; + var updates = { + 'foo': nodeFromJSON('foo-value'), + 'bar': nodeFromJSON('bar-value') + }; + compoundWrite = compoundWrite.addWrites(new Path('child-1'), updates); + + var expectedChildOne = { + 'foo': 'foo-value', + 'bar': 'bar-value' + }; + var expected = CHILDREN_NODE.updateImmediateChild('child-1', nodeFromJSON(expectedChildOne)); + assertNodesEqual(expected, compoundWrite.apply(CHILDREN_NODE)); + }); + + it('adding updates at root works', function() { + var compoundWrite = CompoundWrite.Empty; + var updates = { + 'child-1': nodeFromJSON('new-value-1'), + 'child-2': EMPTY_NODE, + 'child-3': nodeFromJSON('value-3') + }; + compoundWrite = compoundWrite.addWrites(Path.Empty, updates); + + var expected = { + 'child-1': 'new-value-1', + 'child-3': 'value-3' + }; + assertNodesEqual(nodeFromJSON(expected), compoundWrite.apply(CHILDREN_NODE)); + }); + + it('child write of root priority works', function() { + var compoundWrite = CompoundWrite.Empty.addWrite(new Path('.priority'), PRIO_NODE); + assertNodesEqual(PRIO_NODE, compoundWrite.childCompoundWrite(new Path('.priority')).apply(EMPTY_NODE)); + }); + + it('complete children only returns complete overwrites', function() { + var compoundWrite = CompoundWrite.Empty; + compoundWrite = compoundWrite.addWrite(new Path('child-1'), LEAF_NODE); + expect(compoundWrite.getCompleteChildren()).to.deep.equal([new NamedNode('child-1', LEAF_NODE)]); + }); + + it('complete children only returns empty overwrites', function() { + var compoundWrite = CompoundWrite.Empty; + compoundWrite = compoundWrite.addWrite(new Path('child-1'), EMPTY_NODE); + expect(compoundWrite.getCompleteChildren()).to.deep.equal([new NamedNode('child-1', EMPTY_NODE)]); + }); + + it('complete children doesnt return deep overwrites', function() { + var compoundWrite = CompoundWrite.Empty; + compoundWrite = compoundWrite.addWrite(new Path('child-1/deep/path'), LEAF_NODE); + expect(compoundWrite.getCompleteChildren()).to.deep.equal([]); + }); + + it('complete children return all complete children but no incomplete', function() { + var compoundWrite = CompoundWrite.Empty; + compoundWrite = compoundWrite.addWrite(new Path('child-1/deep/path'), LEAF_NODE); + compoundWrite = compoundWrite.addWrite(new Path('child-2'), LEAF_NODE); + compoundWrite = compoundWrite.addWrite(new Path('child-3'), EMPTY_NODE); + var expected = { + 'child-2': LEAF_NODE, + 'child-3': EMPTY_NODE + }; + var actual = { }; + var completeChildren = compoundWrite.getCompleteChildren(); + for (var i = 0; i < completeChildren.length; i++) { + actual[completeChildren[i].name] = completeChildren[i].node; + } + expect(actual).to.deep.equal(expected); + }); + + it('complete children return all children for root set', function() { + var compoundWrite = CompoundWrite.Empty; + compoundWrite = compoundWrite.addWrite(Path.Empty, CHILDREN_NODE); + + var expected = { + 'child-1': nodeFromJSON('value-1'), + 'child-2': nodeFromJSON('value-2') + }; + + var actual = { }; + var completeChildren = compoundWrite.getCompleteChildren(); + for (var i = 0; i < completeChildren.length; i++) { + actual[completeChildren[i].name] = completeChildren[i].node; + } + expect(actual).to.deep.equal(expected); + }); + + it('empty merge has no shadowing write', function() { + expect(CompoundWrite.Empty.hasCompleteWrite(Path.Empty)).to.be.false; + }); + + it('compound write with empty root has shadowing write', function() { + var compoundWrite = CompoundWrite.Empty.addWrite(Path.Empty, EMPTY_NODE); + expect(compoundWrite.hasCompleteWrite(Path.Empty)).to.be.true; + expect(compoundWrite.hasCompleteWrite(new Path('child'))).to.be.true; + }); + + it('compound write with root has shadowing write', function() { + var compoundWrite = CompoundWrite.Empty.addWrite(Path.Empty, LEAF_NODE); + expect(compoundWrite.hasCompleteWrite(Path.Empty)).to.be.true; + expect(compoundWrite.hasCompleteWrite(new Path('child'))).to.be.true; + }); + + it('compound write with deep update has shadowing write', function() { + var compoundWrite = CompoundWrite.Empty.addWrite(new Path('deep/update'), LEAF_NODE); + expect(compoundWrite.hasCompleteWrite(Path.Empty)).to.be.false; + expect(compoundWrite.hasCompleteWrite(new Path('deep'))).to.be.false; + expect(compoundWrite.hasCompleteWrite(new Path('deep/update'))).to.be.true; + }); + + it('compound write with priority update has shadowing write', function() { + var compoundWrite = CompoundWrite.Empty.addWrite(new Path('.priority'), PRIO_NODE); + expect(compoundWrite.hasCompleteWrite(Path.Empty)).to.be.false; + expect(compoundWrite.hasCompleteWrite(new Path('.priority'))).to.be.true; + }); + + it('updates can be removed', function() { + var compoundWrite = CompoundWrite.Empty; + var update = nodeFromJSON({ 'foo': 'foo-value', 'bar': 'bar-value' }); + compoundWrite = compoundWrite.addWrite(new Path('child-1'), update); + compoundWrite = compoundWrite.removeWrite(new Path('child-1')); + assertNodesEqual(CHILDREN_NODE, compoundWrite.apply(CHILDREN_NODE)); + }); + + it('deep removes has no effect on overlaying set', function() { + var compoundWrite = CompoundWrite.Empty; + var updateOne = nodeFromJSON({ 'foo': 'foo-value', 'bar': 'bar-value' }); + var updateTwo = nodeFromJSON('baz-value'); + var updateThree = nodeFromJSON('new-foo-value'); + compoundWrite = compoundWrite.addWrite(new Path('child-1'), updateOne); + compoundWrite = compoundWrite.addWrite(new Path('child-1/baz'), updateTwo); + compoundWrite = compoundWrite.addWrite(new Path('child-1/foo'), updateThree); + compoundWrite = compoundWrite.removeWrite(new Path('child-1/foo')); + var expectedChildOne = { + 'foo': 'new-foo-value', + 'bar': 'bar-value', + 'baz': 'baz-value' + }; + var expected = CHILDREN_NODE.updateImmediateChild('child-1', nodeFromJSON(expectedChildOne)); + assertNodesEqual(expected, compoundWrite.apply(CHILDREN_NODE)); + }); + + it('remove at path without set is without effect', function() { + var compoundWrite = CompoundWrite.Empty; + var updateOne = nodeFromJSON({ 'foo': 'foo-value', 'bar': 'bar-value' }); + var updateTwo = nodeFromJSON('baz-value'); + var updateThree = nodeFromJSON('new-foo-value'); + compoundWrite = compoundWrite.addWrite(new Path('child-1'), updateOne); + compoundWrite = compoundWrite.addWrite(new Path('child-1/baz'), updateTwo); + compoundWrite = compoundWrite.addWrite(new Path('child-1/foo'), updateThree); + compoundWrite = compoundWrite.removeWrite(new Path('child-2')); + var expectedChildOne = { + 'foo': 'new-foo-value', + 'bar': 'bar-value', + 'baz': 'baz-value' + }; + var expected = CHILDREN_NODE.updateImmediateChild('child-1', nodeFromJSON(expectedChildOne)); + assertNodesEqual(expected, compoundWrite.apply(CHILDREN_NODE)); + }); + + it('can remove priority', function() { + var compoundWrite = CompoundWrite.Empty; + compoundWrite = compoundWrite.addWrite(new Path('.priority'), PRIO_NODE); + compoundWrite = compoundWrite.removeWrite(new Path('.priority')); + assertNodeGetsCorrectPriority(compoundWrite, LEAF_NODE, EMPTY_NODE); + }); + + it('removing only affects removed path', function() { + var compoundWrite = CompoundWrite.Empty; + var updates = { + 'child-1': nodeFromJSON('new-value-1'), + 'child-2': EMPTY_NODE, + 'child-3': nodeFromJSON('value-3') + }; + compoundWrite = compoundWrite.addWrites(Path.Empty, updates); + compoundWrite = compoundWrite.removeWrite(new Path('child-2')); + + var expected = { + 'child-1': 'new-value-1', + 'child-2': 'value-2', + 'child-3': 'value-3' + }; + assertNodesEqual(nodeFromJSON(expected), compoundWrite.apply(CHILDREN_NODE)); + }); + + it('remove removes all deeper sets', function() { + var compoundWrite = CompoundWrite.Empty; + var updateTwo = nodeFromJSON('baz-value'); + var updateThree = nodeFromJSON('new-foo-value'); + compoundWrite = compoundWrite.addWrite(new Path('child-1/baz'), updateTwo); + compoundWrite = compoundWrite.addWrite(new Path('child-1/foo'), updateThree); + compoundWrite = compoundWrite.removeWrite(new Path('child-1')); + assertNodesEqual(CHILDREN_NODE, compoundWrite.apply(CHILDREN_NODE)); + }); + + it('remove at root also removes priority', function() { + var compoundWrite = CompoundWrite.Empty; + compoundWrite = compoundWrite.addWrite(Path.Empty, new LeafNode('foo', PRIO_NODE)); + compoundWrite = compoundWrite.removeWrite(Path.Empty); + var node = nodeFromJSON('value'); + assertNodeGetsCorrectPriority(compoundWrite, node, EMPTY_NODE); + }); + + it('updating priority doesnt overwrite leaf node', function() { + var compoundWrite = CompoundWrite.Empty; + compoundWrite = compoundWrite.addWrite(Path.Empty, LEAF_NODE); + compoundWrite = compoundWrite.addWrite(new Path('child/.priority'), PRIO_NODE); + assertNodesEqual(LEAF_NODE, compoundWrite.apply(EMPTY_NODE)); + }); + + it("updating empty node doesn't overwrite leaf node", function() { + var compoundWrite = CompoundWrite.Empty; + compoundWrite = compoundWrite.addWrite(Path.Empty, LEAF_NODE); + compoundWrite = compoundWrite.addWrite(new Path('child'), EMPTY_NODE); + assertNodesEqual(LEAF_NODE, compoundWrite.apply(EMPTY_NODE)); + }); + + it('Overwrites existing child', function() { + var compoundWrite = CompoundWrite.Empty; + var path = new Path('child-1'); + compoundWrite = compoundWrite.addWrite(path, LEAF_NODE); + expect(compoundWrite.apply(CHILDREN_NODE)).to.deep.equal(CHILDREN_NODE.updateImmediateChild(path.getFront(), LEAF_NODE)); + }); + + it('Updates existing child', function() { + var compoundWrite = CompoundWrite.Empty; + var path = new Path('child-1/foo'); + compoundWrite = compoundWrite.addWrite(path, LEAF_NODE); + expect(compoundWrite.apply(CHILDREN_NODE)).to.deep.equal(CHILDREN_NODE.updateChild(path, LEAF_NODE)); + }); + + it("Doesn't update priority on empty node.", function() { + var compoundWrite = CompoundWrite.Empty; + compoundWrite = compoundWrite.addWrite(new Path('.priority'), PRIO_NODE); + assertNodeGetsCorrectPriority(compoundWrite, EMPTY_NODE, EMPTY_NODE); + }); + + it('Updates priority on node', function() { + var compoundWrite = CompoundWrite.Empty; + compoundWrite = compoundWrite.addWrite(new Path('.priority'), PRIO_NODE); + var node = nodeFromJSON('value'); + assertNodeGetsCorrectPriority(compoundWrite, node, PRIO_NODE); + }); + + it('Updates priority of child', function() { + var compoundWrite = CompoundWrite.Empty; + var path = new Path('child-1/.priority'); + compoundWrite = compoundWrite.addWrite(path, PRIO_NODE); + assertNodesEqual(CHILDREN_NODE.updateChild(path, PRIO_NODE), compoundWrite.apply(CHILDREN_NODE)); + }); + + it("Doesn't update priority of nonexistent child.", function() { + var compoundWrite = CompoundWrite.Empty; + var path = new Path('child-3/.priority'); + compoundWrite = compoundWrite.addWrite(path, PRIO_NODE); + assertNodesEqual(CHILDREN_NODE, compoundWrite.apply(CHILDREN_NODE)); + }); + + it('Deep update existing updates', function() { + var compoundWrite = CompoundWrite.Empty; + var updateOne = nodeFromJSON({ 'foo': 'foo-value', 'bar': 'bar-value' }); + var updateTwo = nodeFromJSON('baz-value'); + var updateThree = nodeFromJSON('new-foo-value'); + compoundWrite = compoundWrite.addWrite(new Path('child-1'), updateOne); + compoundWrite = compoundWrite.addWrite(new Path('child-1/baz'), updateTwo); + compoundWrite = compoundWrite.addWrite(new Path('child-1/foo'), updateThree); + var expectedChildOne = { + 'foo': 'new-foo-value', + 'bar': 'bar-value', + 'baz': 'baz-value' + }; + var expected = CHILDREN_NODE.updateImmediateChild('child-1', nodeFromJSON(expectedChildOne)); + assertNodesEqual(expected, compoundWrite.apply(CHILDREN_NODE)); + }); + + it("child priority doesn't update empty node priority on child merge", function() { + var compoundWrite = CompoundWrite.Empty; + compoundWrite = compoundWrite.addWrite(new Path('child-1/.priority'), PRIO_NODE); + assertNodeGetsCorrectPriority(compoundWrite.childCompoundWrite(new Path('child-1')), EMPTY_NODE, EMPTY_NODE); + }); + + it('Child priority updates priority on child write', function() { + var compoundWrite = CompoundWrite.Empty; + compoundWrite = compoundWrite.addWrite(new Path('child-1/.priority'), PRIO_NODE); + var node = nodeFromJSON('value'); + assertNodeGetsCorrectPriority(compoundWrite.childCompoundWrite(new Path('child-1')), node, PRIO_NODE); + }); +}); \ No newline at end of file diff --git a/tests/database/database.test.ts b/tests/database/database.test.ts new file mode 100644 index 00000000000..3514be58b45 --- /dev/null +++ b/tests/database/database.test.ts @@ -0,0 +1,92 @@ +import { expect } from "chai"; +import firebase from "../../src/app"; +import { + TEST_PROJECT, + patchFakeAuthFunctions, +} from "./helpers/util"; +import "../../src/database"; + +describe('Database Tests', function() { + var defaultApp; + + beforeEach(function() { + defaultApp = firebase.initializeApp({databaseURL: TEST_PROJECT.databaseURL}); + patchFakeAuthFunctions(defaultApp); + }); + + afterEach(function() { + return defaultApp.delete(); + }); + + it('Can get database.', function() { + var db = firebase.database(); + expect(db).to.not.be.undefined; + expect(db).not.to.be.null; + }); + + it('Illegal to call constructor', function() { + expect(function() { + var db = new firebase.database.Database('url'); + }).to.throw(/don't call new Database/i); + }); + + it('Can get app', function() { + var db = firebase.database(); + expect(db.app).to.not.be.undefined; + expect((db.app as any) instanceof firebase.app.App); + }); + + it('Can get root ref', function() { + var db = firebase.database(); + + var ref = db.ref(); + + expect(ref instanceof firebase.database.Reference).to.be.true; + expect(ref.key).to.be.null; + }); + + it('Can get child ref', function() { + var db = firebase.database(); + + var ref = db.ref('child'); + + expect(ref instanceof firebase.database.Reference).to.be.true; + expect(ref.key).to.equal('child'); + }); + + it('Can get deep child ref', function() { + var db = firebase.database(); + + var ref = db.ref('child/grand-child'); + + expect(ref instanceof firebase.database.Reference).to.be.true; + expect(ref.key).to.equal('grand-child'); + }); + + it('ref() validates arguments', function() { + var db = firebase.database(); + expect(function() { + var ref = (db as any).ref('path', 'extra'); + }).to.throw(/Expects no more than 1/); + }); + + it('Can get refFromURL()', function() { + var db = firebase.database(); + var ref = db.refFromURL(TEST_PROJECT.databaseURL + '/path/to/data'); + expect(ref.key).to.equal('data'); + }); + + it('refFromURL() validates domain', function() { + var db = firebase.database(); + expect(function() { + var ref = db.refFromURL('https://thisisnotarealfirebase.firebaseio.com/path/to/data'); + }).to.throw(/does not match.*database/i); + }); + + it('refFromURL() validates argument', function() { + var db = firebase.database(); + expect(function() { + var ref = (db as any).refFromURL(); + }).to.throw(/Expects at least 1/); + }); +}); diff --git a/tests/database/datasnapshot.test.ts b/tests/database/datasnapshot.test.ts new file mode 100644 index 00000000000..fbf6a1ba847 --- /dev/null +++ b/tests/database/datasnapshot.test.ts @@ -0,0 +1,209 @@ +import { expect } from "chai"; +import { nodeFromJSON } from "../../src/database/core/snap/nodeFromJSON"; +import { PRIORITY_INDEX } from "../../src/database/core/snap/indexes/PriorityIndex"; +import { getRandomNode } from "./helpers/util"; +import { DataSnapshot } from "../../src/database/api/DataSnapshot"; +import { Reference } from "../../src/database/api/Reference"; + +describe("DataSnapshot Tests", function () { + /** @return {!DataSnapshot} */ + var snapshotForJSON = function(json) { + var dummyRef = getRandomNode(); + return new DataSnapshot(nodeFromJSON(json), dummyRef, PRIORITY_INDEX); + }; + + it("DataSnapshot.hasChildren() works.", function() { + var snap = snapshotForJSON({}); + expect(snap.hasChildren()).to.equal(false); + + snap = snapshotForJSON(5); + expect(snap.hasChildren()).to.equal(false); + + snap = snapshotForJSON({'x': 5}); + expect(snap.hasChildren()).to.equal(true); + }); + + it("DataSnapshot.exists() works.", function() { + var snap = snapshotForJSON({}); + expect(snap.exists()).to.equal(false); + + snap = snapshotForJSON({ '.priority':1 }); + expect(snap.exists()).to.equal(false); + + snap = snapshotForJSON(null); + expect(snap.exists()).to.equal(false); + + snap = snapshotForJSON(true); + expect(snap.exists()).to.equal(true); + + snap = snapshotForJSON(5); + expect(snap.exists()).to.equal(true); + + snap = snapshotForJSON({'x': 5}); + expect(snap.exists()).to.equal(true); + }); + + it("DataSnapshot.val() works.", function() { + var snap = snapshotForJSON(5); + expect(snap.val()).to.equal(5); + + snap = snapshotForJSON({ }); + expect(snap.val()).to.equal(null); + + var json = + { + x: 5, + y: { + ya: 1, + yb: 2, + yc: { yca: 3 } + } + }; + snap = snapshotForJSON(json); + expect(snap.val()).to.deep.equal(json); + }); + + it("DataSnapshot.child() works.", function() { + var snap = snapshotForJSON({x: 5, y: { yy: 3, yz: 4}}); + expect(snap.child('x').val()).to.equal(5); + expect(snap.child('y').val()).to.deep.equal({yy: 3, yz: 4}); + expect(snap.child('y').child('yy').val()).to.equal(3); + expect(snap.child('y/yz').val()).to.equal(4); + expect(snap.child('z').val()).to.equal(null); + expect(snap.child('x/y').val()).to.equal(null); + expect(snap.child('x').child('y').val()).to.equal(null) + }); + + it("DataSnapshot.hasChild() works.", function() { + var snap = snapshotForJSON({x: 5, y: { yy: 3, yz: 4}}); + expect(snap.hasChild('x')).to.equal(true); + expect(snap.hasChild('y/yy')).to.equal(true); + expect(snap.hasChild('dinosaur')).to.equal(false); + expect(snap.child('x').hasChild('anything')).to.equal(false); + expect(snap.hasChild('x/anything/at/all')).to.equal(false); + }); + + it("DataSnapshot.key works.", function() { + var snap = snapshotForJSON({a: { b: { c: 5 }}}); + expect(snap.child('a').key).to.equal('a'); + expect(snap.child('a/b/c').key).to.equal('c'); + expect(snap.child('/a/b/c/').key).to.equal('c'); + expect(snap.child('////a////b/c///').key).to.equal('c'); + expect(snap.child('///').key).to.equal(snap.key); + + // Should also work for nonexistent paths. + expect(snap.child('/z/q/r/v/m').key).to.equal('m'); + }); + + it("DataSnapshot.forEach() works: no priorities.", function() { + var snap = snapshotForJSON({a: 1, z: 26, m: 13, n: 14, c: 3, b: 2, e: 5}); + var out = ''; + snap.forEach(function(child) { + out = out + child.key + ':' + child.val() + ':'; + }); + + expect(out).to.equal('a:1:b:2:c:3:e:5:m:13:n:14:z:26:'); + }); + + it("DataSnapshot.forEach() works: numeric priorities.", function() { + var snap = snapshotForJSON({ + a: {'.value': 1, '.priority': 26}, + z: {'.value': 26, '.priority': 1}, + m: {'.value': 13, '.priority': 14}, + n: {'.value': 14, '.priority': 12}, + c: {'.value': 3, '.priority': 24}, + b: {'.value': 2, '.priority': 25}, + e: {'.value': 5, '.priority': 22}}); + + var out = ''; + snap.forEach(function(child) { + out = out + child.key + ':' + child.val() + ':'; + }); + + expect(out).to.equal('z:26:n:14:m:13:e:5:c:3:b:2:a:1:'); + }); + + it("DataSnapshot.forEach() works: numeric priorities as strings.", function() { + var snap = snapshotForJSON({ + a: {'.value': 1, '.priority': '26'}, + z: {'.value': 26, '.priority': '1'}, + m: {'.value': 13, '.priority': '14'}, + n: {'.value': 14, '.priority': '12'}, + c: {'.value': 3, '.priority': '24'}, + b: {'.value': 2, '.priority': '25'}, + e: {'.value': 5, '.priority': '22'}}); + + var out = ''; + snap.forEach(function(child) { + out = out + child.key + ':' + child.val() + ':'; + }); + + expect(out).to.equal('z:26:n:14:m:13:e:5:c:3:b:2:a:1:'); + }); + + it("DataSnapshot.forEach() works: alpha priorities.", function() { + var snap = snapshotForJSON({ + a: {'.value': 1, '.priority': 'first'}, + z: {'.value': 26, '.priority': 'second'}, + m: {'.value': 13, '.priority': 'third'}, + n: {'.value': 14, '.priority': 'fourth'}, + c: {'.value': 3, '.priority': 'fifth'}, + b: {'.value': 2, '.priority': 'sixth'}, + e: {'.value': 5, '.priority': 'seventh'}}); + + var out = ''; + snap.forEach(function(child) { + out = out + child.key + ':' + child.val() + ':'; + }); + + expect(out).to.equal('c:3:a:1:n:14:z:26:e:5:b:2:m:13:'); + }); + + it("DataSnapshot.foreach() works: mixed alpha and numeric priorities", function() { + var json = { + "alpha42": {'.value': 1, '.priority': "zed" }, + "noPriorityC": {'.value': 1, '.priority': null }, + "num41": {'.value': 1, '.priority': 500 }, + "noPriorityB": {'.value': 1, '.priority': null }, + "num80": {'.value': 1, '.priority': 4000.1 }, + "num50": {'.value': 1, '.priority': 4000 }, + "num10": {'.value': 1, '.priority': 24 }, + "alpha41": {'.value': 1, '.priority': "zed" }, + "alpha20": {'.value': 1, '.priority': "horse" }, + "num20": {'.value': 1, '.priority': 123 }, + "num70": {'.value': 1, '.priority': 4000.01 }, + "noPriorityA": {'.value': 1, '.priority': null }, + "alpha30": {'.value': 1, '.priority': "tree" }, + "num30": {'.value': 1, '.priority': 300 }, + "num60": {'.value': 1, '.priority': 4000.001 }, + "alpha10": {'.value': 1, '.priority': "0horse" }, + "num42": {'.value': 1, '.priority': 500 }, + "alpha40": {'.value': 1, '.priority': "zed" }, + "num40": {'.value': 1, '.priority': 500 } }; + + var snap = snapshotForJSON(json); + var out = ''; + snap.forEach(function(child) { + out = out + child.key + ', '; + }); + + expect(out).to.equal("noPriorityA, noPriorityB, noPriorityC, num10, num20, num30, num40, num41, num42, num50, num60, num70, num80, alpha10, alpha20, alpha30, alpha40, alpha41, alpha42, "); + }); + + it(".val() exports array-like data as arrays.", function() { + var array = ['bob', 'and', 'becky', 'seem', 'really', 'nice', 'yeah?']; + var snap = snapshotForJSON(array); + var snapVal = snap.val(); + expect(snapVal).to.deep.equal(array); + expect(snapVal instanceof Array).to.equal(true); // to.equal doesn't verify type. + }); + + it("DataSnapshot can be JSON serialized", function() { + var json = { + "foo": "bar", + ".priority": 1 + }; + var snap = snapshotForJSON(json); + expect(JSON.parse(JSON.stringify(snap))).to.deep.equal(json); + }); +}); diff --git a/tests/database/helpers/EventAccumulator.ts b/tests/database/helpers/EventAccumulator.ts new file mode 100644 index 00000000000..8a2ad2ec635 --- /dev/null +++ b/tests/database/helpers/EventAccumulator.ts @@ -0,0 +1,53 @@ +export const EventAccumulatorFactory = { + waitsForCount: maxCount => { + let count = 0; + const condition = () => ea.eventData.length >= count; + const ea = new EventAccumulator(condition) + ea.onReset(() => { count = 0; }); + ea.onEvent(() => { count++; }); + return ea; + } +} + +export class EventAccumulator { + public eventData = []; + public promise; + public resolve; + public reject; + private onResetFxn; + private onEventFxn; + constructor(public condition: Function) { + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } + addEvent(eventData?: any) { + this.eventData = [ + ...this.eventData, + eventData + ]; + if (typeof this.onEventFxn === 'function') this.onEventFxn(); + if (this._testCondition()) { + this.resolve(this.eventData); + } + } + reset(condition?: Function) { + this.eventData = []; + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + if (typeof this.onResetFxn === 'function') this.onResetFxn(); + if (typeof condition === 'function') this.condition = condition; + } + onEvent(cb: Function) { + this.onEventFxn = cb; + } + onReset(cb: Function) { + this.onResetFxn = cb; + } + _testCondition() { + return this.condition(); + } +} \ No newline at end of file diff --git a/tests/database/helpers/events.ts b/tests/database/helpers/events.ts new file mode 100644 index 00000000000..d5a8079840c --- /dev/null +++ b/tests/database/helpers/events.ts @@ -0,0 +1,215 @@ +import { TEST_PROJECT } from "./util"; + +/** + * A set of functions to clean up event handlers. + * @type {function()} + */ +export let eventCleanupHandlers = []; + + +/** Clean up outstanding event handlers */ +export function eventCleanup() { + for (var i = 0; i < eventCleanupHandlers.length; ++i) { + eventCleanupHandlers[i](); + } + eventCleanupHandlers = []; +}; + +/** + * The path component of the firebaseRef url + * @param {Firebase} firebaseRef + * @return {string} + */ +function rawPath(firebaseRef) { + return firebaseRef.toString().replace(TEST_PROJECT.databaseURL, ''); +}; + +/** + * Creates a struct which waits for many events. + * @param {Array} pathAndEvents an array of tuples of [Firebase, [event type strings]] + * @param {string=} opt_helperName + * @return {{waiter: waiter, watchesInitializedWaiter: watchesInitializedWaiter, unregister: unregister, addExpectedEvents: addExpectedEvents}} + */ +export function eventTestHelper(pathAndEvents, helperName?) { + let resolve, reject; + let promise = new Promise((pResolve, pReject) => { + resolve = pResolve; + reject = pReject; + }); + let resolveInit, rejectInit; + const initPromise = new Promise((pResolve, pReject) => { + resolveInit = pResolve; + rejectInit = pReject; + }); + var expectedPathAndEvents = []; + var actualPathAndEvents = []; + var pathEventListeners = {}; + var initializationEvents = 0; + + helperName = helperName ? helperName + ': ' : ''; + + // Listen on all of the required paths, with a callback function that just + // appends to actualPathAndEvents. + var make_eventCallback = function(type) { + return function(snap) { + // Get the ref of where the snapshot came from. + var ref = type === 'value' ? snap.ref : snap.ref.parent; + + actualPathAndEvents.push([rawPath(ref), [type, snap.key]]); + + if (!pathEventListeners[ref].initialized) { + initializationEvents++; + if (type === 'value') { + pathEventListeners[ref].initialized = true; + } + } else { + // Call waiter here to trigger exceptions when the event is fired, rather than later when the + // test framework is calling the waiter... makes for easier debugging. + waiter(); + } + + // We want to trigger the promise resolution if valid, so try to call waiter as events + // are coming back. + try { + if (waiter()) { + resolve(); + } + } catch(e) {} + }; + }; + + // returns a function which indicates whether the events have been received + // in the correct order. If anything is wrong (too many events or + // incorrect events, we throw). Else we return false, indicating we should + // keep waiting. + var waiter = function() { + var pathAndEventToString = function(pathAndEvent) { + return '{path: ' + pathAndEvent[0] + ', event:[' + pathAndEvent[1][0] + ', ' + pathAndEvent[1][1] + ']}'; + }; + + var i = 0; + while (i < expectedPathAndEvents.length && i < actualPathAndEvents.length) { + var expected = expectedPathAndEvents[i]; + var actual = actualPathAndEvents[i]; + + if (expected[0] != actual[0] || expected[1][0] != actual[1][0] || expected[1][1] != actual[1][1]) { + throw helperName + 'Event ' + i + ' incorrect. Expected: ' + pathAndEventToString(expected) + + ' Actual: ' + pathAndEventToString(actual); + } + i++; + } + + if (expectedPathAndEvents.length < actualPathAndEvents.length) { + throw helperName + "Extra event detected '" + pathAndEventToString(actualPathAndEvents[i]) + "'."; + } + + // If we haven't thrown and both arrays are the same length, then we're + // done. + return expectedPathAndEvents.length == actualPathAndEvents.length; + }; + + var listenOnPath = function(path) { + var valueCB = make_eventCallback('value'); + var addedCB = make_eventCallback('child_added'); + var removedCB = make_eventCallback('child_removed'); + var movedCB = make_eventCallback('child_moved'); + var changedCB = make_eventCallback('child_changed'); + path.on('child_removed', removedCB); + path.on('child_added', addedCB); + path.on('child_moved', movedCB); + path.on('child_changed', changedCB); + path.on('value', valueCB); + return function() { + path.off('child_removed', removedCB); + path.off('child_added', addedCB); + path.off('child_moved', movedCB); + path.off('child_changed', changedCB); + path.off('value', valueCB); + } + }; + + + var addExpectedEvents = function(pathAndEvents) { + var pathsToListenOn = []; + for (var i = 0; i < pathAndEvents.length; i++) { + + var pathAndEvent = pathAndEvents[i]; + + var path = pathAndEvent[0]; + //var event = pathAndEvent[1]; + + pathsToListenOn.push(path); + + pathAndEvent[0] = rawPath(path); + + if (pathAndEvent[1][0] === 'value') + pathAndEvent[1][1] = path.key; + + expectedPathAndEvents.push(pathAndEvent); + } + + // There's some trickiness with event order depending on the order you attach event callbacks: + // + // When you listen on a/b/c, a/b, and a, we dedupe that to just listening on a. But if you do it in that + // order, we'll send "listen a/b/c, listen a/b, unlisten a/b/c, listen a, unlisten a/b" which will result in you + // getting events something like "a/b/c: value, a/b: child_added c, a: child_added b, a/b: value, a: value" + // + // BUT, if all of the listens happen before you are connected to firebase (e.g. this is the first test you're + // running), the dedupe will have taken affect and we'll just send "listen a", which results in: + // "a/b/c: value, a/b: child_added c, a/b: value, a: child_added b, a: value" + // Notice the 3rd and 4th events are swapped. + // To mitigate this, we re-ordeer your event registrations and do them in order of shortest path to longest. + + pathsToListenOn.sort(function(a, b) { return a.toString().length - b.toString().length; }); + for (i = 0; i < pathsToListenOn.length; i++) { + path = pathsToListenOn[i]; + if (!pathEventListeners[path.toString()]) { + pathEventListeners[path.toString()] = { }; + pathEventListeners[path.toString()].initialized = false; + pathEventListeners[path.toString()].unlisten = listenOnPath(path); + } + } + + promise = new Promise((pResolve, pReject) => { + resolve = pResolve; + reject = pReject; + }); + }; + + addExpectedEvents(pathAndEvents); + + var watchesInitializedWaiter = function() { + for (var path in pathEventListeners) { + if (!pathEventListeners[path].initialized) + return false; + } + + // Remove any initialization events. + actualPathAndEvents.splice(actualPathAndEvents.length - initializationEvents, initializationEvents); + initializationEvents = 0; + + resolveInit(); + return true; + }; + + var unregister = function() { + for (var path in pathEventListeners) { + if (pathEventListeners.hasOwnProperty(path)) { + pathEventListeners[path].unlisten(); + } + } + }; + + eventCleanupHandlers.push(unregister); + return { + promise, + initPromise, + waiter, + watchesInitializedWaiter, + unregister, + + addExpectedEvents: function(moreEvents) { + addExpectedEvents(moreEvents); + } + }; +}; \ No newline at end of file diff --git a/tests/database/helpers/util.ts b/tests/database/helpers/util.ts new file mode 100644 index 00000000000..67ee1b1398c --- /dev/null +++ b/tests/database/helpers/util.ts @@ -0,0 +1,223 @@ +import { globalScope } from "../../../src/utils/globalScope"; +import firebase from "../../../src/app"; +import '../../../src/database'; +import { Reference } from "../../../src/database/api/Reference"; +import { Query } from "../../../src/database/api/Query"; +import { expect } from "chai"; +import { ConnectionTarget } from "../../../src/database/api/test_access"; + + +export const TEST_PROJECT = require('../../config/project.json'); + +var qs = {}; +if ('location' in this) { + var search = (this.location.search.substr(1) || '').split('&'); + for (var i = 0; i < search.length; ++i) { + var parts = search[i].split('='); + qs[parts[0]] = parts[1] || true; // support for foo= + } +} + +let numDatabases = 0; + +/** + * Fake Firebase App Authentication functions for testing. + * @param {!FirebaseApp} app + * @return {!FirebaseApp} + */ +export function patchFakeAuthFunctions(app) { + var token_ = null; + + app['INTERNAL'] = app['INTERNAL'] || {}; + + app['INTERNAL']['getToken'] = function(forceRefresh) { + return Promise.resolve(token_); + }; + + app['INTERNAL']['addAuthTokenListener'] = function(listener) { + }; + + app['INTERNAL']['removeAuthTokenListener'] = function(listener) { + }; + + return app; +}; + +/** + * Gets or creates a root node to the test namespace. All calls sharing the + * value of opt_i will share an app context. + * @param {=} opt_i + * @param {string=} opt_ref + * @return {Firebase} + */ +export function getRootNode(i?, ref?) { + if (i === undefined) { + i = 0; + } + if (i + 1 > numDatabases) { + numDatabases = i + 1; + } + var app; + var db; + try { + app = firebase.app("TEST-" + i); + } catch(e) { + app = firebase.initializeApp({ databaseURL: TEST_PROJECT.databaseURL }, "TEST-" + i); + patchFakeAuthFunctions(app); + } + db = app.database(); + return db.ref(ref); +}; + +/** + * Create multiple refs to the same top level + * push key - each on it's own Firebase.Context. + * @param {int=} opt_numNodes + * @return {Firebase|Array} + */ +export function getRandomNode(numNodes?): Reference | Array { + if (numNodes === undefined) { + return getRandomNode(1)[0]; + } + + var child; + var nodeList = []; + for (var i = 0; i < numNodes; i++) { + var ref = getRootNode(i); + if (child === undefined) { + child = ref.push().key; + } + + nodeList[i] = ref.child(child); + } + + return >nodeList; +}; + +export function getQueryValue(query: Query) { + return query.once('value').then(snap => snap.val()); +} + +export function pause(milliseconds: number) { + return new Promise(resolve => { + setTimeout(() => resolve(), milliseconds); + }); +} + +export function getPath(query: Query) { + return query.toString().replace(TEST_PROJECT.databaseURL, ''); +} + +export function shuffle(arr, randFn?) { + var randFn = randFn || Math.random; + for (var i = arr.length - 1;i > 0;i--) { + var j = Math.floor(randFn() * (i + 1)); + var tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; + } +} + +export function testAuthTokenProvider(app) { + var token_ = null; + var nextToken_ = null; + var hasNextToken_ = false; + var listeners_ = []; + + app['INTERNAL'] = app['INTERNAL'] || {}; + + app['INTERNAL']['getToken'] = function(forceRefresh) { + if (forceRefresh && hasNextToken_) { + token_ = nextToken_; + hasNextToken_ = false; + } + return Promise.resolve({accessToken: token_}); + }; + + app['INTERNAL']['addAuthTokenListener'] = function(listener) { + var token = token_; + listeners_.push(listener); + var async = Promise.resolve(); + async.then(function() { + listener(token) + }); + }; + + app['INTERNAL']['removeAuthTokenListener'] = function(listener) { + throw Error('removeAuthTokenListener not supported in testing'); + }; + + return { + setToken: function(token) { + token_ = token; + var async = Promise.resolve(); + for (var i = 0; i < listeners_.length; i++) { + async.then((function(idx) { + return function() { + listeners_[idx](token); + } + }(i))); + } + + // Any future thens are guaranteed to be resolved after the listeners have been notified + return async; + }, + setNextToken: function(token) { + nextToken_ = token; + hasNextToken_ = true; + } + }; +} + +let freshRepoId = 1; +const activeFreshApps = []; + +export function getFreshRepo(url, path?) { + var app = firebase.initializeApp({databaseURL: url}, 'ISOLATED_REPO_' + freshRepoId++); + patchFakeAuthFunctions(app); + activeFreshApps.push(app); + return app.database().ref(path); +} + +export function getFreshRepoFromReference(ref) { + var host = ref.root.toString(); + var path = ref.toString().replace(host, ''); + return getFreshRepo(host, path); +} + +// Little helpers to get the currently cached snapshot / value. +export function getSnap(path) { + var snap; + var callback = function(snapshot) { snap = snapshot; }; + path.once('value', callback); + return snap; +}; + +export function getVal(path) { + var snap = getSnap(path); + return snap ? snap.val() : undefined; +}; + +export function canCreateExtraConnections() { + return globalScope.MozWebSocket || globalScope.WebSocket; +}; + +export function buildObjFromKey(key) { + var keys = key.split('.'); + var obj = {}; + var parent = obj; + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + parent[key] = i < keys.length - 1 ? {} : 'test_value'; + parent = parent[key]; + } + return obj; +}; + +export function testRepoInfo(url) { + const regex = /https?:\/\/(.*).firebaseio.com/; + const match = url.match(regex); + if (!match) throw new Error('Couldnt get Namespace from passed URL'); + const [,ns] = match; + return new ConnectionTarget(`${ns}.firebaseio.com`, true, ns, false); +} diff --git a/tests/database/info.test.ts b/tests/database/info.test.ts new file mode 100644 index 00000000000..deece96dc98 --- /dev/null +++ b/tests/database/info.test.ts @@ -0,0 +1,177 @@ +import { expect } from "chai"; +import { + getFreshRepo, + getRootNode, + getRandomNode, + getPath +} from "./helpers/util"; +import { Reference } from "../../src/database/api/Reference"; +import { EventAccumulator } from "./helpers/EventAccumulator"; + +/** + * We have a test that depends on leveraging two properly + * configured Firebase instances. we are skiping the test + * but I want to leave the test here for when we can refactor + * to remove the prod firebase dependency. + */ +declare var runs; +declare var waitsFor; +declare var TEST_ALT_NAMESPACE; +declare var TEST_NAMESPACE; + +describe(".info Tests", function () { + it("Can get a reference to .info nodes.", function() { + var f = (getRootNode() as Reference); + expect(getPath(f.child('.info'))).to.equal('/.info'); + expect(getPath(f.child('.info/foo'))).to.equal('/.info/foo'); + }); + + it("Can't write to .info", function() { + var f = (getRootNode() as Reference).child('.info'); + expect(function() {f.set('hi');}).to.throw; + expect(function() {f.setWithPriority('hi', 5);}).to.throw; + expect(function() {f.setPriority('hi');}).to.throw; + expect(function() {f.transaction(function() { });}).to.throw; + expect(function() {f.push();}).to.throw; + expect(function() {f.remove();}).to.throw; + + expect(function() {f.child('test').set('hi');}).to.throw; + var f2 = f.child('foo/baz'); + expect(function() {f2.set('hi');}).to.throw; + }); + + it("Can watch .info/connected.", function() { + return new Promise(resolve => { + var f = (getRandomNode() as Reference).root; + f.child('.info/connected').on('value', function(snap) { + if (snap.val() === true) resolve(); + }); + }) + }); + + + it('.info/connected correctly goes to false when disconnected.', async function() { + var f = (getRandomNode() as Reference).root; + var everConnected = false; + var connectHistory = ''; + + const ea = new EventAccumulator(() => everConnected); + f.child('.info/connected').on('value', function(snap) { + if (snap.val() === true) + everConnected = true; + + if (everConnected) + connectHistory += snap.val() + ','; + ea.addEvent(); + }); + + await ea.promise; + + ea.reset(() => connectHistory); + f.database.goOffline(); + f.database.goOnline(); + + return ea.promise; + }); + + // Skipping this test as it is expecting a server time diff from a + // local Firebase + it.skip(".info/serverTimeOffset", async function() { + var ref = (getRootNode() as Reference); + + // make sure push works + var child = ref.push(); + + var offsets = []; + + const ea = new EventAccumulator(() => offsets.length === 1); + + ref.child('.info/serverTimeOffset').on('value', function(snap) { + offsets.push(snap.val()); + ea.addEvent(); + }); + + await ea.promise; + + expect(typeof offsets[0]).to.equal('number'); + expect(offsets[0]).not.to.be.greaterThan(0); + + // Make sure push still works + ref.push(); + ref.child('.info/serverTimeOffset').off(); + }); + + it.skip("database.goOffline() / database.goOnline() connection management", function() { + var ref = getFreshRepo(TEST_NAMESPACE); + var refAlt = getFreshRepo(TEST_ALT_NAMESPACE); + var ready; + + // Wait until we're connected to both Firebases + runs(function() { + ready = 0; + var eventHandler = function(snap) { + if (snap.val() === true) { + snap.ref.off(); + ready += 1; + } + }; + ref.child(".info/connected").on("value", eventHandler); + refAlt.child(".info/connected").on("value", eventHandler); + }); + waitsFor(function() { return (ready == 2); }); + + runs(function() { + ref.database.goOffline(); + refAlt.database.goOffline(); + }); + + // Ensure we're disconnected from both Firebases + runs(function() { + ready = 0; + var eventHandler = function(snap) { + expect(snap.val() === false); + ready += 1; + } + ref.child(".info/connected").once("value", eventHandler); + refAlt.child(".info/connected").once("value", eventHandler); + }); + waitsFor(function() { return (ready == 2); }); + + // Ensure that we don't automatically reconnect upon Reference creation + runs(function() { + ready = 0; + var refDup = ref.database.ref(); + refDup.child(".info/connected").on("value", function(snap) { + ready = (snap.val() === true) || ready; + }); + setTimeout(function() { + expect(ready).to.equal(0); + refDup.child(".info/connected").off(); + ready = -1; + }, 500); + }); + waitsFor(function() { return ready == -1; }); + + runs(function() { + ref.database.goOnline(); + refAlt.database.goOnline(); + }); + + // Ensure we're connected to both Firebases + runs(function() { + ready = 0; + var eventHandler = function(snap) { + if (snap.val() === true) { + snap.ref.off(); + ready += 1; + } + }; + ref.child(".info/connected").on("value", eventHandler); + refAlt.child(".info/connected").on("value", eventHandler); + }); + + waitsFor(function() { + return (ready == 2); + }); + }); +}); diff --git a/tests/database/node.test.ts b/tests/database/node.test.ts new file mode 100644 index 00000000000..6dc26e3e9f5 --- /dev/null +++ b/tests/database/node.test.ts @@ -0,0 +1,255 @@ +import { expect } from "chai"; +import { PRIORITY_INDEX } from "../../src/database/core/snap/indexes/PriorityIndex"; +import { LeafNode } from "../../src/database/core/snap/LeafNode"; +import { IndexMap } from "../../src/database/core/snap/IndexMap"; +import { Path } from "../../src/database/core/util/Path"; +import { SortedMap } from "../../src/database/core/util/SortedMap"; +import { ChildrenNode } from "../../src/database/core/snap/ChildrenNode"; +import { NAME_COMPARATOR } from "../../src/database/core/snap/comparators"; +import { nodeFromJSON } from "../../src/database/core/snap/nodeFromJSON"; + +describe('Node Tests', function() { + var DEFAULT_INDEX = PRIORITY_INDEX; + + it('Create leaf nodes of various types.', function() { + var x = new LeafNode(5, new LeafNode(42)); + expect(x.getValue()).to.equal(5); + expect(x.getPriority().val()).to.equal(42); + expect(x.isLeafNode()).to.equal(true); + + x = new LeafNode('test'); + expect(x.getValue()).to.equal('test'); + x = new LeafNode(true); + expect(x.getValue()).to.equal(true); + }); + + it("LeafNode.updatePriority returns a new leaf node without changing the old.", function() { + var x = new LeafNode("test", new LeafNode(42)); + var y = x.updatePriority(new LeafNode(187)); + + // old node is the same. + expect(x.getValue()).to.equal("test"); + expect(x.getPriority().val()).to.equal(42); + + // new node has the new priority but the old value. + expect(y.getValue()).to.equal("test"); + expect(y.getPriority().val()).to.equal(187); + }); + + it("LeafNode.updateImmediateChild returns a new children node.", function() { + var x = new LeafNode("test", new LeafNode(42)); + var y = x.updateImmediateChild('test', new LeafNode("foo")); + + expect(y.isLeafNode()).to.equal(false); + expect(y.getPriority().val()).to.equal(42); + expect(y.getImmediateChild('test').getValue()).to.equal('foo'); + }); + + it("LeafNode.getImmediateChild returns an empty node.", function() { + var x = new LeafNode("test"); + expect(x.getImmediateChild('foo')).to.equal(ChildrenNode.EMPTY_NODE); + }); + + it("LeafNode.getChild returns an empty node.", function() { + var x = new LeafNode('test'); + expect(x.getChild(new Path('foo/bar'))).to.equal(ChildrenNode.EMPTY_NODE); + }); + + it('ChildrenNode.updatePriority returns a new internal node without changing the old.', function() { + var x = ChildrenNode.EMPTY_NODE.updateImmediateChild("child", new LeafNode(5)); + var children = x.children_; + var y = x.updatePriority(new LeafNode(17)); + expect(y.children_).to.equal(x.children_); + expect(x.children_).to.equal(children); + expect(x.getPriority().val()).to.equal(null); + expect(y.getPriority().val()).to.equal(17); + }); + + it('ChildrenNode.updateImmediateChild returns a new internal node with the new child, without changing the old.', + function() { + var children = new SortedMap(NAME_COMPARATOR); + var x = new ChildrenNode(children, ChildrenNode.EMPTY_NODE, IndexMap.Default); + var newValue = new LeafNode('new value'); + var y = x.updateImmediateChild('test', newValue); + expect(x.children_).to.equal(children); + expect(y.children_.get('test')).to.equal(newValue); + }); + + it("ChildrenNode.updateChild returns a new internal node with the new child, without changing the old.", function() { + var children = new SortedMap(NAME_COMPARATOR); + var x = new ChildrenNode(children, ChildrenNode.EMPTY_NODE, IndexMap.Default); + var newValue = new LeafNode("new value"); + var y = x.updateChild(new Path('test/foo'), newValue); + expect(x.children_).to.equal(children); + expect(y.getChild(new Path('test/foo'))).to.equal(newValue); + }); + + it("Node.hash() works correctly.", function() { + var node = nodeFromJSON({ + intNode:4, + doubleNode:4.5623, + stringNode:"hey guys", + boolNode:true + }); + + // !!!NOTE!!! These hashes must match what the server generates. If you change anything so these hashes change, + // make sure you change the corresponding server code. + expect(node.getImmediateChild("intNode").hash()).to.equal("eVih19a6ZDz3NL32uVBtg9KSgQY="); + expect(node.getImmediateChild("doubleNode").hash()).to.equal("vf1CL0tIRwXXunHcG/irRECk3lY="); + expect(node.getImmediateChild("stringNode").hash()).to.equal("CUNLXWpCVoJE6z7z1vE57lGaKAU="); + expect(node.getImmediateChild("boolNode").hash()).to.equal("E5z61QM0lN/U2WsOnusszCTkR8M="); + + expect(node.hash()).to.equal("6Mc4jFmNdrLVIlJJjz2/MakTK9I="); + }); + + it("Node.hash() works correctly with priorities.", function() { + var node = nodeFromJSON({ + root: {c: {'.value': 99, '.priority': 'abc'}, '.priority': 'def'} + }); + + expect(node.hash()).to.equal("Fm6tzN4CVEu5WxFDZUdTtqbTVaA="); + }); + + it("Node.hash() works correctly with number priorities.", function() { + var node = nodeFromJSON({ + root: {c: {'.value': 99, '.priority': 42}, '.priority': 3.14} + }); + + expect(node.hash()).to.equal("B15QCqrzCxrI5zz1y00arWqFRFg="); + }); + + it("Node.hash() stress...", function() { + var node = nodeFromJSON({ + a:-1.7976931348623157e+308, + b:1.7976931348623157e+308, + c:"unicode ✔ 🐵 🌴 x͢", + d:3.14159265358979323846264338327950, + e: { + '.value': 12345678901234568, + '.priority': "🐵" + }, + "✔": "foo", + '.priority':"✔" + }); + expect(node.getImmediateChild('a').hash()).to.equal('7HxgOBDEC92uQwhCuuvKA2rbXDA='); + expect(node.getImmediateChild('b').hash()).to.equal('8R+ekVQmxs6ZWP0fdzFHxVeGnWo='); + expect(node.getImmediateChild('c').hash()).to.equal('JoKoFUnbmg3/DlY70KaDWslfYPk='); + expect(node.getImmediateChild('d').hash()).to.equal('Y41iC5+92GIqXfabOm33EanRI8s='); + expect(node.getImmediateChild('e').hash()).to.equal('+E+Mxlqh5MhT+On05bjsZ6JaaxI='); + expect(node.getImmediateChild('✔').hash()).to.equal('MRRL/+aA/uibaL//jghUpxXS/uY='); + expect(node.hash()).to.equal('CyC0OU8GSkOAKnsPjheWtWC0Yxo='); + }); + + it("ChildrenNode.getPredecessorChild works correctly.", function() { + var node = nodeFromJSON({ + d: true, a: true, g: true, c: true, e: true + }); + + // HACK: Pass null instead of the actual childNode, since it's not actually needed. + expect(node.getPredecessorChildName('a', null, DEFAULT_INDEX)).to.equal(null); + expect(node.getPredecessorChildName('c', null, DEFAULT_INDEX)).to.equal('a'); + expect(node.getPredecessorChildName('d', null, DEFAULT_INDEX)).to.equal('c'); + expect(node.getPredecessorChildName('e', null, DEFAULT_INDEX)).to.equal('d'); + expect(node.getPredecessorChildName('g', null, DEFAULT_INDEX)).to.equal('e'); + }); + + it("SortedChildrenNode.getPredecessorChild works correctly.", function() { + var node = nodeFromJSON({ + d: { '.value': true, '.priority' : 22 }, + a: { '.value': true, '.priority' : 25 }, + g: { '.value': true, '.priority' : 19 }, + c: { '.value': true, '.priority' : 23 }, + e: { '.value': true, '.priority' : 21 } + }); + + expect(node.getPredecessorChildName('a', node.getImmediateChild('a'), DEFAULT_INDEX)).to.equal('c'); + expect(node.getPredecessorChildName('c', node.getImmediateChild('c'), DEFAULT_INDEX)).to.equal('d'); + expect(node.getPredecessorChildName('d', node.getImmediateChild('d'), DEFAULT_INDEX)).to.equal('e'); + expect(node.getPredecessorChildName('e', node.getImmediateChild('e'), DEFAULT_INDEX)).to.equal('g'); + expect(node.getPredecessorChildName('g', node.getImmediateChild('g'), DEFAULT_INDEX)).to.equal(null); + }); + + it("SortedChildrenNode.updateImmediateChild works correctly.", function() { + var node = nodeFromJSON({ + d: { '.value': true, '.priority' : 22 }, + a: { '.value': true, '.priority' : 25 }, + g: { '.value': true, '.priority' : 19 }, + c: { '.value': true, '.priority' : 23 }, + e: { '.value': true, '.priority' : 21 }, + '.priority' : 1000 + }); + + node = node.updateImmediateChild('c', nodeFromJSON(false)); + expect(node.getImmediateChild('c').getValue()).to.equal(false); + expect(node.getImmediateChild('c').getPriority().val()).to.equal(null); + expect(node.getPriority().val()).to.equal(1000); + }); + + it("removing nodes correctly removes intermediate nodes with no remaining children", function() { + var json = {a: {b: {c: 1}}}; + var node = nodeFromJSON(json); + var newNode = node.updateChild(new Path('a/b/c'), ChildrenNode.EMPTY_NODE); + expect(newNode.isEmpty()).to.equal(true); + }); + + it("removing nodes leaves intermediate nodes with other children", function() { + var json = {a: {b: {c: 1}, d: 2}}; + var node = nodeFromJSON(json); + var newNode = node.updateChild(new Path('a/b/c'), ChildrenNode.EMPTY_NODE); + expect(newNode.isEmpty()).to.equal(false); + expect(newNode.getChild(new Path('a/b/c')).isEmpty()).to.equal(true); + expect(newNode.getChild(new Path('a/d')).val()).to.equal(2); + }); + + it("removing nodes leaves other leaf nodes", function() { + var json = {a: {b: {c: 1, d: 2}}}; + var node = nodeFromJSON(json); + var newNode = node.updateChild(new Path('a/b/c'), ChildrenNode.EMPTY_NODE); + expect(newNode.isEmpty()).to.equal(false); + expect(newNode.getChild(new Path('a/b/c')).isEmpty()).to.equal(true); + expect(newNode.getChild(new Path('a/b/d')).val()).to.equal(2); + }); + + it("removing nodes correctly removes the root", function() { + var json = null; + var node = nodeFromJSON(json); + var newNode = node.updateChild(new Path(''), ChildrenNode.EMPTY_NODE); + expect(newNode.isEmpty()).to.equal(true); + + json = {a: 1}; + node = nodeFromJSON(json); + newNode = node.updateChild(new Path('a'), ChildrenNode.EMPTY_NODE); + expect(newNode.isEmpty()).to.equal(true); + }); + + it("ignores null values", function() { + var json = {a: 1, b: null}; + var node = nodeFromJSON(json); + expect(node.children_.get('b')).to.equal(null); + }); + + it("Leading zeroes in path are handled properly", function() { + var json = {"1": 1, "01": 2, "001": 3}; + var tree = nodeFromJSON(json); + expect(tree.getChild(new Path("1")).val()).to.equal(1); + expect(tree.getChild(new Path("01")).val()).to.equal(2); + expect(tree.getChild(new Path("001")).val()).to.equal(3); + }); + + it("Treats leading zeroes as objects, not array", function() { + var json = {"3": 1, "03": 2}; + var tree = nodeFromJSON(json); + var val = tree.val(); + expect(val).to.deep.equal(json); + }); + + it("Updating empty children doesn't overwrite leaf node", function() { + var empty = ChildrenNode.EMPTY_NODE; + var node = nodeFromJSON("value"); + expect(node).to.deep.equal(node.updateChild(new Path(".priority"), empty)); + expect(node).to.deep.equal(node.updateChild(new Path("child"), empty)); + expect(node).to.deep.equal(node.updateChild(new Path("child/.priority"), empty)); + expect(node).to.deep.equal(node.updateImmediateChild("child", empty)); + expect(node).to.deep.equal(node.updateImmediateChild(".priority", empty)); + }); +}); diff --git a/tests/database/node/connection.test.ts b/tests/database/node/connection.test.ts new file mode 100644 index 00000000000..2683dce1c5b --- /dev/null +++ b/tests/database/node/connection.test.ts @@ -0,0 +1,40 @@ +import { expect } from "chai"; +import { TEST_PROJECT, testRepoInfo } from "../helpers/util"; +import { Connection } from "../../../src/database/realtime/Connection"; +import "../../../src/utils/nodePatches"; + +describe('Connection', () => { + it('return the session id', function(done) { + new Connection('1', + testRepoInfo(TEST_PROJECT.databaseURL), + message => {}, + (timestamp, sessionId) => { + expect(sessionId).not.to.be.null; + expect(sessionId).not.to.equal(''); + done(); + }, + () => {}, + reason => {}); + }); + + // TODO(koss) - Flakey Test. When Dev Tools is closed on my Mac, this test + // fails about 20% of the time (open - it never fails). In the failing + // case a long-poll is opened first. + it.skip('disconnect old session on new connection', function(done) { + const info = testRepoInfo(TEST_PROJECT.databaseURL); + new Connection('1', info, + message => {}, + (timestamp, sessionId) => { + new Connection('2', info, + message => {}, + (timestamp, sessionId) => {}, + () => {}, + reason => {}, + sessionId); + }, + () => { + done(); // first connection was disconnected + }, + reason => {}); + }); +}); diff --git a/tests/database/order.test.ts b/tests/database/order.test.ts new file mode 100644 index 00000000000..1529b43fa6d --- /dev/null +++ b/tests/database/order.test.ts @@ -0,0 +1,543 @@ +import { expect } from "chai"; +import { getRandomNode } from './helpers/util'; +import { Reference } from '../../src/database/api/Reference'; +import { EventAccumulator } from './helpers/EventAccumulator'; +import { + eventTestHelper, +} from "./helpers/events"; + +describe('Order Tests', function () { + // Kind of a hack, but a lot of these tests are written such that they'll fail if run before we're + // connected to Firebase because they do a bunch of sets and then a listen and assume that they'll + // arrive in that order. But if we aren't connected yet, the "reconnection" code will send them + // in the opposite order. + beforeEach(function() { + return new Promise(resolve => { + var ref = (getRandomNode() as Reference), connected = false; + ref.root.child('.info/connected').on('value', function(s) { + connected = s.val() == true; + if (connected) resolve(); + }); + }) + }); + + it("Push a bunch of data, enumerate it back; ensure order is correct.", async function () { + var node = (getRandomNode() as Reference); + for (var i = 0; i < 10; i++) { + node.push().set(i); + } + + const snap = await node.once('value'); + + var expected = 0; + snap.forEach(function (child) { + expect(child.val()).to.equal(expected); + expected++; + }); + expect(expected).to.equal(10); + }); + + it("Push a bunch of paths, then write; ensure order is correct.", async function() { + var node = (getRandomNode() as Reference); + var paths = []; + // Push them first to try to call push() multiple times in the same ms. + for (var i = 0; i < 20; i++) { + paths[i] = node.push(); + } + for (i = 0; i < 20; i++) { + paths[i].set(i); + } + + const snap = await node.once('value'); + + var expected = 0; + snap.forEach(function (child) { + expect(child.val()).to.equal(expected); + expected++; + }); + expect(expected).to.equal(20); + }); + + it("Push a bunch of data, reconnect, read it back; ensure order is chronological.", async function () { + var nodePair = (getRandomNode(2) as Reference[]); + var expected; + + var node = nodePair[0]; + var nodesSet = 0; + for (var i = 0; i < 10; i++) { + node.push().set(i, function() { ++nodesSet }); + } + + // read it back locally and make sure it's correct. + const snap = await node.once('value'); + + expected = 0; + snap.forEach(function (child) { + expect(child.val()).to.equal(expected); + expected++; + }); + expect(expected).to.equal(10); + + // read it back + var readSnap; + const ea = new EventAccumulator(() => readSnap); + nodePair[1].on('value', function(snap) { + readSnap = snap; + ea.addEvent(); + }); + + await ea.promise; + + expected = 0; + readSnap.forEach(function (child) { + expect(child.val()).to.equal(expected); + expected++; + }); + expect(expected).to.equal(10); + }); + + it("Push a bunch of data with explicit priority, reconnect, read it back; ensure order is correct.", async function () { + var nodePair = (getRandomNode(2) as Reference[]); + var expected; + + var node = nodePair[0]; + var nodesSet = 0; + for (var i = 0; i < 10; i++) { + var pushedNode = node.push(); + pushedNode.setWithPriority(i, 10 - i, function() { ++nodesSet }); + } + + // read it back locally and make sure it's correct. + const snap = await node.once('value'); + expected = 9; + snap.forEach(function (child) { + expect(child.val()).to.equal(expected); + expected--; + }); + expect(expected).to.equal(-1); + + // local SETs are visible immediately, but the second node is in a separate repo, so it is considered remote. + // We need confirmation that the server has gotten all the data before we can expect to receive it all + + // read it back + var readSnap; + const ea = new EventAccumulator(() => readSnap); + nodePair[1].on('value', function(snap) { + readSnap = snap; + ea.addEvent(); + }); + await ea.promise; + + expected = 9; + readSnap.forEach(function (child) { + expect(child.val()).to.equal(expected); + expected--; + }); + expect(expected).to.equal(-1); + }); + + it("Push data with exponential priority and ensure order is correct.", async function () { + var nodePair = (getRandomNode(2) as Reference[]); + var expected; + + var node = nodePair[0]; + var nodesSet = 0; + for (var i = 0; i < 10; i++) { + var pushedNode = node.push(); + pushedNode.setWithPriority(i, 111111111111111111111111111111 / Math.pow(10, i), function() { ++nodesSet }); + } + + // read it back locally and make sure it's correct. + const snap = await node.once('value'); + expected = 9; + snap.forEach(function (child) { + expect(child.val()).to.equal(expected); + expected--; + }); + expect(expected).to.equal(-1); + + // read it back + var readSnap; + const ea = new EventAccumulator(() => readSnap); + nodePair[1].on('value', function(snap) { + readSnap = snap; + ea.addEvent(); + }); + + await ea.promise; + + expected = 9; + readSnap.forEach(function (child) { + expect(child.val()).to.equal(expected); + expected--; + }); + expect(expected).to.equal(-1); + }); + + it("Verify nodes without values aren't enumerated.", async function() { + var node = (getRandomNode() as Reference); + node.child('foo'); + node.child('bar').set('test'); + + var items = 0; + const snap = await node.once('value'); + snap.forEach(function (child) { + items++; + expect(child.key).to.equal('bar'); + }); + + expect(items).to.equal(1); + }); + + it.skip("Receive child_moved event when priority changes.", async function() { + var node = (getRandomNode() as Reference); + + // const ea = new EventAccumulator(() => eventHelper.watchesInitializedWaiter); + + var eventHelper = eventTestHelper([ + [ node, ['child_added', 'a'] ], + [ node, ['value', ''] ], + [ node, ['child_added', 'b'] ], + [ node, ['value', ''] ], + [ node, ['child_added', 'c'] ], + [ node, ['value', ''] ], + [ node, ['child_moved', 'a'] ], + [ node, ['child_changed', 'a'] ], + [ node, ['value', ''] ] + ]); + + // await ea.promise; + + node.child('a').setWithPriority('first', 1); + node.child('b').setWithPriority('second', 5); + node.child('c').setWithPriority('third', 10); + + expect(eventHelper.waiter()).to.equal(false); + + node.child('a').setPriority(15); + + expect(eventHelper.waiter()).to.equal(true); + }); + + it.skip("Can reset priority to null.", async function() { + var node = (getRandomNode() as Reference); + + node.child('a').setWithPriority('a', 1); + node.child('b').setWithPriority('b', 2); + var eventHelper; + + // const ea = new EventAccumulator(() => eventHelper.waiter()); + eventHelper = eventTestHelper([ + [ node, ['child_added', 'a'] ], + [ node, ['child_added', 'b'] ], + [ node, ['value', ''] ] + ]); + + // await ea.promise; + + eventHelper.addExpectedEvents([ + [ node, ['child_moved', 'b'] ], + [ node, ['child_changed', 'b'] ], + [ node, ['value', '']] + ]); + + node.child('b').setPriority(null); + expect(eventHelper.waiter()).to.equal(true); + + expect((await node.once('value')).child('b').getPriority()).to.equal(null); + }); + + it("Inserting a node under a leaf node preserves its priority.", function() { + var node = (getRandomNode() as Reference); + + var snap = null; + node.on('value', function(s) {snap = s;}); + + node.setWithPriority('a', 10); + node.child('deeper').set('deeper'); + expect(snap.getPriority()).to.equal(10); + }); + + it("Verify order of mixed numbers / strings / no priorities.", async function () { + var nodePair = (getRandomNode(2) as Reference[]); + var nodeAndPriorities = [ + "alpha42", "zed", + "noPriorityC", null, + "num41", 500, + "noPriorityB", null, + "num80", 4000.1, + "num50", 4000, + "num10", 24, + "alpha41", "zed", + "alpha20", "horse", + "num20", 123, + "num70", 4000.01, + "noPriorityA", null, + "alpha30", "tree", + "num30", 300, + "num60", 4000.001, + "alpha10", "0horse", + "num42", 500, + "alpha40", "zed", + "num40", 500]; + + var setsCompleted = 0; + for (let i = 0; i < nodeAndPriorities.length; i++) { + var n = nodePair[0].child((nodeAndPriorities[i++] as string)); + n.setWithPriority(1, nodeAndPriorities[i], function() { setsCompleted++; }); + } + + var expectedOutput = "noPriorityA, noPriorityB, noPriorityC, num10, num20, num30, num40, num41, num42, num50, num60, num70, num80, alpha10, alpha20, alpha30, alpha40, alpha41, alpha42, "; + + const snap = await nodePair[0].once('value'); + + var output = ""; + snap.forEach(function (n) { + output += n.key + ", "; + }); + + expect(output).to.equal(expectedOutput); + + var eventsFired = false; + var output = ""; + nodePair[1].on('value', function(snap) { + snap.forEach(function (n) { + output += n.key + ", "; + }); + expect(output).to.equal(expectedOutput); + eventsFired = true; + }); + }); + + it("Verify order of integer keys.", async function () { + var ref = (getRandomNode() as Reference); + var keys = [ + "foo", + "bar", + "03", + "0", + "100", + "20", + "5", + "3", + "003", + "9" + ]; + + var setsCompleted = 0; + for (var i = 0; i < keys.length; i++) { + var child = ref.child(keys[i]); + child.set(true, function() { setsCompleted++; }); + } + + var expectedOutput = "0, 3, 03, 003, 5, 9, 20, 100, bar, foo, "; + + const snap = await ref.once('value'); + var output = ""; + snap.forEach(function (n) { + output += n.key + ", "; + }); + + expect(output).to.equal(expectedOutput); + }); + + it("Ensure prevName is correct on child_added event.", function() { + var node = (getRandomNode() as Reference); + + var added = ''; + node.on('child_added', function(snap, prevName) { + added += snap.key + " " + prevName + ", "; + }); + + node.set({"a" : 1, "b": 2, "c": 3}); + + expect(added).to.equal('a null, b a, c b, '); + }); + + it("Ensure prevName is correct when adding new nodes.", function() { + var node = (getRandomNode() as Reference); + + var added = ''; + node.on('child_added', function(snap, prevName) { + added += snap.key + " " + prevName + ", "; + }); + + node.set({"b" : 2, "c": 3, "d": 4}); + + expect(added).to.equal('b null, c b, d c, '); + + added = ''; + node.child('a').set(1); + expect(added).to.equal('a null, '); + + added = ''; + node.child('e').set(5); + expect(added).to.equal('e d, '); + }); + + it("Ensure prevName is correct when adding new nodes with JSON.", function() { + var node = (getRandomNode() as Reference); + + var added = ''; + node.on('child_added', function(snap, prevName) { + added += snap.key + " " + prevName + ", "; + }); + + node.set({"b" : 2, "c": 3, "d": 4}); + + expect(added).to.equal('b null, c b, d c, '); + + added = ''; + node.set({"a": 1, "b" : 2, "c": 3, "d": 4}); + expect(added).to.equal('a null, '); + + added = ''; + node.set({"a": 1, "b" : 2, "c": 3, "d": 4, "e": 5}); + expect(added).to.equal('e d, '); + }); + + it("Ensure prevName is correct when moving nodes.", function() { + var node = (getRandomNode() as Reference); + + var moved = ''; + node.on('child_moved', function(snap, prevName) { + moved += snap.key + " " + prevName + ", "; + }); + + node.child('a').setWithPriority('a', 1); + node.child('b').setWithPriority('b', 2); + node.child('c').setWithPriority('c', 3); + node.child('d').setWithPriority('d', 4); + + node.child('d').setPriority(0); + expect(moved).to.equal('d null, '); + + moved = ''; + node.child('a').setPriority(4); + expect(moved).to.equal('a c, '); + + moved = ''; + node.child('c').setPriority(0.5); + expect(moved).to.equal('c d, '); + }); + + it("Ensure prevName is correct when moving nodes by setting whole JSON.", function() { + var node = (getRandomNode() as Reference); + + var moved = ''; + node.on('child_moved', function(snap, prevName) { + moved += snap.key + " " + prevName + ", "; + }); + + node.set({ + a: {'.value': 'a', '.priority': 1}, + b: {'.value': 'b', '.priority': 2}, + c: {'.value': 'c', '.priority': 3}, + d: {'.value': 'd', '.priority': 4} + }); + + node.set({ + d: {'.value': 'd', '.priority': 0}, + a: {'.value': 'a', '.priority': 1}, + b: {'.value': 'b', '.priority': 2}, + c: {'.value': 'c', '.priority': 3} + }); + expect(moved).to.equal('d null, '); + + moved = ''; + node.set({ + d: {'.value': 'd', '.priority': 0}, + b: {'.value': 'b', '.priority': 2}, + c: {'.value': 'c', '.priority': 3}, + a: {'.value': 'a', '.priority': 4} + }); + expect(moved).to.equal('a c, '); + + moved = ''; + node.set({ + d: {'.value': 'd', '.priority': 0}, + c: {'.value': 'c', '.priority': 0.5}, + b: {'.value': 'b', '.priority': 2}, + a: {'.value': 'a', '.priority': 4} + }); + expect(moved).to.equal('c d, '); + }); + + it("Case 595: Should not get child_moved event when deleting prioritized grandchild.", function() { + var f = (getRandomNode() as Reference); + var moves = 0; + f.on('child_moved', function() { + moves++; + }); + + f.child('test/foo').setWithPriority(42, '5'); + f.child('test/foo2').setWithPriority(42, '10'); + f.child('test/foo').remove(); + f.child('test/foo2').remove(); + + expect(moves).to.equal(0, 'Should *not* have received any move events.'); + }); + + it("Can set value with priority of 0.", function() { + var f = (getRandomNode() as Reference); + + var snap = null; + f.on('value', function(s) { + snap = s; + }); + + f.setWithPriority('test', 0); + + expect(snap.getPriority()).to.equal(0); + }); + + it("Can set object with priority of 0.", function() { + var f = (getRandomNode() as Reference); + + var snap = null; + f.on('value', function(s) { + snap = s; + }); + + f.setWithPriority({x: 'test', y: 7}, 0); + + expect(snap.getPriority()).to.equal(0); + }); + + it("Case 2003: Should get child_moved for any priority change, regardless of whether it affects ordering.", function() { + var f = (getRandomNode() as Reference); + var moved = []; + f.on('child_moved', function(snap) { moved.push(snap.key); }); + f.set({ + a: {'.value': 'a', '.priority': 0}, + b: {'.value': 'b', '.priority': 1}, + c: {'.value': 'c', '.priority': 2}, + d: {'.value': 'd', '.priority': 3} + }); + + expect(moved).to.deep.equal([]); + f.child('b').setWithPriority('b', 1.5); + expect(moved).to.deep.equal(['b']); + }); + + it("Case 2003: Should get child_moved for any priority change, regardless of whether it affects ordering (2).", function() { + var f = (getRandomNode() as Reference); + var moved = []; + f.on('child_moved', function(snap) { moved.push(snap.key); }); + f.set({ + a: {'.value': 'a', '.priority': 0}, + b: {'.value': 'b', '.priority': 1}, + c: {'.value': 'c', '.priority': 2}, + d: {'.value': 'd', '.priority': 3} + }); + + expect(moved).to.deep.equal([]); + f.set({ + a: {'.value': 'a', '.priority': 0}, + b: {'.value': 'b', '.priority': 1.5}, + c: {'.value': 'c', '.priority': 2}, + d: {'.value': 'd', '.priority': 3} + }); + expect(moved).to.deep.equal(['b']); + }); +}); diff --git a/tests/database/order_by.test.ts b/tests/database/order_by.test.ts new file mode 100644 index 00000000000..ed907422fab --- /dev/null +++ b/tests/database/order_by.test.ts @@ -0,0 +1,392 @@ +import { expect } from "chai"; +import { getRandomNode } from "./helpers/util"; +import { EventAccumulatorFactory } from "./helpers/EventAccumulator"; +import { Reference } from "../../src/database/api/Reference"; + +describe('.orderBy tests', function() { + + // TODO: setup spy on console.warn + + var clearRef = (getRandomNode() as Reference); + + it('Snapshots are iterated in order', function() { + var ref = (getRandomNode() as Reference); + + var initial = { + alex: {nuggets: 60}, + rob: {nuggets: 56}, + vassili: {nuggets: 55.5}, + tony: {nuggets: 52}, + greg: {nuggets: 52} + }; + + var expectedOrder = ['greg', 'tony', 'vassili', 'rob', 'alex']; + var expectedPrevNames = [null, 'greg', 'tony', 'vassili', 'rob']; + + var valueOrder = []; + var addedOrder = []; + var addedPrevNames = []; + + var orderedRef = ref.orderByChild('nuggets'); + + orderedRef.on('value', function(snap) { + snap.forEach(function(childSnap) { + valueOrder.push(childSnap.key); + }); + }); + + orderedRef.on('child_added', function(snap, prevName) { + addedOrder.push(snap.key); + addedPrevNames.push(prevName); + }); + + ref.set(initial); + + expect(addedOrder).to.deep.equal(expectedOrder); + expect(valueOrder).to.deep.equal(expectedOrder); + expect(addedPrevNames).to.deep.equal(expectedPrevNames); + }); + + it('Snapshots are iterated in order for value', function() { + var ref = (getRandomNode() as Reference); + + var initial = { + alex: 60, + rob: 56, + vassili: 55.5, + tony: 52, + greg: 52 + }; + + var expectedOrder = ['greg', 'tony', 'vassili', 'rob', 'alex']; + var expectedPrevNames = [null, 'greg', 'tony', 'vassili', 'rob']; + + var valueOrder = []; + var addedOrder = []; + var addedPrevNames = []; + + var orderedRef = ref.orderByValue(); + + orderedRef.on('value', function(snap) { + snap.forEach(function(childSnap) { + valueOrder.push(childSnap.key); + }); + }); + + orderedRef.on('child_added', function(snap, prevName) { + addedOrder.push(snap.key); + addedPrevNames.push(prevName); + }); + + ref.set(initial); + + expect(addedOrder).to.deep.equal(expectedOrder); + expect(valueOrder).to.deep.equal(expectedOrder); + expect(addedPrevNames).to.deep.equal(expectedPrevNames); + }); + + it('Fires child_moved events', function() { + var ref = (getRandomNode() as Reference); + + var initial = { + alex: {nuggets: 60}, + rob: {nuggets: 56}, + vassili: {nuggets: 55.5}, + tony: {nuggets: 52}, + greg: {nuggets: 52} + }; + + var orderedRef = ref.orderByChild('nuggets'); + + var moved = false; + orderedRef.on('child_moved', function(snap, prevName) { + moved = true; + expect(snap.key).to.equal('greg'); + expect(prevName).to.equal('rob'); + expect(snap.val()).to.deep.equal({nuggets: 57}); + }); + + ref.set(initial); + ref.child('greg/nuggets').set(57); + expect(moved).to.equal(true); + }); + + it('Callback removal works', async function() { + var ref = (getRandomNode() as Reference); + + var reads = 0; + var fooCb; + var barCb; + var bazCb; + const ea = EventAccumulatorFactory.waitsForCount(4); + + fooCb = ref.orderByChild('foo').on('value', function() { + reads++; + ea.addEvent(); + }); + barCb = ref.orderByChild('bar').on('value', function() { + reads++; + ea.addEvent(); + }); + bazCb = ref.orderByChild('baz').on('value', function() { + reads++; + ea.addEvent(); + }); + ref.on('value', function() { + reads++; + ea.addEvent(); + }); + + ref.set(1); + + await ea.promise; + + ref.off('value', fooCb); + ref.set(2); + expect(reads).to.equal(7); + + // Should be a no-op, resulting in 3 more reads + ref.orderByChild('foo').off('value', bazCb); + ref.set(3); + expect(reads).to.equal(10); + + ref.orderByChild('bar').off('value'); + ref.set(4); + expect(reads).to.equal(12); + + // Now, remove everything + ref.off(); + ref.set(5); + expect(reads).to.equal(12); + }); + + it('child_added events are in the correct order', function() { + var ref = (getRandomNode() as Reference); + + var initial = { + a: {value: 5}, + c: {value: 3} + }; + + var added = []; + ref.orderByChild('value').on('child_added', function(snap) { + added.push(snap.key); + }); + ref.set(initial); + + expect(added).to.deep.equal(['c', 'a']); + + ref.update({ + b: {value: 4}, + d: {value: 2} + }); + + expect(added).to.deep.equal(['c', 'a', 'd', 'b']); + }); + + it('Can use key index', async function() { + var ref = (getRandomNode() as Reference); + + var data = { + a: { '.priority': 10, '.value': 'a' }, + b: { '.priority': 5, '.value': 'b' }, + c: { '.priority': 20, '.value': 'c' }, + d: { '.priority': 7, '.value': 'd' }, + e: { '.priority': 30, '.value': 'e' }, + f: { '.priority': 8, '.value': 'f' } + }; + + await ref.set(data); + + const snap = await ref.orderByKey().startAt('c').once('value'); + + var keys = []; + snap.forEach(function(child) { + keys.push(child.key); + }); + expect(keys).to.deep.equal(['c', 'd', 'e', 'f']); + + const ea = EventAccumulatorFactory.waitsForCount(5); + var keys = []; + + ref.orderByKey().limitToLast(5).on('child_added', function(child) { + keys.push(child.key); + ea.addEvent(); + }); + + await ea.promise; + + ref.orderByKey().off(); + expect(keys).to.deep.equal(['b', 'c', 'd', 'e', 'f']); + }); + + it('Queries work on leaf nodes', function(done) { + var ref = (getRandomNode() as Reference); + + ref.set('leaf-node', function() { + ref.orderByChild('foo').limitToLast(1).on('value', function(snap) { + expect(snap.val()).to.be.null; + done(); + }); + }); + }); + + it('Updates for unindexed queries work', function(done) { + var refs = (getRandomNode(2) as Reference[]); + var reader = refs[0]; + var writer = refs[1]; + + var value = { + 'one': { 'index': 1, 'value': 'one' }, + 'two': { 'index': 2, 'value': 'two' }, + 'three': { 'index': 3, 'value': 'three' } + }; + + var count = 0; + + writer.set(value, function() { + reader.orderByChild('index').limitToLast(2).on('value', function(snap) { + if (count === 0) { + expect(snap.val()).to.deep.equal({ + 'two': { 'index': 2, 'value': 'two' }, + 'three': { 'index': 3, 'value': 'three' } + }); + // update child which should trigger value event + writer.child('one/index').set(4); + } else if (count === 1) { + expect(snap.val()).to.deep.equal({ + 'three': { 'index': 3, 'value': 'three' }, + 'one': { 'index': 4, 'value': 'one' } + }); + done(); + } + count++; + }); + }); + }); + + it('Server respects KeyIndex', function(done) { + var refs = (getRandomNode(2) as Reference[]); + var reader = refs[0]; + var writer = refs[1]; + + var initial = { + a: 1, + b: 2, + c: 3 + }; + + var expected = ['b', 'c']; + + var actual = []; + + var orderedRef = reader.orderByKey().startAt('b').limitToFirst(2); + writer.set(initial, function() { + orderedRef.on('value', function(snap) { + snap.forEach(function(childSnap) { + actual.push(childSnap.key); + }); + expect(actual).to.deep.equal(expected); + done(); + }); + }); + }); + + it('startAt/endAt works on value index', function() { + var ref = (getRandomNode() as Reference); + + var initial = { + alex: 60, + rob: 56, + vassili: 55.5, + tony: 52, + greg: 52 + }; + + var expectedOrder = ['tony', 'vassili', 'rob']; + var expectedPrevNames = [null, 'tony', 'vassili']; + + var valueOrder = []; + var addedOrder = []; + var addedPrevNames = []; + + var orderedRef = ref.orderByValue().startAt(52, 'tony').endAt(59); + + orderedRef.on('value', function(snap) { + snap.forEach(function(childSnap) { + valueOrder.push(childSnap.key); + }); + }); + + orderedRef.on('child_added', function(snap, prevName) { + addedOrder.push(snap.key); + addedPrevNames.push(prevName); + }); + + ref.set(initial); + + expect(addedOrder).to.deep.equal(expectedOrder); + expect(valueOrder).to.deep.equal(expectedOrder); + expect(addedPrevNames).to.deep.equal(expectedPrevNames); + }); + + it('Removing default listener removes non-default listener that loads all data', function(done) { + var ref = (getRandomNode() as Reference); + + var initial = { key: 'value' }; + ref.set(initial, function(err) { + expect(err).to.be.null; + ref.orderByKey().on('value', function() {}); + ref.on('value', function() {}); + // Should remove both listener and should remove the listen sent to the server + ref.off(); + + // This used to crash because a listener for ref.orderByKey() existed already + ref.orderByKey().once('value', function(snap) { + expect(snap.val()).to.deep.equal(initial); + done(); + }); + }); + }); + + it('Can define and use an deep index', function(done) { + var ref = (getRandomNode() as Reference); + + var initial = { + alex: {deep: {nuggets: 60}}, + rob: {deep: {nuggets: 56}}, + vassili: {deep: {nuggets: 55.5}}, + tony: {deep: {nuggets: 52}}, + greg: {deep: {nuggets: 52}} + }; + + var expectedOrder = ['greg', 'tony', 'vassili']; + var expectedPrevNames = [null, 'greg', 'tony']; + + var valueOrder = []; + var addedOrder = []; + var addedPrevNames = []; + + var orderedRef = ref.orderByChild('deep/nuggets').limitToFirst(3); + + // come before value event + orderedRef.on('child_added', function(snap, prevName) { + addedOrder.push(snap.key); + addedPrevNames.push(prevName); + }); + + orderedRef.once('value', function(snap) { + snap.forEach(function(childSnap) { + valueOrder.push(childSnap.key); + }); + }); + + ref.set(initial, function(err) { + expect(err).to.be.null; + expect(addedOrder).to.deep.equal(expectedOrder); + expect(valueOrder).to.deep.equal(expectedOrder); + expect(addedPrevNames).to.deep.equal(expectedPrevNames); + done(); + }); + }); +}); diff --git a/tests/database/path.test.ts b/tests/database/path.test.ts new file mode 100644 index 00000000000..ee9c99e00ea --- /dev/null +++ b/tests/database/path.test.ts @@ -0,0 +1,59 @@ +import { expect } from "chai"; +import { Path } from "../../src/database/core/util/Path"; + +describe('Path Tests', function () { + var expectGreater = function(left, right) { + expect(Path.comparePaths(new Path(left), new Path(right))).to.equal(1) + expect(Path.comparePaths(new Path(right), new Path(left))).to.equal(-1) + }; + + var expectEqual = function(left, right) { + expect(Path.comparePaths(new Path(left), new Path(right))).to.equal(0) + }; + + it('contains() contains the path and any child path.', function () { + expect(new Path('/').contains(new Path('/a/b/c'))).to.equal(true); + expect(new Path('/a').contains(new Path('/a/b/c'))).to.equal(true); + expect(new Path('/a/b').contains(new Path('/a/b/c'))).to.equal(true); + expect(new Path('/a/b/c').contains(new Path('/a/b/c'))).to.equal(true); + + expect(new Path('/a/b/c').contains(new Path('/a/b'))).to.equal(false); + expect(new Path('/a/b/c').contains(new Path('/a'))).to.equal(false); + expect(new Path('/a/b/c').contains(new Path('/'))).to.equal(false); + + expect(new Path('/a/b/c').popFront().contains(new Path('/b/c'))).to.equal(true); + expect(new Path('/a/b/c').popFront().contains(new Path('/b/c/d'))).to.equal(true); + + expect(new Path('/a/b/c').contains(new Path('/b/c'))).to.equal(false); + expect(new Path('/a/b/c').contains(new Path('/a/c/b'))).to.equal(false); + + expect(new Path('/a/b/c').popFront().contains(new Path('/a/b/c'))).to.equal(false); + expect(new Path('/a/b/c').popFront().contains(new Path('/b/c'))).to.equal(true); + expect(new Path('/a/b/c').popFront().contains(new Path('/b/c/d'))).to.equal(true); + }); + + it('popFront() returns the parent', function() { + expect(new Path('/a/b/c').popFront().toString()).to.equal('/b/c') + expect(new Path('/a/b/c').popFront().popFront().toString()).to.equal('/c'); + expect(new Path('/a/b/c').popFront().popFront().popFront().toString()).to.equal('/'); + expect(new Path('/a/b/c').popFront().popFront().popFront().popFront().toString()).to.equal('/'); + }); + + it('parent() returns the parent', function() { + expect(new Path('/a/b/c').parent().toString()).to.equal('/a/b'); + expect(new Path('/a/b/c').parent().parent().toString()).to.equal('/a'); + expect(new Path('/a/b/c').parent().parent().parent().toString()).to.equal('/'); + expect(new Path('/a/b/c').parent().parent().parent().parent()).to.equal(null); + }); + + it('comparePaths() works as expected', function() { + expectEqual('/', ''); + expectEqual('/a', '/a'); + expectEqual('/a', '/a//'); + expectEqual('/a///b/b//', '/a/b/b'); + expectGreater('/b', '/a'); + expectGreater('/ab', '/a'); + expectGreater('/a/b', '/a'); + expectGreater('/a/b', '/a//'); + }); +}); diff --git a/tests/database/promise.test.ts b/tests/database/promise.test.ts new file mode 100644 index 00000000000..bb61cf093dd --- /dev/null +++ b/tests/database/promise.test.ts @@ -0,0 +1,194 @@ +import { expect } from "chai"; +import { getRandomNode, getRootNode } from "./helpers/util"; +import { Reference } from "../../src/database/api/Reference"; + +describe('Promise Tests', function() { + /** + * Enabling test retires, wrapping the onDisconnect + * methods seems to be flakey + */ + this.retries(3); + it('wraps Query.once', function() { + return (getRandomNode() as Reference).once('value').then(function(snap) { + expect(snap.val()).to.equal(null); + }); + }); + + it('wraps Firebase.set', function() { + var ref = (getRandomNode() as Reference); + return ref.set(5).then(function() { + return ref.once('value'); + }).then(function(read) { + expect(read.val()).to.equal(5); + }); + }); + + it('wraps Firebase.push when no value is passed', function() { + var ref = (getRandomNode() as Reference); + var pushed = ref.push(); + return pushed.then(function(childRef) { + expect(pushed.ref.parent.toString()).to.equal(ref.toString()); + expect(pushed.toString()).to.equal(childRef.toString()); + return pushed.once('value'); + }) + .then(function(snap) { + expect(snap.val()).to.equal(null); + expect(snap.ref.toString()).to.equal(pushed.toString()); + }); + }); + + it('wraps Firebase.push when a value is passed', function() { + var ref = (getRandomNode() as Reference); + var pushed = ref.push(6); + return pushed.then(function(childRef) { + expect(pushed.ref.parent.toString()).to.equal(ref.toString()); + expect(pushed.toString()).to.equal(childRef.toString()); + return pushed.once('value'); + }).then(function(snap) { + expect(snap.val()).to.equal(6); + expect(snap.ref.toString()).to.equal(pushed.toString()); + }); + }); + + it('wraps Firebase.remove', function() { + var ref = (getRandomNode() as Reference); + return ref.set({'a': 'b'}).then(function() { + var p = ref.child('a').remove(); + expect(typeof p.then === 'function').to.equal(true); + return p; + }).then(function() { + return ref.once('value'); + }).then(function(snap) { + expect(snap.val()).to.equal(null); + }); + }); + + it('wraps Firebase.update', function() { + var ref = (getRandomNode() as Reference); + return ref.set({'a': 'b'}).then(function() { + var p = ref.update({'c': 'd'}); + expect(typeof p.then === 'function').to.equal(true); + return p; + }).then(function() { + return ref.once('value'); + }).then(function(snap) { + expect(snap.val()).to.deep.equal({'a': 'b', 'c': 'd'}); + }); + }); + + it('wraps Fireabse.setPriority', function() { + var ref = (getRandomNode() as Reference); + return ref.set({'a': 'b'}).then(function() { + var p = ref.child('a').setPriority(5); + expect(typeof p.then === 'function').to.equal(true); + return p; + }).then(function() { + return ref.once('value'); + }).then(function(snap) { + expect(snap.child('a').getPriority()).to.equal(5); + }); + }); + + it('wraps Firebase.setWithPriority', function() { + var ref = (getRandomNode() as Reference); + return ref.setWithPriority('hi', 5).then(function() { + return ref.once('value'); + }).then(function(snap) { + expect(snap.getPriority()).to.equal(5); + expect(snap.val()).to.equal('hi'); + }); + }); + + it('wraps Firebase.transaction', function() { + var ref = (getRandomNode() as Reference); + return ref.transaction(function() { + return 5; + }).then(function(result) { + expect(result.committed).to.equal(true); + expect(result.snapshot.val()).to.equal(5); + return ref.transaction(function() { return undefined; }); + }).then(function(result) { + expect(result.committed).to.equal(false); + }); + }); + + it('exposes catch in the return of Firebase.push', function() { + // Catch is a pain in the bum to provide safely because "catch" is a reserved word and ES3 and below require + // you to use quotes to define it, but the closure linter really doesn't want you to do that either. + var ref = (getRandomNode() as Reference); + var pushed = ref.push(6); + + expect(typeof ref.then === 'function').to.equal(false); + expect(typeof ref.catch === 'function').to.equal(false); + expect(typeof pushed.then === 'function').to.equal(true); + expect(typeof pushed.catch === 'function').to.equal(true); + return pushed; + }); + + it('wraps onDisconnect.remove', function() { + var refs = (getRandomNode(2) as Reference[]); + var writer = refs[0]; + var reader = refs[1]; + var refInfo = getRootNode(0, '.info/connected'); + + refInfo.once('value', function(snapshot) { + expect(snapshot.val()).to.equal(true); + }); + + return writer.child('here today').set('gone tomorrow').then(function() { + var p = writer.child('here today').onDisconnect().remove(); + expect(typeof p.then === 'function').to.equal(true); + return p; + }).then(function() { + writer.database.goOffline(); + writer.database.goOnline(); + return reader.once('value'); + }).then(function(snap) { + expect(snap.val()).to.equal(null); + }); + }); + + it('wraps onDisconnect.update', function() { + var refs = (getRandomNode(2) as Reference[]); + var writer = refs[0]; + var reader = refs[1]; + return writer.set({'foo': 'baz'}).then(function() { + var p = writer.onDisconnect().update({'foo': 'bar'}); + expect(typeof p.then === 'function').to.equal(true); + return p; + }).then(function() { + writer.database.goOffline(); + writer.database.goOnline(); + return reader.once('value'); + }).then(function(snap) { + expect(snap.val()).to.deep.equal({'foo': 'bar'}); + }); + }); + + it('wraps onDisconnect.set', function() { + var refs = (getRandomNode(2) as Reference[]); + var writer = refs[0]; + var reader = refs[1]; + return writer.child('hello').onDisconnect().set('world').then(function() { + writer.database.goOffline(); + writer.database.goOnline(); + return reader.once('value'); + }).then(function(snap) { + expect(snap.val()).to.deep.equal({'hello': 'world'}); + }); + }); + + it('wraps onDisconnect.setWithPriority', function() { + var refs = (getRandomNode(2) as Reference[]); + var writer = refs[0]; + var reader = refs[1]; + return writer.child('meaning of life').onDisconnect().setWithPriority('ultimate question', 42).then(function() { + writer.database.goOffline(); + writer.database.goOnline(); + return reader.once('value'); + }).then(function(snap) { + expect(snap.val()).to.deep.equal({'meaning of life': 'ultimate question'}); + expect(snap.child('meaning of life').getPriority()).to.equal(42); + }); + }); +}); diff --git a/tests/database/query.test.ts b/tests/database/query.test.ts new file mode 100644 index 00000000000..1ca8f697c37 --- /dev/null +++ b/tests/database/query.test.ts @@ -0,0 +1,2717 @@ +import { expect } from "chai"; +import firebase from '../../src/app'; +import { Reference } from "../../src/database/api/Reference"; +import { Query } from "../../src/database/api/Query"; +import "../../src/database/core/snap/ChildrenNode"; +import { + getQueryValue, + getRandomNode, + getPath, + pause +} from "./helpers/util"; +import { + EventAccumulator, + EventAccumulatorFactory +} from "./helpers/EventAccumulator"; + +const _ = require('lodash'); + +type TaskList = [Query, any][]; + +describe('Query Tests', function() { + // Little helper class for testing event callbacks w/ contexts. + var EventReceiver = function() { + this.gotValue = false; + this.gotChildAdded = false; + }; + EventReceiver.prototype.onValue = function() { + this.gotValue = true; + }; + EventReceiver.prototype.onChildAdded = function() { + this.gotChildAdded = true; + }; + + it('Can create basic queries.', function() { + var path = (getRandomNode() as Reference); + + path.limitToLast(10); + path.startAt('199').limitToFirst(10); + path.startAt('199', 'test').limitToFirst(10); + path.endAt('199').limitToLast(1); + path.startAt('50', 'test').endAt('100', 'tree'); + path.startAt('4').endAt('10'); + path.startAt().limitToFirst(10); + path.endAt().limitToLast(10); + path.orderByKey().startAt('foo'); + path.orderByKey().endAt('foo'); + path.orderByKey().equalTo('foo'); + path.orderByChild("child"); + path.orderByChild("child/deep/path"); + path.orderByValue(); + path.orderByPriority(); + }); + + it('Exposes database as read-only property', function() { + var path = (getRandomNode() as Reference); + var child = path.child('child'); + + var db = path.database; + var dbChild = child.database; + + expect(db).to.equal(dbChild); + /** + * TS throws an error here (as is expected) + * casting to any to allow the code to run + */ + expect(() => (path as any).database = "can't overwrite").to.throw(); + expect(path.database).to.equal(db); + }); + + it('Invalid queries throw', function() { + var path = (getRandomNode() as Reference); + + /** + * Because we are testing invalid queries, I am casting + * to `any` to avoid the typechecking error. This can + * occur when a user uses the SDK through a pure JS + * client, rather than typescript + */ + expect(function() { (path as any).limitToLast(); }).to.throw(); + expect(function() { (path as any).limitToLast('100'); }).to.throw(); + expect(function() { (path as any).limitToLast({ x: 5 }); }).to.throw(); + expect(function() { path.limitToLast(100).limitToLast(100); }).to.throw(); + expect(function() { path.limitToLast(100).limitToFirst(100); }).to.throw(); + expect(function() { path.limitToLast(100).limitToLast(100); }).to.throw(); + expect(function() { path.limitToFirst(100).limitToLast(100); }).to.throw(); + expect(function() { path.limitToFirst(100).limitToFirst(100); }).to.throw(); + expect(function() { path.limitToFirst(100).limitToLast(100); }).to.throw(); + expect(function() { path.limitToLast(100).limitToLast(100); }).to.throw(); + expect(function() { path.limitToLast(100).limitToFirst(100); }).to.throw(); + expect(function() { path.limitToLast(100).limitToLast(100); }).to.throw(); + expect(function() { path.orderByPriority().orderByPriority(); }).to.throw(); + expect(function() { path.orderByPriority().orderByKey(); }).to.throw(); + expect(function() { path.orderByPriority().orderByChild('foo'); }).to.throw(); + expect(function() { path.orderByPriority().startAt(true); }).to.throw(); + expect(function() { path.orderByPriority().endAt(false); }).to.throw(); + expect(function() { path.orderByPriority().equalTo(true); }).to.throw(); + expect(function() { path.orderByKey().orderByPriority(); }).to.throw(); + expect(function() { path.orderByKey().orderByKey(); }).to.throw(); + expect(function() { path.orderByKey().orderByChild('foo'); }).to.throw(); + expect(function() { path.orderByChild('foo').orderByPriority(); }).to.throw(); + expect(function() { path.orderByChild('foo').orderByKey(); }).to.throw(); + expect(function() { path.orderByChild('foo').orderByChild('foo'); }).to.throw(); + expect(function() { (path as any).orderByChild('foo').startAt({a: 1}); }).to.throw(); + expect(function() { (path as any).orderByChild('foo').endAt({a: 1}); }).to.throw(); + expect(function() { (path as any).orderByChild('foo').equalTo({a: 1}); }).to.throw(); + expect(function() { path.startAt('foo').startAt('foo')}).to.throw(); + expect(function() { path.startAt('foo').equalTo('foo')}).to.throw(); + expect(function() { path.endAt('foo').endAt('foo')}).to.throw(); + expect(function() { path.endAt('foo').equalTo('foo')}).to.throw(); + expect(function() { path.equalTo('foo').startAt('foo')}).to.throw(); + expect(function() { path.equalTo('foo').endAt('foo')}).to.throw(); + expect(function() { path.equalTo('foo').equalTo('foo')}).to.throw(); + expect(function() { path.orderByKey().startAt('foo', 'foo')}).to.throw(); + expect(function() { path.orderByKey().endAt('foo', 'foo')}).to.throw(); + expect(function() { path.orderByKey().equalTo('foo', 'foo')}).to.throw(); + expect(function() { path.orderByKey().startAt(1)}).to.throw(); + expect(function() { path.orderByKey().startAt(true)}).to.throw(); + expect(function() { path.orderByKey().startAt(null)}).to.throw(); + expect(function() { path.orderByKey().endAt(1)}).to.throw(); + expect(function() { path.orderByKey().endAt(true)}).to.throw(); + expect(function() { path.orderByKey().endAt(null)}).to.throw(); + expect(function() { path.orderByKey().equalTo(1)}).to.throw(); + expect(function() { path.orderByKey().equalTo(true)}).to.throw(); + expect(function() { path.orderByKey().equalTo(null)}).to.throw(); + expect(function() { path.startAt('foo', 'foo').orderByKey()}).to.throw(); + expect(function() { path.endAt('foo', 'foo').orderByKey()}).to.throw(); + expect(function() { path.equalTo('foo', 'foo').orderByKey()}).to.throw(); + expect(function() { path.startAt(1).orderByKey()}).to.throw(); + expect(function() { path.startAt(true).orderByKey()}).to.throw(); + expect(function() { path.endAt(1).orderByKey()}).to.throw(); + expect(function() { path.endAt(true).orderByKey()}).to.throw(); + }); + + it('can produce a valid ref', function() { + var path = (getRandomNode() as Reference); + + var query = path.limitToLast(1); + var ref = query.ref; + + expect(ref.toString()).to.equal(path.toString()); + }); + + it('Passing invalidKeys to startAt / endAt throws.', function() { + var f = (getRandomNode() as Reference); + var badKeys = ['.test', 'test.', 'fo$o', '[what', 'ever]', 'ha#sh', '/thing', 'th/ing', 'thing/']; + // Changed from basic array iteration to avoid closure issues accessing mutable state + _.each(badKeys, function(badKey) { + expect(function() { f.startAt(null, badKey); }).to.throw(); + expect(function() { f.endAt(null, badKey); }).to.throw(); + }); + }); + + it('Passing invalid paths to orderBy throws', function() { + var ref = (getRandomNode() as Reference); + expect(function() { ref.orderByChild('$child/foo'); }).to.throw(); + expect(function() { ref.orderByChild('$key'); }).to.throw(); + expect(function() { ref.orderByChild('$priority'); }).to.throw(); + }); + + it('Query.queryIdentifier works.', function() { + var path = (getRandomNode() as Reference); + var queryId = function(query) { + return query.queryIdentifier(query); + }; + + expect(queryId(path)).to.equal('default'); + + expect(queryId(path.startAt('pri', 'name'))) + .to.equal('{"sn":"name","sp":"pri"}'); + expect(queryId(path.startAt('spri').endAt('epri'))) + .to.equal('{"ep":"epri","sp":"spri"}'); + expect(queryId(path.startAt('spri', 'sname').endAt('epri', 'ename'))) + .to.equal('{"en":"ename","ep":"epri","sn":"sname","sp":"spri"}'); + expect(queryId(path.startAt('pri').limitToFirst(100))) + .to.equal('{"l":100,"sp":"pri","vf":"l"}'); + expect(queryId(path.startAt('bar').orderByChild('foo'))) + .to.equal('{"i":"foo","sp":"bar"}'); + }); + + it('Passing invalid queries to isEqual throws', function() { + var ref = (getRandomNode() as Reference); + expect(function() { (ref as any).isEqual(); }).to.throw(); + expect(function() { (ref as any).isEqual(''); }).to.throw(); + expect(function() { (ref as any).isEqual('foo'); }).to.throw(); + expect(function() { (ref as any).isEqual({}); }).to.throw(); + expect(function() { (ref as any).isEqual([]); }).to.throw(); + expect(function() { (ref as any).isEqual(0); }).to.throw(); + expect(function() { (ref as any).isEqual(1); }).to.throw(); + expect(function() { (ref as any).isEqual(NaN); }).to.throw(); + expect(function() { ref.isEqual(null); }).to.throw(); + expect(function() { (ref as any).isEqual({a:1}); }).to.throw(); + expect(function() { (ref as any).isEqual(ref, 'extra'); }).to.throw(); + }); + + it('Query.isEqual works.', function() { + var path = (getRandomNode() as Reference); + var rootRef = path.root; + var childRef = rootRef.child('child'); + + // Equivalent refs + expect(path.isEqual(path), 'Query.isEqual - 1').to.be.true; + expect(rootRef.isEqual(rootRef), 'Query.isEqual - 2').to.be.true; + expect(rootRef.isEqual(childRef.parent), 'Query.isEqual - 3').to.be.true; + expect(rootRef.child('child').isEqual(childRef), 'Query.isEqual - 4').to.be.true; + + // Refs with different repos + // var rootRefDifferentRepo = TESTS.getFreshRepo(TEST_ALT_NAMESPACE); + // rootRefDifferentRepo.database.goOffline(); + + // expect(rootRef.isEqual(rootRefDifferentRepo), 'Query.isEqual - 5').to.be.false; + // expect(childRef.isEqual(rootRefDifferentRepo.child('child')), 'Query.isEqual - 6').to.be.false; + + // Refs with different paths + expect(rootRef.isEqual(childRef), 'Query.isEqual - 7').to.be.false; + expect(childRef.isEqual(rootRef.child('otherChild')), 'Query.isEqual - 8').to.be.false; + + var childQueryLast25 = childRef.limitToLast(25); + var childQueryOrderedByKey = childRef.orderByKey(); + var childQueryOrderedByPriority = childRef.orderByPriority(); + var childQueryOrderedByTimestamp = childRef.orderByChild("timestamp"); + var childQueryStartAt1 = childQueryOrderedByTimestamp.startAt(1); + var childQueryStartAt2 = childQueryOrderedByTimestamp.startAt(2); + var childQueryEndAt2 = childQueryOrderedByTimestamp.endAt(2); + var childQueryStartAt1EndAt2 = childQueryOrderedByTimestamp.startAt(1).endAt(2); + + // Equivalent queries + expect(childRef.isEqual(childQueryLast25.ref), 'Query.isEqual - 9').to.be.true; + expect(childQueryLast25.isEqual(childRef.limitToLast(25)), 'Query.isEqual - 10').to.be.true; + expect(childQueryStartAt1EndAt2.isEqual(childQueryOrderedByTimestamp.startAt(1).endAt(2)), 'Query.isEqual - 11').to.be.true; + + // Non-equivalent queries + expect(childQueryLast25.isEqual(childRef), 'Query.isEqual - 12').to.be.false; + expect(childQueryLast25.isEqual(childQueryOrderedByKey), 'Query.isEqual - 13').to.be.false; + expect(childQueryLast25.isEqual(childQueryOrderedByPriority), 'Query.isEqual - 14').to.be.false; + expect(childQueryLast25.isEqual(childQueryOrderedByTimestamp), 'Query.isEqual - 15').to.be.false; + expect(childQueryOrderedByKey.isEqual(childQueryOrderedByPriority), 'Query.isEqual - 16').to.be.false; + expect(childQueryOrderedByKey.isEqual(childQueryOrderedByTimestamp), 'Query.isEqual - 17').to.be.false; + expect(childQueryStartAt1.isEqual(childQueryStartAt2), 'Query.isEqual - 18').to.be.false; + expect(childQueryStartAt1.isEqual(childQueryStartAt1EndAt2), 'Query.isEqual - 19').to.be.false; + expect(childQueryEndAt2.isEqual(childQueryStartAt2), 'Query.isEqual - 20').to.be.false; + expect(childQueryEndAt2.isEqual(childQueryStartAt1EndAt2), 'Query.isEqual - 21').to.be.false; + }); + + it('Query.off can be called on the default query.', function() { + var path = (getRandomNode() as Reference); + var eventFired = false; + + var callback = function() { eventFired = true; }; + path.limitToLast(5).on('value', callback); + + path.set({a: 5, b: 6}); + expect(eventFired).to.be.true; + eventFired = false; + + path.off('value', callback); + path.set({a: 6, b: 5}); + expect(eventFired).to.be.false; + }); + + it('Query.off can be called on the specific query.', function() { + var path = (getRandomNode() as Reference); + var eventFired = false; + + var callback = function() { eventFired = true; }; + path.limitToLast(5).on('value', callback); + + path.set({a: 5, b: 6}); + expect(eventFired).to.be.true; + eventFired = false; + + path.limitToLast(5).off('value', callback); + path.set({a: 6, b: 5}); + expect(eventFired).to.be.false; + }); + + it('Query.off can be called without a callback specified.', function() { + var path = (getRandomNode() as Reference); + var eventFired = false; + + var callback1 = function() { eventFired = true; }; + var callback2 = function() { eventFired = true; }; + path.on('value', callback1); + path.limitToLast(5).on('value', callback2); + + path.set({a: 5, b: 6}); + expect(eventFired).to.be.true; + eventFired = false; + + path.off('value'); + path.set({a: 6, b: 5}); + expect(eventFired).to.be.false; + }); + + it('Query.off can be called without an event type or callback specified.', function() { + var path = (getRandomNode() as Reference); + var eventFired = false; + + var callback1 = function() { eventFired = true; }; + var callback2 = function() { eventFired = true; }; + path.on('value', callback1); + path.limitToLast(5).on('value', callback2); + + path.set({a: 5, b: 6}); + expect(eventFired).to.be.true; + eventFired = false; + + path.off(); + path.set({a: 6, b: 5}); + expect(eventFired).to.be.false; + }); + + it('Query.off respects provided context (for value events).', function() { + var ref = (getRandomNode() as Reference); + + var a = new EventReceiver(), + b = new EventReceiver(); + + ref.on('value', a.onValue, a); + ref.on('value', b.onValue, b); + + ref.set('hello!'); + expect(a.gotValue).to.be.true; + expect(b.gotValue).to.be.true; + a.gotValue = b.gotValue = false; + + // unsubscribe b + ref.off('value', b.onValue, b); + + // Only a should get this event. + ref.set(42); + expect(a.gotValue).to.be.true; + expect(b.gotValue).to.be.false; + + ref.off('value', a.onValue, a); + }); + + it('Query.off respects provided context (for child events).', function() { + var ref = (getRandomNode() as Reference); + + var a = new EventReceiver(), + b = new EventReceiver(); + + ref.on('child_added', a.onChildAdded, a); + ref.on('child_added', b.onChildAdded, b); + + ref.push('hello!'); + expect(a.gotChildAdded).to.be.true; + expect(b.gotChildAdded).to.be.true; + a.gotChildAdded = b.gotChildAdded = false; + + // unsubscribe b. + ref.off('child_added', b.onChildAdded, b); + + // Only a should get this event. + ref.push(42); + expect(a.gotChildAdded).to.be.true; + expect(b.gotChildAdded).to.be.false; + + ref.off('child_added', a.onChildAdded, a); + }); + + it('Query.off with no callback/context removes all callbacks, even with contexts (for value events).', function() { + var ref = (getRandomNode() as Reference); + + var a = new EventReceiver(), + b = new EventReceiver(); + + ref.on('value', a.onValue, a); + ref.on('value', b.onValue, b); + + ref.set('hello!'); + expect(a.gotValue).to.be.true; + expect(b.gotValue).to.be.true; + a.gotValue = b.gotValue = false; + + // unsubscribe value events. + ref.off('value'); + + // Should get no events. + ref.set(42); + expect(a.gotValue).to.be.false; + expect(b.gotValue).to.be.false; + }); + + it('Query.off with no callback/context removes all callbacks, even with contexts (for child events).', function() { + var ref = (getRandomNode() as Reference); + + var a = new EventReceiver(), + b = new EventReceiver(); + + ref.on('child_added', a.onChildAdded, a); + ref.on('child_added', b.onChildAdded, b); + + ref.push('hello!'); + expect(a.gotChildAdded).to.be.true; + expect(b.gotChildAdded).to.be.true; + a.gotChildAdded = b.gotChildAdded = false; + + // unsubscribe child_added. + ref.off('child_added'); + + // Should get no events. + ref.push(42); + expect(a.gotChildAdded).to.be.false; + expect(b.gotChildAdded).to.be.false; + }); + + it('Query.off with no event type / callback removes all callbacks (even those with contexts).', function() { + var ref = (getRandomNode() as Reference); + + var a = new EventReceiver(), + b = new EventReceiver(); + + ref.on('value', a.onValue, a); + ref.on('value', b.onValue, b); + ref.on('child_added', a.onChildAdded, a); + ref.on('child_added', b.onChildAdded, b); + + ref.set(null); + ref.push('hello!'); + expect(a.gotChildAdded).to.be.true; + expect(a.gotValue).to.be.true; + expect(b.gotChildAdded).to.be.true; + expect(b.gotValue).to.be.true; + a.gotValue = b.gotValue = a.gotChildAdded = b.gotChildAdded = false; + + // unsubscribe all events. + ref.off(); + + // We should get no events. + ref.push(42); + expect(a.gotChildAdded).to.be.false; + expect(b.gotChildAdded).to.be.false; + expect(a.gotValue).to.be.false; + expect(b.gotValue).to.be.false; + }); + + it('Set a limit of 5, add a bunch of nodes, ensure only last 5 items are kept.', function() { + var node = (getRandomNode() as Reference); + var snap = null; + node.limitToLast(5).on('value', function(s) { snap = s; }); + + node.set({}); + for (var i = 0; i < 10; i++) { + node.push().set(i); + } + + var expected = 5; + snap.forEach(function(child) { + expect(child.val()).to.equal(expected); + expected++; + }); + + expect(expected).to.equal(10); + }); + + it('Set a limit of 5, add a bunch of nodes, ensure only last 5 items are sent from server.', async function() { + var node = (getRandomNode() as Reference); + await node.set({}); + + const pushPromises = []; + + for (let i = 0; i < 10; i++) { + let promise = node.push().set(i); + pushPromises.push(promise); + } + + await Promise.all(pushPromises); + + const ea = EventAccumulatorFactory.waitsForCount(1); + + node.limitToLast(5).on('value', snap => { + ea.addEvent(snap); + }); + + const [snap] = await ea.promise; + + let expected = 5; + + snap.forEach(function(child) { + expect(child.val()).to.equal(expected); + expected++; + }); + + expect(expected).to.equal(10); + }); + + it('Set various limits, ensure resulting data is correct.', async function() { + var node = (getRandomNode() as Reference); + + await node.set({a: 1, b: 2, c: 3}); + + const tasks: TaskList = [ + [node.limitToLast(1), {c: 3}],, + [node.endAt().limitToLast(1), {c: 3}], + [node.limitToLast(2), {b: 2, c: 3}], + [node.limitToLast(3), {a: 1, b: 2, c: 3}], + [node.limitToLast(4), {a: 1, b: 2, c: 3}] + ]; + + return Promise.all(tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + })); + }); + + it('Set various limits with a startAt name, ensure resulting data is correct.', async function() { + var node = (getRandomNode() as Reference); + + await node.set({a: 1, b: 2, c: 3}); + + const tasks: TaskList = [ + [node.startAt().limitToFirst(1), {a: 1}], + [node.startAt(null, 'c').limitToFirst(1), {c: 3}], + [node.startAt(null, 'b').limitToFirst(1), {b: 2}], + [node.startAt(null, 'b').limitToFirst(2), {b: 2, c: 3}], + [node.startAt(null, 'b').limitToFirst(3), {b: 2, c: 3}], + [node.startAt(null, 'b').limitToLast(1), {c: 3}], + [node.startAt(null, 'b').limitToLast(1), {c: 3}], + [node.startAt(null, 'b').limitToLast(2), {b: 2, c: 3}], + [node.startAt(null, 'b').limitToLast(3), {b: 2, c: 3}], + [node.limitToFirst(1).startAt(null, 'c'), {c: 3}], + [node.limitToFirst(1).startAt(null, 'b'), {b: 2}], + [node.limitToFirst(2).startAt(null, 'b'), {b: 2, c: 3}], + [node.limitToFirst(3).startAt(null, 'b'), {b: 2, c: 3}], + [node.limitToLast(1).startAt(null, 'b'), {c: 3}], + [node.limitToLast(1).startAt(null, 'b'), {c: 3}], + [node.limitToLast(2).startAt(null, 'b'), {b: 2, c: 3}], + [node.limitToLast(3).startAt(null, 'b'), {b: 2, c: 3}], + ]; + + return Promise.all(tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + })); + }); + + it('Set various limits with a endAt name, ensure resulting data is correct.', async function() { + var node = (getRandomNode() as Reference); + + await node.set({a: 1, b: 2, c: 3}); + + const tasks: TaskList = [ + [node.endAt().limitToFirst(1), {a: 1}], + [node.endAt(null, 'c').limitToFirst(1), {a: 1}], + [node.endAt(null, 'b').limitToFirst(1), {a: 1}], + [node.endAt(null, 'b').limitToFirst(2), {a: 1, b: 2}], + [node.endAt(null, 'b').limitToFirst(3), {a: 1, b: 2}], + [node.endAt(null, 'c').limitToLast(1), {c: 3}], + [node.endAt(null, 'b').limitToLast(1), {b: 2}], + [node.endAt(null, 'b').limitToLast(2), {a: 1, b: 2}], + [node.endAt(null, 'b').limitToLast(3), {a: 1, b: 2}], + [node.limitToFirst(1).endAt(null, 'c'), {a: 1}], + [node.limitToFirst(1).endAt(null, 'b'), {a: 1}], + [node.limitToFirst(2).endAt(null, 'b'), {a: 1, b: 2}], + [node.limitToFirst(3).endAt(null, 'b'), {a: 1, b: 2}], + [node.limitToLast(1).endAt(null, 'c'), {c: 3}], + [node.limitToLast(1).endAt(null, 'b'), {b: 2}], + [node.limitToLast(2).endAt(null, 'b'), {a: 1, b: 2}], + [node.limitToLast(3).endAt(null, 'b'), {a: 1, b: 2}], + ]; + + return Promise.all(tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + })); + }); + + it('Set various limits with a startAt name, ensure resulting data is correct from the server.', async function() { + var node = (getRandomNode() as Reference); + + await node.set({a: 1, b: 2, c: 3}); + + const tasks: TaskList = [ + [node.startAt().limitToFirst(1), {a: 1}], + [node.startAt(null, 'c').limitToFirst(1), {c: 3}], + [node.startAt(null, 'b').limitToFirst(1), {b: 2}], + // NOTE: technically there is a race condition here. The limitToFirst(1) query will return a single value, which will be + // raised for the limitToFirst(2) callback as well, if it exists already. However, once the server gets the limitToFirst(2) + // query, it will send more data and the correct state will be returned. + [node.startAt(null, 'b').limitToFirst(2), {b: 2, c: 3}], + [node.startAt(null, 'b').limitToFirst(3), {b: 2, c: 3}], + ]; + + return Promise.all(tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + })); + }); + + it('Set limit, ensure child_removed and child_added events are fired when limit is hit.', function() { + var node = (getRandomNode() as Reference); + var added = '', removed = ''; + node.limitToLast(2).on('child_added', function(snap) { added += snap.key + ' '}); + node.limitToLast(2).on('child_removed', function(snap) { removed += snap.key + ' '}); + node.set({a: 1, b: 2, c: 3}); + + expect(added).to.equal('b c '); + expect(removed).to.equal(''); + + added = ''; + node.child('d').set(4); + expect(added).to.equal('d '); + expect(removed).to.equal('b '); + }); + + it('Set limit, ensure child_removed and child_added events are fired when limit is hit, using server data', async function() { + var node = (getRandomNode() as Reference); + + await node.set({a: 1, b: 2, c: 3}); + + const ea = EventAccumulatorFactory.waitsForCount(2); + + var added = '', removed = ''; + node.limitToLast(2).on('child_added', function(snap) { + added += snap.key + ' '; + ea.addEvent(); + }); + node.limitToLast(2).on('child_removed', function(snap) { + removed += snap.key + ' ' + }); + + await ea.promise; + + expect(added).to.equal('b c '); + expect(removed).to.equal(''); + + added = ''; + await node.child('d').set(4); + + expect(added).to.equal('d '); + expect(removed).to.equal('b '); + }); + + it('Set start and limit, ensure child_removed and child_added events are fired when limit is hit.', function() { + var node = (getRandomNode() as Reference); + + var added = '', removed = ''; + node.startAt(null, 'a').limitToFirst(2).on('child_added', function(snap) { added += snap.key + ' '}); + node.startAt(null, 'a').limitToFirst(2).on('child_removed', function(snap) { removed += snap.key + ' '}); + node.set({a: 1, b: 2, c: 3}); + expect(added).to.equal('a b '); + expect(removed).to.equal(''); + + added = ''; + node.child('aa').set(4); + expect(added).to.equal('aa '); + expect(removed).to.equal('b '); + }); + + it('Set start and limit, ensure child_removed and child_added events are fired when limit is hit, using server data', async function() { + var node = getRandomNode() + + await node.set({a: 1, b: 2, c: 3}); + const ea = EventAccumulatorFactory.waitsForCount(2); + + var added = '', removed = ''; + node.startAt(null, 'a').limitToFirst(2).on('child_added', function(snap) { + added += snap.key + ' '; + ea.addEvent(); + }); + node.startAt(null, 'a').limitToFirst(2).on('child_removed', function(snap) { + removed += snap.key + ' ' + }); + + await ea.promise; + + expect(added).to.equal('a b '); + expect(removed).to.equal(''); + + added = ''; + await node.child('aa').set(4); + + expect(added).to.equal('aa '); + expect(removed).to.equal('b '); + }); + + it("Set start and limit, ensure child_added events are fired when limit isn't hit yet.", function() { + var node = (getRandomNode() as Reference); + + var added = '', removed = ''; + node.startAt(null, 'a').limitToFirst(2).on('child_added', function(snap) { added += snap.key + ' '}); + node.startAt(null, 'a').limitToFirst(2).on('child_removed', function(snap) { removed += snap.key + ' '}); + node.set({c: 3}); + expect(added).to.equal('c '); + expect(removed).to.equal(''); + + added = ''; + node.child('b').set(4); + expect(added).to.equal('b '); + expect(removed).to.equal(''); + }); + + it("Set start and limit, ensure child_added events are fired when limit isn't hit yet, using server data", async function() { + var node = (getRandomNode() as Reference); + + await node.set({c: 3}); + + const ea = EventAccumulatorFactory.waitsForCount(1); + + let added = ''; + let removed = ''; + node.startAt(null, 'a').limitToFirst(2).on('child_added', function(snap) { + added += snap.key + ' ' + ea.addEvent(); + }); + node.startAt(null, 'a').limitToFirst(2).on('child_removed', function(snap) { + removed += snap.key + ' ' + }); + + await ea.promise; + + expect(added).to.equal('c '); + expect(removed).to.equal(''); + + added = ''; + await node.child('b').set(4); + + expect(added).to.equal('b '); + expect(removed).to.equal(''); + }); + + it('Set a limit, ensure child_removed and child_added events are fired when limit is satisfied and you remove an item.', async function() { + var node = (getRandomNode() as Reference); + const ea = EventAccumulatorFactory.waitsForCount(1); + + var added = '', removed = ''; + node.limitToLast(2).on('child_added', function(snap) { + added += snap.key + ' ' + ea.addEvent(); + }); + node.limitToLast(2).on('child_removed', function(snap) { removed += snap.key + ' '}); + node.set({a: 1, b: 2, c: 3}); + expect(added).to.equal('b c '); + expect(removed).to.equal(''); + + added = ''; + node.child('b').remove(); + expect(removed).to.equal('b '); + + await ea.promise; + }); + + it('Set a limit, ensure child_removed and child_added events are fired when limit is satisfied and you remove an item. Using server data', async function() { + var node = (getRandomNode() as Reference); + + await node.set({a: 1, b: 2, c: 3}); + + let ea = EventAccumulatorFactory.waitsForCount(2); + var added = '', removed = ''; + node.limitToLast(2).on('child_added', function(snap) { + added += snap.key + ' '; + ea.addEvent(); + }); + node.limitToLast(2).on('child_removed', function(snap) { + removed += snap.key + ' ' + }); + + await ea.promise; + + expect(added).to.equal('b c '); + expect(removed).to.equal(''); + + // We are going to wait for one more event before closing + ea = EventAccumulatorFactory.waitsForCount(1); + added = ''; + await node.child('b').remove(); + + expect(removed).to.equal('b '); + + await ea.promise; + expect(added).to.equal('a '); + }); + + it('Set a limit, ensure child_removed events are fired when limit is satisfied, you remove an item, and there are no more.', function() { + var node = (getRandomNode() as Reference); + + var added = '', removed = ''; + node.limitToLast(2).on('child_added', function(snap) { added += snap.key + ' '}); + node.limitToLast(2).on('child_removed', function(snap) { removed += snap.key + ' '}); + node.set({b: 2, c: 3}); + expect(added).to.equal('b c '); + expect(removed).to.equal(''); + + added = ''; + node.child('b').remove(); + expect(added).to.equal(''); + expect(removed).to.equal('b '); + node.child('c').remove(); + expect(removed).to.equal('b c '); + }); + + it('Set a limit, ensure child_removed events are fired when limit is satisfied, you remove an item, and there are no more. Using server data', async function() { + var node = (getRandomNode() as Reference); + const ea = EventAccumulatorFactory.waitsForCount(2); + let added = ''; + let removed = ''; + await node.set({b: 2, c: 3}); + + node.limitToLast(2).on('child_added', function(snap) { + added += snap.key + ' '; + ea.addEvent(); + }); + node.limitToLast(2).on('child_removed', function(snap) { + removed += snap.key + ' ' + }); + + await ea.promise; + + expect(added).to.equal('b c '); + expect(removed).to.equal(''); + + added = ''; + + await node.child('b').remove(); + + expect(added).to.equal(''); + expect(removed).to.equal('b '); + }); + + it('Ensure startAt / endAt with priority works.', async function() { + var node = (getRandomNode() as Reference); + + const tasks: TaskList = [ + [node.startAt('w').endAt('y'), {b: 2, c: 3, d: 4}], + [node.startAt('w').endAt('w'), {d: 4 }], + [node.startAt('a').endAt('c'), null], + ] + + await node.set({ + a: {'.value': 1, '.priority': 'z'}, + b: {'.value': 2, '.priority': 'y'}, + c: {'.value': 3, '.priority': 'x'}, + d: {'.value': 4, '.priority': 'w'} + }); + + return Promise.all(tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + })); + }); + + it('Ensure startAt / endAt with priority work with server data.', async function() { + var node = (getRandomNode() as Reference); + + await node.set({ + a: {'.value': 1, '.priority': 'z'}, + b: {'.value': 2, '.priority': 'y'}, + c: {'.value': 3, '.priority': 'x'}, + d: {'.value': 4, '.priority': 'w'} + }); + + const tasks: TaskList = [ + [node.startAt('w').endAt('y'), {b: 2, c: 3, d: 4}], + [node.startAt('w').endAt('w'), {d: 4 }], + [node.startAt('a').endAt('c'), null], + ]; + + return Promise.all(tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + })); + }); + + it('Ensure startAt / endAt with priority and name works.', async function() { + var node = (getRandomNode() as Reference); + + await node.set({ + a: {'.value': 1, '.priority': 1}, + b: {'.value': 2, '.priority': 1}, + c: {'.value': 3, '.priority': 2}, + d: {'.value': 4, '.priority': 2} + }); + + const tasks: TaskList = [ + [node.startAt(1, 'a').endAt(2, 'd'), {a: 1, b: 2, c: 3, d: 4}], + [node.startAt(1, 'b').endAt(2, 'c'), {b: 2, c: 3}], + [node.startAt(1, 'c').endAt(2), {c: 3, d: 4}], + ]; + + return Promise.all(tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + })); + }); + + it('Ensure startAt / endAt with priority and name work with server data', async function() { + var node = (getRandomNode() as Reference); + + await node.set({ + a: {'.value': 1, '.priority': 1}, + b: {'.value': 2, '.priority': 1}, + c: {'.value': 3, '.priority': 2}, + d: {'.value': 4, '.priority': 2} + }); + const tasks: TaskList = [ + [node.startAt(1, 'a').endAt(2, 'd'), {a: 1, b: 2, c: 3, d: 4}], + [node.startAt(1, 'b').endAt(2, 'c'), {b: 2, c: 3}], + [node.startAt(1, 'c').endAt(2), {c: 3, d: 4}], + ]; + return Promise.all(tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + })); + }); + + it('Ensure startAt / endAt with priority and name works (2).', function() { + var node = (getRandomNode() as Reference); + + const tasks: TaskList = [ + [node.startAt(1, 'c').endAt(2, 'b'), {a: 1, b: 2, c: 3, d: 4}], + [node.startAt(1, 'd').endAt(2, 'a'), {d: 4, a: 1}], + [node.startAt(1, 'e').endAt(2), {a: 1, b: 2}], + ] + + node.set({ + c: {'.value': 3, '.priority': 1}, + d: {'.value': 4, '.priority': 1}, + a: {'.value': 1, '.priority': 2}, + b: {'.value': 2, '.priority': 2} + }); + + return Promise.all(tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + })); + }); + + it('Ensure startAt / endAt with priority and name works (2). With server data', async function() { + var node = (getRandomNode() as Reference); + + await node.set({ + c: {'.value': 3, '.priority': 1}, + d: {'.value': 4, '.priority': 1}, + a: {'.value': 1, '.priority': 2}, + b: {'.value': 2, '.priority': 2} + }); + + const tasks: TaskList = [ + [node.startAt(1, 'c').endAt(2, 'b'), {a: 1, b: 2, c: 3, d: 4}], + [node.startAt(1, 'd').endAt(2, 'a'), {d: 4, a: 1}], + [node.startAt(1, 'e').endAt(2), {a: 1, b: 2}], + ]; + + return Promise.all(tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + })); + }); + + it('Set a limit, add some nodes, ensure prevName works correctly.', function() { + var node = (getRandomNode() as Reference); + + var added = ''; + node.limitToLast(2).on('child_added', function(snap, prevName) { + added += snap.key + ' ' + prevName + ', '; + }); + + node.child('a').set(1); + expect(added).to.equal('a null, '); + + added = ''; + node.child('c').set(3); + expect(added).to.equal('c a, '); + + added = ''; + node.child('b').set(2); + expect(added).to.equal('b null, '); + + added = ''; + node.child('d').set(4); + expect(added).to.equal('d c, '); + }); + + it('Set a limit, add some nodes, ensure prevName works correctly. With server data', async function() { + var node = (getRandomNode() as Reference); + + let added = ''; + await node.child('a').set(1); + + const ea = EventAccumulatorFactory.waitsForCount(1); + node.limitToLast(2).on('child_added', function(snap, prevName) { + added += snap.key + ' ' + prevName + ', '; + ea.addEvent(); + }); + + await ea.promise; + + expect(added).to.equal('a null, '); + + added = ''; + await node.child('c').set(3); + + expect(added).to.equal('c a, '); + + added = ''; + await node.child('b').set(2); + + expect(added).to.equal('b null, '); + + added = ''; + await node.child('d').set(4); + + expect(added).to.equal('d c, '); + }); + + it('Set a limit, move some nodes, ensure prevName works correctly.', function() { + var node = (getRandomNode() as Reference); + var moved = ''; + node.limitToLast(2).on('child_moved', function(snap, prevName) { + moved += snap.key + ' ' + prevName + ', '; + }); + + node.child('a').setWithPriority('a', 10); + node.child('b').setWithPriority('b', 20); + node.child('c').setWithPriority('c', 30); + node.child('d').setWithPriority('d', 40); + + node.child('c').setPriority(50); + expect(moved).to.equal('c d, '); + + moved = ''; + node.child('c').setPriority(35); + expect(moved).to.equal('c null, '); + + moved = ''; + node.child('b').setPriority(33); + expect(moved).to.equal(''); + }); + + it('Set a limit, move some nodes, ensure prevName works correctly, with server data', async function() { + var node = (getRandomNode() as Reference); + var moved = ''; + + node.child('a').setWithPriority('a', 10); + node.child('b').setWithPriority('b', 20); + node.child('c').setWithPriority('c', 30); + await node.child('d').setWithPriority('d', 40); + + node.limitToLast(2).on('child_moved', async function(snap, prevName) { + moved += snap.key + ' ' + prevName + ', '; + }); + // Need to load the data before the set so we'll see the move + await node.limitToLast(2).once('value'); + + await node.child('c').setPriority(50); + + expect(moved).to.equal('c d, '); + + moved = ''; + await node.child('c').setPriority(35); + + expect(moved).to.equal('c null, '); + moved = ''; + await node.child('b').setPriority(33); + + expect(moved).to.equal(''); + }); + + it('Numeric priorities: Set a limit, move some nodes, ensure prevName works correctly.', function() { + var node = (getRandomNode() as Reference); + + var moved = ''; + node.limitToLast(2).on('child_moved', function(snap, prevName) { + moved += snap.key + ' ' + prevName + ', '; + }); + + node.child('a').setWithPriority('a', 1); + node.child('b').setWithPriority('b', 2); + node.child('c').setWithPriority('c', 3); + node.child('d').setWithPriority('d', 4); + + node.child('c').setPriority(10); + expect(moved).to.equal('c d, '); + }); + + it('Numeric priorities: Set a limit, move some nodes, ensure prevName works correctly. With server data', async function() { + var node = (getRandomNode() as Reference); + let moved = ''; + + node.child('a').setWithPriority('a', 1); + node.child('b').setWithPriority('b', 2); + node.child('c').setWithPriority('c', 3); + await node.child('d').setWithPriority('d', 4); + + node.limitToLast(2).on('child_moved', function(snap, prevName) { + moved += snap.key + ' ' + prevName + ', '; + }); + // Need to load the data before the set so we'll see the move + await node.limitToLast(2).once('value'); + + await node.child('c').setPriority(10); + + expect(moved).to.equal('c d, '); + }); + + it('Set a limit, add a bunch of nodes, ensure local events are correct.', function() { + var node = (getRandomNode() as Reference); + node.set({}); + var eventHistory = ''; + + node.limitToLast(2).on('child_added', function(snap) { + eventHistory = eventHistory + snap.val() + ' added, '; + }); + node.limitToLast(2).on('child_removed', function(snap) { + eventHistory = eventHistory + snap.val() + ' removed, '; + }); + + for (var i = 0; i < 5; i++) { + var n = node.push(); + n.set(i); + } + + expect(eventHistory).to.equal('0 added, 1 added, 0 removed, 2 added, 1 removed, 3 added, 2 removed, 4 added, '); + }); + + it('Set a limit, add a bunch of nodes, ensure remote events are correct.', async function() { + var nodePair = getRandomNode(2); + var writeNode = nodePair[0]; + var readNode = nodePair[1]; + const ea = new EventAccumulator(() => { + try { + expect(eventHistory).to.equal('3 added, 4 added, '); + return true; + } catch(err) { + return false; + } + }); + var eventHistory = ''; + + readNode.limitToLast(2).on('child_added', function(snap) { + eventHistory = eventHistory + snap.val() + ' added, '; + ea.addEvent(); + }); + readNode.limitToLast(2).on('child_removed', function(snap) { + eventHistory = eventHistory.replace(snap.val() + ' added, ', ''); + /** + * This test expects this code NOT to fire, so by adding this + * I trigger the resolve early if it happens to fire and fail + * the expect at the end + */ + ea.addEvent(); + }); + + const promises = []; + for (var i = 0; i < 5; i++) { + var n = writeNode.push(); + n.set(i); + } + + await ea.promise; + }); + + it('Ensure on() returns callback function.', function() { + var node = (getRandomNode() as Reference); + var callback = function() { }; + var ret = node.on('value', callback); + expect(ret).to.equal(callback); + }); + + it("Limit on unsynced node fires 'value'.", function(done) { + var f = (getRandomNode() as Reference); + f.limitToLast(1).on('value', function() { + done(); + }); + }); + + it('Filtering to only null priorities works.', async function() { + var f = (getRandomNode() as Reference); + + const ea = EventAccumulatorFactory.waitsForCount(1); + f.root.child('.info/connected').on('value', function(snap) { + ea.addEvent(); + }); + + await ea.promise; + + f.set({ + a: {'.priority': null, '.value': 0}, + b: {'.priority': null, '.value': 1}, + c: {'.priority': '2', '.value': 2}, + d: {'.priority': 3, '.value': 3}, + e: {'.priority': 'hi', '.value': 4} + }); + + const snapAcc = EventAccumulatorFactory.waitsForCount(1); + f.startAt(null).endAt(null).on('value', snap => { + snapAcc.addEvent(snap.val()); + }); + + const [val] = await snapAcc.promise; + expect(val).to.deep.equal({a: 0, b: 1}); + }); + + it('null priorities included in endAt(2).', async function() { + var f = (getRandomNode() as Reference); + + f.set({ + a: {'.priority': null, '.value': 0}, + b: {'.priority': null, '.value': 1}, + c: {'.priority': 2, '.value': 2}, + d: {'.priority': 3, '.value': 3}, + e: {'.priority': 'hi', '.value': 4} + }); + + const ea = EventAccumulatorFactory.waitsForCount(1); + f.endAt(2).on('value', snap => { + ea.addEvent(snap.val()); + }); + + const [val] = await ea.promise; + expect(val).to.deep.equal({a: 0, b: 1, c: 2}); + }); + + it('null priorities not included in startAt(2).', async function() { + var f = (getRandomNode() as Reference); + + f.set({ + a: {'.priority': null, '.value': 0}, + b: {'.priority': null, '.value': 1}, + c: {'.priority': 2, '.value': 2}, + d: {'.priority': 3, '.value': 3}, + e: {'.priority': 'hi', '.value': 4} + }); + + const ea = EventAccumulatorFactory.waitsForCount(1); + + f.startAt(2).on('value', snap => { + ea.addEvent(snap.val()); + }); + + const [val] = await ea.promise; + expect(val).to.deep.equal({c: 2, d: 3, e: 4}); + }); + + function dumpListens(node: Query) { + var listens = node.repo.persistentConnection_.listens_; + var nodePath = getPath(node); + var listenPaths = []; + for (var path in listens) { + if (path.substring(0, nodePath.length) === nodePath) { + listenPaths.push(path); + } + } + + listenPaths.sort(); + var dumpPieces = []; + for (var i = 0; i < listenPaths.length; i++) { + + var queryIds = []; + for (var queryId in listens[listenPaths[i]]) { + queryIds.push(queryId); + } + queryIds.sort(); + if (queryIds.length > 0) { + dumpPieces.push(listenPaths[i].substring(nodePath.length) + ':' + queryIds.join(',')); + } + } + + return dumpPieces.join(';'); + } + + it('Dedupe listens: listen on parent.', function() { + var node = (getRandomNode() as Reference); + expect(dumpListens(node)).to.equal(''); + + var aOn = node.child('a').on('value', function() { }); + expect(dumpListens(node)).to.equal('/a:default'); + + var rootOn = node.on('value', function() {}); + expect(dumpListens(node)).to.equal(':default'); + + node.off('value', rootOn); + expect(dumpListens(node)).to.equal('/a:default'); + + node.child('a').off('value', aOn); + expect(dumpListens(node)).to.equal(''); + }); + + it('Dedupe listens: listen on grandchild.', function() { + var node = (getRandomNode() as Reference); + + var rootOn = node.on('value', function() {}); + expect(dumpListens(node)).to.equal(':default'); + + var aaOn = node.child('a/aa').on('value', function() { }); + expect(dumpListens(node)).to.equal(':default'); + + node.off('value', rootOn); + node.child('a/aa').off('value', aaOn); + expect(dumpListens(node)).to.equal(''); + }); + + it('Dedupe listens: listen on grandparent of two children.', function() { + var node = (getRandomNode() as Reference); + expect(dumpListens(node)).to.equal(''); + + var aaOn = node.child('a/aa').on('value', function() { }); + expect(dumpListens(node)).to.equal('/a/aa:default'); + + var bbOn = node.child('a/bb').on('value', function() { }); + expect(dumpListens(node)).to.equal('/a/aa:default;/a/bb:default'); + + var rootOn = node.on('value', function() {}); + expect(dumpListens(node)).to.equal(':default'); + + node.off('value', rootOn); + expect(dumpListens(node)).to.equal('/a/aa:default;/a/bb:default'); + + node.child('a/aa').off('value', aaOn); + expect(dumpListens(node)).to.equal('/a/bb:default'); + + node.child('a/bb').off('value', bbOn); + expect(dumpListens(node)).to.equal(''); + }); + + it('Dedupe queried listens: multiple queried listens; no dupes', function() { + var node = (getRandomNode() as Reference); + expect(dumpListens(node)).to.equal(''); + + var aLim1On = node.child('a').limitToLast(1).on('value', function() { }); + expect(dumpListens(node)).to.equal('/a:{"l":1,"vf":"r"}'); + + var rootLim1On = node.limitToLast(1).on('value', function() { }); + expect(dumpListens(node)).to.equal(':{"l":1,"vf":"r"};/a:{"l":1,"vf":"r"}'); + + var aLim5On = node.child('a').limitToLast(5).on('value', function() { }); + expect(dumpListens(node)).to.equal(':{"l":1,"vf":"r"};/a:{"l":1,"vf":"r"},{"l":5,"vf":"r"}'); + + node.limitToLast(1).off('value', rootLim1On); + expect(dumpListens(node)).to.equal('/a:{"l":1,"vf":"r"},{"l":5,"vf":"r"}'); + + node.child('a').limitToLast(1).off('value', aLim1On); + node.child('a').limitToLast(5).off('value', aLim5On); + expect(dumpListens(node)).to.equal(''); + }); + + it('Dedupe queried listens: listen on parent of queried children.', function() { + var node = (getRandomNode() as Reference); + + var aLim1On = node.child('a').limitToLast(1).on('value', function() { }); + expect(dumpListens(node)).to.equal('/a:{"l":1,"vf":"r"}'); + + var bLim1On = node.child('b').limitToLast(1).on('value', function() { }); + expect(dumpListens(node)).to.equal('/a:{"l":1,"vf":"r"};/b:{"l":1,"vf":"r"}'); + + var rootOn = node.on('value', function() { }); + expect(dumpListens(node)).to.equal(':default'); + + // remove in slightly random order. + node.child('a').limitToLast(1).off('value', aLim1On); + expect(dumpListens(node)).to.equal(':default'); + + node.off('value', rootOn); + expect(dumpListens(node)).to.equal('/b:{"l":1,"vf":"r"}'); + + node.child('b').limitToLast(1).off('value', bLim1On); + expect(dumpListens(node)).to.equal(''); + }); + + it('Limit with mix of null and non-null priorities.', function() { + var node = (getRandomNode() as Reference); + + var children = []; + node.limitToLast(5).on('child_added', function(childSnap) { + children.push(childSnap.key); + }); + + node.set({ + 'Vikrum': {'.priority': 1000, 'score': 1000, 'name': 'Vikrum'}, + 'Mike': {'.priority': 500, 'score': 500, 'name': 'Mike'}, + 'Andrew': {'.priority': 50, 'score': 50, 'name': 'Andrew'}, + 'James': {'.priority': 7, 'score': 7, 'name': 'James'}, + 'Sally': {'.priority': -7, 'score': -7, 'name': 'Sally'}, + 'Fred': {'score': 0, 'name': 'Fred'} + }); + + expect(children.join(',')).to.equal('Sally,James,Andrew,Mike,Vikrum'); + }); + + it('Limit with mix of null and non-null priorities using server data', async function() { + var node = getRandomNode(), + done, count; + + var children = []; + await node.set({ + 'Vikrum': {'.priority': 1000, 'score': 1000, 'name': 'Vikrum'}, + 'Mike': {'.priority': 500, 'score': 500, 'name': 'Mike'}, + 'Andrew': {'.priority': 50, 'score': 50, 'name': 'Andrew'}, + 'James': {'.priority': 7, 'score': 7, 'name': 'James'}, + 'Sally': {'.priority': -7, 'score': -7, 'name': 'Sally'}, + 'Fred': {'score': 0, 'name': 'Fred'} + }); + + const ea = EventAccumulatorFactory.waitsForCount(5); + node.limitToLast(5).on('child_added', function(childSnap) { + children.push(childSnap.key); + ea.addEvent(); + }); + + await ea.promise; + + expect(children.join(',')).to.equal('Sally,James,Andrew,Mike,Vikrum'); + }); + + it('.on() with a context works.', function() { + var ref = (getRandomNode() as Reference); + + var ListenerDoohickey = function() { this.snap = null; }; + ListenerDoohickey.prototype.onEvent = function(snap) { + this.snap = snap; + }; + + var l = new ListenerDoohickey(); + ref.on('value', l.onEvent, l); + + ref.set('test'); + expect(l.snap.val()).to.equal('test'); + + ref.off('value', l.onEvent, l); + + // Ensure we don't get any more events. + ref.set('blah'); + expect(l.snap.val()).to.equal('test'); + }); + + it('.once() with a context works.', function() { + var ref = (getRandomNode() as Reference); + + var ListenerDoohickey = function() { this.snap = null; }; + ListenerDoohickey.prototype.onEvent = function(snap) { + this.snap = snap; + }; + + var l = new ListenerDoohickey(); + ref.once('value', l.onEvent, l); + + ref.set('test'); + expect(l.snap.val()).to.equal('test'); + + // Shouldn't get any more events. + ref.set('blah'); + expect(l.snap.val()).to.equal('test'); + }); + + it('handles an update that deletes the entire window in a query', function() { + var ref = (getRandomNode() as Reference); + + var snaps = []; + ref.limitToLast(2).on('value', function(snap) { + snaps.push(snap.val()); + }); + + ref.set({ + a: {'.value': 1, '.priority': 1}, + b: {'.value': 2, '.priority': 2}, + c: {'.value': 3, '.priority': 3} + }); + ref.update({ + b: null, + c: null + }); + + expect(snaps.length).to.equal(2); + expect(snaps[0]).to.deep.equal({b: 2, c: 3}); + // The original set is still outstanding (synchronous API), so we have a full cache to re-window against + expect(snaps[1]).to.deep.equal({a: 1}); + }); + + it('handles an out-of-view query on a child', function() { + var ref = (getRandomNode() as Reference); + + var parent = null; + ref.limitToLast(1).on('value', function(snap) { + parent = snap.val(); + }); + + var child = null; + ref.child('a').on('value', function(snap) { + child = snap.val(); + }); + + ref.set({a: 1, b: 2}); + expect(parent).to.deep.equal({b: 2}); + expect(child).to.equal(1); + + ref.update({c: 3}); + expect(parent).to.deep.equal({c: 3}); + expect(child).to.equal(1); + }); + + it('handles a child query going out of view of the parent', function() { + var ref = (getRandomNode() as Reference); + + var parent = null; + ref.limitToLast(1).on('value', function(snap) { + parent = snap.val(); + }); + + var child = null; + ref.child('a').on('value', function(snap) { + child = snap.val(); + }); + + ref.set({a: 1}); + expect(parent).to.deep.equal({a: 1}); + expect(child).to.equal(1); + ref.child('b').set(2); + expect(parent).to.deep.equal({b: 2}); + expect(child).to.equal(1); + ref.child('b').remove(); + expect(parent).to.deep.equal({a: 1}); + expect(child).to.equal(1); + }); + + it('handles diverging views', function() { + var ref = (getRandomNode() as Reference); + + var c = null; + ref.limitToLast(1).endAt(null, 'c').on('value', function(snap) { + c = snap.val(); + }); + + var d = null; + ref.limitToLast(1).endAt(null, 'd').on('value', function(snap) { + d = snap.val(); + }); + + ref.set({a: 1, b: 2, c: 3}); + expect(c).to.deep.equal({c: 3}); + expect(d).to.deep.equal({c: 3}); + ref.child('d').set(4); + expect(c).to.deep.equal({c: 3}); + expect(d).to.deep.equal({d: 4}); + }); + + it('handles removing a queried element', async function() { + var ref = (getRandomNode() as Reference); + + var val; + const ea = EventAccumulatorFactory.waitsForCount(1); + ref.limitToLast(1).on('child_added', function(snap) { + val = snap.val(); + ea.addEvent(); + }); + + ref.set({a: 1, b: 2}); + expect(val).to.equal(2); + + ref.child('b').remove(); + + await ea.promise; + + expect(val).to.equal(1); + }); + + it('.startAt().limitToFirst(1) works.', function(done) { + var ref = (getRandomNode() as Reference); + ref.set({a: 1, b: 2}); + + var val; + ref.startAt().limitToFirst(1).on('child_added', function(snap) { + val = snap.val(); + if (val === 1) { + done(); + } + }); + }); + + it('.startAt().limitToFirst(1) and then remove first child (case 1664).', async function() { + var ref = (getRandomNode() as Reference); + ref.set({a: 1, b: 2}); + + const ea = EventAccumulatorFactory.waitsForCount(1); + var val; + ref.startAt().limitToFirst(1).on('child_added', function(snap) { + val = snap.val(); + ea.addEvent(); + }); + + await ea.promise; + expect(val).to.equal(1); + + ea.reset(); + ref.child('a').remove(); + + await ea.promise; + expect(val).to.equal(2); + }); + + it('.startAt() with two arguments works properly (case 1169).', function(done) { + var ref = (getRandomNode() as Reference); + const data = { + 'Walker': { + name: 'Walker', + score: 20, + '.priority': 20 + }, + 'Michael': { + name: 'Michael', + score: 100, + '.priority': 100 + } + }; + ref.set(data, function() { + ref.startAt(20, 'Walker').limitToFirst(2).on('value', function(s) { + var childNames = []; + s.forEach(function(node) { childNames.push(node.key); }); + expect(childNames).to.deep.equal(['Walker', 'Michael']); + done(); + }); + }); + }); + + it('handles multiple queries on the same node', async function() { + var ref = (getRandomNode() as Reference); + + await ref.set({ + a: 1, + b: 2, + c: 3, + d: 4, + e: 5, + f: 6 + }); + + const ea = EventAccumulatorFactory.waitsForCount(1); + + var firstListen = false + ref.limitToLast(2).on('value', function(snap) { + // This shouldn't get called twice, we don't update the values here + expect(firstListen).to.be.false; + firstListen = true; + ea.addEvent(); + }); + + await ea.promise; + + // now do consecutive once calls + await ref.limitToLast(1).once('value'); + const snap = await ref.limitToLast(1).once('value'); + var val = snap.val(); + expect(val).to.deep.equal({f: 6}); + }); + + it('handles once called on a node with a default listener', async function() { + var ref = (getRandomNode() as Reference); + + await ref.set({ + a: 1, + b: 2, + c: 3, + d: 4, + e: 5, + f: 6 + }); + + const ea = EventAccumulatorFactory.waitsForCount(1); + // Setup value listener + ref.on('value', function(snap) { + ea.addEvent(); + }); + + await ea.promise; + + // now do the once call + const snap = await ref.limitToLast(1).once('child_added'); + var val = snap.val(); + expect(val).to.equal(6); + }); + + + it('handles once called on a node with a default listener and non-complete limit', async function() { + var ref = getRandomNode(), + ready, done; + + await ref.set({ + a: 1, + b: 2, + c: 3 + }); + + const ea = EventAccumulatorFactory.waitsForCount(1); + // Setup value listener + ref.on('value', function(snap) { + ea.addEvent(); + }); + + await ea.promise; + + // now do the once call + const snap = await ref.limitToLast(5).once('value'); + var val = snap.val(); + expect(val).to.deep.equal({a: 1, b: 2, c: 3}); + }); + + it('Remote remove triggers events.', function(done) { + var refPair = getRandomNode(2), writeRef = refPair[0], readRef = refPair[1]; + + writeRef.set({ a: 'a', b: 'b', c: 'c', d: 'd', e: 'e' }, function() { + + // Wait to get the initial data, and then remove 'c' remotely and wait for new data. + var count = 0; + readRef.limitToLast(5).on('value', function(s) { + count++; + if (count == 1) { + expect(s.val()).to.deep.equal({a: 'a', b: 'b', c: 'c', d: 'd', e: 'e' }); + writeRef.child('c').remove(); + } else { + expect(count).to.equal(2); + expect(s.val()).to.deep.equal({a: 'a', b: 'b', d: 'd', e: 'e' }); + done(); + } + }); + }); + }); + + it(".endAt(null, 'f').limitToLast(5) returns the right set of children.", function(done) { + var ref = (getRandomNode() as Reference); + ref.set({ a: 'a', b: 'b', c: 'c', d: 'd', e: 'e', f: 'f', g: 'g', h: 'h' }, function() { + ref.endAt(null, 'f').limitToLast(5).on('value', function(s) { + expect(s.val()).to.deep.equal({b: 'b', c: 'c', d: 'd', e: 'e', f: 'f' }); + done(); + }); + }); + }); + + it('complex update() at query root raises correct value event', function(done) { + var nodePair = getRandomNode(2); + var writer = nodePair[0]; + var reader = nodePair[1]; + + var readerLoaded = false, numEventsReceived = 0; + writer.child('foo').set({a: 1, b: 2, c: 3, d: 4, e: 5}, function(error, dummy) { + reader.child('foo').startAt().limitToFirst(4).on('value', function(snapshot) { + var val = snapshot.val(); + if (!readerLoaded) { + readerLoaded = true; + expect(val).to.deep.equal({a: 1, b: 2, c: 3, d: 4}); + + // This update causes the following to happen: + // 1. An in-view child is set to null (b) + // 2. An in-view child has its value changed (c) + // 3. An in-view child is changed and bumped out-of-view (d) + // We expect to get null values for b and d, along with the new children and updated value for c + writer.child('foo').update({b: null, c: 'a', cc: 'new', cd: 'new2', d: 'gone'}); + } else { + done(); + expect(val).to.deep.equal({a: 1, c: 'a', cc: 'new', cd: 'new2'}); + } + }); + }); + }); + + it('update() at query root raises correct value event', function(done) { + var nodePair = getRandomNode(2); + var writer = nodePair[0]; + var reader = nodePair[1]; + + var readerLoaded = false, numEventsReceived = 0; + writer.child('foo').set({ 'bar': 'a', 'baz': 'b', 'bam': 'c' }, function(error, dummy) { + reader.child('foo').limitToLast(10).on('value', function(snapshot) { + var val = snapshot.val(); + if (!readerLoaded) { + readerLoaded = true; + expect(val.bar).to.equal('a'); + expect(val.baz).to.equal('b'); + expect(val.bam).to.equal('c'); + writer.child('foo').update({ 'bar': 'd', 'bam': null, 'bat': 'e' }); + } else { + expect(val.bar).to.equal('d'); + expect(val.baz).to.equal('b'); + expect(val.bat).to.equal('e'); + expect(val.bam).to.equal(undefined); + done(); + } + }); + }); + }); + + it('set() at query root raises correct value event', function(done) { + var nodePair = getRandomNode(2); + var writer = nodePair[0]; + var reader = nodePair[1]; + + var readerLoaded = false, numEventsReceived = 0; + writer.child('foo').set({ 'bar': 'a', 'baz': 'b', 'bam': 'c' }, function(error, dummy) { + reader.child('foo').limitToLast(10).on('value', function(snapshot) { + var val = snapshot.val(); + if (!readerLoaded) { + readerLoaded = true; + expect(val.bar).to.equal('a'); + expect(val.baz).to.equal('b'); + expect(val.bam).to.equal('c'); + writer.child('foo').set({ 'bar': 'd', 'baz': 'b', 'bat': 'e' }); + } else { + expect(val.bar).to.equal('d'); + expect(val.baz).to.equal('b'); + expect(val.bat).to.equal('e'); + expect(val.bam).to.equal(undefined); + done(); + } + }); + }); + }); + + + it('listen for child_added events with limit and different types fires properly', function(done) { + var nodePair = getRandomNode(2); + var writer = nodePair[0]; + var reader = nodePair[1]; + + var numEventsReceived = 0, gotA = false, gotB = false, gotC = false; + writer.child('a').set(1, function(error, dummy) { + writer.child('b').set('b', function(error, dummy) { + writer.child('c').set({ 'deep': 'path', 'of': { 'stuff': true }}, function(error, dummy) { + reader.limitToLast(3).on('child_added', function(snap) { + var val = snap.val(); + switch (snap.key) { + case 'a': + gotA = true; + expect(val).to.equal(1); + break; + case 'b': + gotB = true; + expect(val).to.equal('b'); + break; + case 'c': + gotC = true; + expect(val.deep).to.equal('path'); + expect(val.of.stuff).to.be.true; + break; + default: + expect(false).to.be.true; + } + numEventsReceived += 1; + expect(numEventsReceived).to.be.lessThan(4); + if (gotA && gotB && gotC) done(); + }); + }); + }); + }); + }); + + it('listen for child_changed events with limit and different types fires properly', function(done) { + var nodePair = getRandomNode(2); + var writer = nodePair[0]; + var reader = nodePair[1]; + + var numEventsReceived = 0, gotA = false, gotB = false, gotC = false, readerLoaded = false; + writer.set({ a: 'something', b: "we'll", c: 'overwrite '}, function(error, dummy) { + reader.limitToLast(3).on('value', function(snapshot) { + if (!readerLoaded) { + readerLoaded = true; + // Set up listener for upcoming change events + reader.limitToLast(3).on('child_changed', function(snap) { + var val = snap.val(); + switch (snap.key) { + case 'a': + gotA = true; + expect(val).to.equal(1); + break; + case 'b': + gotB = true; + expect(val).to.equal('b'); + break; + case 'c': + gotC = true; + expect(val.deep).to.equal('path'); + expect(val.of.stuff).to.be.true; + break; + default: + expect(false).to.be.true; + } + numEventsReceived += 1; + expect(numEventsReceived).to.be.lessThan(4); + if (gotA && gotB && gotC) done(); + }); + + // Begin changing every key + writer.child('a').set(1); + writer.child('b').set('b'); + writer.child('c').set({ 'deep': 'path', 'of': { 'stuff': true }}); + } + }); + }); + }); + + it('listen for child_remove events with limit and different types fires properly', function(done) { + var nodePair = getRandomNode(2); + var writer = nodePair[0]; + var reader = nodePair[1]; + + var numEventsReceived = 0, gotA = false, gotB = false, gotC = false, readerLoaded = false; + writer.set({ a: 1, b: 'b', c: { 'deep': 'path', 'of': { 'stuff': true }} }, function(error, dummy) { + reader.limitToLast(3).on('value', function(snapshot) { + if (!readerLoaded) { + readerLoaded = true; + + // Set up listener for upcoming change events + reader.limitToLast(3).on('child_removed', function(snap) { + var val = snap.val(); + switch (snap.key) { + case 'a': + gotA = true; + expect(val).to.equal(1); + break; + case 'b': + gotB = true; + expect(val).to.equal('b'); + break; + case 'c': + gotC = true; + expect(val.deep).to.equal('path'); + expect(val.of.stuff).to.be.true; + break; + default: + expect(false).to.be.true; + } + numEventsReceived += 1; + expect(numEventsReceived).to.be.lessThan(4); + if (gotA && gotB && gotC) done(); + }); + + // Begin removing every key + writer.child('a').remove(); + writer.child('b').remove(); + writer.child('c').remove(); + } + }); + }); + }); + + it('listen for child_remove events when parent removed', function(done) { + var nodePair = getRandomNode(2); + var writer = nodePair[0]; + var reader = nodePair[1]; + + var numEventsReceived = 0, gotA = false, gotB = false, gotC = false, readerLoaded = false; + writer.set({ a: 1, b: 'b', c: { 'deep': 'path', 'of': { 'stuff': true }} }, function(error, dummy) { + + reader.limitToLast(3).on('value', function(snapshot) { + if (!readerLoaded) { + readerLoaded = true; + + // Set up listener for upcoming change events + reader.limitToLast(3).on('child_removed', function(snap) { + var val = snap.val(); + switch (snap.key) { + case 'a': + gotA = true; + expect(val).to.equal(1); + break; + case 'b': + gotB = true; + expect(val).to.equal('b'); + break; + case 'c': + gotC = true; + expect(val.deep).to.equal('path'); + expect(val.of.stuff).to.be.true; + break; + default: + expect(false).to.be.true; + } + numEventsReceived += 1; + expect(numEventsReceived).to.be.lessThan(4); + if (gotA && gotB && gotC) done(); + }); + + // Remove the query parent + writer.remove(); + } + }); + }); + }); + + it('listen for child_remove events when parent set to scalar', function(done) { + var nodePair = getRandomNode(2); + var writer = nodePair[0]; + var reader = nodePair[1]; + + var numEventsReceived = 0, gotA = false, gotB = false, gotC = false, readerLoaded = false; + writer.set({ a: 1, b: 'b', c: { 'deep': 'path', 'of': { 'stuff': true }} }, function(error, dummy) { + + reader.limitToLast(3).on('value', function(snapshot) { + if (!readerLoaded) { + readerLoaded = true; + + // Set up listener for upcoming change events + reader.limitToLast(3).on('child_removed', function(snap) { + var val = snap.val(); + switch (snap.key) { + case 'a': + gotA = true; + expect(val).to.equal(1); + break; + case 'b': + gotB = true; + expect(val).to.equal('b'); + break; + case 'c': + gotC = true; + expect(val.deep).to.equal('path'); + expect(val.of.stuff).to.be.true; + break; + default: + expect(false).to.be.true; + } + numEventsReceived += 1; + expect(numEventsReceived).to.be.lessThan(4); + if (gotA && gotB && gotC) done(); + }); + + // Set the parent to a scalar + writer.set('scalar'); + } + }); + }); + }); + + + it('Queries behave wrong after .once().', async function() { + var refPair = getRandomNode(2), + writeRef = refPair[0], + readRef = refPair[1], + done, startAtCount, defaultCount; + + await writeRef.set({a: 1, b: 2, c: 3, d: 4 }); + + await readRef.once('value'); + + const ea = EventAccumulatorFactory.waitsForCount(5); + startAtCount = 0; + readRef.startAt(null, 'd').on('child_added', function() { + startAtCount++; + ea.addEvent(); + }); + expect(startAtCount).to.equal(0); + + defaultCount = 0; + readRef.on('child_added', function() { + defaultCount++; + ea.addEvent(); + }); + expect(defaultCount).to.equal(0); + + readRef.on('child_removed', function() { + expect(false).to.be.true; + }); + + return ea.promise; + }); + + it('Case 2003: Correctly get events for startAt/endAt queries when priority changes.', function() { + var ref = (getRandomNode() as Reference); + var addedFirst = [], removedFirst = [], addedSecond = [], removedSecond = []; + ref.startAt(0).endAt(10).on('child_added', function(snap) { addedFirst.push(snap.key); }); + ref.startAt(0).endAt(10).on('child_removed', function(snap) { removedFirst.push(snap.key); }); + ref.startAt(10).endAt(20).on('child_added', function(snap) { addedSecond.push(snap.key); }); + ref.startAt(10).endAt(20).on('child_removed', function(snap) { removedSecond.push(snap.key); }); + + ref.child('a').setWithPriority('a', 5); + expect(addedFirst).to.deep.equal(['a']); + ref.child('a').setWithPriority('a', 15); + expect(removedFirst).to.deep.equal(['a']); + expect(addedSecond).to.deep.equal(['a']); + + ref.child('a').setWithPriority('a', 10); + expect(addedFirst).to.deep.equal(['a', 'a']); + + ref.child('a').setWithPriority('a', 5); + expect(removedSecond).to.deep.equal(['a']); + }); + + it('Behaves with diverging queries', async function() { + var refs = getRandomNode(2); + var writer = refs[0]; + var reader = refs[1]; + + await writer.set({ + a: {b: 1, c: 2}, + e: 3 + }); + + var childCount = 0; + + reader.child('a/b').on('value', function(snap) { + var val = snap.val(); + childCount++; + if (childCount == 1) { + expect(val).to.equal(1); + } else { + // fail this, nothing should have changed + expect(true).to.be.false; + } + }); + + const ea = EventAccumulatorFactory.waitsForCount(1); + var count = 0; + reader.limitToLast(2).on('value', function(snap) { + ea.addEvent(); + var val = snap.val(); + count++; + if (count == 1) { + expect(val).to.deep.equal({a: {b: 1, c: 2}, e: 3}); + } else if (count == 2) { + expect(val).to.deep.equal({d: 4, e: 3}); + } + }); + + await ea.promise; + + ea.reset(); + writer.child('d').set(4); + + return ea.promise; + }); + + it('Priority-only updates are processed correctly by server.', async function() { + var refPair = (getRandomNode(2) as Reference[]), readRef = refPair[0], writeRef = refPair[1]; + + const ea = EventAccumulatorFactory.waitsForCount(1); + var readVal; + readRef.limitToLast(2).on('value', function(s) { + readVal = s.val(); + if (readVal) { + ea.addEvent(); + } + }); + writeRef.set({ + a: { '.priority': 10, '.value': 1}, + b: { '.priority': 20, '.value': 2}, + c: { '.priority': 30, '.value': 3} + }); + + await ea.promise; + expect(readVal).to.deep.equal({ b: 2, c: 3 }); + + ea.reset(); + writeRef.child('a').setPriority(25); + + await ea.promise; + expect(readVal).to.deep.equal({ a: 1, c: 3 }); + }); + + it('Server: Test re-listen', function(done) { + var refPair = (getRandomNode(2) as Reference[]), ref = refPair[0], ref2 = refPair[1]; + ref.set({ + a: 'a', + b: 'b', + c: 'c', + d: 'd', + e: 'e', + f: 'f', + g: 'g' + }); + + var before; + ref.startAt(null, 'a').endAt(null, 'b').on('value', function(b) { + before = b.val(); + }); + + ref.child('aa').set('aa', function() { + ref2.startAt(null, 'a').endAt(null, 'b').on('value', function(b) { + expect(b.val()).to.deep.equal(before); + done(); + }); + }); + }); + + it('Server: Test re-listen 2', function(done) { + var refPair = getRandomNode(2), ref = refPair[0], ref2 = refPair[1]; + ref.set({ + a: 'a', + b: 'b', + c: 'c', + d: 'd', + e: 'e', + f: 'f', + g: 'g' + }); + + var before; + ref.startAt(null, 'b').limitToFirst(3).on('value', function(b) { + before = b.val(); + }); + + ref.child('aa').update({ 'a': 5, 'aa': 4, 'b': 7, 'c': 4, 'd': 4, 'dd': 3 }, function() { + ref2.startAt(null, 'b').limitToFirst(3).on('value', function(b) { + expect(b.val()).to.deep.equal(before); + done(); + }); + }); + }); + + it('Server: Test re-listen 3', function(done) { + var refPair = getRandomNode(2), ref = refPair[0], ref2 = refPair[1]; + ref.set({ + a: 'a', + b: 'b', + c: 'c', + d: 'd', + e: 'e', + f: 'f', + g: 'g' + }); + + var before; + ref.limitToLast(3).on('value', function(b) { + before = b.val(); + }); + + ref.child('h').set('h', function() { + ref2.limitToLast(3).on('value', function(b) { + expect(b.val()).to.deep.equal(before); + done(); + }); + }); + }); + + it('Server limit below limit works properly.', async function() { + var refPair = getRandomNode(2), + readRef = refPair[0], + writeRef = refPair[1], + childData; + + await writeRef.set({ + a: { + aa: {'.priority': 1, '.value': 1 }, + ab: {'.priority': 1, '.value': 1 } + } + }); + + readRef.limitToLast(1).on('value', function(s) { + expect(s.val()).to.deep.equal({a: { aa: 1, ab: 1}}); + }); + + const ea = EventAccumulatorFactory.waitsForCount(1); + readRef.child('a').startAt(1).endAt(1).on('value', function(s) { + childData = s.val(); + if (childData) { + ea.addEvent(); + } + }); + + await ea.promise; + expect(childData).to.deep.equal({ aa: 1, ab: 1 }); + + // This should remove an item from the child query, but *not* the parent query. + ea.reset(); + writeRef.child('a/ab').setWithPriority(1, 2); + + await ea.promise + + expect(childData).to.deep.equal({ aa: 1 }); + }); + + it('Server: Setting grandchild of item in limit works.', async function() { + var refPair = getRandomNode(2), ref = refPair[0], ref2 = refPair[1]; + + ref.set({ a: { + name: 'Mike' + }}); + + const ea = EventAccumulatorFactory.waitsForCount(1); + var snaps = []; + ref2.limitToLast(1).on('value', function(s) { + var val = s.val(); + if (val !== null) { + snaps.push(val); + ea.addEvent(); + } + }); + + await ea.promise; + expect(snaps).to.deep.equal( [{ a: { name: 'Mike' } }]); + + ea.reset(); + ref.child('a/name').set('Fred'); + + await ea.promise; + expect(snaps).to.deep.equal([{ a: { name: 'Mike' } }, { a: { name: 'Fred' } }]); + }); + + it('Server: Updating grandchildren of item in limit works.', async function() { + var refPair = getRandomNode(2), ref = refPair[0], ref2 = refPair[1]; + + ref.set({ a: { + name: 'Mike' + }}); + + const ea = EventAccumulatorFactory.waitsForCount(1); + var snaps = []; + ref2.limitToLast(1).on('value', function(s) { + var val = s.val(); + if (val !== null) { + snaps.push(val); + ea.addEvent(); + } + }); + + /** + * If I put this artificial pause here, this test works however + * something about the timing is broken + */ + await ea.promise; + expect(snaps).to.deep.equal([{ a: { name: 'Mike' } }]); + + ea.reset(); + ref.child('a').update({ name: null, Name: 'Fred' }); + await ea.promise; + + expect(snaps).to.deep.equal([{ a: { name: 'Mike' } }, { a: { Name: 'Fred' } }]); + }); + + it('Server: New child at end of limit shows up.', async function() { + var refPair = getRandomNode(2), ref = refPair[0], ref2 = refPair[1]; + + const ea = EventAccumulatorFactory.waitsForCount(1); + var snap; + ref2.limitToLast(1).on('value', function(s) { + snap = s.val(); + ea.addEvent(); + }); + + await ea.promise; + expect(snap).to.be.null; + ea.reset(); + + ref.child('a').set('new child'); + + /** + * If I put this artificial pause here, this test works however + * something about the timing is broken + */ + await ea.promise; + expect(snap).to.deep.equal({ a: 'new child' }); + }); + + it('Server: Priority-only updates are processed correctly by server (1).', async function() { + var refPair = getRandomNode(2), readRef = refPair[0], writeRef = refPair[1]; + + const ea = EventAccumulatorFactory.waitsForCount(1); + var readVal; + readRef.limitToLast(2).on('value', function(s) { + readVal = s.val(); + if (readVal) { + ea.addEvent(); + } + }); + writeRef.set({ + a: { '.priority': 10, '.value': 1}, + b: { '.priority': 20, '.value': 2}, + c: { '.priority': 30, '.value': 3} + }); + + await ea.promise + expect(readVal).to.deep.equal({ b: 2, c: 3 }); + + ea.reset(); + writeRef.child('a').setPriority(25); + + await ea.promise; + expect(readVal).to.deep.equal({ a: 1, c: 3 }); + }); + + // Same as above but with an endAt() so we hit CompoundQueryView instead of SimpleLimitView. + it('Server: Priority-only updates are processed correctly by server (2).', async function() { + var refPair = getRandomNode(2), readRef = refPair[0], writeRef = refPair[1]; + + const ea = EventAccumulatorFactory.waitsForCount(1); + var readVal; + readRef.endAt(50).limitToLast(2).on('value', function(s) { + readVal = s.val(); + if (readVal) { + ea.addEvent(); + } + }); + + writeRef.set({ + a: { '.priority': 10, '.value': 1}, + b: { '.priority': 20, '.value': 2}, + c: { '.priority': 30, '.value': 3} + }); + + await ea.promise; + expect(readVal).to.deep.equal({ b: 2, c: 3 }); + + ea.reset(); + writeRef.child('a').setPriority(25); + + await ea.promise; + expect(readVal).to.deep.equal({ a: 1, c: 3 }); + }); + + it('Latency compensation works with limit and pushed object.', function() { + var ref = (getRandomNode() as Reference); + var events = []; + ref.limitToLast(3).on('child_added', function(s) { events.push(s.val()); }); + + // If you change this to ref.push('foo') it works. + ref.push({a: 'foo'}); + + // Should have synchronously gotten an event. + expect(events.length).to.equal(1); + }); + + it("Cache doesn't remove items that have fallen out of view.", async function() { + var refPair = getRandomNode(2), readRef = refPair[0], writeRef = refPair[1]; + + let ea = EventAccumulatorFactory.waitsForCount(1); + var readVal; + readRef.limitToLast(2).on('value', function(s) { + readVal = s.val(); + ea.addEvent(); + }); + + await ea.promise; + expect(readVal).to.be.null; + + ea = EventAccumulatorFactory.waitsForCount(4) + for (var i = 0; i < 4; i++) { + writeRef.child('k' + i).set(i); + } + + await ea.promise; + + await pause(500); + expect(readVal).to.deep.equal({'k2': 2, 'k3': 3}); + + ea = EventAccumulatorFactory.waitsForCount(1) + writeRef.remove(); + + await ea.promise; + expect(readVal).to.be.null; + }); + + it('handles an update that moves another child that has a deeper listener out of view', async function() { + var refs = getRandomNode(2); + var reader = refs[0]; + var writer = refs[1]; + + await writer.set({ + a: { '.priority': 10, '.value': 1}, + b: { '.priority': 20, d: 4 }, + c: { '.priority': 30, '.value': 3} + }); + + reader.child('b/d').on('value', function(snap) { + expect(snap.val()).to.equal(4); + }); + + const ea = EventAccumulatorFactory.waitsForCount(1); + var val; + reader.limitToLast(2).on('value', function(snap) { + val = snap.val(); + if (val) { + ea.addEvent(); + } + }); + + await ea.promise; + expect(val).to.deep.equal({b: {d: 4}, c: 3}); + + ea.reset(); + writer.child('a').setWithPriority(1, 40); + + await ea.promise; + expect(val).to.deep.equal({c: 3, a: 1}); + }); + + it('Integer keys behave numerically 1.', function(done) { + var ref = (getRandomNode() as Reference); + ref.set({1: true, 50: true, 550: true, 6: true, 600: true, 70: true, 8: true, 80: true }, function() { + ref.startAt(null, '80').once('value', function(s) { + expect(s.val()).to.deep.equal({80: true, 550: true, 600: true }); + done(); + }); + }); + }); + + it('Integer keys behave numerically 2.', function(done) { + var ref = (getRandomNode() as Reference); + ref.set({1: true, 50: true, 550: true, 6: true, 600: true, 70: true, 8: true, 80: true }, function() { + ref.endAt(null, '50').once('value', function(s) { + expect(s.val()).to.deep.equal({1: true, 6: true, 8: true, 50: true }); + done(); + }); + }); + }); + + it('Integer keys behave numerically 3.', function(done) { + var ref = (getRandomNode() as Reference); + ref.set({1: true, 50: true, 550: true, 6: true, 600: true, 70: true, 8: true, 80: true}, function() { + ref.startAt(null, '50').endAt(null, '80').once('value', function(s) { + expect(s.val()).to.deep.equal({50: true, 70: true, 80: true }); + done(); + }); + }); + }); + + it('.limitToLast() on node with priority.', function(done) { + var ref = (getRandomNode() as Reference); + ref.set({'a': 'blah', '.priority': 'priority'}, function() { + ref.limitToLast(2).once('value', function(s) { + expect(s.exportVal()).to.deep.equal({a: 'blah' }); + done(); + }); + }); + }); + + it('.equalTo works', async function() { + var ref = (getRandomNode() as Reference); + var done = false; + + await ref.set({ + a: 1, + b: {'.priority': 2, '.value': 2}, + c: {'.priority': '3', '.value': 3} + }); + + const snap1 = await ref.equalTo(2).once('value'); + var val1 = snap1.exportVal(); + expect(val1).to.deep.equal({b: {'.priority': 2, '.value': 2}}); + + const snap2 = await ref.equalTo('3', 'c').once('value'); + + var val2 = snap2.exportVal(); + expect(val2).to.deep.equal({c: {'.priority': '3', '.value': 3}}); + + const snap3 = await ref.equalTo(null, 'c').once('value'); + var val3 = snap3.exportVal(); + expect(val3).to.be.null; + }); + + it('Handles fallback for orderBy', async function() { + var ref = (getRandomNode() as Reference); + + const children = []; + + ref.orderByChild('foo').on('child_added', function(snap) { + children.push(snap.key); + }); + + // Set initial data + await ref.set({ + a: {foo: 3}, + b: {foo: 1}, + c: {foo: 2} + }); + + expect(children).to.deep.equal(['b', 'c', 'a']); + }); + + it("Get notified of deletes that happen while offline.", async function() { + var refPair = getRandomNode(2); + var queryRef = refPair[0]; + var writerRef = refPair[1]; + var readSnapshot = null; + + // Write 3 children and then start our limit query. + await writerRef.set({a: 1, b: 2, c: 3}); + + const ea = EventAccumulatorFactory.waitsForCount(1); + queryRef.limitToLast(3).on('value', function(s) { + readSnapshot = s; + if (readSnapshot) { + ea.addEvent(); + } + }); + + // Wait for us to read the 3 children. + await ea.promise; + + expect(readSnapshot.val()).to.deep.equal({a: 1, b: 2, c: 3 }); + + queryRef.database.goOffline(); + + // Delete an item in the query and then bring our connection back up. + ea.reset(); + await writerRef.child('b').remove(); + queryRef.database.goOnline(); + + await ea.promise; + expect(readSnapshot.child('b').val()).to.be.null; + }); + + it('Snapshot children respect default ordering', function(done) { + var refPair = getRandomNode(2); + var queryRef = refPair[0], writerRef = refPair[1]; + + var list = { + 'a': { + thisvaluefirst: { '.value': true, '.priority': 1 }, + name: { '.value': 'Michael', '.priority': 2 }, + thisvaluelast: { '.value': true, '.priority': 3 } + }, + 'b': { + thisvaluefirst: { '.value': true, '.priority': null }, + name: { '.value': 'Rob', '.priority': 2 }, + thisvaluelast: { '.value': true, '.priority': 3 } + }, + 'c': { + thisvaluefirst: { '.value': true, '.priority': 1 }, + name: { '.value': 'Jonny', '.priority': 2 }, + thisvaluelast: { '.value': true, '.priority': 'somestring' } + } + }; + + writerRef.set(list, function() { + queryRef.orderByChild('name').once('value', function(snap) { + var expectedKeys = ['thisvaluefirst', 'name', 'thisvaluelast']; + var expectedNames = ['Jonny', 'Michael', 'Rob']; + + + // Validate that snap.child() resets order to default for child snaps + var orderedKeys = []; + snap.child('b').forEach(function(childSnap) { + orderedKeys.push(childSnap.key); + }); + expect(orderedKeys).to.deep.equal(expectedKeys); + + // Validate that snap.forEach() resets ordering to default for child snaps + var orderedNames = []; + snap.forEach(function(childSnap) { + orderedNames.push(childSnap.child('name').val()); + var orderedKeys = []; + childSnap.forEach(function(grandchildSnap) { + orderedKeys.push(grandchildSnap.key); + }); + expect(orderedKeys).to.deep.equal(['thisvaluefirst', 'name', 'thisvaluelast']); + }); + expect(orderedNames).to.deep.equal(expectedNames); + done(); + }); + }); + }); + + it('Adding listens for the same paths does not check fail', function(done) { + // This bug manifests itself if there's a hierarchy of query listener, default listener and one-time listener + // underneath. During one-time listener registration, sync-tree traversal stopped as soon as it found a complete + // server cache (this is the case for not indexed query view). The problem is that the same traversal was + // looking for a ancestor default view, and the early exit prevented from finding the default listener above the + // one-time listener. Event removal code path wasn't removing the listener because it stopped as soon as it + // found the default view. This left the zombie one-time listener and check failed on the second attempt to + // create a listener for the same path (asana#61028598952586). + var ref = getRandomNode(1)[0]; + + ref.child('child').set({name: "John"}, function() { + ref.orderByChild('name').equalTo('John').on('value', function(snap) { + ref.child('child').on('value', function(snap) { + ref.child('child').child('favoriteToy').once('value', function (snap) { + ref.child('child').child('favoriteToy').once('value', function (snap) { + done(); + }); + }); + }); + }); + }); + }); + + it('Can JSON serialize refs', function() { + var ref = (getRandomNode() as Reference); + expect(JSON.stringify(ref)).to.equal('"' + ref.toString() + '"'); + }); +}); diff --git a/tests/database/repoinfo.test.ts b/tests/database/repoinfo.test.ts new file mode 100644 index 00000000000..daba6f371fe --- /dev/null +++ b/tests/database/repoinfo.test.ts @@ -0,0 +1,19 @@ +import { testRepoInfo } from "./helpers/util"; +import { CONSTANTS } from "../../src/database/realtime/Constants"; +import { expect } from "chai"; + +describe('RepoInfo', function() { + it('should return the correct URL', function() { + var repoInfo = testRepoInfo('https://test-ns.firebaseio.com'); + + var urlParams = {}; + urlParams[CONSTANTS.VERSION_PARAM] = CONSTANTS.PROTOCOL_VERSION; + urlParams[CONSTANTS.LAST_SESSION_PARAM] = 'test'; + + var websocketUrl = repoInfo.connectionURL(CONSTANTS.WEBSOCKET, urlParams); + expect(websocketUrl).to.equal('wss://test-ns.firebaseio.com/.ws?v=5&ls=test'); + + var longPollingUrl = repoInfo.connectionURL(CONSTANTS.LONG_POLLING, urlParams); + expect(longPollingUrl).to.equal('https://test-ns.firebaseio.com/.lp?v=5&ls=test'); + }); +}); diff --git a/tests/database/sortedmap.test.ts b/tests/database/sortedmap.test.ts new file mode 100644 index 00000000000..a4a8fdf9201 --- /dev/null +++ b/tests/database/sortedmap.test.ts @@ -0,0 +1,392 @@ +import { expect } from "chai"; +import { + SortedMap, + LLRBNode +} from "../../src/database/core/util/SortedMap"; +import { shuffle } from "./helpers/util"; + + +// Many of these were adapted from the mugs source code. +// http://mads379.github.com/mugs/ +describe("SortedMap Tests", function() { + var defaultCmp = function(a, b) { + if (a === b) { + return 0; + } else if (a < b) { + return -1; + } else { + return 1; + } + }; + + it("Create node", function() { + var map = new SortedMap(defaultCmp).insert("key", "value"); + expect(map.root_.left.isEmpty()).to.equal(true); + expect(map.root_.right.isEmpty()).to.equal(true); + }); + + it("You can search a map for a specific key", function() { + var map = new SortedMap(defaultCmp).insert(1,1).insert(2,2); + expect(map.get(1)).to.equal(1); + expect(map.get(2)).to.equal(2); + expect(map.get(3)).to.equal(null); + }); + + it("You can insert a new key/value pair into the tree", function() { + var map = new SortedMap(defaultCmp).insert(1,1).insert(2,2); + expect(map.root_.key).to.equal(2); + expect(map.root_.left.key).to.equal(1); + }); + + it("You can remove a key/value pair from the map",function() { + var map = new SortedMap(defaultCmp).insert(1,1).insert(2,2); + var newMap = map.remove(1); + expect(newMap.get(2)).to.equal(2); + expect(newMap.get(1)).to.equal(null); + }); + + it("More removals",function(){ + var map = new SortedMap(defaultCmp) + .insert(1,1) + .insert(50,50) + .insert(3,3) + .insert(4,4) + .insert(7,7) + .insert(9,9) + .insert(20,20) + .insert(18,18) + .insert(2,2) + .insert(71,71) + .insert(42,42) + .insert(88,88); + + var m1 = map.remove(7); + var m2 = m1.remove(3); + var m3 = m2.remove(1); + expect(m3.count()).to.equal(9); + expect(m3.get(1)).to.equal(null); + expect(m3.get(3)).to.equal(null); + expect(m3.get(7)).to.equal(null); + expect(m3.get(20)).to.equal(20); + }); + + it("Removal bug", function() { + var map = new SortedMap(defaultCmp) + .insert(1, 1) + .insert(2, 2) + .insert(3, 3); + + var m1 = map.remove(2); + expect(m1.get(1)).to.equal(1); + expect(m1.get(3)).to.equal(3); + }); + + it("Test increasing", function(){ + var total = 100; + var item; + var map = new SortedMap(defaultCmp).insert(1,1); + for (item = 2; item < total ; item++) { + map = map.insert(item,item); + } + expect(map.root_.checkMaxDepth_()).to.equal(true); + for (item = 2; item < total ; item++) { + map = map.remove(item); + } + expect(map.root_.checkMaxDepth_()).to.equal(true); + }); + + it("The structure should be valid after insertion (1)",function(){ + var map = new SortedMap(defaultCmp).insert(1,1).insert(2,2).insert(3,3); + + expect(map.root_.key).to.equal(2); + expect(map.root_.left.key).to.equal(1); + expect(map.root_.right.key).to.equal(3); + }); + + it("The structure should be valid after insertion (2)",function(){ + var map = new SortedMap(defaultCmp) + .insert(1,1) + .insert(2,2) + .insert(3,3) + .insert(4,4) + .insert(5,5) + .insert(6,6) + .insert(7,7) + .insert(8,8) + .insert(9,9) + .insert(10,10) + .insert(11,11) + .insert(12,12); + + expect(map.count()).to.equal(12); + expect(map.root_.checkMaxDepth_()).to.equal(true); + }); + + it("Rotate left leaves the tree in a valid state",function(){ + var node = new LLRBNode(4,4,false, + new LLRBNode(2,2,false,null, null), + new LLRBNode(7,7,true, + new LLRBNode(5,5,false,null,null), + new LLRBNode(8,8,false,null,null))); + + var node2 = node.rotateLeft_(); + expect(node2.count()).to.equal(5); + expect(node2.checkMaxDepth_()).to.equal(true); + }); + + it("Rotate right leaves the tree in a valid state", function(){ + var node = new LLRBNode(7,7,false, + new LLRBNode(4,4,true, + new LLRBNode(2,2,false, null, null), + new LLRBNode(5,5,false, null, null)), + new LLRBNode(8,8,false, null, null)); + + var node2 = node.rotateRight_(); + expect(node2.count()).to.equal(5); + expect(node2.key).to.equal(4); + expect(node2.left.key).to.equal(2); + expect(node2.right.key).to.equal(7); + expect(node2.right.left.key).to.equal(5); + expect(node2.right.right.key).to.equal(8); + }); + + it("The structure should be valid after insertion (3)",function(){ + var map = new SortedMap(defaultCmp) + .insert(1,1) + .insert(50,50) + .insert(3,3) + .insert(4,4) + .insert(7,7) + .insert(9,9); + + expect(map.count()).to.equal(6); + expect(map.root_.checkMaxDepth_()).to.equal(true); + + var m2 = map + .insert(20,20) + .insert(18,18) + .insert(2,2); + + expect(m2.count()).to.equal(9); + expect(m2.root_.checkMaxDepth_()).to.equal(true); + + var m3 = m2 + .insert(71,71) + .insert(42,42) + .insert(88,88); + + expect(m3.count()).to.equal(12); + expect(m3.root_.checkMaxDepth_()).to.equal(true); + }); + + it("you can overwrite a value",function(){ + var map = new SortedMap(defaultCmp).insert(10,10).insert(10,8); + expect(map.get(10)).to.equal(8); + }); + + it("removing the last element returns an empty map",function() { + var map = new SortedMap(defaultCmp).insert(10,10).remove(10); + expect(map.isEmpty()).to.equal(true); + }); + + it("empty .get()",function() { + var empty = new SortedMap(defaultCmp); + expect(empty.get("something")).to.equal(null); + }); + + it("empty .count()",function() { + var empty = new SortedMap(defaultCmp); + expect(empty.count()).to.equal(0); + }); + + it("empty .remove()",function() { + var empty = new SortedMap(defaultCmp); + expect(empty.remove("something").count()).to.equal(0); + }); + + it(".reverseTraversal() works.", function() { + var map = new SortedMap(defaultCmp).insert(1, 1).insert(5, 5).insert(3, 3).insert(2, 2).insert(4, 4); + var next = 5; + map.reverseTraversal(function(key, value) { + expect(key).to.equal(next); + next--; + }); + expect(next).to.equal(0); + }); + + it("insertion and removal of 100 items in random order.", function() { + var N = 100; + var toInsert = [], toRemove = []; + for(var i = 0; i < N; i++) { + toInsert.push(i); + toRemove.push(i); + } + + shuffle(toInsert); + shuffle(toRemove); + + var map = new SortedMap(defaultCmp); + + for (i = 0 ; i < N ; i++ ) { + map = map.insert(toInsert[i], toInsert[i]); + expect(map.root_.checkMaxDepth_()).to.equal(true); + } + expect(map.count()).to.equal(N); + + // Ensure order is correct. + var next = 0; + map.inorderTraversal(function(key, value) { + expect(key).to.equal(next); + expect(value).to.equal(next); + next++; + }); + expect(next).to.equal(N); + + for (i = 0 ; i < N ; i++ ) { + expect(map.root_.checkMaxDepth_()).to.equal(true); + map = map.remove(toRemove[i]); + } + expect(map.count()).to.equal(0); + }); + + // A little perf test for convenient benchmarking. + xit("Perf", function() { + for(var j = 0; j < 5; j++) { + var map = new SortedMap(defaultCmp); + var start = new Date().getTime(); + for(var i = 0; i < 50000; i++) { + map = map.insert(i, i); + } + + for(var i = 0; i < 50000; i++) { + map = map.remove(i); + } + var end = new Date().getTime(); + // console.log(end-start); + } + }); + + xit("Perf: Insertion and removal with various # of items.", function() { + var verifyTraversal = function(map, max) { + var next = 0; + map.inorderTraversal(function(key, value) { + expect(key).to.equal(next); + expect(value).to.equal(next); + next++; + }); + expect(next).to.equal(max); + }; + + for(var N = 10; N <= 100000; N *= 10) { + var toInsert = [], toRemove = []; + for(var i = 0; i < N; i++) { + toInsert.push(i); + toRemove.push(i); + } + + shuffle(toInsert); + shuffle(toRemove); + + var map = new SortedMap(defaultCmp); + + var start = new Date().getTime(); + for (i = 0 ; i < N ; i++ ) { + map = map.insert(toInsert[i], toInsert[i]); + } + + // Ensure order is correct. + verifyTraversal(map, N); + + for (i = 0 ; i < N ; i++ ) { + map = map.remove(toRemove[i]); + } + + var elapsed = new Date().getTime() - start; + // console.log(N + ": " +elapsed); + } + }); + + xit("Perf: Comparison with {}: Insertion and removal with various # of items.", function() { + var verifyTraversal = function(tree, max) { + var keys = []; + for(var k in tree) + keys.push(k); + + keys.sort(); + expect(keys.length).to.equal(max); + for(var i = 0; i < max; i++) + expect(tree[i]).to.equal(i); + }; + + for(var N = 10; N <= 100000; N *= 10) { + var toInsert = [], toRemove = []; + for(var i = 0; i < N; i++) { + toInsert.push(i); + toRemove.push(i); + } + + shuffle(toInsert); + shuffle(toRemove); + + var tree = { }; + + var start = new Date().getTime(); + for (i = 0 ; i < N ; i++ ) { + tree[i] = i; + } + + // Ensure order is correct. + //verifyTraversal(tree, N); + + for (i = 0 ; i < N ; i++ ) { + delete tree[i]; + } + + var elapsed = (new Date().getTime()) - start; + // console.log(N + ": " +elapsed); + } + }); + + it("SortedMapIterator empty test.", function() { + var map = new SortedMap(defaultCmp); + var iterator = map.getIterator(); + expect(iterator.getNext()).to.equal(null); + }); + + it("SortedMapIterator test with 10 items.", function() { + var items = []; + for(var i = 0; i < 10; i++) + items.push(i); + shuffle(items); + + var map = new SortedMap(defaultCmp); + for(i = 0; i < 10; i++) + map = map.insert(items[i], items[i]); + + var iterator = map.getIterator(); + var n, expected = 0; + while ((n = iterator.getNext()) !== null) { + expect(n.key).to.equal(expected); + expect(n.value).to.equal(expected); + expected++; + } + expect(expected).to.equal(10); + }); + + it("SortedMap.getPredecessorKey works.", function() { + var map = new SortedMap(defaultCmp) + .insert(1,1) + .insert(50,50) + .insert(3,3) + .insert(4,4) + .insert(7,7) + .insert(9,9); + + expect(map.getPredecessorKey(1)).to.equal(null); + expect(map.getPredecessorKey(3)).to.equal(1); + expect(map.getPredecessorKey(4)).to.equal(3); + expect(map.getPredecessorKey(7)).to.equal(4); + expect(map.getPredecessorKey(9)).to.equal(7); + expect(map.getPredecessorKey(50)).to.equal(9); + }); +}); diff --git a/tests/database/sparsesnapshottree.test.ts b/tests/database/sparsesnapshottree.test.ts new file mode 100644 index 00000000000..4ee627050a0 --- /dev/null +++ b/tests/database/sparsesnapshottree.test.ts @@ -0,0 +1,178 @@ +import { expect } from "chai"; +import { SparseSnapshotTree } from "../../src/database/core/SparseSnapshotTree"; +import { Path } from "../../src/database/core/util/Path"; +import { nodeFromJSON } from "../../src/database/core/snap/nodeFromJSON"; +import { ChildrenNode } from "../../src/database/core/snap/ChildrenNode"; + +describe("SparseSnapshotTree Tests", function () { + it("Basic remember and find.", function () { + var st = new SparseSnapshotTree(); + var path = new Path("a/b"); + var node = nodeFromJSON("sdfsd"); + + st.remember(path, node); + expect(st.find(new Path("a/b")).isEmpty()).to.equal(false); + expect(st.find(new Path("a"))).to.equal(null); + }); + + + it("Find inside an existing snapshot", function () { + var st = new SparseSnapshotTree(); + var path = new Path("t/tt"); + var node = nodeFromJSON({ a: "sdfsd", x: 5, "999i": true }); + node = node.updateImmediateChild("apples", nodeFromJSON({ "goats": 88 })); + st.remember(path, node); + + expect(st.find(new Path("t/tt")).isEmpty()).to.equal(false); + expect(st.find(new Path("t/tt/a")).val()).to.equal("sdfsd"); + expect(st.find(new Path("t/tt/999i")).val()).to.equal(true); + expect(st.find(new Path("t/tt/apples")).isEmpty()).to.equal(false); + expect(st.find(new Path("t/tt/apples/goats")).val()).to.equal(88); + }); + + + it("Write a snapshot inside a snapshot.", function () { + var st = new SparseSnapshotTree(); + st.remember(new Path("t"), nodeFromJSON({ a: { b: "v" } })); + st.remember(new Path("t/a/rr"), nodeFromJSON(19)); + expect(st.find(new Path("t/a/b")).val()).to.equal("v"); + expect(st.find(new Path("t/a/rr")).val()).to.equal(19); + }); + + + it("Write a null value and confirm it is remembered.", function () { + var st = new SparseSnapshotTree(); + st.remember(new Path("awq/fff"), nodeFromJSON(null)); + expect(st.find(new Path("awq/fff"))).to.equal(ChildrenNode.EMPTY_NODE); + expect(st.find(new Path("awq/sdf"))).to.equal(null); + expect(st.find(new Path("awq/fff/jjj"))).to.equal(ChildrenNode.EMPTY_NODE); + expect(st.find(new Path("awq/sdf/sdf/q"))).to.equal(null); + }); + + + it("Overwrite with null and confirm it is remembered.", function () { + var st = new SparseSnapshotTree(); + st.remember(new Path("t"), nodeFromJSON({ a: { b: "v" } })); + expect(st.find(new Path("t")).isEmpty()).to.equal(false); + st.remember(new Path("t"), ChildrenNode.EMPTY_NODE); + expect(st.find(new Path("t")).isEmpty()).to.equal(true); + }); + + + it("Simple remember and forget.", function () { + var st = new SparseSnapshotTree(); + st.remember(new Path("t"), nodeFromJSON({ a: { b: "v" } })); + expect(st.find(new Path("t")).isEmpty()).to.equal(false); + st.forget(new Path("t")); + expect(st.find(new Path("t"))).to.equal(null); + }); + + + it("Forget the root.", function () { + var st = new SparseSnapshotTree(); + st.remember(new Path("t"), nodeFromJSON({ a: { b: "v" } })); + expect(st.find(new Path("t")).isEmpty()).to.equal(false); + st.forget(new Path("")); + expect(st.find(new Path("t"))).to.equal(null); + }); + + + it("Forget snapshot inside snapshot.", function () { + var st = new SparseSnapshotTree(); + st.remember(new Path("t"), nodeFromJSON({ a: { b: "v", c: 9, art: false } })); + expect(st.find(new Path("t/a/c")).isEmpty()).to.equal(false); + expect(st.find(new Path("t")).isEmpty()).to.equal(false); + + st.forget(new Path("t/a/c")); + expect(st.find(new Path("t"))).to.equal(null); + expect(st.find(new Path("t/a"))).to.equal(null); + expect(st.find(new Path("t/a/b")).val()).to.equal("v"); + expect(st.find(new Path("t/a/c"))).to.equal(null); + expect(st.find(new Path("t/a/art")).val()).to.equal(false); + }); + + + it("Forget path shallower than snapshots.", function () { + var st = new SparseSnapshotTree(); + st.remember(new Path("t/x1"), nodeFromJSON(false)); + st.remember(new Path("t/x2"), nodeFromJSON(true)); + st.forget(new Path("t")); + expect(st.find(new Path("t"))).to.equal(null); + }); + + + it("Iterate children.", function () { + var st = new SparseSnapshotTree(); + st.remember(new Path("t"), nodeFromJSON({ b: "v", c: 9, art: false })); + st.remember(new Path("q"), ChildrenNode.EMPTY_NODE); + + var num = 0, gotT = false, gotQ = false; + st.forEachChild(function(key, child) { + num += 1; + if (key === "t") { + gotT = true; + } else if (key === "q") { + gotQ = true; + } else { + expect(false).to.equal(true); + } + }); + + expect(gotT).to.equal(true); + expect(gotQ).to.equal(true); + expect(num).to.equal(2); + }); + + + it("Iterate trees.", function () { + var st = new SparseSnapshotTree(); + + var count = 0; + st.forEachTree(new Path(""), function(path, tree) { + count += 1; + }); + expect(count).to.equal(0); + + st.remember(new Path("t"), nodeFromJSON(1)); + st.remember(new Path("a/b"), nodeFromJSON(2)); + st.remember(new Path("a/x/g"), nodeFromJSON(3)); + st.remember(new Path("a/x/null"), nodeFromJSON(null)); + + var num = 0, got1 = false, got2 = false, got3 = false, got4 = false; + st.forEachTree(new Path("q"), function(path, node) { + num += 1; + var pathString = path.toString(); + if (pathString === "/q/t") { + got1 = true; + expect(node.val()).to.equal(1); + } else if (pathString === "/q/a/b") { + got2 = true; + expect(node.val()).to.equal(2); + } else if (pathString === "/q/a/x/g") { + got3 = true; + expect(node.val()).to.equal(3); + } else if (pathString === "/q/a/x/null") { + got4 = true; + expect(node.val()).to.equal(null); + } else { + expect(false).to.equal(true); + } + }); + + expect(got1).to.equal(true); + expect(got2).to.equal(true); + expect(got3).to.equal(true); + expect(got4).to.equal(true); + expect(num).to.equal(4); + }); + + it("Set leaf, then forget deeper path", function() { + var st = new SparseSnapshotTree(); + + st.remember(new Path('foo'), nodeFromJSON('bar')); + var safeToRemove = st.forget(new Path('foo/baz')); + // it's not safe to remove this node + expect(safeToRemove).to.equal(false); + }); + +}); diff --git a/tests/database/transaction.test.ts b/tests/database/transaction.test.ts new file mode 100644 index 00000000000..d266a146893 --- /dev/null +++ b/tests/database/transaction.test.ts @@ -0,0 +1,1238 @@ +import { expect } from "chai"; +import { Reference } from "../../src/database/api/Reference"; +import { + canCreateExtraConnections, + getFreshRepoFromReference, + getRandomNode, + getVal, +} from "./helpers/util"; +import { eventTestHelper } from "./helpers/events"; +import { EventAccumulator, EventAccumulatorFactory } from "./helpers/EventAccumulator"; +import { hijackHash } from "../../src/database/api/test_access"; +import firebase from "../../src/app"; +import "../../src/database"; + +// declare var runs; +// declare var waitsFor; +declare var TEST_TIMEOUT; + +describe('Transaction Tests', function() { + it('New value is immediately visible.', function() { + var node = (getRandomNode() as Reference); + node.child('foo').transaction(function() { + return 42; + }); + + var val = null; + node.child('foo').on('value', function(snap) { + val = snap.val(); + }); + expect(val).to.equal(42); + }); + + it.skip('Event is raised for new value.', function() { + var node = (getRandomNode() as Reference); + var fooNode = node.child('foo'); + var eventHelper = eventTestHelper([ + [fooNode, ['value', '']] + ]); + + node.child('foo').transaction(function() { + return 42; + }); + + expect(eventHelper.waiter()).to.equal(true); + }); + + it('Non-aborted transaction sets committed to true in callback.', function(done) { + var node = (getRandomNode() as Reference); + + node.transaction(function() { + return 42; + }, + function(error, committed, snapshot) { + expect(error).to.equal(null); + expect(committed).to.equal(true); + expect(snapshot.val()).to.equal(42); + done(); + }); + }); + + it('Aborted transaction sets committed to false in callback.', function(done) { + var node = (getRandomNode() as Reference); + + node.transaction(function() {}, + function(error, committed, snapshot) { + expect(error).to.equal(null); + expect(committed).to.equal(false); + expect(snapshot.val()).to.be.null; + done(); + }); + }); + + it('Tetris bug test - set data, reconnect, do transaction that aborts once data arrives, verify correct events.', + async function() { + var nodePair = (getRandomNode(2) as Reference[]); + var node = nodePair[0]; + var dataWritten = false; + var eventsReceived = 0; + const ea = EventAccumulatorFactory.waitsForCount(2); + + await node.child('foo').set(42); + + node = nodePair[1]; + node.child('foo').on('value', function(snap) { + if (eventsReceived === 0) { + expect(snap.val()).to.equal('temp value'); + } + else if (eventsReceived === 1) { + expect(snap.val()).to.equal(42); + } + else { + // Extra event detected. + expect(true).to.equal(false); + } + eventsReceived++; + ea.addEvent(); + }); + + node.child('foo').transaction(function(value) { + if (value === null) + return 'temp value'; + else + return; + }, function(error, committed, snapshot) { + expect(error).to.equal(null); + expect(committed).to.equal(false); + expect(snapshot.val()).to.equal(42); + }); + + return ea.promise; + }); + + it('Use transaction to create a node, make sure exactly one event is received.', function() { + var node = (getRandomNode() as Reference); + var events = 0, done = false; + + const ea = new EventAccumulator(() => done && events === 1); + + node.child('a').on('value', function() { + events++; + ea.addEvent(); + if (events > 1) throw 'Expected 1 event on a, but got two.'; + }); + + node.child('a').transaction(function() { + return 42; + }, function() { + done = true; + ea.addEvent(); + }); + + return ea.promise; + }); + + it('Use transaction to update one of two existing child nodes. ' + + 'Make sure events are only raised for the changed node.', async function() { + var nodePair = (getRandomNode(2) as Reference[]); + var node = nodePair[0].child('foo'); + var writesDone = 0; + + await Promise.all([ + node.child('a').set(42), + node.child('b').set(42) + ]); + + node = nodePair[1].child('foo'); + const eventHelper = eventTestHelper([ + [node.child('a'), ['value', '']], + [node.child('b'), ['value', '']] + ]); + + await eventHelper.promise; + + eventHelper.addExpectedEvents([ + [node.child('b'), ['value', '']] + ]); + + const transaction = node.transaction(function() { + return {a: 42, b: 87}; + }, function(error, committed, snapshot) { + expect(error).to.be.null; + expect(committed).to.equal(true); + expect(snapshot.val()).to.deep.equal({a: 42, b: 87}); + }); + + return Promise.all([ + eventHelper.promise, + transaction + ]); + }); + + it('Transaction is only called once when initializing an empty node.', function() { + var node = (getRandomNode() as Reference); + var updateCalled = 0; + + const ea = EventAccumulatorFactory.waitsForCount(1); + node.transaction(function(value) { + expect(value).to.equal(null); + updateCalled++; + ea.addEvent(); + if (updateCalled > 1) + throw 'Transaction called too many times.'; + + if (value === null) { + return { a: 5, b: 3 }; + } + }); + + return ea.promise; + }); + + it('Second transaction gets run immediately on previous output and only runs once.', function(done) { + var nodePair = (getRandomNode(2) as Reference[]); + var firstRun = false, firstDone = false, secondRun = false, secondDone = false; + + function onComplete() { + if (firstDone && secondDone) { + nodePair[1].on('value', function(snap) { + expect(snap.val()).to.equal(84); + done(); + }); + } + } + + nodePair[0].transaction(function() { + expect(firstRun).to.equal(false); + firstRun = true; + return 42; + }, function(error, committed, snapshot) { + expect(error).to.equal(null); + expect(committed).to.equal(true); + firstDone = true; + onComplete(); + }); + expect(firstRun).to.equal(true); + + nodePair[0].transaction(function(value) { + expect(secondRun).to.equal(false); + secondRun = true; + expect(value).to.equal(42); + return 84; + }, function(error, committed, snapshot) { + expect(error).to.equal(null); + expect(committed).to.equal(true); + secondDone = true; + onComplete(); + }); + expect(secondRun).to.equal(true); + + expect(getVal(nodePair[0])).to.equal(84); + }); + + it('Set() cancels pending transactions and re-runs affected transactions.', async function() { + // We do 3 transactions: 1) At /foo, 2) At /, and 3) At /bar. + // Only #1 is sent to the server immediately (since 2 depends on 1 and 3 depends on 2). + // We set /foo to 0. + // Transaction #1 should complete as planned (since it was already sent). + // Transaction #2 should be aborted by the set. + // Transaction #3 should be re-run after #2 is reverted, and then be sent to the server and succeed. + var firstDone = false, secondDone = false, thirdDone = false; + var node = (getRandomNode() as Reference); + var nodeSnap = null; + var nodeFooSnap = null; + + node.on('value', function(s) { + var str = JSON.stringify(s.val()); + nodeSnap = s; + }); + node.child('foo').on('value', function(s) { + var str = JSON.stringify(s.val()); + nodeFooSnap = s; + }); + + + var firstRun = false, secondRun = false, thirdRunCount = 0; + const ea = new EventAccumulator(() => firstDone && thirdDone); + node.child('foo').transaction( + function() { + expect(firstRun).to.equal(false); + firstRun = true; + return 42; + }, + function(error, committed, snapshot) { + expect(error).to.equal(null); + expect(committed).to.equal(true); + expect(snapshot.val()).to.equal(42); + firstDone = true; + ea.addEvent(); + }); + expect(nodeFooSnap.val()).to.deep.equal(42); + + node.transaction( + function() { + expect(secondRun).to.equal(false); + secondRun = true; + return { 'foo' : 84, 'bar' : 1}; + }, + function(error, committed, snapshot) { + expect(committed).to.equal(false); + secondDone = true; + ea.addEvent(); + } + ); + expect(secondRun).to.equal(true); + expect(nodeSnap.val()).to.deep.equal({'foo': 84, 'bar': 1}); + + node.child('bar').transaction(function(val) { + thirdRunCount++; + if (thirdRunCount === 1) { + expect(val).to.equal(1); + return 'first'; + } else if (thirdRunCount === 2) { + expect(val).to.equal(null); + return 'second'; + } else { + throw new Error('Called too many times!'); + } + }, function(error, committed, snapshot) { + expect(error).to.equal(null); + expect(committed).to.equal(true); + expect(snapshot.val()).to.equal('second'); + thirdDone = true; + ea.addEvent(); + }); + expect(thirdRunCount).to.equal(1); + expect(nodeSnap.val()).to.deep.equal({'foo' : 84, 'bar': 'first'}); + + // This rolls back the second transaction, and triggers a re-run of the third. + // However, a new value event won't be triggered until the listener is complete, + // so we're left with the last value event + node.child('foo').set(0); + + expect(firstDone).to.equal(false); + expect(secondDone).to.equal(true); + expect(thirdRunCount).to.equal(2); + // Note that the set actually raises two events, one overlaid on top of the original transaction value, and a + // second one with the re-run value from the third transaction + + await ea.promise; + + expect(nodeSnap.val()).to.deep.equal({'foo' : 0, 'bar': 'second'}); + }); + + it('transaction(), set(), set() should work.', function(done) { + var ref = (getRandomNode() as Reference); + ref.transaction(function(curr) { + expect(curr).to.equal(null); + return 'hi!'; + }, function(error, committed) { + expect(error).to.equal(null); + expect(committed).to.equal(true); + done(); + }); + + ref.set('foo'); + ref.set('bar'); + }); + + it('Priority is preserved when setting data.', async function() { + var node = (getRandomNode() as Reference), complete = false; + var snap; + node.on('value', function(s) { snap = s; }); + node.setWithPriority('test', 5); + expect(snap.getPriority()).to.equal(5); + + const promise = node.transaction( + function() { return 'new value'}, + function() { complete = true; } + ); + + expect(snap.val()).to.equal('new value'); + expect(snap.getPriority()).to.equal(5); + + await promise; + expect(snap.getPriority()).to.equal(5); + }); + + it('Tetris bug test - Can do transactions from transaction callback.', async function() { + var nodePair = (getRandomNode(2) as Reference[]), writeDone = false; + await nodePair[0].child('foo').set(42); + + var transactionTwoDone = false; + + var node = nodePair[1]; + + return new Promise(resolve => { + node.child('foo').transaction(function(val) { + if (val === null) + return 84; + }, function() { + node.child('bar').transaction(function(val) { + resolve(); + return 168; + }); + }); + }) + }); + + it('Resulting snapshot is passed to onComplete callback.', async function() { + var nodePair = (getRandomNode(2) as Reference[]); + var done = false; + await nodePair[0].transaction(function(v) { + if (v === null) + return 'hello!'; + }, function(error, committed, snapshot) { + expect(error).to.equal(null); + expect(committed).to.equal(true); + expect(snapshot.val()).to.equal('hello!'); + }); + + // Do it again for the aborted case. + await nodePair[0].transaction(function(v) { + if (v === null) + return 'hello!'; + }, function(error, committed, snapshot) { + expect(committed).to.equal(false); + expect(snapshot.val()).to.equal('hello!'); + }); + + // Do it again on a fresh connection, for the aborted case. + await nodePair[1].transaction(function(v) { + if (v === null) + return 'hello!'; + }, function(error, committed, snapshot) { + expect(committed).to.equal(false); + expect(snapshot.val()).to.equal('hello!'); + }); + }); + + it('Transaction aborts after 25 retries.', function(done) { + var restoreHash = hijackHash(function() { + return 'duck, duck, goose.'; + }); + + var node = (getRandomNode() as Reference); + var tries = 0; + node.transaction(function(curr) { + expect(tries).to.be.lessThan(25); + tries++; + return 'hello!'; + }, function(error, committed, snapshot) { + expect(error.message).to.equal('maxretry'); + expect(committed).to.equal(false); + expect(tries).to.equal(25); + restoreHash(); + done(); + }); + }); + + it('Set should cancel already sent transactions that come back as datastale.', function(done) { + var nodePair = (getRandomNode(2) as Reference[]); + var transactionCalls = 0; + nodePair[0].set(5, function() { + nodePair[1].transaction(function(old) { + expect(transactionCalls).to.equal(0); + expect(old).to.equal(null); + transactionCalls++; + return 72; + }, function(error, committed, snapshot) { + expect(error.message).to.equal('set'); + expect(committed).to.equal(false); + done(); + }); + + // Transaction should get sent but fail due to stale data, and then aborted because of the below set(). + nodePair[1].set(32); + }); + }); + + it('Update should not cancel unrelated transactions', async function() { + var node = (getRandomNode() as Reference); + var fooTransactionDone = false; + var barTransactionDone = false; + var restoreHash = hijackHash(function() { + return 'foobar'; + }); + + await node.child('foo').set(5); + + // 'foo' gets overwritten in the update so the transaction gets cancelled. + node.child('foo').transaction(function(old) { + return 72; + }, function(error, committed, snapshot) { + expect(error.message).to.equal('set'); + expect(committed).to.equal(false); + fooTransactionDone = true; + }); + + // 'bar' does not get touched during the update and the transaction succeeds. + node.child('bar').transaction(function(old) { + return 72; + }, function(error, committed, snapshot) { + expect(error).to.equal(null); + expect(committed).to.equal(true); + barTransactionDone = true; + }); + + await node.update({ + 'foo': 'newValue', + 'boo': 'newValue', + 'loo' : { + 'doo' : { + 'boo': 'newValue' + } + } + }); + + expect(fooTransactionDone).to.equal(true); + expect(barTransactionDone).to.equal(false); + restoreHash(); + }); + + it('Test transaction on wacky unicode data.', function(done) { + var nodePair = (getRandomNode(2) as Reference[]); + nodePair[0].set('♜♞♝♛♚♝♞♜', function() { + nodePair[1].transaction(function(current) { + if (current !== null) + expect(current).to.equal('♜♞♝♛♚♝♞♜'); + return '♖♘♗♕♔♗♘♖'; + }, function(error, committed, snapshot) { + expect(error).to.equal(null); + expect(committed).to.equal(true); + done(); + }); + }); + }); + + it('Test immediately aborted transaction.', function(done) { + var node = (getRandomNode() as Reference); + // without callback. + node.transaction(function(curr) { + return; + }); + + // with callback. + node.transaction(function(curr) { + return; + }, function(error, committed, snapshot) { + expect(committed).to.equal(false); + done(); + }); + }); + + it('Test adding to an array with a transaction.', function(done) { + var node = (getRandomNode() as Reference); + node.set(['cat', 'horse'], function() { + node.transaction(function(current) { + if (current) { + current.push('dog'); + } else { + current = ['dog']; + } + return current; + }, function(error, committed, snapshot) { + expect(error).to.equal(null); + expect(committed).to.equal(true); + expect(snapshot.val()).to.deep.equal(['cat', 'horse', 'dog']); + done(); + }); + }); + }); + + it('Merged transactions have correct snapshot in onComplete.', async function() { + var nodePair = (getRandomNode(2) as Reference[]), node1 = nodePair[0], node2 = nodePair[1]; + var transaction1Done, transaction2Done; + await node1.set({a: 0}); + + const tx1 = node2.transaction(function(val) { + if (val !== null) { + expect(val).to.deep.equal({a: 0}); + } + return {a: 1}; + }, function(error, committed, snapshot) { + expect(error).to.equal(null); + expect(committed).to.equal(true); + expect(snapshot.key).to.equal(node2.key); + // Per new behavior, will include the accepted value of the transaction, if it was successful. + expect(snapshot.val()).to.deep.equal({a: 1}); + transaction1Done = true; + }); + + const tx2 = node2.child('a').transaction(function(val) { + if (val !== null) { + expect(val).to.equal(1); // should run after the first transaction. + } + return 2; + }, function(error, committed, snapshot) { + expect(error).to.equal(null) + expect(committed).to.equal(true); + expect(snapshot.key).to.equal('a'); + expect(snapshot.val()).to.deep.equal(2); + transaction2Done = true; + }); + + return Promise.all([ tx1, tx2 ]) + }); + + it('Doing set() in successful transaction callback works. Case 870.', function(done) { + var node = (getRandomNode() as Reference); + var transactionCalled = false; + var callbackCalled = false; + node.transaction(function(val) { + expect(transactionCalled).to.not.be.ok; + transactionCalled = true; + return 'hi'; + }, function() { + expect(callbackCalled).to.not.be.ok; + callbackCalled = true; + node.set('transaction done', function() { + done(); + }); + }); + }); + + it('Doing set() in aborted transaction callback works. Case 870.', function(done) { + var nodePair = (getRandomNode(2) as Reference[]), node1 = nodePair[0], node2 = nodePair[1]; + + node1.set('initial', function() { + var transactionCalled = false; + var callbackCalled = false; + node2.transaction(function(val) { + // Return dummy value until we're called with the actual current value. + if (val === null) + return 'hi'; + + expect(transactionCalled).to.not.be.ok; + transactionCalled = true; + return; + }, function(error, committed, snapshot) { + expect(callbackCalled).to.not.be.ok; + callbackCalled = true; + node2.set('transaction done', function() { + done(); + }); + }); + }); + }); + + it('Pending transactions are canceled on disconnect.', function(done) { + var ref = (getRandomNode() as Reference); + + // wait to be connected and some data set. + ref.set('initial', function() { + ref.transaction(function(current) { + return 'new'; + }, function(error, committed, snapshot) { + expect(committed).to.equal(false); + expect(error.message).to.equal('disconnect'); + done(); + }); + + // Kill the connection, which should cancel the outstanding transaction, since we don't know if it was + // committed on the server or not. + ref.database.goOffline(); + ref.database.goOnline(); + }); + }); + + it('Transaction without local events (1)', async function() { + var ref = (getRandomNode() as Reference), actions = []; + let ea = EventAccumulatorFactory.waitsForCount(1); + + ref.on('value', function(s) { + actions.push('value ' + s.val()); + ea.addEvent(); + }); + + await ea.promise; + + ea = new EventAccumulator(() => actions.length >= 4); + + ref.transaction(function() { + return 'hello!'; + }, function(error, committed, snapshot) { + expect(error).to.be.null; + expect(committed).to.equal(true); + expect(snapshot.val()).to.equal('hello!'); + + actions.push('txn completed'); + ea.addEvent(); + }, /*applyLocally=*/false); + + // Shouldn't have gotten any events yet. + expect(actions).to.deep.equal(['value null']); + actions.push('txn run'); + ea.addEvent(); + + await ea.promise; + + expect(actions).to.deep.equal(['value null', 'txn run', 'value hello!', 'txn completed']); + }); + + // This test is meant to ensure that with applyLocally=false, while the transaction is outstanding, we continue + // to get events from other clients. + it('Transaction without local events (2)', function(done) { + var refPair = (getRandomNode(2) as Reference[]), ref1 = refPair[0], ref2 = refPair[1]; + var restoreHash = hijackHash(function() { return 'badhash'; }); + var SETS = 4; + var events = [], retries = 0, setsDone = 0; + var ready = false; + + function txn1(next) { + // Do a transaction on the first connection which will keep retrying (cause we hijacked the hash). + // Make sure we're getting events for the sets happening on the second connection. + ref1.transaction(function(current) { + retries++; + // We should be getting server events while the transaction is outstanding. + for (var i = 0; i < (current || 0); i++) { + expect(events[i]).to.equal(i); + } + + if (current === SETS - 1) { + restoreHash(); + } + return 'txn result'; + }, function(error, committed, snapshot) { + expect(error).to.equal(null); + expect(committed).to.equal(true); + + expect(snapshot && snapshot.val()).to.equal('txn result'); + next() + }, /*applyLocally=*/false); + + + // Meanwhile, do sets from the second connection. + var doSet = function() { + ref2.set(setsDone, function() { + setsDone++; + if (setsDone < SETS) + doSet(); + }); + }; + doSet(); + } + + ref1.set(0, function() { + ref1.on('value', function(snap) { + events.push(snap.val()); + if (events.length === 1 && events[0] === 0) { + txn1(function() { + // Sanity check stuff. + expect(setsDone).to.equal(SETS); + if (retries === 0) + throw 'Transaction should have had to retry!'; + + // Validate we got the correct events. + for (var i = 0; i < SETS; i++) { + expect(events[i]).to.equal(i); + } + expect(events[SETS]).to.equal('txn result'); + + restoreHash(); + done(); + }); + } + }); + }); + }); + + it('Transaction from value callback.', function(done) { + var ref = (getRandomNode() as Reference); + var COUNT = 1; + ref.on('value', function(snap) { + var shouldCommit = true; + ref.transaction(function(current) { + if (current == null) { + return 0; + } else if (current < COUNT) { + return current + 1; + } else { + shouldCommit = false; + } + + if (snap.val() === COUNT) { + done(); + } + }, function(error, committed, snap) { + expect(committed).to.equal(shouldCommit); + }); + }); + }); + + it('Transaction runs on null only once after reconnect (Case 1981).', async function() { + if (!canCreateExtraConnections()) return; + + var ref = (getRandomNode() as Reference); + await ref.set(42); + var newRef = getFreshRepoFromReference(ref); + var run = 0; + return newRef.transaction(function(curr) { + run++; + if (run === 1) { + expect(curr).to.equal(null); + } else if (run === 2) { + expect(curr).to.equal(42); + } + return 3.14; + }, function(error, committed, resultSnapshot) { + expect(error).to.equal(null); + expect(committed).to.equal(true); + expect(run).to.equal(2); + expect(resultSnapshot.val()).to.equal(3.14); + }); + }); + + // Provided by bk@thinkloop.com, this was failing when we sent puts before listens, but passes now. + it('makeFriends user test case.', function() { + const ea = EventAccumulatorFactory.waitsForCount(12); + if (!canCreateExtraConnections()) return; + + function makeFriends(accountID, friendAccountIDs, firebase) { + var friendAccountID, + i; + + // add friend relationships + for (i in friendAccountIDs) { + if (friendAccountIDs.hasOwnProperty(i)) { + friendAccountID = friendAccountIDs[i]; + makeFriend(friendAccountID, accountID, firebase); + makeFriend(accountID, friendAccountID, firebase); + } + } + } + + function makeFriend(accountID, friendAccountID, firebase) { + firebase.child(accountID).child(friendAccountID).transaction(function(r) { + if (r == null) { + r = { accountID: accountID, friendAccountID: friendAccountID, percentCommon: 0 }; + } + + return r; + }, + function(error, committed, snapshot) { + if (error) { + throw error; + } + else if (!committed) { + throw 'All should be committed!'; + } + else { + count++; + ea.addEvent(); + snapshot.ref.setPriority(snapshot.val().percentCommon); + } + }, false); + } + + var firebase = (getRandomNode() as Reference); + firebase.database.goOffline(); + firebase.database.goOnline(); + var count = 0; + makeFriends('a1', ['a2', 'a3'], firebase); + makeFriends('a2', ['a1', 'a3'], firebase); + makeFriends('a3', ['a1', 'a2'], firebase); + return ea.promise; + }); + + it('transaction() respects .priority.', function(done) { + var ref = (getRandomNode() as Reference); + var values = []; + ref.on('value', function(s) { values.push(s.exportVal()); }); + + ref.transaction(function(curr) { + expect(curr).to.equal(null); + return {'.value': 5, '.priority': 5}; + }, function() { + ref.transaction(function(curr) { + expect(curr).to.equal(5); + return {'.value': 10, '.priority': 10 }; + }, function() { + expect(values).to.deep.equal([ + {'.value': 5, '.priority': 5}, + {'.value': 10, '.priority': 10} + ]); + done(); + }); + }); + }); + + it('Transaction properly reverts data when you add a deeper listen.', function(done) { + var refPair = (getRandomNode(2) as Reference[]), ref1 = refPair[0], ref2 = refPair[1]; + var gotTest; + ref1.child('y').set('test', function() { + ref2.transaction(function(curr) { + if (curr === null) { + return { x: 1 }; + } + }); + + ref2.child('y').on('value', function(s) { + if (s.val() === 'test') { + done(); + }; + }); + }); + }); + + it('Transaction with integer keys', function(done) { + var ref = (getRandomNode() as Reference); + ref.set({1: 1, 5: 5, 10: 10, 20: 20}, function() { + ref.transaction(function(current) { + return 42; + }, function(error, committed) { + expect(error).to.be.null; + expect(committed).to.equal(true); + done(); + }); + }); + }); + + it('Return null from first run of transaction.', function(done) { + var ref = (getRandomNode() as Reference); + ref.transaction(function(c) { + return null; + }, function(error, committed) { + expect(error).to.equal(null); + expect(committed).to.equal(true); + done(); + }); + }); + + // https://app.asana.com/0/5673976843758/9259161251948 + it('Bubble-app transaction bug.', function(done) { + var ref = (getRandomNode() as Reference); + ref.child('a').transaction(function() { + return 1; + }); + ref.child('a').transaction(function(current) { + return current + 42; + }); + ref.child('b').transaction(function() { + return 7; + }); + ref.transaction(function(current) { + if (current && current.a && current.b) { + return current.a + current.b; + } else { + return 'dummy'; + } + }, function(error, committed, snap) { + expect(error).to.equal(null); + expect(committed).to.equal(true); + expect(snap.val()).to.deep.equal(50); + done(); + }); + }); + + it('Transaction and priority: Can set priority in transaction on empty node', async function() { + var ref = (getRandomNode() as Reference); + var done = false; + + await ref.transaction(function(current) { + return { '.value': 42, '.priority': 7 }; + }); + + return ref.once('value', function(s) { + expect(s.exportVal()).to.deep.equal({ '.value': 42, '.priority': 7}); + }); + }); + + it("Transaction and priority: Transaction doesn't change priority.", async function() { + var ref = (getRandomNode() as Reference); + var done = false; + + await ref.set({ '.value': 42, '.priority': 7 }); + + await ref.transaction(function(current) { + return 12; + }); + + const snap = await ref.once('value'); + + expect(snap.exportVal()).to.deep.equal({ '.value': 12, '.priority': 7}); + }); + + it('Transaction and priority: Transaction can change priority on non-empty node.', async function() { + var ref = (getRandomNode() as Reference); + var done = false; + + await ref.set({ '.value': 42, '.priority': 7 }); + + await ref.transaction(function(current) { + return { '.value': 43, '.priority': 8 }; + }, function() { + done = true; + }); + + return ref.once('value', function(s) { + expect(s.exportVal()).to.deep.equal({ '.value': 43, '.priority': 8}); + }); + }); + + it('Transaction and priority: Changing priority on siblings.', async function() { + var ref = (getRandomNode() as Reference); + var done = false, done2 = false; + + await ref.set({ + a: { '.value': 'a', '.priority': 'a' }, + b: { '.value': 'b', '.priority': 'b' } + }); + + const tx1 = ref.child('a').transaction(function(current) { + return { '.value': 'a2', '.priority': 'a2' }; + }); + + const tx2 = ref.child('b').transaction(function(current) { + return { '.value': 'b2', '.priority': 'b2' }; + }); + + await Promise.all([tx1, tx2]); + + return ref.once('value', function(s) { + expect(s.exportVal()).to.deep.equal({ a: { '.value': 'a2', '.priority': 'a2' }, b: { '.value': 'b2', '.priority': 'b2' } }); + }); + }); + + it('Transaction and priority: Leaving priority on siblings.', async function() { + var ref = (getRandomNode() as Reference); + var done = false, done2 = false; + + await ref.set({a: {'.value': 'a', '.priority': 'a'}, b: {'.value': 'b', '.priority': 'b'}}); + + const tx1 = ref.child('a').transaction(function(current) { + return 'a2'; + }); + + const tx2 = ref.child('b').transaction(function(current) { + return 'b2'; + }); + + await Promise.all([tx1, tx2]); + + return ref.once('value', function(s) { + expect(s.exportVal()).to.deep.equal({ a: { '.value': 'a2', '.priority': 'a' }, b: { '.value': 'b2', '.priority': 'b' } }); + }); + }); + + it('transaction() doesn\'t pick up cached data from previous once().', function(done) { + var refPair = (getRandomNode(2) as Reference[]); + var me = refPair[0], other = refPair[1]; + me.set('not null', function() { + me.once('value', function(snapshot) { + other.set(null, function() { + me.transaction(function(snapshot) { + if (snapshot === null) { + return 'it was null!'; + } else { + return 'it was not null!'; + } + }, function(err, committed, snapshot) { + expect(err).to.equal(null); + expect(committed).to.equal(true); + expect(snapshot.val()).to.deep.equal('it was null!'); + done(); + }); + }); + }); + }); + }); + + it('transaction() doesn\'t pick up cached data from previous transaction.', function(done) { + var refPair = (getRandomNode(2) as Reference[]); + var me = refPair[0], other = refPair[1]; + me.transaction(function() { + return 'not null'; + }, function(err, committed) { + expect(err).to.equal(null); + expect(committed).to.equal(true); + other.set(null, function() { + me.transaction(function(snapshot) { + if (snapshot === null) { + return 'it was null!'; + } else { + return 'it was not null!'; + } + }, function(err, committed, snapshot) { + expect(err).to.equal(null); + expect(committed).to.equal(true); + expect(snapshot.val()).to.deep.equal('it was null!'); + done(); + }); + }); + }); + }); + + it("server values: local timestamp should eventually (but not immediately) match the server with txns", function(done) { + var refPair = (getRandomNode(2) as Reference[]), + writer = refPair[0], + reader = refPair[1], + readSnaps = [], writeSnaps = []; + + var evaluateCompletionCriteria = function() { + if (readSnaps.length === 1 && writeSnaps.length === 2) { + expect(Math.abs(new Date().getTime() - writeSnaps[0].val()) < 10000).to.equal(true); + expect(Math.abs(new Date().getTime() - writeSnaps[0].getPriority()) < 10000).to.equal(true); + expect(Math.abs(new Date().getTime() - writeSnaps[1].val()) < 10000).to.equal(true); + expect(Math.abs(new Date().getTime() - writeSnaps[1].getPriority()) < 10000).to.equal(true); + + expect(writeSnaps[0].val() === writeSnaps[1].val()).to.equal(false); + expect(writeSnaps[0].getPriority() === writeSnaps[1].getPriority()).to.equal(false); + expect(writeSnaps[1].val() === readSnaps[0].val()).to.equal(true); + expect(writeSnaps[1].getPriority() === readSnaps[0].getPriority()).to.equal(true); + done(); + } + }; + + // 1st non-null event = actual server timestamp + reader.on('value', function(snap) { + if (snap.val() === null) return; + readSnaps.push(snap); + evaluateCompletionCriteria(); + }); + + // 1st non-null event = local timestamp estimate + // 2nd non-null event = actual server timestamp + writer.on('value', function(snap) { + if (snap.val() === null) return; + writeSnaps.push(snap); + evaluateCompletionCriteria(); + }); + + // Generate the server value offline to make sure there's a time gap between the client's guess of the timestamp + // and the server's actual timestamp. + writer.database.goOffline(); + + writer.transaction(function(current) { + return { + '.value' : firebase.database.ServerValue.TIMESTAMP, + '.priority' : firebase.database.ServerValue.TIMESTAMP + }; + }); + + writer.database.goOnline(); + }); + + it("transaction() still works when there's a query listen.", function(done) { + var ref = (getRandomNode() as Reference); + + ref.set({ + a: 1, + b: 2 + }, function() { + ref.limitToFirst(1).on('child_added', function() {}); + + ref.child('a').transaction(function(current) { + return current; + }, function(error, committed, snapshot) { + expect(error).to.equal(null); + expect(committed).to.equal(true); + if (!error) { + expect(snapshot.val()).to.deep.equal(1); + } + done(); + }, false); + }); + }); + + it("transaction() on queried location doesn't run initially on null (firebase-worker-queue depends on this).", + function(done) { + var ref = (getRandomNode() as Reference); + ref.push({ a: 1, b: 2}, function() { + ref.startAt().limitToFirst(1).on('child_added', function(snap) { + snap.ref.transaction(function(current) { + expect(current).to.deep.equal({a: 1, b: 2}); + return null; + }, function(error, committed, snapshot) { + expect(error).to.equal(null); + expect(committed).to.equal(true); + expect(snapshot.val()).to.equal(null); + done(); + }); + }); + }); + }); + + it('transactions raise correct child_changed events on queries', async function() { + var ref = (getRandomNode() as Reference); + + var value = { foo: { value: 1 } }; + var txnDone = false; + var snapshots = []; + + await ref.set(value) + + var query = ref.endAt(Number.MIN_VALUE); + query.on('child_added', function(snapshot) { + snapshots.push(snapshot); + }); + + query.on('child_changed', function(snapshot) { + snapshots.push(snapshot); + }); + + await ref.child('foo').transaction(function(current) { + return {value: 2}; + }, function(error, committed, snapshot) { + expect(error).to.equal(null); + expect(committed).to.equal(true); + }, false); + + expect(snapshots.length).to.equal(2); + var addedSnapshot = snapshots[0]; + expect(addedSnapshot.key).to.equal('foo'); + expect(addedSnapshot.val()).to.deep.equal({ value: 1 }); + var changedSnapshot = snapshots[1]; + expect(changedSnapshot.key).to.equal('foo'); + expect(changedSnapshot.val()).to.deep.equal({ value: 2 }); + }); + + it('transactions can use local merges', function(done) { + var ref = (getRandomNode() as Reference); + + ref.update({'foo': 'bar'}); + + ref.child('foo').transaction(function(current) { + expect(current).to.equal('bar'); + return current; + }, function(error, committed, snapshot) { + expect(error).to.equal(null); + expect(committed).to.equal(true); + done(); + }); + }); + + it('transactions works with merges without the transaction path', function(done) { + var ref = (getRandomNode() as Reference); + + ref.update({'foo': 'bar'}); + + ref.child('non-foo').transaction(function(current) { + expect(current).to.equal(null); + return current; + }, function(error, committed, snapshot) { + expect(error).to.equal(null); + expect(committed).to.equal(true); + done(); + }); + }); + + //See https://app.asana.com/0/15566422264127/23303789496881 + it('out of order remove writes are handled correctly', function(done) { + var ref = (getRandomNode() as Reference); + + ref.set({foo: 'bar'}); + ref.transaction(function() { + return 'transaction-1'; + }, function() { }); + ref.transaction(function() { + return 'transaction-2'; + }, function() { }); + + // This will trigger an abort of the transaction which should not cause the client to crash + ref.update({qux: 'quu' }, function(error) { + expect(error).to.equal(null); + done(); + }); + }); +}); diff --git a/tests/package/binary/browser/binary_namespace.test.ts b/tests/package/binary/browser/binary_namespace.test.ts index a423a5ab42d..216742a3e57 100644 --- a/tests/package/binary/browser/binary_namespace.test.ts +++ b/tests/package/binary/browser/binary_namespace.test.ts @@ -23,7 +23,7 @@ import { FirebaseNamespace } from "../../../../src/app/firebase_app"; import { firebaseSpec } from "../../utils/definitions/firebase"; import { storageInstanceSpec } from "../../utils/definitions/storage"; import { authInstanceSpec } from "../../utils/definitions/auth"; -import { compiledMessagingInstanceSpec } from "../../utils/definitions/messaging"; +import { messagingInstanceSpec } from "../../utils/definitions/messaging"; import { databaseInstanceSpec } from "../../utils/definitions/database"; const appConfig = { @@ -66,7 +66,7 @@ describe('Binary Namespace Test', () => { }); describe('firebase.messaging() Verification', () => { it('firebase.messaging() should expose proper namespace', () => { - checkProps('firebase.messaging()', (firebase as any).messaging(), compiledMessagingInstanceSpec); + checkProps('firebase.messaging()', (firebase as any).messaging(), messagingInstanceSpec); }); }); }); diff --git a/tsconfig.json b/tsconfig.json index 671c4b66356..cb38bbcf432 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "dom" ], "module": "es2015", + "moduleResolution": "node", "noImplicitAny": false, "outDir": "dist/es2015", "rootDir": "src", diff --git a/tsconfig.test.json b/tsconfig.test.json index df5542946d6..3ea71ea523f 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -2,10 +2,12 @@ "extends": "./tsconfig.json", "compilerOptions": { "rootDir": ".", - "module": "CommonJS", + "module": "commonjs", "target": "es5", "allowJs": true, - "declaration": false + "declaration": false, + "outDir": "./dist", + "sourceMap": true }, "compileOnSave": true, "include": [ diff --git a/yarn.lock b/yarn.lock index 1da081e53ba..35c02035110 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1534,6 +1534,13 @@ create-hmac@^1.1.0, create-hmac@^1.1.2: create-hash "^1.1.0" inherits "^2.0.1" +cross-env@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.0.1.tgz#ff4e72ea43b47da2486b43a7f2043b2609e44913" + dependencies: + cross-spawn "^5.1.0" + is-windows "^1.0.0" + cross-spawn@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" @@ -1541,7 +1548,7 @@ cross-spawn@^4.0.2: lru-cache "^4.0.1" which "^1.2.9" -cross-spawn@^5.0.1: +cross-spawn@^5.0.1, cross-spawn@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" dependencies: @@ -3259,6 +3266,10 @@ is-windows@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c" +is-windows@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.1.tgz#310db70f742d259a16a369202b51af84233310d9" + isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"