1 | import test from 'ava';
|
2 | import td from 'testdouble';
|
3 | import _ from 'lodash';
|
4 |
|
5 | import Coupler from './Coupler.js';
|
6 | import Bridge from './Bridge.js';
|
7 | import Dispatcher from './Dispatcher.js';
|
8 |
|
9 | td.verifyNoCalls = call => td.verify(call, {times: 0, ignoreExtraArgs: true});
|
10 |
|
11 | test.beforeEach(t => {
|
12 | t.context = {
|
13 | rootUrl: 'https://example.firebaseio.com',
|
14 | bridge: td.object(Bridge),
|
15 | dispatcher: td.object(Dispatcher),
|
16 | applySnapshot: td.function(),
|
17 | prunePath: td.function()
|
18 | };
|
19 | t.context.coupler = new Coupler(
|
20 | t.context.rootUrl, t.context.bridge, t.context.dispatcher, t.context.applySnapshot,
|
21 | t.context.prunePath
|
22 | );
|
23 | t.context.op1 = td.object({_disconnect: _.noop});
|
24 | t.context.op2 = td.object({});
|
25 | t.context.op3 = td.object({});
|
26 | t.context.verifyOn = (url, times = 1) => td.verify(t.context.bridge.on(
|
27 | url, url, null, 'value', td.matchers.isA(Function), td.matchers.isA(Function),
|
28 | td.matchers.anything(), {sync: true}
|
29 | ), {times});
|
30 | t.context.verifyOff = (url, times = 1) => td.verify(t.context.bridge.off(
|
31 | url, url, null, 'value', td.matchers.isA(Function), td.matchers.anything()
|
32 | ), {times});
|
33 | });
|
34 |
|
35 | test.afterEach(t => {
|
36 | t.context.coupler.destroy();
|
37 | });
|
38 |
|
39 | test('couple, decouple on root node', t => {
|
40 | const that = t.context.coupler;
|
41 | const url = t.context.rootUrl + '/';
|
42 | t.false(that.isTrunkCoupled('/'));
|
43 |
|
44 | that.couple('/', t.context.op1);
|
45 | t.is(that._root.count, 1);
|
46 | t.deepEqual(that._root.operations, [t.context.op1]);
|
47 | t.true(that._root.listening);
|
48 | t.true(that.isTrunkCoupled('/'));
|
49 | t.context.verifyOn(url);
|
50 | td.verifyNoCalls(t.context.bridge.off());
|
51 |
|
52 | that.decouple('/', t.context.op1);
|
53 | t.is(that._root.count, 0);
|
54 | t.deepEqual(that._root.operations, []);
|
55 | t.false(that._root.listening);
|
56 | t.false(that.isTrunkCoupled('/'));
|
57 | t.context.verifyOn(url);
|
58 | t.context.verifyOff(url);
|
59 | });
|
60 |
|
61 | test('couple, decouple, on root child', t => {
|
62 | const that = t.context.coupler;
|
63 | const url = t.context.rootUrl + '/foo';
|
64 | t.false(that.isTrunkCoupled('/foo'));
|
65 |
|
66 | that.couple('/foo', t.context.op1);
|
67 | t.is(that._root.children.foo.count, 1);
|
68 | t.true(that._root.children.foo.listening);
|
69 | t.true(that.isTrunkCoupled('/foo'));
|
70 | t.context.verifyOn(url);
|
71 | td.verifyNoCalls(t.context.bridge.off());
|
72 |
|
73 | that.decouple('/foo', t.context.op1);
|
74 | t.true(_.isEmpty(that._root.children));
|
75 | t.false(that.isTrunkCoupled('/foo'));
|
76 | t.context.verifyOn(url);
|
77 | t.context.verifyOff(url);
|
78 | });
|
79 |
|
80 | test('couple, decouple, on root descendant', t => {
|
81 | const that = t.context.coupler;
|
82 | const url = t.context.rootUrl + '/foo/bar';
|
83 | t.false(that.isTrunkCoupled('/foo/bar'));
|
84 |
|
85 | that.couple('/foo/bar', t.context.op1);
|
86 | t.is(that._root.children.foo.children.bar.count, 1);
|
87 | t.true(that._root.children.foo.children.bar.listening);
|
88 | t.true(that.isTrunkCoupled('/foo/bar'));
|
89 | t.context.verifyOn(url);
|
90 | td.verifyNoCalls(t.context.bridge.off());
|
91 |
|
92 | that.decouple('/foo/bar', t.context.op1);
|
93 | t.true(_.isEmpty(that._root.children));
|
94 | t.false(that.isTrunkCoupled('/foo/bar'));
|
95 | t.context.verifyOn(url);
|
96 | t.context.verifyOff(url);
|
97 | });
|
98 |
|
99 | test('multiple coupler on same node', t => {
|
100 | const that = t.context.coupler;
|
101 | const url = t.context.rootUrl + '/foo';
|
102 |
|
103 | that.couple('/foo', t.context.op1);
|
104 | that.couple('/foo', t.context.op2);
|
105 | t.is(that._root.children.foo.count, 2);
|
106 | t.deepEqual(that._root.children.foo.operations, [t.context.op1, t.context.op2]);
|
107 | t.true(that._root.children.foo.listening);
|
108 | t.true(that.isTrunkCoupled('/foo'));
|
109 | t.context.verifyOn(url);
|
110 | td.verifyNoCalls(t.context.bridge.off());
|
111 |
|
112 | that.decouple('/foo', t.context.op1);
|
113 | t.is(that._root.children.foo.count, 1);
|
114 | t.deepEqual(that._root.children.foo.operations, [t.context.op2]);
|
115 | t.true(that._root.children.foo.listening);
|
116 | t.true(that.isTrunkCoupled('/foo'));
|
117 | t.context.verifyOn(url);
|
118 | td.verifyNoCalls(t.context.bridge.off());
|
119 | });
|
120 |
|
121 | test('override child coupling', t => {
|
122 | const that = t.context.coupler;
|
123 | const rootUrl = t.context.rootUrl;
|
124 |
|
125 | that.couple('/foo/bar', t.context.op1);
|
126 | that.couple('/foo', t.context.op2);
|
127 | t.is(that._root.children.foo.count, 1);
|
128 | t.true(that._root.children.foo.listening);
|
129 | t.true(that.isTrunkCoupled('/foo'));
|
130 | t.is(that._root.children.foo.children.bar.count, 1);
|
131 | t.true(that._root.children.foo.children.bar.listening);
|
132 | t.true(that.isTrunkCoupled('/foo/bar'));
|
133 | t.context.verifyOn(rootUrl + '/foo/bar');
|
134 | t.context.verifyOn(rootUrl + '/foo');
|
135 |
|
136 | that._root.children.foo._handleSnapshot(td.object({path: '/foo'}));
|
137 | t.false(that._root.children.foo.children.bar.listening);
|
138 | t.context.verifyOff(rootUrl + '/foo/bar');
|
139 |
|
140 | that.decouple('/foo', t.context.op2);
|
141 | t.is(that._root.children.foo.count, 0);
|
142 | t.false(that._root.children.foo.listening);
|
143 | t.false(that.isTrunkCoupled('/foo'));
|
144 | t.is(that._root.children.foo.children.bar.count, 1);
|
145 | t.true(that._root.children.foo.children.bar.listening);
|
146 | t.true(that.isTrunkCoupled('/foo/bar'));
|
147 | t.context.verifyOn(rootUrl + '/foo/bar', 2);
|
148 | t.context.verifyOn(rootUrl + '/foo');
|
149 | t.context.verifyOff(rootUrl + '/foo/bar');
|
150 | t.context.verifyOff(rootUrl + '/foo');
|
151 | });
|
152 |
|
153 | test('superseded coupling', t => {
|
154 | const that = t.context.coupler;
|
155 | const rootUrl = t.context.rootUrl;
|
156 |
|
157 | that.couple('/foo', t.context.op1);
|
158 | that.couple('/foo/bar', t.context.op2);
|
159 | t.is(that._root.children.foo.count, 1);
|
160 | t.true(that._root.children.foo.listening);
|
161 | t.true(that.isTrunkCoupled('/foo'));
|
162 | t.is(that._root.children.foo.children.bar.count, 1);
|
163 | t.falsy(that._root.children.foo.children.bar.listening);
|
164 | t.true(that.isTrunkCoupled('/foo/bar'));
|
165 | t.context.verifyOn(rootUrl + '/foo');
|
166 | t.context.verifyOff(rootUrl + '/foo', 0);
|
167 | t.context.verifyOn(rootUrl + '/foo/bar', 0);
|
168 | t.context.verifyOff(rootUrl + '/foo/bar', 0);
|
169 | });
|
170 |
|
171 | test('uncoupled parents with coupled children are not deleted', t => {
|
172 | const that = t.context.coupler;
|
173 |
|
174 | that.couple('/foo', t.context.op1);
|
175 | that.couple('/foo/bar', t.context.op2);
|
176 | that.couple('/foo/baz', t.context.op3);
|
177 | that.decouple('/foo/bar', t.context.op2);
|
178 | t.is(that._root.children.foo.children.baz.count, 1);
|
179 | });
|
180 |
|
181 | test('handle snapshot', t => {
|
182 | const that = t.context.coupler;
|
183 |
|
184 | that.couple('/foo/bar', t.context.op1);
|
185 | const node = that._root.children.foo.children.bar;
|
186 | t.falsy(node.ready);
|
187 | node._handleSnapshot({path: '/foo/bar'});
|
188 | t.true(node.ready);
|
189 | node._handleSnapshot({path: '/foo/bar/baz'});
|
190 | node._handleSnapshot({path: '/foo'});
|
191 |
|
192 | td.verify(t.context.applySnapshot({path: '/foo/bar'}), {times: 1});
|
193 | td.verify(t.context.applySnapshot({path: '/foo/bar/baz'}), {times: 1});
|
194 | td.verify(t.context.applySnapshot({path: '/foo'}), {times: 0});
|
195 | });
|
196 |
|
197 | test('handle error', t => {
|
198 | const that = t.context.coupler;
|
199 | const error = new Error('test');
|
200 |
|
201 | that.couple('/foo/bar', t.context.op1);
|
202 | that.couple('/foo/bar/baz', t.context.op2);
|
203 | const bar = that._root.children.foo.children.bar;
|
204 | const baz = bar.children.baz;
|
205 |
|
206 | t.is(bar.count, 1);
|
207 | t.true(bar.listening);
|
208 | t.is(baz.count, 1);
|
209 | t.falsy(baz.listening);
|
210 |
|
211 | bar._handleSnapshot({path: '/foo/bar'});
|
212 | t.true(bar.ready);
|
213 |
|
214 | baz._handleError(error);
|
215 |
|
216 | t.is(bar.count, 1);
|
217 | t.true(bar.listening);
|
218 | t.is(baz.count, 1);
|
219 | t.falsy(baz.listening);
|
220 |
|
221 | td.when(t.context.dispatcher.retry(t.context.op1, error), {times: 1}).thenResolve(true);
|
222 | const handlerPromise = bar._handleError(error);
|
223 | t.false(bar.ready);
|
224 | t.false(baz.ready);
|
225 | t.false(bar.listening);
|
226 |
|
227 | return handlerPromise.then(() => {
|
228 | t.true(bar.listening);
|
229 | bar._handleSnapshot({path: '/foo/bar'});
|
230 | t.true(bar.ready);
|
231 |
|
232 | td.when(t.context.dispatcher.retry(t.context.op1, error), {times: 1}).thenResolve(false);
|
233 | td.when(t.context.op1._disconnect(error), {times: 1}).thenDo(() => {
|
234 | that.decouple('/foo/bar', t.context.op1);
|
235 | });
|
236 | return bar._handleError(error);
|
237 |
|
238 | }).then(() => {
|
239 | t.false(bar.listening);
|
240 | t.is(bar.count, 0);
|
241 | t.true(baz.listening);
|
242 | t.is(baz.count, 1);
|
243 | });
|
244 | });
|