Files
Josh SorefandGitHub b4e60981a5 Spelling (#8280)
Numerous fixes to spelling across the SDK.
2024-07-18 16:53:00 -04:00

259 lines
7.8 KiB
TypeScript

/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { pathParent } from '../../../database/src/core/util/Path';
import { Reference } from '../../src/api/Reference';
import { TEST_PROJECT } from './util';
/**
* A set of functions to clean up event handlers.
*/
export let eventCleanupHandlers = [];
/** Clean up outstanding event handlers */
export function eventCleanup() {
for (let i = 0; i < eventCleanupHandlers.length; ++i) {
eventCleanupHandlers[i]();
}
eventCleanupHandlers = [];
}
/**
* The path component of the firebaseRef url
*/
function rawPath(firebaseRef: Reference) {
return firebaseRef.toString().replace(TEST_PROJECT.databaseURL, '');
}
/**
* Creates a struct which waits for many events.
* @param pathAndEvents - an array of tuples of [Firebase, [event type strings]]
*/
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;
});
const expectedPathAndEvents = [];
const actualPathAndEvents = [];
const pathEventListeners = {};
let initializationEvents = 0;
helperName = helperName ? helperName + ': ' : '';
// Listen on all of the required paths, with a callback function that just
// appends to actualPathAndEvents.
const makeEventCallback = function (type) {
return function (snap) {
// Get the ref of where the snapshot came from.
const ref = type === 'value' ? snap.ref : pathParent(this);
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.
const waiter = function () {
const pathAndEventToString = function (pathAndEvent) {
return (
'{path: ' +
pathAndEvent[0] +
', event:[' +
pathAndEvent[1][0] +
', ' +
pathAndEvent[1][1] +
']}'
);
};
let i = 0;
while (i < expectedPathAndEvents.length && i < actualPathAndEvents.length) {
const expected = expectedPathAndEvents[i];
const 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;
};
const listenOnPath = function (path) {
const valueCB = makeEventCallback('value');
const addedCB = makeEventCallback('child_added');
const removedCB = makeEventCallback('child_removed');
const movedCB = makeEventCallback('child_moved');
const changedCB = makeEventCallback('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);
};
};
const addExpectedEvents = function (pathAndEvents) {
const pathsToListenOn = [];
for (let i = 0; i < pathAndEvents.length; i++) {
const pathAndEvent = pathAndEvents[i];
const 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-order your event registrations and do them in order of shortest path to longest.
pathsToListenOn.sort((a, b) => {
return a.toString().length - b.toString().length;
});
for (let i = 0; i < pathsToListenOn.length; i++) {
const 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);
const watchesInitializedWaiter = function () {
for (const path in pathEventListeners) {
if (!pathEventListeners[path].initialized) {
return false;
}
}
// Remove any initialization events.
actualPathAndEvents.splice(
actualPathAndEvents.length - initializationEvents,
initializationEvents
);
initializationEvents = 0;
resolveInit();
return true;
};
const unregister = function () {
for (const path in pathEventListeners) {
if (pathEventListeners.hasOwnProperty(path)) {
pathEventListeners[path].unlisten();
}
}
};
eventCleanupHandlers.push(unregister);
return {
promise,
initPromise,
waiter,
watchesInitializedWaiter,
unregister,
addExpectedEvents(moreEvents) {
addExpectedEvents(moreEvents);
}
};
}