Support React.memo in ReactShallowRenderer (#14816)
* Support React.memo in ReactShallowRenderer ReactShallowRenderer uses element.type frequently, but with React.memo elements the actual type is element.type.type. This updates ReactShallowRenderer so it uses the correct element type for Memo components and also validates the inner props for the wrapped components. * Allow Rect.memo to prevent re-renders * Support memo(forwardRef()) * Dont call memo comparison function on initial render * Fix test * Small tweaks
Brandon Dail committed
Mar 15, 2019 at 15:17 UTC
b283d75c17ff40cba1a49956d23b3985b9eb6abe
3 files changed
+1717
-39
packages/react-test-renderer/src/ReactShallowRenderer.js
+86
-39
@@ -8,7 +8,7 @@
8
*/
9
10
import React from 'react';
11
-import {isForwardRef} from 'react-is';
11
+import {isForwardRef, isMemo, ForwardRef} from 'react-is';
12
import describeComponentFrame from 'shared/describeComponentFrame';
13
import getComponentName from 'shared/getComponentName';
14
import shallowEqual from 'shared/shallowEqual';
@@ -500,7 +500,8 @@ class ReactShallowRenderer {
500
element.type,
501
);
502
invariant(
503
- isForwardRef(element) || typeof element.type === 'function',
503
+ isForwardRef(element) ||
504
+ (typeof element.type === 'function' || isMemo(element.type)),
505
'ReactShallowRenderer render(): Shallow rendering works only with custom ' +
506
'components, but the provided element type was `%s`.',
507
Array.isArray(element.type)
@@ -514,22 +515,36 @@ class ReactShallowRenderer {
515
return;
516
}
517
518
+ const elementType = isMemo(element.type) ? element.type.type : element.type;
519
+ const previousElement = this._element;
520
+
521
this._rendering = true;
522
this._element = element;
519
- this._context = getMaskedContext(element.type.contextTypes, context);
523
+ this._context = getMaskedContext(elementType.contextTypes, context);
524
+
525
+ // Inner memo component props aren't currently validated in createElement.
526
+ if (isMemo(element.type) && elementType.propTypes) {
527
+ currentlyValidatingElement = element;
528
+ checkPropTypes(
529
+ elementType.propTypes,
530
+ element.props,
531
+ 'prop',
532
+ getComponentName(elementType),
533
+ getStackAddendum,
534
+ );
535
+ }
536
537
if (this._instance) {
522
- this._updateClassComponent(element, this._context);
538
+ this._updateClassComponent(elementType, element, this._context);
539
} else {
524
- if (shouldConstruct(element.type)) {
525
- this._instance = new element.type(
540
+ if (shouldConstruct(elementType)) {
541
+ this._instance = new elementType(
542
element.props,
543
this._context,
544
this._updater,
545
);
530
-
531
- if (typeof element.type.getDerivedStateFromProps === 'function') {
532
- const partialState = element.type.getDerivedStateFromProps.call(
546
+ if (typeof elementType.getDerivedStateFromProps === 'function') {
547
+ const partialState = elementType.getDerivedStateFromProps.call(
548
null,
549
element.props,
550
this._instance.state,
@@ -543,39 +558,59 @@ class ReactShallowRenderer {
558
}
559
}
560
546
- if (element.type.hasOwnProperty('contextTypes')) {
561
+ if (elementType.contextTypes) {
562
currentlyValidatingElement = element;
548
-
563
checkPropTypes(
550
- element.type.contextTypes,
564
+ elementType.contextTypes,
565
this._context,
566
'context',
553
- getName(element.type, this._instance),
567
+ getName(elementType, this._instance),
568
getStackAddendum,
569
);
570
571
currentlyValidatingElement = null;
572
}
573
560
- this._mountClassComponent(element, this._context);
574
+ this._mountClassComponent(elementType, element, this._context);
575
} else {
562
- const prevDispatcher = ReactCurrentDispatcher.current;
563
- ReactCurrentDispatcher.current = this._dispatcher;
564
- this._prepareToUseHooks(element.type);
565
- try {
566
- if (isForwardRef(element)) {
567
- this._rendered = element.type.render(element.props, element.ref);
568
- } else {
569
- this._rendered = element.type.call(
570
- undefined,
571
- element.props,
572
- this._context,
573
- );
576
+ let shouldRender = true;
577
+ if (
578
+ isMemo(element.type) &&
579
+ elementType === this._previousComponentIdentity &&
580
+ previousElement !== null
581
+ ) {
582
+ // This is a Memo component that is being re-rendered.
583
+ const compare = element.type.compare || shallowEqual;
584
+ if (compare(previousElement.props, element.props)) {
585
+ shouldRender = false;
586
+ }
587
+ }
588
+ if (shouldRender) {
589
+ const prevDispatcher = ReactCurrentDispatcher.current;
590
+ ReactCurrentDispatcher.current = this._dispatcher;
591
+ this._prepareToUseHooks(elementType);
592
+ try {
593
+ // elementType could still be a ForwardRef if it was
594
+ // nested inside Memo.
595
+ if (elementType.$$typeof === ForwardRef) {
596
+ invariant(
597
+ typeof elementType.render === 'function',
598
+ 'forwardRef requires a render function but was given %s.',
599
+ typeof elementType.render,
600
+ );
601
+ this._rendered = elementType.render.call(
602
+ undefined,
603
+ element.props,
604
+ element.ref,
605
+ );
606
+ } else {
607
+ this._rendered = elementType(element.props, this._context);
608
+ }
609
+ } finally {
610
+ ReactCurrentDispatcher.current = prevDispatcher;
611
}
575
- } finally {
576
- ReactCurrentDispatcher.current = prevDispatcher;
612
+ this._finishHooks(element, context);
613
}
578
- this._finishHooks(element, context);
614
}
615
}
616
@@ -601,7 +636,11 @@ class ReactShallowRenderer {
636
this._instance = null;
637
}
638
604
- _mountClassComponent(element: ReactElement, context: null | Object) {
639
+ _mountClassComponent(
640
+ elementType: Function,
641
+ element: ReactElement,
642
+ context: null | Object,
643
+ ) {
644
this._instance.context = context;
645
this._instance.props = element.props;
646
this._instance.state = this._instance.state || null;
@@ -616,7 +655,7 @@ class ReactShallowRenderer {
655
// In order to support react-lifecycles-compat polyfilled components,
656
// Unsafe lifecycles should not be invoked for components using the new APIs.
657
if (
619
- typeof element.type.getDerivedStateFromProps !== 'function' &&
658
+ typeof elementType.getDerivedStateFromProps !== 'function' &&
659
typeof this._instance.getSnapshotBeforeUpdate !== 'function'
660
) {
661
if (typeof this._instance.componentWillMount === 'function') {
@@ -638,8 +677,12 @@ class ReactShallowRenderer {
677
// because DOM refs are not available.
678
}
679
641
- _updateClassComponent(element: ReactElement, context: null | Object) {
642
- const {props, type} = element;
680
+ _updateClassComponent(
681
+ elementType: Function,
682
+ element: ReactElement,
683
+ context: null | Object,
684
+ ) {
685
+ const {props} = element;
686
687
const oldState = this._instance.state || emptyObject;
688
const oldProps = this._instance.props;
@@ -648,7 +691,7 @@ class ReactShallowRenderer {
691
// In order to support react-lifecycles-compat polyfilled components,
692
// Unsafe lifecycles should not be invoked for components using the new APIs.
693
if (
651
- typeof element.type.getDerivedStateFromProps !== 'function' &&
694
+ typeof elementType.getDerivedStateFromProps !== 'function' &&
695
typeof this._instance.getSnapshotBeforeUpdate !== 'function'
696
) {
697
if (typeof this._instance.componentWillReceiveProps === 'function') {
@@ -664,8 +707,8 @@ class ReactShallowRenderer {
707
708
// Read state after cWRP in case it calls setState
709
let state = this._newState || oldState;
667
- if (typeof type.getDerivedStateFromProps === 'function') {
668
- const partialState = type.getDerivedStateFromProps.call(
710
+ if (typeof elementType.getDerivedStateFromProps === 'function') {
711
+ const partialState = elementType.getDerivedStateFromProps.call(
712
null,
713
props,
714
state,
@@ -685,7 +728,10 @@ class ReactShallowRenderer {
728
state,
729
context,
730
);
688
- } else if (type.prototype && type.prototype.isPureReactComponent) {
731
+ } else if (
732
+ elementType.prototype &&
733
+ elementType.prototype.isPureReactComponent
734
+ ) {
735
shouldUpdate =
736
!shallowEqual(oldProps, props) || !shallowEqual(oldState, state);
737
}
@@ -694,7 +740,7 @@ class ReactShallowRenderer {
740
// In order to support react-lifecycles-compat polyfilled components,
741
// Unsafe lifecycles should not be invoked for components using the new APIs.
742
if (
697
- typeof element.type.getDerivedStateFromProps !== 'function' &&
743
+ typeof elementType.getDerivedStateFromProps !== 'function' &&
744
typeof this._instance.getSnapshotBeforeUpdate !== 'function'
745
) {
746
if (typeof this._instance.componentWillUpdate === 'function') {
@@ -729,7 +775,8 @@ function getDisplayName(element) {
775
} else if (typeof element.type === 'string') {
776
return element.type;
777
} else {
732
- return element.type.displayName || element.type.name || 'Unknown';
778
+ const elementType = isMemo(element.type) ? element.type.type : element.type;
779
+ return elementType.displayName || elementType.name || 'Unknown';
780
}
781
}
782
packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js
+111
@@ -1454,4 +1454,115 @@ describe('ReactShallowRenderer', () => {
1454
shallowRenderer.render(<Foo foo="bar" />);
1455
expect(logs).toEqual([undefined]);
1456
});
1457
+
1458
+ it('should handle memo', () => {
1459
+ function Foo() {
1460
+ return <div>foo</div>;
1461
+ }
1462
+ const MemoFoo = React.memo(Foo);
1463
+ const shallowRenderer = createRenderer();
1464
+ shallowRenderer.render(<MemoFoo />);
1465
+ });
1466
+
1467
+ it('should enable React.memo to prevent a re-render', () => {
1468
+ const logs = [];
1469
+ const Foo = React.memo(({count}) => {
1470
+ logs.push(`Foo: ${count}`);
1471
+ return <div>{count}</div>;
1472
+ });
1473
+ const Bar = React.memo(({count}) => {
1474
+ logs.push(`Bar: ${count}`);
1475
+ return <div>{count}</div>;
1476
+ });
1477
+ const shallowRenderer = createRenderer();
1478
+ shallowRenderer.render(<Foo count={1} />);
1479
+ expect(logs).toEqual(['Foo: 1']);
1480
+ logs.length = 0;
1481
+ // Rendering the same element with the same props should be prevented
1482
+ shallowRenderer.render(<Foo count={1} />);
1483
+ expect(logs).toEqual([]);
1484
+ // A different element with the same props should cause a re-render
1485
+ shallowRenderer.render(<Bar count={1} />);
1486
+ expect(logs).toEqual(['Bar: 1']);
1487
+ });
1488
+
1489
+ it('should respect a custom comparison function with React.memo', () => {
1490
+ let renderCount = 0;
1491
+ function areEqual(props, nextProps) {
1492
+ return props.foo === nextProps.foo;
1493
+ }
1494
+ const Foo = React.memo(({foo, bar}) => {
1495
+ renderCount++;
1496
+ return (
1497
+ <div>
1498
+ {foo} {bar}
1499
+ </div>
1500
+ );
1501
+ }, areEqual);
1502
+
1503
+ const shallowRenderer = createRenderer();
1504
+ shallowRenderer.render(<Foo foo={1} bar={1} />);
1505
+ expect(renderCount).toBe(1);
1506
+ // Change a prop that the comparison funciton ignores
1507
+ shallowRenderer.render(<Foo foo={1} bar={2} />);
1508
+ expect(renderCount).toBe(1);
1509
+ shallowRenderer.render(<Foo foo={2} bar={2} />);
1510
+ expect(renderCount).toBe(2);
1511
+ });
1512
+
1513
+ it('should not call the comparison function with React.memo on the initial render', () => {
1514
+ const areEqual = jest.fn(() => false);
1515
+ const SomeComponent = React.memo(({foo}) => {
1516
+ return <div>{foo}</div>;
1517
+ }, areEqual);
1518
+ const shallowRenderer = createRenderer();
1519
+ shallowRenderer.render(<SomeComponent foo={1} />);
1520
+ expect(areEqual).not.toHaveBeenCalled();
1521
+ expect(shallowRenderer.getRenderOutput()).toEqual(<div>{1}</div>);
1522
+ });
1523
+
1524
+ it('should handle memo(forwardRef())', () => {
1525
+ const testRef = React.createRef();
1526
+ const SomeComponent = React.forwardRef((props, ref) => {
1527
+ expect(ref).toEqual(testRef);
1528
+ return (
1529
+ <div>
1530
+ <span className="child1" />
1531
+ <span className="child2" />
1532
+ </div>
1533
+ );
1534
+ });
1535
+
1536
+ const SomeMemoComponent = React.memo(SomeComponent);
1537
+
1538
+ const shallowRenderer = createRenderer();
1539
+ const result = shallowRenderer.render(<SomeMemoComponent ref={testRef} />);
1540
+
1541
+ expect(result.type).toBe('div');
1542
+ expect(result.props.children).toEqual([
1543
+ <span className="child1" />,
1544
+ <span className="child2" />,
1545
+ ]);
1546
+ });
1547
+
1548
+ it('should warn for forwardRef(memo())', () => {
1549
+ const testRef = React.createRef();
1550
+ const SomeMemoComponent = React.memo(({foo}) => {
1551
+ return <div>{foo}</div>;
1552
+ });
1553
+ const shallowRenderer = createRenderer();
1554
+ expect(() => {
1555
+ expect(() => {
1556
+ const SomeComponent = React.forwardRef(SomeMemoComponent);
1557
+ shallowRenderer.render(<SomeComponent ref={testRef} />);
1558
+ }).toWarnDev(
1559
+ 'Warning: forwardRef requires a render function but received ' +
1560
+ 'a `memo` component. Instead of forwardRef(memo(...)), use ' +
1561
+ 'memo(forwardRef(...))',
1562
+ {withoutStack: true},
1563
+ );
1564
+ }).toThrowError(
1565
+ 'forwardRef requires a render function but was given object.',
1566
+ );
1567
+ });
1568
});
packages/react-test-renderer/src/__tests__/ReactShallowRendererMemo-test.js
new
+1520
@@ -0,0 +1,1520 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
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
+ * @jest-environment node
9
+ */
10
+
11
+'use strict';
12
+
13
+let createRenderer;
14
+let PropTypes;
15
+let React;
16
+
17
+describe('ReactShallowRendererMemo', () => {
18
+ beforeEach(() => {
19
+ jest.resetModules();
20
+
21
+ createRenderer = require('react-test-renderer/shallow').createRenderer;
22
+ PropTypes = require('prop-types');
23
+ React = require('react');
24
+ });
25
+
26
+ it('should call all of the legacy lifecycle hooks', () => {
27
+ const logs = [];
28
+ const logger = message => () => logs.push(message) || true;
29
+
30
+ const SomeComponent = React.memo(
31
+ class SomeComponent extends React.Component {
32
+ UNSAFE_componentWillMount = logger('componentWillMount');
33
+ componentDidMount = logger('componentDidMount');
34
+ UNSAFE_componentWillReceiveProps = logger('componentWillReceiveProps');
35
+ shouldComponentUpdate = logger('shouldComponentUpdate');
36
+ UNSAFE_componentWillUpdate = logger('componentWillUpdate');
37
+ componentDidUpdate = logger('componentDidUpdate');
38
+ componentWillUnmount = logger('componentWillUnmount');
39
+ render() {
40
+ return <div />;
41
+ }
42
+ },
43
+ );
44
+
45
+ const shallowRenderer = createRenderer();
46
+ shallowRenderer.render(<SomeComponent foo={1} />);
47
+
48
+ // Calling cDU might lead to problems with host component references.
49
+ // Since our components aren't really mounted, refs won't be available.
50
+ expect(logs).toEqual(['componentWillMount']);
51
+
52
+ logs.splice(0);
53
+
54
+ const instance = shallowRenderer.getMountedInstance();
55
+ instance.setState({});
56
+
57
+ expect(logs).toEqual(['shouldComponentUpdate', 'componentWillUpdate']);
58
+
59
+ logs.splice(0);
60
+
61
+ shallowRenderer.render(<SomeComponent foo={2} />);
62
+
63
+ // The previous shallow renderer did not trigger cDU for props changes.
64
+ expect(logs).toEqual([
65
+ 'componentWillReceiveProps',
66
+ 'shouldComponentUpdate',
67
+ 'componentWillUpdate',
68
+ ]);
69
+ });
70
+
71
+ it('should call all of the new lifecycle hooks', () => {
72
+ const logs = [];
73
+ const logger = message => () => logs.push(message) || true;
74
+
75
+ const SomeComponent = React.memo(
76
+ class SomeComponent extends React.Component {
77
+ state = {};
78
+ static getDerivedStateFromProps = logger('getDerivedStateFromProps');
79
+ componentDidMount = logger('componentDidMount');
80
+ shouldComponentUpdate = logger('shouldComponentUpdate');
81
+ componentDidUpdate = logger('componentDidUpdate');
82
+ componentWillUnmount = logger('componentWillUnmount');
83
+ render() {
84
+ return <div />;
85
+ }
86
+ },
87
+ );
88
+
89
+ const shallowRenderer = createRenderer();
90
+ shallowRenderer.render(<SomeComponent foo={1} />);
91
+
92
+ // Calling cDU might lead to problems with host component references.
93
+ // Since our components aren't really mounted, refs won't be available.
94
+ expect(logs).toEqual(['getDerivedStateFromProps']);
95
+
96
+ logs.splice(0);
97
+
98
+ const instance = shallowRenderer.getMountedInstance();
99
+ instance.setState({});
100
+
101
+ expect(logs).toEqual(['getDerivedStateFromProps', 'shouldComponentUpdate']);
102
+
103
+ logs.splice(0);
104
+
105
+ shallowRenderer.render(<SomeComponent foo={2} />);
106
+
107
+ // The previous shallow renderer did not trigger cDU for props changes.
108
+ expect(logs).toEqual(['getDerivedStateFromProps', 'shouldComponentUpdate']);
109
+ });
110
+
111
+ it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new static gDSFP is present', () => {
112
+ const Component = React.memo(
113
+ class Component extends React.Component {
114
+ state = {};
115
+ static getDerivedStateFromProps() {
116
+ return null;
117
+ }
118
+ componentWillMount() {
119
+ throw Error('unexpected');
120
+ }
121
+ componentWillReceiveProps() {
122
+ throw Error('unexpected');
123
+ }
124
+ componentWillUpdate() {
125
+ throw Error('unexpected');
126
+ }
127
+ render() {
128
+ return null;
129
+ }
130
+ },
131
+ );
132
+
133
+ const shallowRenderer = createRenderer();
134
+ shallowRenderer.render(<Component />);
135
+ });
136
+
137
+ it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new getSnapshotBeforeUpdate is present', () => {
138
+ const Component = React.memo(
139
+ class Component extends React.Component {
140
+ getSnapshotBeforeUpdate() {
141
+ return null;
142
+ }
143
+ componentWillMount() {
144
+ throw Error('unexpected');
145
+ }
146
+ componentWillReceiveProps() {
147
+ throw Error('unexpected');
148
+ }
149
+ componentWillUpdate() {
150
+ throw Error('unexpected');
151
+ }
152
+ render() {
153
+ return null;
154
+ }
155
+ },
156
+ );
157
+
158
+ const shallowRenderer = createRenderer();
159
+ shallowRenderer.render(<Component value={1} />);
160
+ shallowRenderer.render(<Component value={2} />);
161
+ });
162
+
163
+ it('should not call getSnapshotBeforeUpdate or componentDidUpdate when updating since refs wont exist', () => {
164
+ const Component = React.memo(
165
+ class Component extends React.Component {
166
+ getSnapshotBeforeUpdate() {
167
+ throw Error('unexpected');
168
+ }
169
+ componentDidUpdate() {
170
+ throw Error('unexpected');
171
+ }
172
+ render() {
173
+ return null;
174
+ }
175
+ },
176
+ );
177
+
178
+ const shallowRenderer = createRenderer();
179
+ shallowRenderer.render(<Component value={1} />);
180
+ shallowRenderer.render(<Component value={2} />);
181
+ });
182
+
183
+ it('should only render 1 level deep', () => {
184
+ const Parent = React.memo(function Parent() {
185
+ return (
186
+ <div>
187
+ <Child />
188
+ </div>
189
+ );
190
+ });
191
+
192
+ function Child() {
193
+ throw Error('This component should not render');
194
+ }
195
+
196
+ const shallowRenderer = createRenderer();
197
+ shallowRenderer.render(React.createElement(Parent));
198
+ });
199
+
200
+ it('should have shallow rendering', () => {
201
+ const SomeComponent = React.memo(
202
+ class SomeComponent extends React.Component {
203
+ render() {
204
+ return (
205
+ <div>
206
+ <span className="child1" />
207
+ <span className="child2" />
208
+ </div>
209
+ );
210
+ }
211
+ },
212
+ );
213
+
214
+ const shallowRenderer = createRenderer();
215
+ const result = shallowRenderer.render(<SomeComponent />);
216
+
217
+ expect(result.type).toBe('div');
218
+ expect(result.props.children).toEqual([
219
+ <span className="child1" />,
220
+ <span className="child2" />,
221
+ ]);
222
+ });
223
+
224
+ it('should handle Profiler', () => {
225
+ const SomeComponent = React.memo(
226
+ class SomeComponent extends React.Component {
227
+ render() {
228
+ return (
229
+ <React.unstable_Profiler id="test" onRender={jest.fn()}>
230
+ <div>
231
+ <span className="child1" />
232
+ <span className="child2" />
233
+ </div>
234
+ </React.unstable_Profiler>
235
+ );
236
+ }
237
+ },
238
+ );
239
+
240
+ const shallowRenderer = createRenderer();
241
+ const result = shallowRenderer.render(<SomeComponent />);
242
+
243
+ expect(result.type).toBe(React.unstable_Profiler);
244
+ expect(result.props.children).toEqual(
245
+ <div>
246
+ <span className="child1" />
247
+ <span className="child2" />
248
+ </div>,
249
+ );
250
+ });
251
+
252
+ it('should enable shouldComponentUpdate to prevent a re-render', () => {
253
+ let renderCounter = 0;
254
+ const SimpleComponent = React.memo(
255
+ class SimpleComponent extends React.Component {
256
+ state = {update: false};
257
+ shouldComponentUpdate(nextProps, nextState) {
258
+ return this.state.update !== nextState.update;
259
+ }
260
+ render() {
261
+ renderCounter++;
262
+ return <div>{`${renderCounter}`}</div>;
263
+ }
264
+ },
265
+ );
266
+
267
+ const shallowRenderer = createRenderer();
268
+ shallowRenderer.render(<SimpleComponent />);
269
+ expect(shallowRenderer.getRenderOutput()).toEqual(<div>1</div>);
270
+
271
+ const instance = shallowRenderer.getMountedInstance();
272
+ instance.setState({update: false});
273
+ expect(shallowRenderer.getRenderOutput()).toEqual(<div>1</div>);
274
+
275
+ instance.setState({update: true});
276
+ expect(shallowRenderer.getRenderOutput()).toEqual(<div>2</div>);
277
+ });
278
+
279
+ it('should enable PureComponent to prevent a re-render', () => {
280
+ let renderCounter = 0;
281
+ const SimpleComponent = React.memo(
282
+ class SimpleComponent extends React.PureComponent {
283
+ state = {update: false};
284
+ render() {
285
+ renderCounter++;
286
+ return <div>{`${renderCounter}`}</div>;
287
+ }
288
+ },
289
+ );
290
+
291
+ const shallowRenderer = createRenderer();
292
+ shallowRenderer.render(<SimpleComponent />);
293
+ expect(shallowRenderer.getRenderOutput()).toEqual(<div>1</div>);
294
+
295
+ const instance = shallowRenderer.getMountedInstance();
296
+ instance.setState({update: false});
297
+ expect(shallowRenderer.getRenderOutput()).toEqual(<div>1</div>);
298
+
299
+ instance.setState({update: true});
300
+ expect(shallowRenderer.getRenderOutput()).toEqual(<div>2</div>);
301
+ });
302
+
303
+ it('should not run shouldComponentUpdate during forced update', () => {
304
+ let scuCounter = 0;
305
+ const SimpleComponent = React.memo(
306
+ class SimpleComponent extends React.Component {
307
+ state = {count: 1};
308
+ shouldComponentUpdate() {
309
+ scuCounter++;
310
+ return false;
311
+ }
312
+ render() {
313
+ return <div>{`${this.state.count}`}</div>;
314
+ }
315
+ },
316
+ );
317
+
318
+ const shallowRenderer = createRenderer();
319
+ shallowRenderer.render(<SimpleComponent />);
320
+ expect(scuCounter).toEqual(0);
321
+ expect(shallowRenderer.getRenderOutput()).toEqual(<div>1</div>);
322
+
323
+ // Force update the initial state. sCU should not fire.
324
+ const instance = shallowRenderer.getMountedInstance();
325
+ instance.forceUpdate();
326
+ expect(scuCounter).toEqual(0);
327
+ expect(shallowRenderer.getRenderOutput()).toEqual(<div>1</div>);
328
+
329
+ // Setting state updates the instance, but doesn't re-render
330
+ // because sCU returned false.
331
+ instance.setState(state => ({count: state.count + 1}));
332
+ expect(scuCounter).toEqual(1);
333
+ expect(instance.state.count).toEqual(2);
334
+ expect(shallowRenderer.getRenderOutput()).toEqual(<div>1</div>);
335
+
336
+ // A force update updates the render output, but doesn't call sCU.
337
+ instance.forceUpdate();
338
+ expect(scuCounter).toEqual(1);
339
+ expect(instance.state.count).toEqual(2);
340
+ expect(shallowRenderer.getRenderOutput()).toEqual(<div>2</div>);
341
+ });
342
+
343
+ it('should rerender when calling forceUpdate', () => {
344
+ let renderCounter = 0;
345
+ const SimpleComponent = React.memo(
346
+ class SimpleComponent extends React.Component {
347
+ render() {
348
+ renderCounter += 1;
349
+ return <div />;
350
+ }
351
+ },
352
+ );
353
+
354
+ const shallowRenderer = createRenderer();
355
+ shallowRenderer.render(<SimpleComponent />);
356
+ expect(renderCounter).toEqual(1);
357
+
358
+ const instance = shallowRenderer.getMountedInstance();
359
+ instance.forceUpdate();
360
+ expect(renderCounter).toEqual(2);
361
+ });
362
+
363
+ it('should shallow render a function component', () => {
364
+ function SomeComponent(props, context) {
365
+ return (
366
+ <div>
367
+ <div>{props.foo}</div>
368
+ <div>{context.bar}</div>
369
+ <span className="child1" />
370
+ <span className="child2" />
371
+ </div>
372
+ );
373
+ }
374
+ const SomeMemoComponent = React.memo(SomeComponent);
375
+
376
+ SomeComponent.contextTypes = {
377
+ bar: PropTypes.string,
378
+ };
379
+
380
+ const shallowRenderer = createRenderer();
381
+ const result = shallowRenderer.render(<SomeMemoComponent foo={'FOO'} />, {
382
+ bar: 'BAR',
383
+ });
384
+
385
+ expect(result.type).toBe('div');
386
+ expect(result.props.children).toEqual([
387
+ <div>FOO</div>,
388
+ <div>BAR</div>,
389
+ <span className="child1" />,
390
+ <span className="child2" />,
391
+ ]);
392
+ });
393
+
394
+ it('should shallow render a component returning strings directly from render', () => {
395
+ const Text = React.memo(({value}) => value);
396
+
397
+ const shallowRenderer = createRenderer();
398
+ const result = shallowRenderer.render(<Text value="foo" />);
399
+ expect(result).toEqual('foo');
400
+ });
401
+
402
+ it('should shallow render a component returning numbers directly from render', () => {
403
+ const Text = React.memo(({value}) => value);
404
+
405
+ const shallowRenderer = createRenderer();
406
+ const result = shallowRenderer.render(<Text value={10} />);
407
+ expect(result).toEqual(10);
408
+ });
409
+
410
+ it('should shallow render a fragment', () => {
411
+ class SomeComponent extends React.Component {
412
+ render() {
413
+ return <div />;
414
+ }
415
+ }
416
+ class Fragment extends React.Component {
417
+ render() {
418
+ return [<div key="a" />, <span key="b" />, <SomeComponent />];
419
+ }
420
+ }
421
+ const shallowRenderer = createRenderer();
422
+ const result = shallowRenderer.render(<Fragment />);
423
+ expect(result).toEqual([
424
+ <div key="a" />,
425
+ <span key="b" />,
426
+ <SomeComponent />,
427
+ ]);
428
+ });
429
+
430
+ it('should shallow render a React.fragment', () => {
431
+ class SomeComponent extends React.Component {
432
+ render() {
433
+ return <div />;
434
+ }
435
+ }
436
+ class Fragment extends React.Component {
437
+ render() {
438
+ return (
439
+ <React.Fragment>
440
+ <div />
441
+ <span />
442
+ <SomeComponent />
443
+ </React.Fragment>
444
+ );
445
+ }
446
+ }
447
+ const shallowRenderer = createRenderer();
448
+ const result = shallowRenderer.render(<Fragment />);
449
+ expect(result).toEqual(
450
+ <React.Fragment>
451
+ <div />
452
+ <span />
453
+ <SomeComponent />
454
+ </React.Fragment>,
455
+ );
456
+ });
457
+
458
+ it('should throw for invalid elements', () => {
459
+ class SomeComponent extends React.Component {
460
+ render() {
461
+ return <div />;
462
+ }
463
+ }
464
+
465
+ const shallowRenderer = createRenderer();
466
+ expect(() => shallowRenderer.render(SomeComponent)).toThrowError(
467
+ 'ReactShallowRenderer render(): Invalid component element. Instead of ' +
468
+ 'passing a component class, make sure to instantiate it by passing it ' +
469
+ 'to React.createElement.',
470
+ );
471
+ expect(() => shallowRenderer.render(<div />)).toThrowError(
472
+ 'ReactShallowRenderer render(): Shallow rendering works only with ' +
473
+ 'custom components, not primitives (div). Instead of calling ' +
474
+ '`.render(el)` and inspecting the rendered output, look at `el.props` ' +
475
+ 'directly instead.',
476
+ );
477
+ });
478
+
479
+ it('should have shallow unmounting', () => {
480
+ const componentWillUnmount = jest.fn();
481
+
482
+ class SomeComponent extends React.Component {
483
+ componentWillUnmount = componentWillUnmount;
484
+ render() {
485
+ return <div />;
486
+ }
487
+ }
488
+
489
+ const shallowRenderer = createRenderer();
490
+ shallowRenderer.render(<SomeComponent />);
491
+ shallowRenderer.unmount();
492
+
493
+ expect(componentWillUnmount).toBeCalled();
494
+ });
495
+
496
+ it('can shallow render to null', () => {
497
+ class SomeComponent extends React.Component {
498
+ render() {
499
+ return null;
500
+ }
501
+ }
502
+
503
+ const shallowRenderer = createRenderer();
504
+ const result = shallowRenderer.render(<SomeComponent />);
505
+
506
+ expect(result).toBe(null);
507
+ });
508
+
509
+ it('can shallow render with a ref', () => {
510
+ class SomeComponent extends React.Component {
511
+ render() {
512
+ return <div ref="hello" />;
513
+ }
514
+ }
515
+
516
+ const shallowRenderer = createRenderer();
517
+ // Shouldn't crash.
518
+ shallowRenderer.render(<SomeComponent />);
519
+ });
520
+
521
+ it('lets you update shallowly rendered components', () => {
522
+ class SomeComponent extends React.Component {
523
+ state = {clicked: false};
524
+
525
+ onClick = () => {
526
+ this.setState({clicked: true});
527
+ };
528
+
529
+ render() {
530
+ const className = this.state.clicked ? 'was-clicked' : '';
531
+
532
+ if (this.props.aNew === 'prop') {
533
+ return (
534
+ <a href="#" onClick={this.onClick} className={className}>
535
+ Test link
536
+ </a>
537
+ );
538
+ } else {
539
+ return (
540
+ <div>
541
+ <span className="child1" />
542
+ <span className="child2" />
543
+ </div>
544
+ );
545
+ }
546
+ }
547
+ }
548
+
549
+ const shallowRenderer = createRenderer();
550
+ const result = shallowRenderer.render(<SomeComponent />);
551
+ expect(result.type).toBe('div');
552
+ expect(result.props.children).toEqual([
553
+ <span className="child1" />,
554
+ <span className="child2" />,
555
+ ]);
556
+
557
+ const updatedResult = shallowRenderer.render(<SomeComponent aNew="prop" />);
558
+ expect(updatedResult.type).toBe('a');
559
+
560
+ const mockEvent = {};
561
+ updatedResult.props.onClick(mockEvent);
562
+
563
+ const updatedResultCausedByClick = shallowRenderer.getRenderOutput();
564
+ expect(updatedResultCausedByClick.type).toBe('a');
565
+ expect(updatedResultCausedByClick.props.className).toBe('was-clicked');
566
+ });
567
+
568
+ it('can access the mounted component instance', () => {
569
+ const SimpleComponent = React.memo(
570
+ class SimpleComponent extends React.Component {
571
+ someMethod = () => {
572
+ return this.props.n;
573
+ };
574
+
575
+ render() {
576
+ return <div>{this.props.n}</div>;
577
+ }
578
+ },
579
+ );
580
+
581
+ const shallowRenderer = createRenderer();
582
+ shallowRenderer.render(<SimpleComponent n={5} />);
583
+ expect(shallowRenderer.getMountedInstance().someMethod()).toEqual(5);
584
+ });
585
+
586
+ it('can shallowly render components with contextTypes', () => {
587
+ const SimpleComponent = React.memo(
588
+ class SimpleComponent extends React.Component {
589
+ static contextTypes = {
590
+ name: PropTypes.string,
591
+ };
592
+
593
+ render() {
594
+ return <div />;
595
+ }
596
+ },
597
+ );
598
+
599
+ const shallowRenderer = createRenderer();
600
+ const result = shallowRenderer.render(<SimpleComponent />);
601
+ expect(result).toEqual(<div />);
602
+ });
603
+
604
+ it('passes expected params to legacy component lifecycle methods', () => {
605
+ const componentDidUpdateParams = [];
606
+ const componentWillReceivePropsParams = [];
607
+ const componentWillUpdateParams = [];
608
+ const setStateParams = [];
609
+ const shouldComponentUpdateParams = [];
610
+
611
+ const initialProp = {prop: 'init prop'};
612
+ const initialState = {state: 'init state'};
613
+ const initialContext = {context: 'init context'};
614
+ const updatedState = {state: 'updated state'};
615
+ const updatedProp = {prop: 'updated prop'};
616
+ const updatedContext = {context: 'updated context'};
617
+
618
+ const SimpleComponent = React.memo(
619
+ class SimpleComponent extends React.Component {
620
+ constructor(props, context) {
621
+ super(props, context);
622
+ this.state = initialState;
623
+ }
624
+ static contextTypes = {
625
+ context: PropTypes.string,
626
+ };
627
+ componentDidUpdate(...args) {
628
+ componentDidUpdateParams.push(...args);
629
+ }
630
+ UNSAFE_componentWillReceiveProps(...args) {
631
+ componentWillReceivePropsParams.push(...args);
632
+ this.setState((...innerArgs) => {
633
+ setStateParams.push(...innerArgs);
634
+ return updatedState;
635
+ });
636
+ }
637
+ UNSAFE_componentWillUpdate(...args) {
638
+ componentWillUpdateParams.push(...args);
639
+ }
640
+ shouldComponentUpdate(...args) {
641
+ shouldComponentUpdateParams.push(...args);
642
+ return true;
643
+ }
644
+ render() {
645
+ return null;
646
+ }
647
+ },
648
+ );
649
+
650
+ const shallowRenderer = createRenderer();
651
+ shallowRenderer.render(
652
+ React.createElement(SimpleComponent, initialProp),
653
+ initialContext,
654
+ );
655
+ expect(componentDidUpdateParams).toEqual([]);
656
+ expect(componentWillReceivePropsParams).toEqual([]);
657
+ expect(componentWillUpdateParams).toEqual([]);
658
+ expect(setStateParams).toEqual([]);
659
+ expect(shouldComponentUpdateParams).toEqual([]);
660
+
661
+ // Lifecycle hooks should be invoked with the correct prev/next params on update.
662
+ shallowRenderer.render(
663
+ React.createElement(SimpleComponent, updatedProp),
664
+ updatedContext,
665
+ );
666
+ expect(componentWillReceivePropsParams).toEqual([
667
+ updatedProp,
668
+ updatedContext,
669
+ ]);
670
+ expect(setStateParams).toEqual([initialState, initialProp]);
671
+ expect(shouldComponentUpdateParams).toEqual([
672
+ updatedProp,
673
+ updatedState,
674
+ updatedContext,
675
+ ]);
676
+ expect(componentWillUpdateParams).toEqual([
677
+ updatedProp,
678
+ updatedState,
679
+ updatedContext,
680
+ ]);
681
+ expect(componentDidUpdateParams).toEqual([]);
682
+ });
683
+
684
+ it('passes expected params to new component lifecycle methods', () => {
685
+ const componentDidUpdateParams = [];
686
+ const getDerivedStateFromPropsParams = [];
687
+ const shouldComponentUpdateParams = [];
688
+
689
+ const initialProp = {prop: 'init prop'};
690
+ const initialState = {state: 'init state'};
691
+ const initialContext = {context: 'init context'};
692
+ const updatedProp = {prop: 'updated prop'};
693
+ const updatedContext = {context: 'updated context'};
694
+
695
+ const SimpleComponent = React.memo(
696
+ class SimpleComponent extends React.Component {
697
+ constructor(props, context) {
698
+ super(props, context);
699
+ this.state = initialState;
700
+ }
701
+ static contextTypes = {
702
+ context: PropTypes.string,
703
+ };
704
+ componentDidUpdate(...args) {
705
+ componentDidUpdateParams.push(...args);
706
+ }
707
+ static getDerivedStateFromProps(...args) {
708
+ getDerivedStateFromPropsParams.push(args);
709
+ return null;
710
+ }
711
+ shouldComponentUpdate(...args) {
712
+ shouldComponentUpdateParams.push(...args);
713
+ return true;
714
+ }
715
+ render() {
716
+ return null;
717
+ }
718
+ },
719
+ );
720
+
721
+ const shallowRenderer = createRenderer();
722
+
723
+ // The only lifecycle hook that should be invoked on initial render
724
+ // Is the static getDerivedStateFromProps() methods
725
+ shallowRenderer.render(
726
+ React.createElement(SimpleComponent, initialProp),
727
+ initialContext,
728
+ );
729
+ expect(getDerivedStateFromPropsParams).toEqual([
730
+ [initialProp, initialState],
731
+ ]);
732
+ expect(componentDidUpdateParams).toEqual([]);
733
+ expect(shouldComponentUpdateParams).toEqual([]);
734
+
735
+ // Lifecycle hooks should be invoked with the correct prev/next params on update.
736
+ shallowRenderer.render(
737
+ React.createElement(SimpleComponent, updatedProp),
738
+ updatedContext,
739
+ );
740
+ expect(getDerivedStateFromPropsParams).toEqual([
741
+ [initialProp, initialState],
742
+ [updatedProp, initialState],
743
+ ]);
744
+ expect(shouldComponentUpdateParams).toEqual([
745
+ updatedProp,
746
+ initialState,
747
+ updatedContext,
748
+ ]);
749
+ expect(componentDidUpdateParams).toEqual([]);
750
+ });
751
+
752
+ it('can shallowly render components with ref as function', () => {
753
+ const SimpleComponent = React.memo(
754
+ class SimpleComponent extends React.Component {
755
+ state = {clicked: false};
756
+
757
+ handleUserClick = () => {
758
+ this.setState({clicked: true});
759
+ };
760
+
761
+ render() {
762
+ return (
763
+ <div
764
+ ref={() => {}}
765
+ onClick={this.handleUserClick}
766
+ className={this.state.clicked ? 'clicked' : ''}
767
+ />
768
+ );
769
+ }
770
+ },
771
+ );
772
+
773
+ const shallowRenderer = createRenderer();
774
+ shallowRenderer.render(<SimpleComponent />);
775
+ let result = shallowRenderer.getRenderOutput();
776
+ expect(result.type).toEqual('div');
777
+ expect(result.props.className).toEqual('');
778
+ result.props.onClick();
779
+
780
+ result = shallowRenderer.getRenderOutput();
781
+ expect(result.type).toEqual('div');
782
+ expect(result.props.className).toEqual('clicked');
783
+ });
784
+
785
+ it('can initialize state via static getDerivedStateFromProps', () => {
786
+ const SimpleComponent = React.memo(
787
+ class SimpleComponent extends React.Component {
788
+ state = {
789
+ count: 1,
790
+ };
791
+
792
+ static getDerivedStateFromProps(props, prevState) {
793
+ return {
794
+ count: prevState.count + props.incrementBy,
795
+ other: 'foobar',
796
+ };
797
+ }
798
+
799
+ render() {
800
+ return (
801
+ <div>{`count:${this.state.count}, other:${this.state.other}`}</div>
802
+ );
803
+ }
804
+ },
805
+ );
806
+
807
+ const shallowRenderer = createRenderer();
808
+ const result = shallowRenderer.render(<SimpleComponent incrementBy={2} />);
809
+ expect(result).toEqual(<div>count:3, other:foobar</div>);
810
+ });
811
+
812
+ it('can setState in componentWillMount when shallow rendering', () => {
813
+ const SimpleComponent = React.memo(
814
+ class SimpleComponent extends React.Component {
815
+ UNSAFE_componentWillMount() {
816
+ this.setState({groovy: 'doovy'});
817
+ }
818
+
819
+ render() {
820
+ return <div>{this.state.groovy}</div>;
821
+ }
822
+ },
823
+ );
824
+
825
+ const shallowRenderer = createRenderer();
826
+ const result = shallowRenderer.render(<SimpleComponent />);
827
+ expect(result).toEqual(<div>doovy</div>);
828
+ });
829
+
830
+ it('can setState in componentWillMount repeatedly when shallow rendering', () => {
831
+ const SimpleComponent = React.memo(
832
+ class SimpleComponent extends React.Component {
833
+ state = {
834
+ separator: '-',
835
+ };
836
+
837
+ UNSAFE_componentWillMount() {
838
+ this.setState({groovy: 'doovy'});
839
+ this.setState({doovy: 'groovy'});
840
+ }
841
+
842
+ render() {
843
+ const {groovy, doovy, separator} = this.state;
844
+
845
+ return <div>{`${groovy}${separator}${doovy}`}</div>;
846
+ }
847
+ },
848
+ );
849
+
850
+ const shallowRenderer = createRenderer();
851
+ const result = shallowRenderer.render(<SimpleComponent />);
852
+ expect(result).toEqual(<div>doovy-groovy</div>);
853
+ });
854
+
855
+ it('can setState in componentWillMount with an updater function repeatedly when shallow rendering', () => {
856
+ const SimpleComponent = React.memo(
857
+ class SimpleComponent extends React.Component {
858
+ state = {
859
+ separator: '-',
860
+ };
861
+
862
+ UNSAFE_componentWillMount() {
863
+ this.setState(state => ({groovy: 'doovy'}));
864
+ this.setState(state => ({doovy: state.groovy}));
865
+ }
866
+
867
+ render() {
868
+ const {groovy, doovy, separator} = this.state;
869
+
870
+ return <div>{`${groovy}${separator}${doovy}`}</div>;
871
+ }
872
+ },
873
+ );
874
+
875
+ const shallowRenderer = createRenderer();
876
+ const result = shallowRenderer.render(<SimpleComponent />);
877
+ expect(result).toEqual(<div>doovy-doovy</div>);
878
+ });
879
+
880
+ it('can setState in componentWillReceiveProps when shallow rendering', () => {
881
+ const SimpleComponent = React.memo(
882
+ class SimpleComponent extends React.Component {
883
+ state = {count: 0};
884
+
885
+ UNSAFE_componentWillReceiveProps(nextProps) {
886
+ if (nextProps.updateState) {
887
+ this.setState({count: 1});
888
+ }
889
+ }
890
+
891
+ render() {
892
+ return <div>{this.state.count}</div>;
893
+ }
894
+ },
895
+ );
896
+
897
+ const shallowRenderer = createRenderer();
898
+ let result = shallowRenderer.render(
899
+ <SimpleComponent updateState={false} />,
900
+ );
901
+ expect(result.props.children).toEqual(0);
902
+
903
+ result = shallowRenderer.render(<SimpleComponent updateState={true} />);
904
+ expect(result.props.children).toEqual(1);
905
+ });
906
+
907
+ it('can update state with static getDerivedStateFromProps when shallow rendering', () => {
908
+ const SimpleComponent = React.memo(
909
+ class SimpleComponent extends React.Component {
910
+ state = {count: 1};
911
+
912
+ static getDerivedStateFromProps(nextProps, prevState) {
913
+ if (nextProps.updateState) {
914
+ return {count: nextProps.incrementBy + prevState.count};
915
+ }
916
+
917
+ return null;
918
+ }
919
+
920
+ render() {
921
+ return <div>{this.state.count}</div>;
922
+ }
923
+ },
924
+ );
925
+
926
+ const shallowRenderer = createRenderer();
927
+ let result = shallowRenderer.render(
928
+ <SimpleComponent updateState={false} incrementBy={0} />,
929
+ );
930
+ expect(result.props.children).toEqual(1);
931
+
932
+ result = shallowRenderer.render(
933
+ <SimpleComponent updateState={true} incrementBy={2} />,
934
+ );
935
+ expect(result.props.children).toEqual(3);
936
+
937
+ result = shallowRenderer.render(
938
+ <SimpleComponent updateState={false} incrementBy={2} />,
939
+ );
940
+ expect(result.props.children).toEqual(3);
941
+ });
942
+
943
+ it('should not override state with stale values if prevState is spread within getDerivedStateFromProps', () => {
944
+ const SimpleComponent = React.memo(
945
+ class SimpleComponent extends React.Component {
946
+ state = {value: 0};
947
+
948
+ static getDerivedStateFromProps(nextProps, prevState) {
949
+ return {...prevState};
950
+ }
951
+
952
+ updateState = () => {
953
+ this.setState(state => ({value: state.value + 1}));
954
+ };
955
+
956
+ render() {
957
+ return <div>{`value:${this.state.value}`}</div>;
958
+ }
959
+ },
960
+ );
961
+
962
+ const shallowRenderer = createRenderer();
963
+ let result = shallowRenderer.render(<SimpleComponent />);
964
+ expect(result).toEqual(<div>value:0</div>);
965
+
966
+ let instance = shallowRenderer.getMountedInstance();
967
+ instance.updateState();
968
+ result = shallowRenderer.getRenderOutput();
969
+ expect(result).toEqual(<div>value:1</div>);
970
+ });
971
+
972
+ it('should pass previous state to shouldComponentUpdate even with getDerivedStateFromProps', () => {
973
+ const SimpleComponent = React.memo(
974
+ class SimpleComponent extends React.Component {
975
+ constructor(props) {
976
+ super(props);
977
+ this.state = {
978
+ value: props.value,
979
+ };
980
+ }
981
+
982
+ static getDerivedStateFromProps(nextProps, prevState) {
983
+ if (nextProps.value === prevState.value) {
984
+ return null;
985
+ }
986
+ return {value: nextProps.value};
987
+ }
988
+
989
+ shouldComponentUpdate(nextProps, nextState) {
990
+ return nextState.value !== this.state.value;
991
+ }
992
+
993
+ render() {
994
+ return <div>{`value:${this.state.value}`}</div>;
995
+ }
996
+ },
997
+ );
998
+
999
+ const shallowRenderer = createRenderer();
1000
+ const initialResult = shallowRenderer.render(
1001
+ <SimpleComponent value="initial" />,
1002
+ );
1003
+ expect(initialResult).toEqual(<div>value:initial</div>);
1004
+ const updatedResult = shallowRenderer.render(
1005
+ <SimpleComponent value="updated" />,
1006
+ );
1007
+ expect(updatedResult).toEqual(<div>value:updated</div>);
1008
+ });
1009
+
1010
+ it('can setState with an updater function', () => {
1011
+ let instance;
1012
+
1013
+ const SimpleComponent = React.memo(
1014
+ class SimpleComponent extends React.Component {
1015
+ state = {
1016
+ counter: 0,
1017
+ };
1018
+
1019
+ render() {
1020
+ instance = this;
1021
+ return (
1022
+ <button ref="button" onClick={this.onClick}>
1023
+ {this.state.counter}
1024
+ </button>
1025
+ );
1026
+ }
1027
+ },
1028
+ );
1029
+
1030
+ const shallowRenderer = createRenderer();
1031
+ let result = shallowRenderer.render(<SimpleComponent defaultCount={1} />);
1032
+ expect(result.props.children).toEqual(0);
1033
+
1034
+ instance.setState((state, props) => {
1035
+ return {counter: props.defaultCount + 1};
1036
+ });
1037
+
1038
+ result = shallowRenderer.getRenderOutput();
1039
+ expect(result.props.children).toEqual(2);
1040
+ });
1041
+
1042
+ it('can access component instance from setState updater function', done => {
1043
+ let instance;
1044
+
1045
+ const SimpleComponent = React.memo(
1046
+ class SimpleComponent extends React.Component {
1047
+ state = {};
1048
+
1049
+ render() {
1050
+ instance = this;
1051
+ return null;
1052
+ }
1053
+ },
1054
+ );
1055
+
1056
+ const shallowRenderer = createRenderer();
1057
+ shallowRenderer.render(<SimpleComponent />);
1058
+
1059
+ instance.setState(function updater(state, props) {
1060
+ expect(this).toBe(instance);
1061
+ done();
1062
+ });
1063
+ });
1064
+
1065
+ it('can setState with a callback', () => {
1066
+ let instance;
1067
+
1068
+ const SimpleComponent = React.memo(
1069
+ class SimpleComponent extends React.Component {
1070
+ state = {
1071
+ counter: 0,
1072
+ };
1073
+ render() {
1074
+ instance = this;
1075
+ return <p>{this.state.counter}</p>;
1076
+ }
1077
+ },
1078
+ );
1079
+
1080
+ const shallowRenderer = createRenderer();
1081
+ const result = shallowRenderer.render(<SimpleComponent />);
1082
+ expect(result.props.children).toBe(0);
1083
+
1084
+ const callback = jest.fn(function() {
1085
+ expect(this).toBe(instance);
1086
+ });
1087
+
1088
+ instance.setState({counter: 1}, callback);
1089
+
1090
+ const updated = shallowRenderer.getRenderOutput();
1091
+ expect(updated.props.children).toBe(1);
1092
+ expect(callback).toHaveBeenCalled();
1093
+ });
1094
+
1095
+ it('can replaceState with a callback', () => {
1096
+ let instance;
1097
+
1098
+ const SimpleComponent = React.memo(
1099
+ class SimpleComponent extends React.Component {
1100
+ state = {
1101
+ counter: 0,
1102
+ };
1103
+ render() {
1104
+ instance = this;
1105
+ return <p>{this.state.counter}</p>;
1106
+ }
1107
+ },
1108
+ );
1109
+
1110
+ const shallowRenderer = createRenderer();
1111
+ const result = shallowRenderer.render(<SimpleComponent />);
1112
+ expect(result.props.children).toBe(0);
1113
+
1114
+ const callback = jest.fn(function() {
1115
+ expect(this).toBe(instance);
1116
+ });
1117
+
1118
+ // No longer a public API, but we can test that it works internally by
1119
+ // reaching into the updater.
1120
+ shallowRenderer._updater.enqueueReplaceState(
1121
+ instance,
1122
+ {counter: 1},
1123
+ callback,
1124
+ );
1125
+
1126
+ const updated = shallowRenderer.getRenderOutput();
1127
+ expect(updated.props.children).toBe(1);
1128
+ expect(callback).toHaveBeenCalled();
1129
+ });
1130
+
1131
+ it('can forceUpdate with a callback', () => {
1132
+ let instance;
1133
+
1134
+ const SimpleComponent = React.memo(
1135
+ class SimpleComponent extends React.Component {
1136
+ state = {
1137
+ counter: 0,
1138
+ };
1139
+ render() {
1140
+ instance = this;
1141
+ return <p>{this.state.counter}</p>;
1142
+ }
1143
+ },
1144
+ );
1145
+
1146
+ const shallowRenderer = createRenderer();
1147
+ const result = shallowRenderer.render(<SimpleComponent />);
1148
+ expect(result.props.children).toBe(0);
1149
+
1150
+ const callback = jest.fn(function() {
1151
+ expect(this).toBe(instance);
1152
+ });
1153
+
1154
+ instance.forceUpdate(callback);
1155
+
1156
+ const updated = shallowRenderer.getRenderOutput();
1157
+ expect(updated.props.children).toBe(0);
1158
+ expect(callback).toHaveBeenCalled();
1159
+ });
1160
+
1161
+ it('can pass context when shallowly rendering', () => {
1162
+ const SimpleComponent = React.memo(
1163
+ class SimpleComponent extends React.Component {
1164
+ static contextTypes = {
1165
+ name: PropTypes.string,
1166
+ };
1167
+
1168
+ render() {
1169
+ return <div>{this.context.name}</div>;
1170
+ }
1171
+ },
1172
+ );
1173
+
1174
+ const shallowRenderer = createRenderer();
1175
+ const result = shallowRenderer.render(<SimpleComponent />, {
1176
+ name: 'foo',
1177
+ });
1178
+ expect(result).toEqual(<div>foo</div>);
1179
+ });
1180
+
1181
+ it('should track context across updates', () => {
1182
+ const SimpleComponent = React.memo(
1183
+ class SimpleComponent extends React.Component {
1184
+ static contextTypes = {
1185
+ foo: PropTypes.string,
1186
+ };
1187
+
1188
+ state = {
1189
+ bar: 'bar',
1190
+ };
1191
+
1192
+ render() {
1193
+ return <div>{`${this.context.foo}:${this.state.bar}`}</div>;
1194
+ }
1195
+ },
1196
+ );
1197
+
1198
+ const shallowRenderer = createRenderer();
1199
+ let result = shallowRenderer.render(<SimpleComponent />, {
1200
+ foo: 'foo',
1201
+ });
1202
+ expect(result).toEqual(<div>foo:bar</div>);
1203
+
1204
+ const instance = shallowRenderer.getMountedInstance();
1205
+ instance.setState({bar: 'baz'});
1206
+
1207
+ result = shallowRenderer.getRenderOutput();
1208
+ expect(result).toEqual(<div>foo:baz</div>);
1209
+ });
1210
+
1211
+ it('should filter context by contextTypes', () => {
1212
+ const SimpleComponent = React.memo(
1213
+ class SimpleComponent extends React.Component {
1214
+ static contextTypes = {
1215
+ foo: PropTypes.string,
1216
+ };
1217
+ render() {
1218
+ return <div>{`${this.context.foo}:${this.context.bar}`}</div>;
1219
+ }
1220
+ },
1221
+ );
1222
+
1223
+ const shallowRenderer = createRenderer();
1224
+ let result = shallowRenderer.render(<SimpleComponent />, {
1225
+ foo: 'foo',
1226
+ bar: 'bar',
1227
+ });
1228
+ expect(result).toEqual(<div>foo:undefined</div>);
1229
+ });
1230
+
1231
+ it('can fail context when shallowly rendering', () => {
1232
+ const SimpleComponent = React.memo(
1233
+ class SimpleComponent extends React.Component {
1234
+ static contextTypes = {
1235
+ name: PropTypes.string.isRequired,
1236
+ };
1237
+
1238
+ render() {
1239
+ return <div>{this.context.name}</div>;
1240
+ }
1241
+ },
1242
+ );
1243
+
1244
+ const shallowRenderer = createRenderer();
1245
+ expect(() => shallowRenderer.render(<SimpleComponent />)).toWarnDev(
1246
+ 'Warning: Failed context type: The context `name` is marked as ' +
1247
+ 'required in `SimpleComponent`, but its value is `undefined`.\n' +
1248
+ ' in SimpleComponent (at **)',
1249
+ );
1250
+ });
1251
+
1252
+ it('should warn about propTypes (but only once)', () => {
1253
+ const SimpleComponent = React.memo(
1254
+ class SimpleComponent extends React.Component {
1255
+ static propTypes = {
1256
+ name: PropTypes.string.isRequired,
1257
+ };
1258
+
1259
+ render() {
1260
+ return React.createElement('div', null, this.props.name);
1261
+ }
1262
+ },
1263
+ );
1264
+
1265
+ const shallowRenderer = createRenderer();
1266
+ expect(() =>
1267
+ shallowRenderer.render(React.createElement(SimpleComponent, {name: 123})),
1268
+ ).toWarnDev(
1269
+ 'Warning: Failed prop type: Invalid prop `name` of type `number` ' +
1270
+ 'supplied to `SimpleComponent`, expected `string`.\n' +
1271
+ ' in SimpleComponent',
1272
+ );
1273
+ });
1274
+
1275
+ it('should enable rendering of cloned element', () => {
1276
+ const SimpleComponent = React.memo(
1277
+ class SimpleComponent extends React.Component {
1278
+ constructor(props) {
1279
+ super(props);
1280
+
1281
+ this.state = {
1282
+ bar: 'bar',
1283
+ };
1284
+ }
1285
+
1286
+ render() {
1287
+ return <div>{`${this.props.foo}:${this.state.bar}`}</div>;
1288
+ }
1289
+ },
1290
+ );
1291
+
1292
+ const shallowRenderer = createRenderer();
1293
+ const el = <SimpleComponent foo="foo" />;
1294
+ let result = shallowRenderer.render(el);
1295
+ expect(result).toEqual(<div>foo:bar</div>);
1296
+
1297
+ const cloned = React.cloneElement(el, {foo: 'baz'});
1298
+ result = shallowRenderer.render(cloned);
1299
+ expect(result).toEqual(<div>baz:bar</div>);
1300
+ });
1301
+
1302
+ it('this.state should be updated on setState callback inside componentWillMount', () => {
1303
+ let stateSuccessfullyUpdated = false;
1304
+
1305
+ const Component = React.memo(
1306
+ class Component extends React.Component {
1307
+ constructor(props, context) {
1308
+ super(props, context);
1309
+ this.state = {
1310
+ hasUpdatedState: false,
1311
+ };
1312
+ }
1313
+
1314
+ UNSAFE_componentWillMount() {
1315
+ this.setState(
1316
+ {hasUpdatedState: true},
1317
+ () => (stateSuccessfullyUpdated = this.state.hasUpdatedState),
1318
+ );
1319
+ }
1320
+
1321
+ render() {
1322
+ return <div>{this.props.children}</div>;
1323
+ }
1324
+ },
1325
+ );
1326
+
1327
+ const shallowRenderer = createRenderer();
1328
+ shallowRenderer.render(<Component />);
1329
+ expect(stateSuccessfullyUpdated).toBe(true);
1330
+ });
1331
+
1332
+ it('should handle multiple callbacks', () => {
1333
+ const mockFn = jest.fn();
1334
+ const shallowRenderer = createRenderer();
1335
+
1336
+ const Component = React.memo(
1337
+ class Component extends React.Component {
1338
+ constructor(props, context) {
1339
+ super(props, context);
1340
+ this.state = {
1341
+ foo: 'foo',
1342
+ };
1343
+ }
1344
+
1345
+ UNSAFE_componentWillMount() {
1346
+ this.setState({foo: 'bar'}, () => mockFn());
1347
+ this.setState({foo: 'foobar'}, () => mockFn());
1348
+ }
1349
+
1350
+ render() {
1351
+ return <div>{this.state.foo}</div>;
1352
+ }
1353
+ },
1354
+ );
1355
+
1356
+ shallowRenderer.render(<Component />);
1357
+
1358
+ expect(mockFn).toHaveBeenCalledTimes(2);
1359
+
1360
+ // Ensure the callback queue is cleared after the callbacks are invoked
1361
+ const mountedInstance = shallowRenderer.getMountedInstance();
1362
+ mountedInstance.setState({foo: 'bar'}, () => mockFn());
1363
+ expect(mockFn).toHaveBeenCalledTimes(3);
1364
+ });
1365
+
1366
+ it('should call the setState callback even if shouldComponentUpdate = false', done => {
1367
+ const mockFn = jest.fn().mockReturnValue(false);
1368
+
1369
+ const Component = React.memo(
1370
+ class Component extends React.Component {
1371
+ constructor(props, context) {
1372
+ super(props, context);
1373
+ this.state = {
1374
+ hasUpdatedState: false,
1375
+ };
1376
+ }
1377
+
1378
+ shouldComponentUpdate() {
1379
+ return mockFn();
1380
+ }
1381
+
1382
+ render() {
1383
+ return <div>{this.state.hasUpdatedState}</div>;
1384
+ }
1385
+ },
1386
+ );
1387
+
1388
+ const shallowRenderer = createRenderer();
1389
+ shallowRenderer.render(<Component />);
1390
+
1391
+ const mountedInstance = shallowRenderer.getMountedInstance();
1392
+ mountedInstance.setState({hasUpdatedState: true}, () => {
1393
+ expect(mockFn).toBeCalled();
1394
+ expect(mountedInstance.state.hasUpdatedState).toBe(true);
1395
+ done();
1396
+ });
1397
+ });
1398
+
1399
+ it('throws usefully when rendering badly-typed elements', () => {
1400
+ const shallowRenderer = createRenderer();
1401
+
1402
+ const renderAndVerifyWarningAndError = (Component, typeString) => {
1403
+ expect(() => {
1404
+ expect(() => shallowRenderer.render(<Component />)).toWarnDev(
1405
+ 'React.createElement: type is invalid -- expected a string ' +
1406
+ '(for built-in components) or a class/function (for composite components) ' +
1407
+ `but got: ${typeString}.`,
1408
+ );
1409
+ }).toThrowError(
1410
+ 'ReactShallowRenderer render(): Shallow rendering works only with custom ' +
1411
+ `components, but the provided element type was \`${typeString}\`.`,
1412
+ );
1413
+ };
1414
+
1415
+ renderAndVerifyWarningAndError(undefined, 'undefined');
1416
+ renderAndVerifyWarningAndError(null, 'null');
1417
+ renderAndVerifyWarningAndError([], 'array');
1418
+ renderAndVerifyWarningAndError({}, 'object');
1419
+ });
1420
+
1421
+ it('should have initial state of null if not defined', () => {
1422
+ const SomeComponent = React.memo(
1423
+ class SomeComponent extends React.Component {
1424
+ render() {
1425
+ return <span />;
1426
+ }
1427
+ },
1428
+ );
1429
+
1430
+ const shallowRenderer = createRenderer();
1431
+ shallowRenderer.render(<SomeComponent />);
1432
+
1433
+ expect(shallowRenderer.getMountedInstance().state).toBeNull();
1434
+ });
1435
+
1436
+ it('should invoke both deprecated and new lifecycles if both are present', () => {
1437
+ const log = [];
1438
+
1439
+ const Component = React.memo(
1440
+ class Component extends React.Component {
1441
+ componentWillMount() {
1442
+ log.push('componentWillMount');
1443
+ }
1444
+ componentWillReceiveProps() {
1445
+ log.push('componentWillReceiveProps');
1446
+ }
1447
+ componentWillUpdate() {
1448
+ log.push('componentWillUpdate');
1449
+ }
1450
+ UNSAFE_componentWillMount() {
1451
+ log.push('UNSAFE_componentWillMount');
1452
+ }
1453
+ UNSAFE_componentWillReceiveProps() {
1454
+ log.push('UNSAFE_componentWillReceiveProps');
1455
+ }
1456
+ UNSAFE_componentWillUpdate() {
1457
+ log.push('UNSAFE_componentWillUpdate');
1458
+ }
1459
+ render() {
1460
+ return null;
1461
+ }
1462
+ },
1463
+ );
1464
+
1465
+ const shallowRenderer = createRenderer();
1466
+ shallowRenderer.render(<Component foo="bar" />);
1467
+ expect(log).toEqual(['componentWillMount', 'UNSAFE_componentWillMount']);
1468
+
1469
+ log.length = 0;
1470
+
1471
+ shallowRenderer.render(<Component foo="baz" />);
1472
+ expect(log).toEqual([
1473
+ 'componentWillReceiveProps',
1474
+ 'UNSAFE_componentWillReceiveProps',
1475
+ 'componentWillUpdate',
1476
+ 'UNSAFE_componentWillUpdate',
1477
+ ]);
1478
+ });
1479
+
1480
+ it('should stop the update when setState returns null or undefined', () => {
1481
+ const log = [];
1482
+ let instance;
1483
+ const Component = React.memo(
1484
+ class Component extends React.Component {
1485
+ constructor(props) {
1486
+ super(props);
1487
+ this.state = {
1488
+ count: 0,
1489
+ };
1490
+ }
1491
+ render() {
1492
+ log.push('render');
1493
+ instance = this;
1494
+ return null;
1495
+ }
1496
+ },
1497
+ );
1498
+ const shallowRenderer = createRenderer();
1499
+ shallowRenderer.render(<Component />);
1500
+ log.length = 0;
1501
+ instance.setState(() => null);
1502
+ instance.setState(() => undefined);
1503
+ instance.setState(null);
1504
+ instance.setState(undefined);
1505
+ expect(log).toEqual([]);
1506
+ instance.setState(state => ({count: state.count + 1}));
1507
+ expect(log).toEqual(['render']);
1508
+ });
1509
+
1510
+ it('should not get this in a function component', () => {
1511
+ const logs = [];
1512
+ const Foo = React.memo(function Foo() {
1513
+ logs.push(this);
1514
+ return <div>foo</div>;
1515
+ });
1516
+ const shallowRenderer = createRenderer();
1517
+ shallowRenderer.render(<Foo foo="bar" />);
1518
+ expect(logs).toEqual([undefined]);
1519
+ });
1520
+});