Skip to content

Commit c51999a

Browse files
committed
WIP: refactor EventAccumulator.ts for data.test.ts
1 parent 342d527 commit c51999a

File tree

4 files changed

+294
-50
lines changed

4 files changed

+294
-50
lines changed
Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
1+
export const EventAccumulatorFactory = {
2+
waitsForCount: maxCount => {
3+
let count = 0;
4+
const condition = () => ea.eventData.length >= count;
5+
const ea = new EventAccumulator(condition)
6+
ea.onReset(() => { count = 0; });
7+
ea.onEvent(() => { count++; });
8+
return ea;
9+
}
10+
}
11+
112
export class EventAccumulator {
213
public eventData = [];
314
public promise;
415
public resolve;
516
public reject;
6-
constructor(private expectedEvents: number) {
7-
if (!this.expectedEvents) throw new Error('EventAccumulator:You must pass a number of expected events to the constructor');
17+
private onResetFxn;
18+
private onEventFxn;
19+
constructor(public condition: Function) {
820
this.promise = new Promise((resolve, reject) => {
921
this.resolve = resolve;
1022
this.reject = reject;
@@ -15,16 +27,27 @@ export class EventAccumulator {
1527
...this.eventData,
1628
eventData
1729
];
18-
if (this.eventData.length >= this.expectedEvents) {
30+
if (typeof this.onEventFxn === 'function') this.onEventFxn();
31+
if (this._testCondition()) {
1932
this.resolve(this.eventData);
2033
}
2134
}
22-
reset(expectedEvents?: number) {
23-
this.expectedEvents = expectedEvents || this.expectedEvents;
35+
reset(condition?: Function) {
2436
this.eventData = [];
2537
this.promise = new Promise((resolve, reject) => {
2638
this.resolve = resolve;
2739
this.reject = reject;
2840
});
41+
if (typeof this.onResetFxn === 'function') this.onResetFxn();
42+
if (typeof condition === 'function') this.condition = condition;
43+
}
44+
onEvent(cb: Function) {
45+
this.onEventFxn = cb;
46+
}
47+
onReset(cb: Function) {
48+
this.onResetFxn = cb;
49+
}
50+
_testCondition() {
51+
return this.condition();
2952
}
3053
}

tests/database/helpers/events.ts

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { EventAccumulator } from './EventAccumulator';
2+
import { TEST_PROJECT } from "./util";
3+
4+
/**
5+
* A set of functions to clean up event handlers.
6+
* @type {function()}
7+
*/
8+
export let eventCleanupHandlers = [];
9+
10+
11+
/** Clean up outstanding event handlers */
12+
export function eventCleanup() {
13+
for (var i = 0; i < eventCleanupHandlers.length; ++i) {
14+
eventCleanupHandlers[i]();
15+
}
16+
eventCleanupHandlers = [];
17+
};
18+
19+
/**
20+
* The path component of the firebaseRef url
21+
* @param {Firebase} firebaseRef
22+
* @return {string}
23+
*/
24+
function rawPath(firebaseRef) {
25+
return firebaseRef.toString().replace(TEST_PROJECT.databaseURL, '');
26+
};
27+
28+
/**
29+
* Creates a struct which waits for many events.
30+
* @param {Array<Array>} pathAndEvents an array of tuples of [Firebase, [event type strings]]
31+
* @param {string=} opt_helperName
32+
* @return {{waiter: waiter, watchesInitializedWaiter: watchesInitializedWaiter, unregister: unregister, addExpectedEvents: addExpectedEvents}}
33+
*/
34+
export function eventTestHelper(pathAndEvents, helperName?, eventAccumulator?: EventAccumulator) {
35+
var expectedPathAndEvents = [];
36+
var actualPathAndEvents = [];
37+
var pathEventListeners = {};
38+
var initializationEvents = 0;
39+
40+
helperName = helperName ? helperName + ': ' : '';
41+
42+
// Listen on all of the required paths, with a callback function that just
43+
// appends to actualPathAndEvents.
44+
var make_eventCallback = function(type) {
45+
return function(snap) {
46+
console.log('evt', type);
47+
// Get the ref of where the snapshot came from.
48+
var ref = type === 'value' ? snap.ref : snap.ref.parent;
49+
eventAccumulator && eventAccumulator.addEvent && eventAccumulator.addEvent();
50+
actualPathAndEvents.push([rawPath(ref), [type, snap.key]]);
51+
52+
if (!pathEventListeners[ref].initialized) {
53+
initializationEvents++;
54+
if (type === 'value') {
55+
pathEventListeners[ref].initialized = true;
56+
}
57+
} else {
58+
// Call waiter here to trigger exceptions when the event is fired, rather than later when the
59+
// test framework is calling the waiter... makes for easier debugging.
60+
waiter();
61+
}
62+
};
63+
};
64+
65+
// returns a function which indicates whether the events have been received
66+
// in the correct order. If anything is wrong (too many events or
67+
// incorrect events, we throw). Else we return false, indicating we should
68+
// keep waiting.
69+
var waiter = function() {
70+
var pathAndEventToString = function(pathAndEvent) {
71+
return '{path: ' + pathAndEvent[0] + ', event:[' + pathAndEvent[1][0] + ', ' + pathAndEvent[1][1] + ']}';
72+
};
73+
74+
var i = 0;
75+
while (i < expectedPathAndEvents.length && i < actualPathAndEvents.length) {
76+
var expected = expectedPathAndEvents[i];
77+
var actual = actualPathAndEvents[i];
78+
79+
if (expected[0] != actual[0] || expected[1][0] != actual[1][0] || expected[1][1] != actual[1][1]) {
80+
throw helperName + 'Event ' + i + ' incorrect. Expected: ' + pathAndEventToString(expected) +
81+
' Actual: ' + pathAndEventToString(actual);
82+
}
83+
i++;
84+
}
85+
86+
if (expectedPathAndEvents.length < actualPathAndEvents.length) {
87+
throw helperName + "Extra event detected '" + pathAndEventToString(actualPathAndEvents[i]) + "'.";
88+
}
89+
90+
// If we haven't thrown and both arrays are the same length, then we're
91+
// done.
92+
return expectedPathAndEvents.length == actualPathAndEvents.length;
93+
};
94+
95+
var listenOnPath = function(path) {
96+
var valueCB = make_eventCallback('value');
97+
var addedCB = make_eventCallback('child_added');
98+
var removedCB = make_eventCallback('child_removed');
99+
var movedCB = make_eventCallback('child_moved');
100+
var changedCB = make_eventCallback('child_changed');
101+
path.on('child_removed', removedCB);
102+
path.on('child_added', addedCB);
103+
path.on('child_moved', movedCB);
104+
path.on('child_changed', changedCB);
105+
path.on('value', valueCB);
106+
return function() {
107+
path.off('child_removed', removedCB);
108+
path.off('child_added', addedCB);
109+
path.off('child_moved', movedCB);
110+
path.off('child_changed', changedCB);
111+
path.off('value', valueCB);
112+
}
113+
};
114+
115+
116+
var addExpectedEvents = function(pathAndEvents) {
117+
var pathsToListenOn = [];
118+
for (var i = 0; i < pathAndEvents.length; i++) {
119+
120+
var pathAndEvent = pathAndEvents[i];
121+
122+
var path = pathAndEvent[0];
123+
//var event = pathAndEvent[1];
124+
125+
pathsToListenOn.push(path);
126+
127+
pathAndEvent[0] = rawPath(path);
128+
129+
if (pathAndEvent[1][0] === 'value')
130+
pathAndEvent[1][1] = path.key;
131+
132+
expectedPathAndEvents.push(pathAndEvent);
133+
}
134+
135+
// There's some trickiness with event order depending on the order you attach event callbacks:
136+
//
137+
// 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
138+
// 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
139+
// getting events something like "a/b/c: value, a/b: child_added c, a: child_added b, a/b: value, a: value"
140+
//
141+
// BUT, if all of the listens happen before you are connected to firebase (e.g. this is the first test you're
142+
// running), the dedupe will have taken affect and we'll just send "listen a", which results in:
143+
// "a/b/c: value, a/b: child_added c, a/b: value, a: child_added b, a: value"
144+
// Notice the 3rd and 4th events are swapped.
145+
// To mitigate this, we re-ordeer your event registrations and do them in order of shortest path to longest.
146+
147+
pathsToListenOn.sort(function(a, b) { return a.toString().length - b.toString().length; });
148+
for (i = 0; i < pathsToListenOn.length; i++) {
149+
path = pathsToListenOn[i];
150+
if (!pathEventListeners[path.toString()]) {
151+
pathEventListeners[path.toString()] = { };
152+
pathEventListeners[path.toString()].initialized = false;
153+
pathEventListeners[path.toString()].unlisten = listenOnPath(path);
154+
}
155+
}
156+
};
157+
158+
addExpectedEvents(pathAndEvents);
159+
160+
var watchesInitializedWaiter = function() {
161+
for (var path in pathEventListeners) {
162+
if (!pathEventListeners[path].initialized)
163+
return false;
164+
}
165+
166+
// Remove any initialization events.
167+
actualPathAndEvents.splice(actualPathAndEvents.length - initializationEvents, initializationEvents);
168+
initializationEvents = 0;
169+
170+
return true;
171+
};
172+
173+
var unregister = function() {
174+
for (var path in pathEventListeners) {
175+
if (pathEventListeners.hasOwnProperty(path)) {
176+
pathEventListeners[path].unlisten();
177+
}
178+
}
179+
};
180+
181+
eventCleanupHandlers.push(unregister);
182+
return {
183+
waiter: waiter,
184+
watchesInitializedWaiter: watchesInitializedWaiter,
185+
unregister: unregister,
186+
187+
addExpectedEvents: function(moreEvents) {
188+
addExpectedEvents(moreEvents);
189+
}
190+
};
191+
};

tests/database/helpers/util.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { globalScope } from "../../../src/utils/globalScope";
12
import firebase from "../../../src/app";
23
import '../../../src/database';
34
import { Reference } from "../../../src/database/api/Reference";
@@ -47,7 +48,7 @@ export function patchFakeAuthFunctions(app) {
4748
* @param {string=} opt_ref
4849
* @return {Firebase}
4950
*/
50-
function getRootNode(i?, ref?) {
51+
export function getRootNode(i?, ref?) {
5152
if (i === undefined) {
5253
i = 0;
5354
}
@@ -180,4 +181,33 @@ export function getFreshRepoFromReference(ref) {
180181
var host = ref.root.toString();
181182
var path = ref.toString().replace(host, '');
182183
return getFreshRepo(host, path);
183-
}
184+
}
185+
186+
// Little helpers to get the currently cached snapshot / value.
187+
export function getSnap(path) {
188+
var snap;
189+
var callback = function(snapshot) { snap = snapshot; };
190+
path.once('value', callback);
191+
return snap;
192+
};
193+
194+
export function getVal(path) {
195+
var snap = getSnap(path);
196+
return snap ? snap.val() : undefined;
197+
};
198+
199+
export function canCreateExtraConnections() {
200+
return globalScope.MozWebSocket || globalScope.WebSocket;
201+
};
202+
203+
export function buildObjFromKey(key) {
204+
var keys = key.split('.');
205+
var obj = {};
206+
var parent = obj;
207+
for (var i = 0; i < keys.length; i++) {
208+
var key = keys[i];
209+
parent[key] = i < keys.length - 1 ? {} : 'test_value';
210+
parent = parent[key];
211+
}
212+
return obj;
213+
};

0 commit comments

Comments
 (0)