1
+/**
2
+ * Copyright (c) 2013-present, Facebook, Inc.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+let ReactFeatureFlags = require('shared/ReactFeatureFlags');
13
+ReactFeatureFlags.enableNewContextAPI = true;
14
+
15
+let React = require('react');
16
+let ReactNoop;
17
+let gen;
18
+
19
+describe('ReactNewContext', () => {
20
+ beforeEach(() => {
21
+ jest.resetModules();
22
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
23
+ ReactFeatureFlags.enableNewContextAPI = true;
24
+ React = require('react');
25
+ ReactNoop = require('react-noop-renderer');
26
+ gen = require('random-seed');
27
+ });
28
+
29
+ // function div(...children) {
30
+ // children = children.map(c => (typeof c === 'string' ? {text: c} : c));
31
+ // return {type: 'div', children, prop: undefined};
32
+ // }
33
+
34
+ function span(prop) {
35
+ return {type: 'span', children: [], prop};
36
+ }
37
+
38
+ it('simple mount and update', () => {
39
+ const Context = React.unstable_createContext(1);
40
+
41
+ function Provider(props) {
42
+ return Context.provide(props.value, props.children);
43
+ }
44
+
45
+ function Consumer(props) {
46
+ return Context.consume(value => {
47
+ return <span prop={'Result: ' + value} />;
48
+ });
49
+ }
50
+
51
+ const Indirection = React.Fragment;
52
+
53
+ function App(props) {
54
+ return (
55
+ <Provider value={props.value}>
56
+ <Indirection>
57
+ <Indirection>
58
+ <Consumer />
59
+ </Indirection>
60
+ </Indirection>
61
+ </Provider>
62
+ );
63
+ }
64
+
65
+ ReactNoop.render(<App value={2} />);
66
+ ReactNoop.flush();
67
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
68
+
69
+ // Update
70
+ ReactNoop.render(<App value={3} />);
71
+ ReactNoop.flush();
72
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 3')]);
73
+ });
74
+
75
+ it('propagates through shouldComponentUpdate false', () => {
76
+ const Context = React.unstable_createContext(1);
77
+
78
+ function Provider(props) {
79
+ ReactNoop.yield('Provider');
80
+ return Context.provide(props.value, props.children);
81
+ }
82
+
83
+ function Consumer(props) {
84
+ ReactNoop.yield('Consumer');
85
+ return Context.consume(value => {
86
+ ReactNoop.yield('Consumer render prop');
87
+ return <span prop={'Result: ' + value} />;
88
+ });
89
+ }
90
+
91
+ class Indirection extends React.Component {
92
+ shouldComponentUpdate() {
93
+ return false;
94
+ }
95
+ render() {
96
+ ReactNoop.yield('Indirection');
97
+ return this.props.children;
98
+ }
99
+ }
100
+
101
+ function App(props) {
102
+ ReactNoop.yield('App');
103
+ return (
104
+ <Provider value={props.value}>
105
+ <Indirection>
106
+ <Indirection>
107
+ <Consumer />
108
+ </Indirection>
109
+ </Indirection>
110
+ </Provider>
111
+ );
112
+ }
113
+
114
+ ReactNoop.render(<App value={2} />);
115
+ expect(ReactNoop.flush()).toEqual([
116
+ 'App',
117
+ 'Provider',
118
+ 'Indirection',
119
+ 'Indirection',
120
+ 'Consumer',
121
+ 'Consumer render prop',
122
+ ]);
123
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
124
+
125
+ // Update
126
+ ReactNoop.render(<App value={3} />);
127
+ expect(ReactNoop.flush()).toEqual([
128
+ 'App',
129
+ 'Provider',
130
+ 'Consumer render prop',
131
+ ]);
132
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 3')]);
133
+ });
134
+
135
+ it('consumers bail out if context value is the same', () => {
136
+ const Context = React.unstable_createContext(1);
137
+
138
+ function Provider(props) {
139
+ ReactNoop.yield('Provider');
140
+ return Context.provide(props.value, props.children);
141
+ }
142
+
143
+ function Consumer(props) {
144
+ ReactNoop.yield('Consumer');
145
+ return Context.consume(value => {
146
+ ReactNoop.yield('Consumer render prop');
147
+ return <span prop={'Result: ' + value} />;
148
+ });
149
+ }
150
+
151
+ class Indirection extends React.Component {
152
+ shouldComponentUpdate() {
153
+ return false;
154
+ }
155
+ render() {
156
+ ReactNoop.yield('Indirection');
157
+ return this.props.children;
158
+ }
159
+ }
160
+
161
+ function App(props) {
162
+ ReactNoop.yield('App');
163
+ return (
164
+ <Provider value={props.value}>
165
+ <Indirection>
166
+ <Indirection>
167
+ <Consumer />
168
+ </Indirection>
169
+ </Indirection>
170
+ </Provider>
171
+ );
172
+ }
173
+
174
+ ReactNoop.render(<App value={2} />);
175
+ expect(ReactNoop.flush()).toEqual([
176
+ 'App',
177
+ 'Provider',
178
+ 'Indirection',
179
+ 'Indirection',
180
+ 'Consumer',
181
+ 'Consumer render prop',
182
+ ]);
183
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
184
+
185
+ // Update with the same context value
186
+ ReactNoop.render(<App value={2} />);
187
+ expect(ReactNoop.flush()).toEqual([
188
+ 'App',
189
+ 'Provider',
190
+ // Don't call render prop again
191
+ ]);
192
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
193
+ });
194
+
195
+ it('nested providers', () => {
196
+ const Context = React.unstable_createContext(1);
197
+
198
+ function Provider(props) {
199
+ return Context.consume(contextValue =>
200
+ // Multiply previous context value by 2, unless prop overrides
201
+ Context.provide(props.value || contextValue * 2, props.children),
202
+ );
203
+ }
204
+
205
+ function Consumer(props) {
206
+ return Context.consume(value => {
207
+ return <span prop={'Result: ' + value} />;
208
+ });
209
+ }
210
+
211
+ class Indirection extends React.Component {
212
+ shouldComponentUpdate() {
213
+ return false;
214
+ }
215
+ render() {
216
+ return this.props.children;
217
+ }
218
+ }
219
+
220
+ function App(props) {
221
+ return (
222
+ <Provider value={props.value}>
223
+ <Indirection>
224
+ <Provider>
225
+ <Indirection>
226
+ <Provider>
227
+ <Indirection>
228
+ <Consumer />
229
+ </Indirection>
230
+ </Provider>
231
+ </Indirection>
232
+ </Provider>
233
+ </Indirection>
234
+ </Provider>
235
+ );
236
+ }
237
+
238
+ ReactNoop.render(<App value={2} />);
239
+ ReactNoop.flush();
240
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 8')]);
241
+
242
+ // Update
243
+ ReactNoop.render(<App value={3} />);
244
+ ReactNoop.flush();
245
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 12')]);
246
+ });
247
+
248
+ it('multiple consumers in different branches', () => {
249
+ const Context = React.unstable_createContext(1);
250
+
251
+ function Provider(props) {
252
+ return Context.consume(contextValue =>
253
+ // Multiply previous context value by 2, unless prop overrides
254
+ Context.provide(props.value || contextValue * 2, props.children),
255
+ );
256
+ }
257
+
258
+ function Consumer(props) {
259
+ return Context.consume(value => {
260
+ return <span prop={'Result: ' + value} />;
261
+ });
262
+ }
263
+
264
+ class Indirection extends React.Component {
265
+ shouldComponentUpdate() {
266
+ return false;
267
+ }
268
+ render() {
269
+ return this.props.children;
270
+ }
271
+ }
272
+
273
+ function App(props) {
274
+ return (
275
+ <Provider value={props.value}>
276
+ <Indirection>
277
+ <Indirection>
278
+ <Provider>
279
+ <Consumer />
280
+ </Provider>
281
+ </Indirection>
282
+ <Indirection>
283
+ <Consumer />
284
+ </Indirection>
285
+ </Indirection>
286
+ </Provider>
287
+ );
288
+ }
289
+
290
+ ReactNoop.render(<App value={2} />);
291
+ ReactNoop.flush();
292
+ expect(ReactNoop.getChildren()).toEqual([
293
+ span('Result: 4'),
294
+ span('Result: 2'),
295
+ ]);
296
+
297
+ // Update
298
+ ReactNoop.render(<App value={3} />);
299
+ ReactNoop.flush();
300
+ expect(ReactNoop.getChildren()).toEqual([
301
+ span('Result: 6'),
302
+ span('Result: 3'),
303
+ ]);
304
+
305
+ // Another update
306
+ ReactNoop.render(<App value={4} />);
307
+ ReactNoop.flush();
308
+ expect(ReactNoop.getChildren()).toEqual([
309
+ span('Result: 8'),
310
+ span('Result: 4'),
311
+ ]);
312
+ });
313
+
314
+ it('compares context values with Object.is semantics', () => {
315
+ const Context = React.unstable_createContext(1);
316
+
317
+ function Provider(props) {
318
+ ReactNoop.yield('Provider');
319
+ return Context.provide(props.value, props.children);
320
+ }
321
+
322
+ function Consumer(props) {
323
+ ReactNoop.yield('Consumer');
324
+ return Context.consume(value => {
325
+ ReactNoop.yield('Consumer render prop');
326
+ return <span prop={'Result: ' + value} />;
327
+ });
328
+ }
329
+
330
+ class Indirection extends React.Component {
331
+ shouldComponentUpdate() {
332
+ return false;
333
+ }
334
+ render() {
335
+ ReactNoop.yield('Indirection');
336
+ return this.props.children;
337
+ }
338
+ }
339
+
340
+ function App(props) {
341
+ ReactNoop.yield('App');
342
+ return (
343
+ <Provider value={props.value}>
344
+ <Indirection>
345
+ <Indirection>
346
+ <Consumer />
347
+ </Indirection>
348
+ </Indirection>
349
+ </Provider>
350
+ );
351
+ }
352
+
353
+ ReactNoop.render(<App value={NaN} />);
354
+ expect(ReactNoop.flush()).toEqual([
355
+ 'App',
356
+ 'Provider',
357
+ 'Indirection',
358
+ 'Indirection',
359
+ 'Consumer',
360
+ 'Consumer render prop',
361
+ ]);
362
+ expect(ReactNoop.getChildren()).toEqual([span('Result: NaN')]);
363
+
364
+ // Update
365
+ ReactNoop.render(<App value={NaN} />);
366
+ expect(ReactNoop.flush()).toEqual([
367
+ 'App',
368
+ 'Provider',
369
+ // Consumer should not re-render again
370
+ // 'Consumer render prop',
371
+ ]);
372
+ expect(ReactNoop.getChildren()).toEqual([span('Result: NaN')]);
373
+ });
374
+
375
+ it('context unwinds when interrupted', () => {
376
+ const Context = React.unstable_createContext('Default');
377
+
378
+ function Provider(props) {
379
+ return Context.provide(props.value, props.children);
380
+ }
381
+
382
+ function Consumer(props) {
383
+ return Context.consume(value => {
384
+ return <span prop={'Result: ' + value} />;
385
+ });
386
+ }
387
+
388
+ function BadRender() {
389
+ throw new Error('Bad render');
390
+ }
391
+
392
+ class ErrorBoundary extends React.Component {
393
+ state = {error: null};
394
+ componentDidCatch(error) {
395
+ this.setState({error});
396
+ }
397
+ render() {
398
+ if (this.state.error) {
399
+ return null;
400
+ }
401
+ return this.props.children;
402
+ }
403
+ }
404
+
405
+ function App(props) {
406
+ return (
407
+ <React.Fragment>
408
+ <Provider value="Does not unwind">
409
+ <ErrorBoundary>
410
+ <Provider value="Unwinds after BadRender throws">
411
+ <BadRender />
412
+ </Provider>
413
+ </ErrorBoundary>
414
+ <Consumer />
415
+ </Provider>
416
+ </React.Fragment>
417
+ );
418
+ }
419
+
420
+ ReactNoop.render(<App value="A" />);
421
+ ReactNoop.flush();
422
+ expect(ReactNoop.getChildren()).toEqual([
423
+ // The second provider should use the default value. This proves the
424
+ span('Result: Does not unwind'),
425
+ ]);
426
+ });
427
+
428
+ it('can skip consumers with bitmask', () => {
429
+ const Context = React.unstable_createContext({foo: 0, bar: 0}, (a, b) => {
430
+ let result = 0;
431
+ if (a.foo !== b.foo) {
432
+ result |= 0b01;
433
+ }
434
+ if (a.bar !== b.bar) {
435
+ result |= 0b10;
436
+ }
437
+ return result;
438
+ });
439
+
440
+ function Provider(props) {
441
+ return Context.provide({foo: props.foo, bar: props.bar}, props.children);
442
+ }
443
+
444
+ function Foo() {
445
+ return Context.consume(value => {
446
+ ReactNoop.yield('Foo');
447
+ return <span prop={'Foo: ' + value.foo} />;
448
+ }, 0b01);
449
+ }
450
+
451
+ function Bar() {
452
+ return Context.consume(value => {
453
+ ReactNoop.yield('Bar');
454
+ return <span prop={'Bar: ' + value.bar} />;
455
+ }, 0b10);
456
+ }
457
+
458
+ class Indirection extends React.Component {
459
+ shouldComponentUpdate() {
460
+ return false;
461
+ }
462
+ render() {
463
+ return this.props.children;
464
+ }
465
+ }
466
+
467
+ function App(props) {
468
+ return (
469
+ <Provider foo={props.foo} bar={props.bar}>
470
+ <Indirection>
471
+ <Indirection>
472
+ <Foo />
473
+ </Indirection>
474
+ <Indirection>
475
+ <Bar />
476
+ </Indirection>
477
+ </Indirection>
478
+ </Provider>
479
+ );
480
+ }
481
+
482
+ ReactNoop.render(<App foo={1} bar={1} />);
483
+ expect(ReactNoop.flush()).toEqual(['Foo', 'Bar']);
484
+ expect(ReactNoop.getChildren()).toEqual([span('Foo: 1'), span('Bar: 1')]);
485
+
486
+ // Update only foo
487
+ ReactNoop.render(<App foo={2} bar={1} />);
488
+ expect(ReactNoop.flush()).toEqual(['Foo']);
489
+ expect(ReactNoop.getChildren()).toEqual([span('Foo: 2'), span('Bar: 1')]);
490
+
491
+ // Update only bar
492
+ ReactNoop.render(<App foo={2} bar={2} />);
493
+ expect(ReactNoop.flush()).toEqual(['Bar']);
494
+ expect(ReactNoop.getChildren()).toEqual([span('Foo: 2'), span('Bar: 2')]);
495
+
496
+ // Update both
497
+ ReactNoop.render(<App foo={3} bar={3} />);
498
+ expect(ReactNoop.flush()).toEqual(['Foo', 'Bar']);
499
+ expect(ReactNoop.getChildren()).toEqual([span('Foo: 3'), span('Bar: 3')]);
500
+ });
501
+
502
+ it('warns if calculateChangedBits returns larger than a 31-bit integer', () => {
503
+ spyOnDev(console, 'error');
504
+
505
+ const Context = React.unstable_createContext(
506
+ 0,
507
+ (a, b) => Math.pow(2, 32) - 1, // Return 32 bit int
508
+ );
509
+
510
+ function Provider(props) {
511
+ return Context.provide(props.value, props.children);
512
+ }
513
+
514
+ ReactNoop.render(<Provider value={1} />);
515
+ ReactNoop.flush();
516
+
517
+ // Update
518
+ ReactNoop.render(<Provider value={2} />);
519
+ ReactNoop.flush();
520
+
521
+ if (__DEV__) {
522
+ expect(console.error.calls.count()).toBe(1);
523
+ expect(console.error.calls.argsFor(0)[0]).toContain(
524
+ 'calculateChangedBits: Expected the return value to be a 31-bit ' +
525
+ 'integer. Instead received: 4294967295',
526
+ );
527
+ }
528
+ });
529
+
530
+ it('warns if multiple renderers concurrently render the same context', () => {
531
+ spyOnDev(console, 'error');
532
+ const Context = React.unstable_createContext(0);
533
+
534
+ function Foo(props) {
535
+ ReactNoop.yield('Foo');
536
+ return null;
537
+ }
538
+ function Provider(props) {
539
+ return Context.provide(props.value, props.children);
540
+ }
541
+
542
+ function App(props) {
543
+ return (
544
+ <Provider value={props.value}>
545
+ <Foo />
546
+ <Foo />
547
+ </Provider>
548
+ );
549
+ }
550
+
551
+ ReactNoop.render(<App value={1} />);
552
+ // Render past the Provider, but don't commit yet
553
+ ReactNoop.flushThrough(['Foo']);
554
+
555
+ // Get a new copy of ReactNoop
556
+ jest.resetModules();
557
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
558
+ ReactFeatureFlags.enableNewContextAPI = true;
559
+ React = require('react');
560
+ ReactNoop = require('react-noop-renderer');
561
+
562
+ // Render the provider again using a different renderer
563
+ ReactNoop.render(<App value={1} />);
564
+ ReactNoop.flush();
565
+
566
+ if (__DEV__) {
567
+ expect(console.error.calls.argsFor(0)[0]).toContain(
568
+ 'Detected multiple renderers concurrently rendering the same ' +
569
+ 'context provider. This is currently unsupported',
570
+ );
571
+ }
572
+ });
573
+
574
+ describe('fuzz test', () => {
575
+ const Fragment = React.Fragment;
576
+ const contextKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
577
+
578
+ const FLUSH_ALL = 'FLUSH_ALL';
579
+ function flushAll() {
580
+ return {
581
+ type: FLUSH_ALL,
582
+ toString() {
583
+ return `flushAll()`;
584
+ },
585
+ };
586
+ }
587
+
588
+ const FLUSH = 'FLUSH';
589
+ function flush(unitsOfWork) {
590
+ return {
591
+ type: FLUSH,
592
+ unitsOfWork,
593
+ toString() {
594
+ return `flush(${unitsOfWork})`;
595
+ },
596
+ };
597
+ }
598
+
599
+ const UPDATE = 'UPDATE';
600
+ function update(key, value) {
601
+ return {
602
+ type: UPDATE,
603
+ key,
604
+ value,
605
+ toString() {
606
+ return `update('${key}', ${value})`;
607
+ },
608
+ };
609
+ }
610
+
611
+ function randomInteger(min, max) {
612
+ min = Math.ceil(min);
613
+ max = Math.floor(max);
614
+ return Math.floor(Math.random() * (max - min)) + min;
615
+ }
616
+
617
+ function randomAction() {
618
+ switch (randomInteger(0, 3)) {
619
+ case 0:
620
+ return flushAll();
621
+ case 1:
622
+ return flush(randomInteger(0, 500));
623
+ case 2:
624
+ const key = contextKeys[randomInteger(0, contextKeys.length)];
625
+ const value = randomInteger(1, 10);
626
+ return update(key, value);
627
+ default:
628
+ throw new Error('Switch statement should be exhaustive');
629
+ }
630
+ }
631
+
632
+ function randomActions(n) {
633
+ let actions = [];
634
+ for (let i = 0; i < n; i++) {
635
+ actions.push(randomAction());
636
+ }
637
+ return actions;
638
+ }
639
+
640
+ function ContextSimulator(maxDepth) {
641
+ const contexts = new Map(
642
+ contextKeys.map(key => {
643
+ const Context = React.unstable_createContext(0);
644
+ Context.displayName = 'Context' + key;
645
+ return [key, Context];
646
+ }),
647
+ );
648
+
649
+ class ConsumerTree extends React.Component {
650
+ shouldComponentUpdate() {
651
+ return false;
652
+ }
653
+ render() {
654
+ if (this.props.depth >= this.props.maxDepth) {
655
+ return null;
656
+ }
657
+ const consumers = [0, 1, 2].map(i => {
658
+ const randomKey =
659
+ contextKeys[
660
+ this.props.rand.intBetween(0, contextKeys.length - 1)
661
+ ];
662
+ const Context = contexts.get(randomKey);
663
+ return Context.consume(
664
+ value => (
665
+ <Fragment>
666
+ <span prop={`${randomKey}:${value}`} />
667
+ <ConsumerTree
668
+ rand={this.props.rand}
669
+ depth={this.props.depth + 1}
670
+ maxDepth={this.props.maxDepth}
671
+ />
672
+ </Fragment>
673
+ ),
674
+ null,
675
+ i,
676
+ );
677
+ });
678
+ return consumers;
679
+ }
680
+ }
681
+
682
+ function Root(props) {
683
+ return contextKeys.reduceRight((children, key) => {
684
+ const Context = contexts.get(key);
685
+ const value = props.values[key];
686
+ return Context.provide(value, children);
687
+ }, <ConsumerTree rand={props.rand} depth={0} maxDepth={props.maxDepth} />);
688
+ }
689
+
690
+ const initialValues = contextKeys.reduce(
691
+ (result, key, i) => ({...result, [key]: i + 1}),
692
+ {},
693
+ );
694
+
695
+ function assertConsistentTree(expectedValues = {}) {
696
+ const children = ReactNoop.getChildren();
697
+ children.forEach(child => {
698
+ const text = child.prop;
699
+ const key = text[0];
700
+ const value = parseInt(text[2], 10);
701
+ const expectedValue = expectedValues[key];
702
+ if (expectedValue === undefined) {
703
+ // If an expected value was not explicitly passed to this function,
704
+ // use the first occurrence.
705
+ expectedValues[key] = value;
706
+ } else if (value !== expectedValue) {
707
+ throw new Error(
708
+ `Inconsistent value! Expected: ${key}:${expectedValue}. Actual: ${
709
+ text
710
+ }`,
711
+ );
712
+ }
713
+ });
714
+ }
715
+
716
+ function simulate(seed, actions) {
717
+ const rand = gen.create(seed);
718
+ let finalExpectedValues = initialValues;
719
+ function updateRoot() {
720
+ ReactNoop.render(
721
+ <Root
722
+ maxDepth={maxDepth}
723
+ rand={rand}
724
+ values={finalExpectedValues}
725
+ />,
726
+ );
727
+ }
728
+ updateRoot();
729
+
730
+ actions.forEach(action => {
731
+ switch (action.type) {
732
+ case FLUSH_ALL:
733
+ ReactNoop.flush();
734
+ break;
735
+ case FLUSH:
736
+ ReactNoop.flushUnitsOfWork(action.unitsOfWork);
737
+ break;
738
+ case UPDATE:
739
+ finalExpectedValues = {
740
+ ...finalExpectedValues,
741
+ [action.key]: action.value,
742
+ };
743
+ updateRoot();
744
+ break;
745
+ default:
746
+ throw new Error('Switch statement should be exhaustive');
747
+ }
748
+ assertConsistentTree();
749
+ });
750
+
751
+ ReactNoop.flush();
752
+ assertConsistentTree(finalExpectedValues);
753
+ }
754
+
755
+ return {simulate};
756
+ }
757
+
758
+ it('hard-coded tests', () => {
759
+ const {simulate} = ContextSimulator(5);
760
+ simulate('randomSeed', [flush(3), update('A', 4)]);
761
+ });
762
+
763
+ it('generated tests', () => {
764
+ const {simulate} = ContextSimulator(5);
765
+
766
+ const LIMIT = 100;
767
+ for (let i = 0; i < LIMIT; i++) {
768
+ const seed = Math.random()
769
+ .toString(36)
770
+ .substr(2, 5);
771
+ const actions = randomActions(5);
772
+ try {
773
+ simulate(seed, actions);
774
+ } catch (error) {
775
+ console.error(`
776
+Context fuzz tester error! Copy and paste the following line into the test suite:
777
+ simulate('${seed}', ${actions.join(', ')});
778
+`);
779
+ throw error;
780
+ }
781
+ }
782
+ });
783
+ });
784
+});