UNPKG

21.6 kBJavaScriptView Raw
1/* global model, serverModel, subDoc, subQuery, subLocal, subValue, subApi */
2import { asyncImport, cleanup } from './_globals'
3import { expect } from 'chai'
4import { alias } from './util'
5import range from 'lodash/range'
6import './_server'
7import waitForExpect from 'wait-for-expect'
8import {
9 initSimple as initSimpleOrig,
10 initComplex,
11 tInitHooksSimple,
12 tInitHooksComplex,
13 convertToHooksSubscribeParams,
14 unmount
15} from './_helpers'
16
17const HOOKS = process.env.HOOKS
18const DEPRECATED = process.env.DEPRECATED
19const PREFIX = HOOKS
20 ? 'Hooks. '
21 : DEPRECATED ? 'Class [DEPRECATED]. ' : 'Class. '
22
23// Maybe change initSimple to Hooks-specific version
24const initSimple = HOOKS
25 ? async (initialProps, subscribeFn, params) => {
26 if (typeof initialProps === 'function') {
27 params = subscribeFn
28 subscribeFn = initialProps
29 initialProps = {}
30 }
31 let { renderCount = 0 } = params || {}
32 let w = await tInitHooksSimple(
33 initialProps,
34 convertToHooksSubscribeParams(subscribeFn)
35 )
36 await w.nextRender({ index: renderCount })
37 return w
38 }
39 : initSimpleOrig
40
41// Workaround to init rpc and subscribe only after the server started (which is a global before)
42before(asyncImport)
43
44// Unmount component after each test
45afterEach(cleanup)
46
47describe(PREFIX + 'Helpers', () => {
48 it('test RPC', async () => {
49 let w
50 await serverModel.set(`users.${alias(1)}.name`, alias(1))
51 w = await initSimple(() => ({ items: subDoc('users', alias(1)) }))
52 expect(w.items).to.include(alias(1))
53 unmount()
54
55 await serverModel.set(`users.${alias(1)}.name`, 'Abrakadabra')
56 w = await initSimple(() => ({ items: subDoc('users', alias(1)) }), {
57 renderCount: 1
58 })
59 expect(w.items).to.include('Abrakadabra')
60 unmount()
61
62 await serverModel.set(`users.${alias(1)}.name`, alias(1))
63 w = await initSimple(() => ({ items: subDoc('users', alias(1)) }), {
64 renderCount: 1
65 })
66 expect(w.items).to.include(alias(1))
67 })
68})
69
70describe(PREFIX + 'Docs', () => {
71 it('doc by id', async () => {
72 let w = await initSimple(() => ({ items: subDoc('users', alias(3)) }))
73 expect(w.items)
74 .to.have.lengthOf(1)
75 .and.include(alias(3))
76 })
77
78 it('dynamic data update', async () => {
79 let w = await initSimple(() => ({ items: subDoc('users', alias(1)) }))
80 expect(w.items)
81 .to.have.lengthOf(1)
82 .and.include(alias(1))
83 let updateAndCheckName = async newName => {
84 serverModel.set(`users.${alias(1)}.name`, newName)
85 await w.nextRender()
86 expect(w.items)
87 .to.have.lengthOf(1)
88 .and.include(newName)
89 }
90 for (let i in range(50)) {
91 await updateAndCheckName(`TestUpdate${i}_`)
92 }
93 await updateAndCheckName(alias(1))
94 })
95})
96
97describe(PREFIX + 'Queries', () => {
98 it('all collection', async () => {
99 let w = await initSimple(() => ({ items: subQuery('users', {}) }))
100 expect(w.items)
101 .to.have.lengthOf(5)
102 .and.include.members(alias([1, 2, 3, 4, 5]))
103 })
104
105 it('parametrized 1', async () => {
106 let w = await initSimple(() => ({
107 items: subQuery('users', { color: 'blue' })
108 }))
109 expect(w.items)
110 .to.have.lengthOf(2)
111 .and.include.members(alias([1, 2]))
112 })
113
114 it('parametrized 2', async () => {
115 let w = await initSimple(() => ({
116 items: subQuery('users', { color: 'red' })
117 }))
118 expect(w.items)
119 .to.have.lengthOf(3)
120 .and.include.members(alias([3, 4, 5]))
121 })
122
123 it('dynamic data update', async () => {
124 let w = await initSimple(() => ({
125 items: subQuery('users', { color: 'red' })
126 }))
127 expect(w.items)
128 .to.have.lengthOf(3)
129 .and.include.members(alias([3, 4, 5]))
130 let updateAndCheckItems = async (index, color, indexes, decrease) => {
131 serverModel.set(`users.${alias(index)}.color`, color)
132 // Wait for 2 renders when the item is going to disappear from
133 // the query results.
134 // NOTE: 2 renderings are happening because when
135 // the data is changed in the item which is already loaded to
136 // the client-side model, it is not getting removed from
137 // the query result immediately since the doc ids are updated
138 // by query only from the server-side.
139 // So for some time the document which doesn't match the query
140 // anymore, will still be present in the array.
141 let renders = decrease ? 2 : 1
142 await w.nextRender(renders)
143 expect(w.items)
144 .to.have.lengthOf(indexes.length)
145 .and.include.members(alias(indexes))
146 }
147 await updateAndCheckItems(3, 'blue', [4, 5])
148 await updateAndCheckItems(4, 'blue', [5])
149 await updateAndCheckItems(1, 'red', [1, 5])
150 await updateAndCheckItems(3, 'red', [1, 3, 5])
151 await updateAndCheckItems(5, 'blue', [1, 3])
152 await updateAndCheckItems(1, 'blue', [3])
153 await updateAndCheckItems(3, 'blue', [])
154 await updateAndCheckItems(5, 'red', [5])
155 await updateAndCheckItems(3, 'red', [3, 5])
156 await updateAndCheckItems(4, 'red', [3, 4, 5])
157 })
158
159 it('dynamic update of query param', async () => {
160 let w = await initSimple({ color: 'red' }, ({ color }) => ({
161 items: subQuery('users', { color })
162 }))
163 expect(w.items)
164 .to.have.lengthOf(3)
165 .and.include.members(alias([3, 4, 5]))
166 for (let i = 0; i < 20; i++) {
167 // TODO: For some reason initially it makes extra renderings
168 // (for example when testing ONLY this test)
169 // In which case this works: 1 + (i === 0 ? 2 : 0)
170 // Debug why extra renderings are happening.
171 await w.nextRender(1, () => {
172 w.setProps({ color: 'blue' })
173 })
174 expect(w.items)
175 .to.have.lengthOf(2)
176 .and.include.members(alias([1, 2]))
177 await w.nextRender(1, () => {
178 w.setProps({ color: 'red' })
179 })
180 expect(w.items)
181 .to.have.lengthOf(3)
182 .and.include.members(alias([3, 4, 5]))
183 }
184 })
185})
186
187describe(PREFIX + 'Local', () => {
188 it('should synchronously get local data with the first render', async () => {
189 model.set('_page.document', { id: alias(1), name: alias(1) })
190 let w = await initSimple(() => ({ items: subLocal('_page.document') }), {
191 initWithData: true
192 })
193 expect(w.items)
194 .to.have.lengthOf(1)
195 .and.include(alias(1))
196 model.del('_page.document')
197 await w.nextRender({ index: 1 })
198 expect(w.items).to.have.lengthOf(0)
199 })
200
201 it('when starts from empty data, should update data as soon as it appears', async () => {
202 let w = await initSimple(() => ({ items: subLocal('_page.document') }), {
203 initWithData: true
204 })
205 expect(w.items).to.have.lengthOf(0)
206 model.set('_page.document', { id: alias(1), name: alias(1) })
207 await w.nextRender({ index: 1 })
208 expect(w.items)
209 .to.have.lengthOf(1)
210 .and.include(alias(1))
211 model.del('_page.document')
212 await w.nextRender({ index: 2 })
213 expect(w.items).to.have.lengthOf(0)
214 })
215
216 it('should update data', async () => {
217 let w = await initSimple(() => ({ items: subLocal('_page.document') }), {
218 initWithData: true
219 })
220 expect(w.items).to.have.lengthOf(0)
221
222 model.set('_page.document', { id: alias(1), name: alias(1) })
223 await w.nextRender({ index: 1 })
224 expect(w.items)
225 .to.have.lengthOf(1)
226 .and.include(alias(1))
227
228 model.set('_page.document', { id: alias(2), name: alias(2) })
229 await w.nextRender({ index: 2 })
230 expect(w.items)
231 .to.have.lengthOf(1)
232 .and.include(alias(2))
233
234 model.del('_page.document')
235 await w.nextRender({ index: 3 })
236 expect(w.items).to.have.lengthOf(0)
237
238 model.set('_page.document', { id: alias(3), name: alias(3) })
239 await w.nextRender({ index: 4 })
240 expect(w.items)
241 .to.have.lengthOf(1)
242 .and.include(alias(3))
243
244 model.set('_page.document.name', alias(4))
245 await w.nextRender({ index: 5 })
246 expect(w.items)
247 .to.have.lengthOf(1)
248 .and.include(alias(4))
249
250 model.del('_page.document')
251 await w.nextRender({ index: 6 })
252 expect(w.items).to.have.lengthOf(0)
253 })
254
255 it('should sync get data and then update it', async () => {
256 model.set('_page.document', { id: alias(1), name: alias(1) })
257
258 let w = await initSimple(() => ({ items: subLocal('_page.document') }), {
259 initWithData: true
260 })
261 expect(w.items)
262 .to.have.lengthOf(1)
263 .and.include(alias(1))
264
265 model.set('_page.document', { id: alias(2), name: alias(2) })
266 await w.nextRender({ index: 1 })
267 expect(w.items)
268 .to.have.lengthOf(1)
269 .and.include(alias(2))
270
271 model.del('_page.document')
272 await w.nextRender({ index: 2 })
273 expect(w.items).to.have.lengthOf(0)
274
275 model.set('_page.document', { id: alias(3), name: alias(3) })
276 await w.nextRender({ index: 3 })
277 expect(w.items)
278 .to.have.lengthOf(1)
279 .and.include(alias(3))
280
281 model.set('_page.document.name', alias(4))
282 await w.nextRender({ index: 4 })
283 expect(w.items)
284 .to.have.lengthOf(1)
285 .and.include(alias(4))
286
287 model.del('_page.document')
288 await w.nextRender({ index: 5 })
289 expect(w.items).to.have.lengthOf(0)
290 })
291})
292
293if (!DEPRECATED) {
294 describe.skip(PREFIX + 'Value', () => {
295 const getLocalPath = () => {
296 if (HOOKS) {
297 let docId = Object.keys(model.get('$hooks')).find(i => i !== '__FOO')
298 return `$hooks.${docId}`
299 } else {
300 let docId = Object.keys(model.get('$components')).find(
301 i => i !== '__FOO'
302 )
303 return `$components.${docId}.items`
304 }
305 }
306
307 it('update value', async () => {
308 let w = await initSimple(
309 () => ({ items: subValue({ id: alias(1), name: alias(1) }) }),
310 { initWithData: true }
311 )
312 expect(w.items)
313 .to.have.lengthOf(1)
314 .and.include(alias(1))
315
316 let localPath = getLocalPath()
317 model.set(localPath, { id: alias(2), name: alias(2) })
318 await w.nextRender({ index: 1 })
319 expect(w.items)
320 .to.have.lengthOf(1)
321 .and.include(alias(2))
322
323 model.del(localPath)
324 await w.nextRender({ index: 2 })
325 expect(w.items).to.have.lengthOf(0)
326
327 model.set(localPath, { id: alias(3), name: alias(3) })
328 await w.nextRender({ index: 3 })
329 expect(w.items)
330 .to.have.lengthOf(1)
331 .and.include(alias(3))
332
333 model.set(`${localPath}.name`, alias(4))
334 await w.nextRender({ index: 4 })
335 expect(w.items)
336 .to.have.lengthOf(1)
337 .and.include(alias(4))
338
339 model.del(localPath)
340 await w.nextRender({ index: 5 })
341 expect(w.items).to.have.lengthOf(0)
342 })
343 })
344
345 describe.skip(PREFIX + 'Api', () => {
346 it('should get data from the api', async () => {
347 let w = await initSimple(() => ({
348 items: subApi(
349 '_page.document',
350 index =>
351 new Promise(
352 resolve =>
353 setTimeout(() => {
354 resolve({ id: alias(index), name: alias(index) })
355 }),
356 500
357 ),
358 [1]
359 )
360 }))
361 let count = HOOKS ? 1 : 0
362 await w.nextRender({ index: count++ })
363 expect(w.items)
364 .to.have.lengthOf(1)
365 .and.include(alias(1))
366 model.setDiff('_page.document.name', alias(2))
367 await w.nextRender({ index: count++ })
368 expect(w.items)
369 .to.have.lengthOf(1)
370 .and.include(alias(2))
371 })
372
373 // TODO: Enzyme unmount doesn't trigger destruction for some reason.
374 // For now run this test only for hooks since they
375 // use react-test-renderer
376 if (HOOKS) {
377 it('should remove local path after destroy', async () => {
378 await new Promise(resolve => setTimeout(resolve, 500))
379 expect(model.get('_page.document')).to.be.an('undefined')
380 })
381 } else {
382 it('[cleanup] force clear _page.document from prev test', () => {
383 model.del('_page.document')
384 })
385 }
386 })
387}
388
389describe(PREFIX + 'Edge cases', () => {
390 it('initially null document. Then update to create it.', async () => {
391 let userId = alias(777)
392 let w = await initSimple(() => ({ items: subDoc('users', userId) }))
393 expect(w.items).to.have.lengthOf(0)
394 serverModel.add('users', {
395 id: userId,
396 name: userId
397 })
398 await w.nextRender()
399 expect(w.items)
400 .to.have.lengthOf(1)
401 .and.include(userId)
402 serverModel.set(`users.${userId}.name`, 'Abrakadabra')
403 await w.nextRender()
404 expect(w.items)
405 .to.have.lengthOf(1)
406 .and.include('Abrakadabra')
407 serverModel.set(`users.${userId}.name`, 'Blablabla')
408 await w.nextRender()
409 expect(w.items)
410 .to.have.lengthOf(1)
411 .and.include('Blablabla')
412 serverModel.del(`users.${userId}`)
413 await w.nextRender()
414 expect(w.items).to.have.lengthOf(0)
415 serverModel.add('users', {
416 id: userId,
417 name: userId
418 })
419 await w.nextRender()
420 expect(w.items)
421 .to.have.lengthOf(1)
422 .and.include(userId)
423 serverModel.set(`users.${userId}.name`, 'Abrakadabra')
424 await w.nextRender()
425 expect(w.items)
426 .to.have.lengthOf(1)
427 .and.include('Abrakadabra')
428 serverModel.del(`users.${userId}`)
429 await w.nextRender()
430 })
431
432 it('ref NON existent local document and ensure reactivity', async () => {
433 let w = await initSimple(() => ({ items: subLocal('_page.document') }), {
434 initWithData: true
435 })
436 expect(w.items).to.have.lengthOf(0)
437 await w.nextRender(() => {
438 model.set('_page.document', { id: 'document', name: 'document' })
439 })
440 expect(w.items)
441 .to.have.lengthOf(1)
442 .and.include('document')
443 await w.nextRender(() => model.set('_page.document.name', 'first'))
444 // await new Promise(resolve => setTimeout(resolve, 1000))
445 // await w.nextRender(3, () => {})
446 expect(w.items)
447 .to.have.lengthOf(1)
448 .and.include('first')
449 await w.nextRender(() => model.set('_page.document.name', 'second'))
450 expect(w.items)
451 .to.have.lengthOf(1)
452 .and.include('second')
453 model.del('_page.document')
454 // await w.nextRender(3, () => {})
455 })
456
457 it('ref an existing local document and ensure reactivity', async () => {
458 model.set('_page.document', { id: 'document', name: 'document' })
459 let w = await initSimple(() => ({ items: subLocal('_page.document') }), {
460 initWithData: true
461 })
462 expect(w.items)
463 .to.have.lengthOf(1)
464 .and.include('document')
465 await w.nextRender(() => model.set('_page.document.name', 'first'))
466 expect(w.items)
467 .to.have.lengthOf(1)
468 .and.include('first')
469 await w.nextRender(() => model.set('_page.document.name', 'second'))
470 expect(w.items)
471 .to.have.lengthOf(1)
472 .and.include('second')
473 model.del('_page.document')
474 })
475
476 it('should render only when changing something which was rendered before', async () => {
477 model.set('_page.document', { id: 'document', name: 'document' })
478 let w = await initSimple(() => ({ items: subLocal('_page.document') }), {
479 initWithData: true
480 })
481 expect(w.items)
482 .to.have.lengthOf(1)
483 .and.include('document')
484 await w.nextRender(() => {
485 model.set('_page.document.name', 'first')
486 })
487 expect(w.items)
488 .to.have.lengthOf(1)
489 .and.include('first')
490 await w.nextRender(() => {
491 model.set('_page.document.color', 'red')
492 model.set('_page.document.name', 'second')
493 })
494 expect(w.items)
495 .to.have.lengthOf(1)
496 .and.include('second')
497 await w.nextRender(() => {
498 model.set('_page.document.color', 'green')
499 model.set('_page.document.name', 'third')
500 })
501 expect(w.items)
502 .to.have.lengthOf(1)
503 .and.include('third')
504 await w.nextRender(() => {
505 model.set('_page.document.showColor', true)
506 })
507 expect(w.items)
508 .to.have.lengthOf(1)
509 .and.include('third')
510 await w.nextRender(() => {
511 model.set('_page.document.color', 'yellow')
512 })
513 expect(w.items)
514 .to.have.lengthOf(1)
515 .and.include('third')
516 await w.nextRender(2, () => {
517 model.set('_page.document.color', 'orange')
518 model.set('_page.document.name', 'fourth')
519 })
520 expect(w.items)
521 .to.have.lengthOf(1)
522 .and.include('fourth')
523 await w.nextRender(() => {
524 model.del('_page.document.showColor')
525 })
526 expect(w.items)
527 .to.have.lengthOf(1)
528 .and.include('fourth')
529 await w.nextRender(2, () => {
530 model.set('_page.document.color', 'grey')
531 model.set('_page.document.name', 'fifth')
532 model.set('_page.document.color', 'black')
533 model.set('_page.document.name', 'sixth')
534 })
535 expect(w.items)
536 .to.have.lengthOf(1)
537 .and.include('sixth')
538 model.del('_page.document')
539 })
540
541 it('model.setEach() should batch changes and only render once', async () => {
542 model.set('_page.document', {
543 id: 'document',
544 name: 'document',
545 showColor: true
546 })
547 let w = await initSimple(() => ({ items: subLocal('_page.document') }), {
548 initWithData: true
549 })
550 expect(w.items)
551 .to.have.lengthOf(1)
552 .and.include('document')
553 await w.nextRender(() => {
554 model.set('_page.document.color', 'grey')
555 })
556 expect(w.items)
557 .to.have.lengthOf(1)
558 .and.include('document')
559 await w.nextRender(3, () => {
560 model.setEach('_page.document', { color: 'green', name: 'first' })
561 model.setEach('_page.document', { color: 'black', name: 'second' })
562 model.setEach('_page.document', { color: 'yellow', name: 'third' })
563 })
564 expect(w.items)
565 .to.have.lengthOf(1)
566 .and.include('third')
567 model.del('_page.document')
568 })
569})
570
571// NON-hooks only
572
573if (!HOOKS) {
574 describe(PREFIX + 'Complex', () => {
575 it('multiple subscriptions. Query and Doc. Removal of keys.', async () => {
576 let w = await initComplex(
577 {
578 color0: 'red',
579 color1: 'blue'
580 },
581 ({ color0, color1, hasCar }) => {
582 let res = {
583 items0: color0 && subQuery('users', { color: color0 }),
584 items1: color1 && subQuery('users', { color: color1 })
585 }
586 if (hasCar) res.items2 = subDoc('cars', 'test1_')
587 return res
588 }
589 )
590 expect(w.items[0])
591 .to.have.lengthOf(3)
592 .and.include.members(alias([3, 4, 5]))
593 expect(w.items[1])
594 .to.have.lengthOf(2)
595 .and.include.members(alias([1, 2]))
596 expect(w.items[2]).to.have.lengthOf(0)
597 // 4 renders should happen: for props change and each item's setState
598
599 await w.setProps({
600 color0: 'blue',
601 color1: 'red',
602 hasCar: true
603 })
604
605 await waitForExpect(() => {
606 expect(w.items[0])
607 .to.have.lengthOf(2)
608 .and.include.members(alias([1, 2]))
609 expect(w.items[1])
610 .to.have.lengthOf(3)
611 .and.include.members(alias([3, 4, 5]))
612 expect(w.items[2])
613 .to.have.lengthOf(1)
614 .and.include.members(alias([1]))
615 })
616
617 // 1 render should happen: for props and removeItemData -- sync
618 await w.setProps({ hasCar: false })
619 await waitForExpect(() => {
620 expect(w.items[0])
621 .to.have.lengthOf(2)
622 .and.include.members(alias([1, 2]))
623 expect(w.items[1])
624 .to.have.lengthOf(3)
625 .and.include.members(alias([3, 4, 5]))
626 expect(w.items[2]).to.have.lengthOf(0)
627 })
628 await w.setProps({
629 color0: undefined,
630 color1: { $in: ['red', 'blue'] },
631 hasCar: true
632 })
633 await waitForExpect(() => {
634 expect(w.items[0]).to.have.lengthOf(0)
635 expect(w.items[1])
636 .to.have.lengthOf(5)
637 .and.include.members(alias([1, 2, 3, 4, 5]))
638 expect(w.items[2])
639 .to.have.lengthOf(1)
640 .and.include.members(alias([1]))
641 })
642 await w.setProps({
643 color0: 'red',
644 hasCar: false
645 })
646 await waitForExpect(() => {
647 expect(w.items[0])
648 .to.have.lengthOf(3)
649 .and.include.members(alias([3, 4, 5]))
650 expect(w.items[1])
651 .to.have.lengthOf(5)
652 .and.include.members(alias([1, 2, 3, 4, 5]))
653 expect(w.items[2]).to.have.lengthOf(0)
654 })
655 })
656 })
657}
658
659// Hooks only
660
661if (HOOKS) {
662 describe(PREFIX + 'Complex', () => {
663 it('basic', async () => {
664 let items = [
665 'user',
666 'game1',
667 'game2',
668 'players1',
669 'players2',
670 'usersInGame1',
671 'usersInGame2'
672 ]
673 let w = await tInitHooksComplex()
674 // Go through a bunch of Suspense loading undil the component
675 // is rendered with the initial count of 0.
676 // Suspense renders it with 9999
677 // TODO: Test the amount of times suspense renders
678 await w.nextRender({ index: 0 })
679
680 expect(Object.keys(w.items))
681 .to.have.lengthOf(items.length)
682 .and.include.members(items)
683
684 expect(w.items.user)
685 .to.have.lengthOf(1)
686 .and.include(alias(1))
687
688 expect(w.items.game1)
689 .to.have.lengthOf(1)
690 .and.include(alias(1))
691 expect(w.items.game2)
692 .to.have.lengthOf(1)
693 .and.include(alias(2))
694
695 expect(w.items.players1)
696 .to.have.lengthOf(2)
697 .and.include.members(alias([1, 2]))
698 expect(w.items.players2)
699 .to.have.lengthOf(3)
700 .and.include.members(alias([1, 3, 5]))
701
702 expect(w.items.usersInGame1)
703 .to.have.lengthOf(2)
704 .and.include.members(alias([1, 2]))
705 expect(w.items.usersInGame2)
706 .to.have.lengthOf(3)
707 .and.include.members(alias([1, 3, 5]))
708 })
709 })
710}