Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
81155 views
1
/**
2
* Copyright 2013-2014, Facebook, Inc.
3
* All rights reserved.
4
*
5
* This source code is licensed under the BSD-style license found in the
6
* LICENSE file in the root directory of this source tree. An additional grant
7
* of patent rights can be found in the PATENTS file in the same directory.
8
*
9
* @emails react-core
10
*/
11
12
"use strict";
13
14
require('mock-modules');
15
16
var React = require('React');
17
var ReactTestUtils = require('ReactTestUtils');
18
var ReactMount = require('ReactMount');
19
20
var mapObject = require('mapObject');
21
22
var stripEmptyValues = function(obj) {
23
var ret = {};
24
var name;
25
for (name in obj) {
26
if (!obj.hasOwnProperty(name)) {
27
continue;
28
}
29
if (obj[name] !== null && obj[name] !== undefined) {
30
ret[name] = obj[name];
31
}
32
}
33
return ret;
34
};
35
36
/**
37
* Child key names are wrapped like '.$key:0'. We strip the extra chars out
38
* here. This relies on an implementation detail of the rendering system.
39
*/
40
var getOriginalKey = function(childName) {
41
var match = childName.match(/^\.\$([^.]+)\:0$/);
42
expect(match).not.toBeNull();
43
return match[1];
44
};
45
46
/**
47
* Contains internal static internal state in order to test that updates to
48
* existing children won't reinitialize components, when moving children -
49
* reusing existing DOM/memory resources.
50
*/
51
var StatusDisplay = React.createClass({
52
getInitialState: function() {
53
return { internalState: Math.random() };
54
},
55
56
getStatus: function() {
57
return this.props.status;
58
},
59
60
getInternalState: function() {
61
return this.state.internalState;
62
},
63
64
render: function() {
65
return (
66
<div>
67
{this.state.internalState}
68
</div>
69
);
70
}
71
});
72
73
/**
74
* Displays friends statuses.
75
*/
76
var FriendsStatusDisplay = React.createClass({
77
/**
78
* Retrieves the rendered children in a nice format for comparing to the input
79
* `this.props.usernameToStatus`. Gets the order directly from each rendered
80
* child's `index` field. Refs are not maintained in the rendered order, and
81
* neither is `this._renderedChildren` (surprisingly).
82
*/
83
getStatusDisplays: function() {
84
var name;
85
var orderOfUsernames = [];
86
var statusDisplays = this._renderedComponent._renderedChildren;
87
for (name in statusDisplays) {
88
var child = statusDisplays[name];
89
var isPresent = !!child;
90
if (isPresent) {
91
orderOfUsernames[child._mountIndex] = getOriginalKey(name);
92
}
93
}
94
var res = {};
95
var i;
96
for (i = 0; i < orderOfUsernames.length; i++) {
97
var key = orderOfUsernames[i];
98
res[key] = this.refs[key];
99
}
100
return res;
101
},
102
render: function() {
103
var children = null;
104
var key;
105
for (key in this.props.usernameToStatus) {
106
var status = this.props.usernameToStatus[key];
107
children = children || {};
108
children[key] = !status ? null :
109
<StatusDisplay ref={key} status={status} />;
110
}
111
return (
112
<div>
113
{children}
114
</div>
115
);
116
}
117
});
118
119
120
function getInteralStateByUserName(statusDisplays) {
121
return mapObject(statusDisplays, function(statusDisplay, key) {
122
return statusDisplay.getInternalState();
123
});
124
}
125
126
/**
127
* Verifies that the rendered `StatusDisplay` instances match the `props` that
128
* were responsible for allocating them. Checks the content of the user's status
129
* message as well as the order of them.
130
*/
131
function verifyStatuses(statusDisplays, props) {
132
var nonEmptyStatusDisplays = stripEmptyValues(statusDisplays);
133
var nonEmptyStatusProps = stripEmptyValues(props.usernameToStatus);
134
var username;
135
expect(Object.keys(nonEmptyStatusDisplays).length)
136
.toEqual(Object.keys(nonEmptyStatusProps).length);
137
for (username in nonEmptyStatusDisplays) {
138
if (!nonEmptyStatusDisplays.hasOwnProperty(username)) {
139
continue;
140
}
141
expect(nonEmptyStatusDisplays[username].getStatus())
142
.toEqual(nonEmptyStatusProps[username]);
143
}
144
145
// now go the other way to make sure we got them all.
146
for (username in nonEmptyStatusProps) {
147
if (!nonEmptyStatusProps.hasOwnProperty(username)) {
148
continue;
149
}
150
expect(nonEmptyStatusDisplays[username].getStatus())
151
.toEqual(nonEmptyStatusProps[username]);
152
}
153
154
expect(Object.keys(nonEmptyStatusDisplays))
155
.toEqual(Object.keys(nonEmptyStatusProps));
156
}
157
158
/**
159
* For all statusDisplays that existed in the previous iteration of the
160
* sequence, verify that the state has been preserved. `StatusDisplay` contains
161
* a unique number that allows us to track internal state across ordering
162
* movements.
163
*/
164
function verifyStatesPreserved(lastInternalStates, statusDisplays) {
165
var key;
166
for (key in statusDisplays) {
167
if (!statusDisplays.hasOwnProperty(key)) {
168
continue;
169
}
170
if (lastInternalStates[key]) {
171
expect(lastInternalStates[key])
172
.toEqual(statusDisplays[key].getInternalState());
173
}
174
}
175
}
176
177
178
/**
179
* Verifies that the internal representation of a set of `renderedChildren`
180
* accurately reflects what is in the DOM.
181
*/
182
function verifyDomOrderingAccurate(parentInstance, statusDisplays) {
183
var containerNode = parentInstance.getDOMNode();
184
var statusDisplayNodes = containerNode.childNodes;
185
var i;
186
var orderedDomIds = [];
187
for (i=0; i < statusDisplayNodes.length; i++) {
188
orderedDomIds.push(ReactMount.getID(statusDisplayNodes[i]));
189
}
190
191
var orderedLogicalIds = [];
192
var username;
193
for (username in statusDisplays) {
194
if (!statusDisplays.hasOwnProperty(username)) {
195
continue;
196
}
197
var statusDisplay = statusDisplays[username];
198
orderedLogicalIds.push(statusDisplay._rootNodeID);
199
}
200
expect(orderedDomIds).toEqual(orderedLogicalIds);
201
}
202
203
/**
204
* Todo: Check that internal state is preserved across transitions
205
*/
206
function testPropsSequence(sequence) {
207
var i;
208
var parentInstance = ReactTestUtils.renderIntoDocument(
209
<FriendsStatusDisplay {...sequence[0]} />
210
);
211
var statusDisplays = parentInstance.getStatusDisplays();
212
var lastInternalStates = getInteralStateByUserName(statusDisplays);
213
verifyStatuses(statusDisplays, sequence[0]);
214
215
for (i = 1; i < sequence.length; i++) {
216
parentInstance.replaceProps(sequence[i]);
217
statusDisplays = parentInstance.getStatusDisplays();
218
verifyStatuses(statusDisplays, sequence[i]);
219
verifyStatesPreserved(lastInternalStates, statusDisplays);
220
verifyDomOrderingAccurate(parentInstance, statusDisplays);
221
222
lastInternalStates = getInteralStateByUserName(statusDisplays);
223
}
224
}
225
226
describe('ReactMultiChildReconcile', function() {
227
beforeEach(function() {
228
require('mock-modules').dumpCache();
229
});
230
231
it('should reset internal state if removed then readded', function() {
232
// Test basics.
233
var props = {
234
usernameToStatus: {
235
jcw: 'jcwStatus'
236
}
237
};
238
239
var parentInstance = ReactTestUtils.renderIntoDocument(
240
<FriendsStatusDisplay {...props} />
241
);
242
var statusDisplays = parentInstance.getStatusDisplays();
243
var startingInternalState = statusDisplays.jcw.getInternalState();
244
245
// Now remove the child.
246
parentInstance.replaceProps({ usernameToStatus: {} });
247
statusDisplays = parentInstance.getStatusDisplays();
248
expect(statusDisplays.jcw).toBeFalsy();
249
250
// Now reset the props that cause there to be a child
251
parentInstance.replaceProps(props);
252
statusDisplays = parentInstance.getStatusDisplays();
253
expect(statusDisplays.jcw).toBeTruthy();
254
expect(statusDisplays.jcw.getInternalState())
255
.toNotBe(startingInternalState);
256
});
257
258
it('should create unique identity', function() {
259
// Test basics.
260
var usernameToStatus = {
261
jcw: 'jcwStatus',
262
awalke: 'awalkeStatus',
263
bob: 'bobStatus'
264
};
265
266
testPropsSequence([ { usernameToStatus: usernameToStatus } ]);
267
});
268
269
it('should preserve order if children order has not changed', function() {
270
var PROPS_SEQUENCE = [
271
{
272
usernameToStatus: {
273
jcw: 'jcwStatus',
274
jordanjcw: 'jordanjcwStatus'
275
}
276
},
277
{
278
usernameToStatus: {
279
jcw: 'jcwstatus2',
280
jordanjcw: 'jordanjcwstatus2'
281
}
282
}
283
];
284
testPropsSequence(PROPS_SEQUENCE);
285
});
286
287
it('should transition from zero to one children correctly', function() {
288
var PROPS_SEQUENCE = [
289
{ usernameToStatus: {} },
290
{
291
usernameToStatus: {
292
first: 'firstStatus'
293
}
294
}
295
];
296
testPropsSequence(PROPS_SEQUENCE);
297
});
298
299
it('should transition from one to zero children correctly', function() {
300
var PROPS_SEQUENCE = [
301
{
302
usernameToStatus: {
303
first: 'firstStatus'
304
}
305
},
306
{ usernameToStatus: {} }
307
];
308
testPropsSequence(PROPS_SEQUENCE);
309
});
310
311
it('should transition from one child to null children', function() {
312
testPropsSequence([
313
{
314
usernameToStatus: {
315
first: 'firstStatus'
316
}
317
},
318
{ }
319
]);
320
});
321
322
it('should transition from null children to one child', function() {
323
testPropsSequence([
324
{ },
325
{
326
usernameToStatus: {
327
first: 'firstStatus'
328
}
329
}
330
]);
331
});
332
333
it('should transition from zero children to null children', function() {
334
testPropsSequence([
335
{
336
usernameToStatus: { }
337
},
338
{ }
339
]);
340
});
341
342
it('should transition from null children to zero children', function() {
343
testPropsSequence([
344
{ },
345
{
346
usernameToStatus: { }
347
}
348
]);
349
});
350
351
352
353
/**
354
* `FriendsStatusDisplay` renders nulls as empty children (it's a convention
355
* of `FriendsStatusDisplay`, nothing related to React or these test cases.
356
*/
357
it('should remove nulled out children at the beginning', function() {
358
var PROPS_SEQUENCE = [
359
{
360
usernameToStatus: {
361
jcw: 'jcwStatus',
362
jordanjcw: 'jordanjcwStatus'
363
}
364
},
365
{
366
usernameToStatus: {
367
jcw: null,
368
jordanjcw: 'jordanjcwstatus2'
369
}
370
}
371
];
372
testPropsSequence(PROPS_SEQUENCE);
373
});
374
375
it('should remove nulled out children at the end', function() {
376
var PROPS_SEQUENCE = [
377
{
378
usernameToStatus: {
379
jcw: 'jcwStatus',
380
jordanjcw: 'jordanjcwStatus'
381
}
382
},
383
{
384
usernameToStatus: {
385
jcw: 'jcwstatus2',
386
jordanjcw: null
387
}
388
}
389
];
390
testPropsSequence(PROPS_SEQUENCE);
391
});
392
393
it('should reverse the order of two children', function() {
394
var PROPS_SEQUENCE = [
395
{
396
usernameToStatus: {
397
userOne: 'userOneStatus',
398
userTwo: 'userTwoStatus'
399
}
400
},
401
{
402
usernameToStatus: {
403
userTwo: 'userTwoStatus',
404
userOne: 'userOneStatus'
405
}
406
}
407
];
408
testPropsSequence(PROPS_SEQUENCE);
409
});
410
411
it('should reverse the order of more than two children', function() {
412
var PROPS_SEQUENCE = [
413
{
414
usernameToStatus: {
415
userOne: 'userOneStatus',
416
userTwo: 'userTwoStatus',
417
userThree: 'userThreeStatus'
418
}
419
},
420
{
421
usernameToStatus: {
422
userThree: 'userThreeStatus',
423
userTwo: 'userTwoStatus',
424
userOne: 'userOneStatus'
425
}
426
}
427
];
428
testPropsSequence(PROPS_SEQUENCE);
429
});
430
431
it('should cycle order correctly', function() {
432
var PROPS_SEQUENCE = [
433
{
434
usernameToStatus: {
435
userOne: 'userOneStatus',
436
userTwo: 'userTwoStatus',
437
userThree: 'userThreeStatus',
438
userFour: 'userFourStatus'
439
}
440
},
441
{
442
usernameToStatus: {
443
userTwo: 'userTwoStatus',
444
userThree: 'userThreeStatus',
445
userFour: 'userFourStatus',
446
userOne: 'userOneStatus'
447
}
448
},
449
{
450
usernameToStatus: {
451
userThree: 'userThreeStatus',
452
userFour: 'userFourStatus',
453
userOne: 'userOneStatus',
454
userTwo: 'userTwoStatus'
455
}
456
},
457
{
458
usernameToStatus: {
459
userFour: 'userFourStatus',
460
userOne: 'userOneStatus',
461
userTwo: 'userTwoStatus',
462
userThree: 'userThreeStatus'
463
}
464
},
465
{
466
usernameToStatus: { // Full circle!
467
userOne: 'userOneStatus',
468
userTwo: 'userTwoStatus',
469
userThree: 'userThreeStatus',
470
userFour: 'userFourStatus'
471
}
472
}
473
];
474
testPropsSequence(PROPS_SEQUENCE);
475
});
476
477
it('should cycle order correctly in the other direction', function() {
478
var PROPS_SEQUENCE = [
479
{
480
usernameToStatus: {
481
userOne: 'userOneStatus',
482
userTwo: 'userTwoStatus',
483
userThree: 'userThreeStatus',
484
userFour: 'userFourStatus'
485
}
486
},
487
{
488
usernameToStatus: {
489
userFour: 'userFourStatus',
490
userOne: 'userOneStatus',
491
userTwo: 'userTwoStatus',
492
userThree: 'userThreeStatus'
493
}
494
},
495
{
496
usernameToStatus: {
497
userThree: 'userThreeStatus',
498
userFour: 'userFourStatus',
499
userOne: 'userOneStatus',
500
userTwo: 'userTwoStatus'
501
}
502
},
503
{
504
usernameToStatus: {
505
userTwo: 'userTwoStatus',
506
userThree: 'userThreeStatus',
507
userFour: 'userFourStatus',
508
userOne: 'userOneStatus'
509
}
510
},
511
{
512
usernameToStatus: { // Full circle!
513
userOne: 'userOneStatus',
514
userTwo: 'userTwoStatus',
515
userThree: 'userThreeStatus',
516
userFour: 'userFourStatus'
517
}
518
}
519
];
520
testPropsSequence(PROPS_SEQUENCE);
521
});
522
523
524
it('should remove nulled out children and ignore ' +
525
'new null children', function() {
526
var PROPS_SEQUENCE = [
527
{
528
usernameToStatus: {
529
jcw: 'jcwStatus',
530
jordanjcw: 'jordanjcwStatus'
531
}
532
},
533
{
534
usernameToStatus: {
535
jordanjcw: 'jordanjcwstatus2',
536
jcw: null,
537
another: null
538
}
539
}
540
];
541
testPropsSequence(PROPS_SEQUENCE);
542
});
543
544
it('should remove nulled out children and reorder remaining', function() {
545
var PROPS_SEQUENCE = [
546
{
547
usernameToStatus: {
548
jcw: 'jcwStatus',
549
jordanjcw: 'jordanjcwStatus',
550
john: 'johnStatus', // john will go away
551
joe: 'joeStatus'
552
}
553
},
554
{
555
usernameToStatus: {
556
jordanjcw: 'jordanjcwStatus',
557
joe: 'joeStatus',
558
jcw: 'jcwStatus'
559
}
560
}
561
];
562
testPropsSequence(PROPS_SEQUENCE);
563
});
564
565
it('should append children to the end', function() {
566
var PROPS_SEQUENCE = [
567
{
568
usernameToStatus: {
569
jcw: 'jcwStatus',
570
jordanjcw: 'jordanjcwStatus'
571
}
572
},
573
{
574
usernameToStatus: {
575
jcw: 'jcwStatus',
576
jordanjcw: 'jordanjcwStatus',
577
jordanjcwnew: 'jordanjcwnewStatus'
578
}
579
}
580
];
581
testPropsSequence(PROPS_SEQUENCE);
582
});
583
584
it('should append multiple children to the end', function() {
585
var PROPS_SEQUENCE = [
586
{
587
usernameToStatus: {
588
jcw: 'jcwStatus',
589
jordanjcw: 'jordanjcwStatus'
590
}
591
},
592
{
593
usernameToStatus: {
594
jcw: 'jcwStatus',
595
jordanjcw: 'jordanjcwStatus',
596
jordanjcwnew: 'jordanjcwnewStatus',
597
jordanjcwnew2: 'jordanjcwnewStatus2'
598
}
599
}
600
];
601
testPropsSequence(PROPS_SEQUENCE);
602
});
603
604
it('should prepend children to the beginning', function() {
605
var PROPS_SEQUENCE = [
606
{
607
usernameToStatus: {
608
jcw: 'jcwStatus',
609
jordanjcw: 'jordanjcwStatus'
610
}
611
},
612
{
613
usernameToStatus: {
614
newUsername: 'newUsernameStatus',
615
jcw: 'jcwStatus',
616
jordanjcw: 'jordanjcwStatus'
617
}
618
}
619
];
620
testPropsSequence(PROPS_SEQUENCE);
621
});
622
623
it('should prepend multiple children to the beginning', function() {
624
var PROPS_SEQUENCE = [
625
{
626
usernameToStatus: {
627
jcw: 'jcwStatus',
628
jordanjcw: 'jordanjcwStatus'
629
}
630
},
631
{
632
usernameToStatus: {
633
newNewUsername: 'newNewUsernameStatus',
634
newUsername: 'newUsernameStatus',
635
jcw: 'jcwStatus',
636
jordanjcw: 'jordanjcwStatus'
637
}
638
}
639
];
640
testPropsSequence(PROPS_SEQUENCE);
641
});
642
643
it('should not prepend an empty child to the beginning', function() {
644
var PROPS_SEQUENCE = [
645
{
646
usernameToStatus: {
647
jcw: 'jcwStatus',
648
jordanjcw: 'jordanjcwStatus'
649
}
650
},
651
{
652
usernameToStatus: {
653
emptyUsername: null,
654
jcw: 'jcwStatus',
655
jordanjcw: 'jordanjcwStatus'
656
}
657
}
658
];
659
testPropsSequence(PROPS_SEQUENCE);
660
});
661
662
it('should not append an empty child to the end', function() {
663
var PROPS_SEQUENCE = [
664
{
665
usernameToStatus: {
666
jcw: 'jcwStatus',
667
jordanjcw: 'jordanjcwStatus'
668
}
669
},
670
{
671
usernameToStatus: {
672
jcw: 'jcwStatus',
673
jordanjcw: 'jordanjcwStatus',
674
emptyUsername: null
675
}
676
}
677
];
678
testPropsSequence(PROPS_SEQUENCE);
679
});
680
681
it('should not insert empty children in the middle', function() {
682
var PROPS_SEQUENCE = [
683
{
684
usernameToStatus: {
685
jcw: 'jcwStatus',
686
jordanjcw: 'jordanjcwStatus'
687
}
688
},
689
{
690
usernameToStatus: {
691
jcw: 'jcwstatus2',
692
skipOverMe: null,
693
skipOverMeToo: null,
694
definitelySkipOverMe: null,
695
jordanjcw: 'jordanjcwstatus2'
696
}
697
}
698
];
699
testPropsSequence(PROPS_SEQUENCE);
700
});
701
702
it('should insert one new child in the middle', function() {
703
var PROPS_SEQUENCE = [
704
{
705
usernameToStatus: {
706
jcw: 'jcwStatus',
707
jordanjcw: 'jordanjcwStatus'
708
}
709
},
710
{
711
usernameToStatus: {
712
jcw: 'jcwstatus2',
713
insertThis: 'insertThisStatus',
714
jordanjcw: 'jordanjcwstatus2'
715
}
716
}
717
];
718
testPropsSequence(PROPS_SEQUENCE);
719
});
720
721
it('should insert multiple new truthy children in the middle', function() {
722
var PROPS_SEQUENCE = [
723
{
724
usernameToStatus: {
725
jcw: 'jcwStatus',
726
jordanjcw: 'jordanjcwStatus'
727
}
728
},
729
{
730
usernameToStatus: {
731
jcw: 'jcwstatus2',
732
insertThis: 'insertThisStatus',
733
insertThisToo: 'insertThisTooStatus',
734
definitelyInsertThisToo: 'definitelyInsertThisTooStatus',
735
jordanjcw: 'jordanjcwstatus2'
736
}
737
}
738
];
739
testPropsSequence(PROPS_SEQUENCE);
740
});
741
742
it('should insert non-empty children in middle where nulls were', function() {
743
var PROPS_SEQUENCE = [
744
{
745
usernameToStatus: {
746
jcw: 'jcwStatus',
747
insertThis: null,
748
insertThisToo: null,
749
definitelyInsertThisToo: null,
750
jordanjcw: 'jordanjcwStatus'
751
}
752
},
753
{
754
usernameToStatus: {
755
jcw: 'jcwstatus2',
756
insertThis: 'insertThisStatus',
757
insertThisToo: 'insertThisTooStatus',
758
definitelyInsertThisToo: 'definitelyInsertThisTooStatus',
759
jordanjcw: 'jordanjcwstatus2'
760
}
761
}
762
];
763
testPropsSequence(PROPS_SEQUENCE);
764
});
765
});
766
767