@samitouri / QOS-React / commits / 4feab7fc92

Add hooks support to ReactShallowRenderer (#14567)

* Add hook support to ReactShallowRenderer

Dominic Gannaway committed Jan 17, 2019 at 17:42 UTC 4feab7fc92125c15f8c022058f1ac78deb18d650
2 files changed +723 -10
packages/react-test-renderer/src/ReactShallowRenderer.js
+403 -10
@@ -4,6 +4,7 @@
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 + * @flow
8 */
9
10 import React from 'react';
@@ -13,18 +14,93 @@ import getComponentName from 'shared/getComponentName';
14 import shallowEqual from 'shared/shallowEqual';
15 import invariant from 'shared/invariant';
16 import checkPropTypes from 'prop-types/checkPropTypes';
17 +import ReactSharedInternals from 'shared/ReactSharedInternals';
18 +import {enableHooks} from 'shared/ReactFeatureFlags';
19 +import warning from 'shared/warning';
20 +import is from 'shared/objectIs';
21 +
22 +import typeof {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactFiberDispatcher';
23 +import type {ReactContext} from 'shared/ReactTypes';
24 +import type {ReactElement} from 'shared/ReactElementType';
25 +
26 +type BasicStateAction<S> = (S => S) | S;
27 +type Dispatch<A> = A => void;
28 +
29 +type Update<A> = {
30 + action: A,
31 + next: Update<A> | null,
32 +};
33 +
34 +type UpdateQueue<A> = {
35 + last: Update<A> | null,
36 + dispatch: any,
37 +};
38 +
39 +type Hook = {
40 + memoizedState: any,
41 + queue: UpdateQueue<any> | null,
42 + next: Hook | null,
43 +};
44 +
45 +const {ReactCurrentDispatcher} = ReactSharedInternals;
46 +
47 +const RE_RENDER_LIMIT = 25;
48
49 const emptyObject = {};
50 if (__DEV__) {
51 Object.freeze(emptyObject);
52 }
53
54 +// In DEV, this is the name of the currently executing primitive hook
55 +let currentHookNameInDev: ?string;
56 +
57 +function areHookInputsEqual(
58 + nextDeps: Array<mixed>,
59 + prevDeps: Array<mixed> | null,
60 +) {
61 + if (prevDeps === null) {
62 + warning(
63 + false,
64 + '%s received a final argument during this render, but not during ' +
65 + 'the previous render. Even though the final argument is optional, ' +
66 + 'its type cannot change between renders.',
67 + currentHookNameInDev,
68 + );
69 + return false;
70 + }
71 +
72 + // Don't bother comparing lengths in prod because these arrays should be
73 + // passed inline.
74 + if (nextDeps.length !== prevDeps.length) {
75 + warning(
76 + false,
77 + 'The final argument passed to %s changed size between renders. The ' +
78 + 'order and size of this array must remain constant.\n\n' +
79 + 'Previous: %s\n' +
80 + 'Incoming: %s',
81 + currentHookNameInDev,
82 + `[${nextDeps.join(', ')}]`,
83 + `[${prevDeps.join(', ')}]`,
84 + );
85 + }
86 + for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
87 + if (is(nextDeps[i], prevDeps[i])) {
88 + continue;
89 + }
90 + return false;
91 + }
92 + return true;
93 +}
94 +
95 class Updater {
96 constructor(renderer) {
97 this._renderer = renderer;
98 this._callbacks = [];
99 }
100
101 + _renderer: ReactShallowRenderer;
102 + _callbacks: Array<any>;
103 +
104 _enqueueCallback(callback, publicInstance) {
105 if (typeof callback === 'function' && publicInstance) {
106 this._callbacks.push({
@@ -85,6 +161,18 @@ class Updater {
161 }
162 }
163
164 +function createHook(): Hook {
165 + return {
166 + memoizedState: null,
167 + queue: null,
168 + next: null,
169 + };
170 +}
171 +
172 +function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
173 + return typeof action === 'function' ? action(state) : action;
174 +}
175 +
176 class ReactShallowRenderer {
177 static createRenderer = function() {
178 return new ReactShallowRenderer();
@@ -99,6 +187,289 @@ class ReactShallowRenderer {
187 this._rendering = false;
188 this._forcedUpdate = false;
189 this._updater = new Updater(this);
190 + if (enableHooks) {
191 + this._dispatcher = this._createDispatcher();
192 + this._workInProgressHook = null;
193 + this._firstWorkInProgressHook = null;
194 + this._isReRender = false;
195 + this._didScheduleRenderPhaseUpdate = false;
196 + this._renderPhaseUpdates = null;
197 + this._currentlyRenderingComponent = null;
198 + this._numberOfReRenders = 0;
199 + this._previousComponentIdentity = null;
200 + }
201 + }
202 +
203 + _context: null | Object;
204 + _newState: null | Object;
205 + _instance: any;
206 + _element: null | ReactElement;
207 + _rendered: null | mixed;
208 + _updater: Updater;
209 + _rendering: boolean;
210 + _forcedUpdate: boolean;
211 + _dispatcher: DispatcherType;
212 + _workInProgressHook: null | Hook;
213 + _firstWorkInProgressHook: null | Hook;
214 + _currentlyRenderingComponent: null | Object;
215 + _previousComponentIdentity: null | Object;
216 + _renderPhaseUpdates: Map<UpdateQueue<any>, Update<any>> | null;
217 + _isReRender: boolean;
218 + _didScheduleRenderPhaseUpdate: boolean;
219 + _numberOfReRenders: number;
220 +
221 + _validateCurrentlyRenderingComponent() {
222 + invariant(
223 + this._currentlyRenderingComponent !== null,
224 + 'Hooks can only be called inside the body of a function component.',
225 + );
226 + }
227 +
228 + _createDispatcher(): DispatcherType {
229 + const useReducer = <S, A>(
230 + reducer: (S, A) => S,
231 + initialState: S,
232 + initialAction: A | void | null,
233 + ): [S, Dispatch<A>] => {
234 + this._validateCurrentlyRenderingComponent();
235 + this._createWorkInProgressHook();
236 + const workInProgressHook: Hook = (this._workInProgressHook: any);
237 + if (this._isReRender) {
238 + // This is a re-render. Apply the new render phase updates to the previous
239 + // current hook.
240 + const queue: UpdateQueue<A> = (workInProgressHook.queue: any);
241 + const dispatch: Dispatch<A> = (queue.dispatch: any);
242 + if (this._renderPhaseUpdates !== null) {
243 + // Render phase updates are stored in a map of queue -> linked list
244 + const firstRenderPhaseUpdate = this._renderPhaseUpdates.get(queue);
245 + if (firstRenderPhaseUpdate !== undefined) {
246 + (this._renderPhaseUpdates: any).delete(queue);
247 + let newState = workInProgressHook.memoizedState;
248 + let update = firstRenderPhaseUpdate;
249 + do {
250 + // Process this render phase update. We don't have to check the
251 + // priority because it will always be the same as the current
252 + // render's.
253 + const action = update.action;
254 + newState = reducer(newState, action);
255 + update = update.next;
256 + } while (update !== null);
257 +
258 + workInProgressHook.memoizedState = newState;
259 +
260 + return [newState, dispatch];
261 + }
262 + }
263 + return [workInProgressHook.memoizedState, dispatch];
264 + } else {
265 + if (reducer === basicStateReducer) {
266 + // Special case for `useState`.
267 + if (typeof initialState === 'function') {
268 + initialState = initialState();
269 + }
270 + } else if (initialAction !== undefined && initialAction !== null) {
271 + initialState = reducer(initialState, initialAction);
272 + }
273 + workInProgressHook.memoizedState = initialState;
274 + const queue: UpdateQueue<A> = (workInProgressHook.queue = {
275 + last: null,
276 + dispatch: null,
277 + });
278 + const dispatch: Dispatch<
279 + A,
280 + > = (queue.dispatch = (this._dispatchAction.bind(
281 + this,
282 + (this._currentlyRenderingComponent: any),
283 + queue,
284 + ): any));
285 + return [workInProgressHook.memoizedState, dispatch];
286 + }
287 + };
288 +
289 + const useState = <S>(
290 + initialState: (() => S) | S,
291 + ): [S, Dispatch<BasicStateAction<S>>] => {
292 + return useReducer(
293 + basicStateReducer,
294 + // useReducer has a special case to support lazy useState initializers
295 + (initialState: any),
296 + );
297 + };
298 +
299 + const useMemo = <T>(
300 + nextCreate: () => T,
301 + deps: Array<mixed> | void | null,
302 + ): T => {
303 + this._validateCurrentlyRenderingComponent();
304 + this._createWorkInProgressHook();
305 +
306 + const nextDeps = deps !== undefined && deps !== null ? deps : null;
307 +
308 + if (
309 + this._workInProgressHook !== null &&
310 + this._workInProgressHook.memoizedState !== null
311 + ) {
312 + const prevState = this._workInProgressHook.memoizedState;
313 + const prevDeps = prevState[1];
314 + if (nextDeps !== null) {
315 + if (areHookInputsEqual(nextDeps, prevDeps)) {
316 + return prevState[0];
317 + }
318 + }
319 + }
320 +
321 + const nextValue = nextCreate();
322 + (this._workInProgressHook: any).memoizedState = [nextValue, nextDeps];
323 + return nextValue;
324 + };
325 +
326 + const useRef = <T>(initialValue: T): {current: T} => {
327 + this._validateCurrentlyRenderingComponent();
328 + this._createWorkInProgressHook();
329 + const previousRef = (this._workInProgressHook: any).memoizedState;
330 + if (previousRef === null) {
331 + const ref = {current: initialValue};
332 + if (__DEV__) {
333 + Object.seal(ref);
334 + }
335 + (this._workInProgressHook: any).memoizedState = ref;
336 + return ref;
337 + } else {
338 + return previousRef;
339 + }
340 + };
341 +
342 + const readContext = <T>(
343 + context: ReactContext<T>,
344 + observedBits: void | number | boolean,
345 + ): T => {
346 + return context._currentValue;
347 + };
348 +
349 + const noOp = () => {
350 + this._validateCurrentlyRenderingComponent();
351 + };
352 +
353 + const identity = (fn: Function): Function => {
354 + return fn;
355 + };
356 +
357 + return {
358 + readContext,
359 + useCallback: (identity: any),
360 + useContext: <T>(context: ReactContext<T>): T => {
361 + this._validateCurrentlyRenderingComponent();
362 + return readContext(context);
363 + },
364 + useDebugValue: noOp,
365 + useEffect: noOp,
366 + useImperativeHandle: noOp,
367 + useLayoutEffect: noOp,
368 + useMemo,
369 + useReducer,
370 + useRef,
371 + useState,
372 + };
373 + }
374 +
375 + _dispatchAction<A>(
376 + componentIdentity: Object,
377 + queue: UpdateQueue<A>,
378 + action: A,
379 + ) {
380 + invariant(
381 + this._numberOfReRenders < RE_RENDER_LIMIT,
382 + 'Too many re-renders. React limits the number of renders to prevent ' +
383 + 'an infinite loop.',
384 + );
385 +
386 + if (componentIdentity === this._currentlyRenderingComponent) {
387 + // This is a render phase update. Stash it in a lazily-created map of
388 + // queue -> linked list of updates. After this render pass, we'll restart
389 + // and apply the stashed updates on top of the work-in-progress hook.
390 + this._didScheduleRenderPhaseUpdate = true;
391 + const update: Update<A> = {
392 + action,
393 + next: null,
394 + };
395 + let renderPhaseUpdates = this._renderPhaseUpdates;
396 + if (renderPhaseUpdates === null) {
397 + this._renderPhaseUpdates = renderPhaseUpdates = new Map();
398 + }
399 + const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
400 + if (firstRenderPhaseUpdate === undefined) {
401 + renderPhaseUpdates.set(queue, update);
402 + } else {
403 + // Append the update to the end of the list.
404 + let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
405 + while (lastRenderPhaseUpdate.next !== null) {
406 + lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
407 + }
408 + lastRenderPhaseUpdate.next = update;
409 + }
410 + } else {
411 + // This means an update has happened after the function component has
412 + // returned. On the server this is a no-op. In React Fiber, the update
413 + // would be scheduled for a future render.
414 + }
415 + }
416 +
417 + _createWorkInProgressHook(): Hook {
418 + if (this._workInProgressHook === null) {
419 + // This is the first hook in the list
420 + if (this._firstWorkInProgressHook === null) {
421 + this._isReRender = false;
422 + this._firstWorkInProgressHook = this._workInProgressHook = createHook();
423 + } else {
424 + // There's already a work-in-progress. Reuse it.
425 + this._isReRender = true;
426 + this._workInProgressHook = this._firstWorkInProgressHook;
427 + }
428 + } else {
429 + if (this._workInProgressHook.next === null) {
430 + this._isReRender = false;
431 + // Append to the end of the list
432 + this._workInProgressHook = (this
433 + ._workInProgressHook: any).next = createHook();
434 + } else {
435 + // There's already a work-in-progress. Reuse it.
436 + this._isReRender = true;
437 + this._workInProgressHook = this._workInProgressHook.next;
438 + }
439 + }
440 + return this._workInProgressHook;
441 + }
442 +
443 + _prepareToUseHooks(componentIdentity: Object): void {
444 + if (
445 + this._previousComponentIdentity !== null &&
446 + this._previousComponentIdentity !== componentIdentity
447 + ) {
448 + this._firstWorkInProgressHook = null;
449 + }
450 + this._currentlyRenderingComponent = componentIdentity;
451 + this._previousComponentIdentity = componentIdentity;
452 + }
453 +
454 + _finishHooks(element: ReactElement, context: null | Object) {
455 + if (this._didScheduleRenderPhaseUpdate) {
456 + // Updates were scheduled during the render phase. They are stored in
457 + // the `renderPhaseUpdates` map. Call the component again, reusing the
458 + // work-in-progress hooks and applying the additional updates on top. Keep
459 + // restarting until no more updates are scheduled.
460 + this._didScheduleRenderPhaseUpdate = false;
461 + this._numberOfReRenders += 1;
462 +
463 + // Start over from the beginning of the list
464 + this._workInProgressHook = null;
465 + this._rendering = false;
466 + this.render(element, context);
467 + } else {
468 + this._currentlyRenderingComponent = null;
469 + this._workInProgressHook = null;
470 + this._renderPhaseUpdates = null;
471 + this._numberOfReRenders = 0;
472 + }
473 }
474
475 getMountedInstance() {
@@ -109,7 +480,7 @@ class ReactShallowRenderer {
480 return this._rendered;
481 }
482
112 - render(element, context = emptyObject) {
483 + render(element: ReactElement | null, context: null | Object = emptyObject) {
484 invariant(
485 React.isValidElement(element),
486 'ReactShallowRenderer render(): Invalid component element.%s',
@@ -118,6 +489,7 @@ class ReactShallowRenderer {
489 'it by passing it to React.createElement.'
490 : '',
491 );
492 + element = ((element: any): ReactElement);
493 // Show a special message for host elements since it's a common case.
494 invariant(
495 typeof element.type !== 'string',
@@ -175,11 +547,27 @@ class ReactShallowRenderer {
547
548 this._mountClassComponent(element, this._context);
549 } else {
178 - this._rendered = element.type.call(
179 - undefined,
180 - element.props,
181 - this._context,
182 - );
550 + if (enableHooks) {
551 + const prevDispatcher = ReactCurrentDispatcher.current;
552 + ReactCurrentDispatcher.current = this._dispatcher;
553 + this._prepareToUseHooks(element.type);
554 + try {
555 + this._rendered = element.type.call(
556 + undefined,
557 + element.props,
558 + this._context,
559 + );
560 + } finally {
561 + ReactCurrentDispatcher.current = prevDispatcher;
562 + }
563 + this._finishHooks(element, context);
564 + } else {
565 + this._rendered = element.type.call(
566 + undefined,
567 + element.props,
568 + this._context,
569 + );
570 + }
571 }
572 }
573
@@ -196,6 +584,8 @@ class ReactShallowRenderer {
584 }
585 }
586
587 + this._firstWorkInProgressHook = null;
588 + this._previousComponentIdentity = null;
589 this._context = null;
590 this._element = null;
591 this._newState = null;
@@ -203,7 +593,7 @@ class ReactShallowRenderer {
593 this._instance = null;
594 }
595
206 - _mountClassComponent(element, context) {
596 + _mountClassComponent(element: ReactElement, context: null | Object) {
597 this._instance.context = context;
598 this._instance.props = element.props;
599 this._instance.state = this._instance.state || null;
@@ -240,7 +630,7 @@ class ReactShallowRenderer {
630 // because DOM refs are not available.
631 }
632
243 - _updateClassComponent(element, context) {
633 + _updateClassComponent(element: ReactElement, context: null | Object) {
634 const {props, type} = element;
635
636 const oldState = this._instance.state || emptyObject;
@@ -310,7 +700,10 @@ class ReactShallowRenderer {
700 // because DOM refs are not available.
701 }
702
313 - _updateStateFromStaticLifecycle(props) {
703 + _updateStateFromStaticLifecycle(props: Object) {
704 + if (this._element === null) {
705 + return;
706 + }
707 const {type} = this._element;
708
709 if (typeof type.getDerivedStateFromProps === 'function') {
@@ -373,7 +766,7 @@ function shouldConstruct(Component) {
766 }
767
768 function getMaskedContext(contextTypes, unmaskedContext) {
376 - if (!contextTypes) {
769 + if (!contextTypes || !unmaskedContext) {
770 return emptyObject;
771 }
772 const context = {};
packages/react-test-renderer/src/__tests__/ReactShallowRendererHooks-test.internal.js new
+320
@@ -0,0 +1,320 @@
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 React;
15 +
16 +describe('ReactShallowRenderer with hooks', () => {
17 + beforeEach(() => {
18 + jest.resetModules();
19 + let ReactFeatureFlags = require('shared/ReactFeatureFlags');
20 + // TODO: Switch this test to non-internal once the flag is on by default.
21 + ReactFeatureFlags.enableHooks = true;
22 + createRenderer = require('react-test-renderer/shallow').createRenderer;
23 + React = require('react');
24 + });
25 +
26 + it('should work with useState', () => {
27 + function SomeComponent({defaultName}) {
28 + const [name] = React.useState(defaultName);
29 +
30 + return (
31 + <div>
32 + <p>
33 + Your name is: <span>{name}</span>
34 + </p>
35 + </div>
36 + );
37 + }
38 +
39 + const shallowRenderer = createRenderer();
40 + let result = shallowRenderer.render(
41 + <SomeComponent defaultName={'Dominic'} />,
42 + );
43 +
44 + expect(result).toEqual(
45 + <div>
46 + <p>
47 + Your name is: <span>Dominic</span>
48 + </p>
49 + </div>,
50 + );
51 +
52 + result = shallowRenderer.render(
53 + <SomeComponent defaultName={'Should not use this name'} />,
54 + );
55 +
56 + expect(result).toEqual(
57 + <div>
58 + <p>
59 + Your name is: <span>Dominic</span>
60 + </p>
61 + </div>,
62 + );
63 + });
64 +
65 + it('should work with updating a value from useState', () => {
66 + function SomeComponent({defaultName}) {
67 + const [name, updateName] = React.useState(defaultName);
68 +
69 + if (name !== 'Dan') {
70 + updateName('Dan');
71 + }
72 +
73 + return (
74 + <div>
75 + <p>
76 + Your name is: <span>{name}</span>
77 + </p>
78 + </div>
79 + );
80 + }
81 +
82 + const shallowRenderer = createRenderer();
83 + const result = shallowRenderer.render(
84 + <SomeComponent defaultName={'Dominic'} />,
85 + );
86 +
87 + expect(result).toEqual(
88 + <div>
89 + <p>
90 + Your name is: <span>Dan</span>
91 + </p>
92 + </div>,
93 + );
94 + });
95 +
96 + it('should work with useReducer', () => {
97 + const initialState = {count: 0};
98 +
99 + function reducer(state, action) {
100 + switch (action.type) {
101 + case 'reset':
102 + return initialState;
103 + case 'increment':
104 + return {count: state.count + 1};
105 + case 'decrement':
106 + return {count: state.count - 1};
107 + default:
108 + return state;
109 + }
110 + }
111 +
112 + function SomeComponent({initialCount}) {
113 + const [state] = React.useReducer(reducer, {count: initialCount});
114 +
115 + return (
116 + <div>
117 + <p>
118 + The counter is at: <span>{state.count.toString()}</span>
119 + </p>
120 + </div>
121 + );
122 + }
123 +
124 + const shallowRenderer = createRenderer();
125 + let result = shallowRenderer.render(<SomeComponent initialCount={0} />);
126 +
127 + expect(result).toEqual(
128 + <div>
129 + <p>
130 + The counter is at: <span>0</span>
131 + </p>
132 + </div>,
133 + );
134 +
135 + result = shallowRenderer.render(<SomeComponent initialCount={10} />);
136 +
137 + expect(result).toEqual(
138 + <div>
139 + <p>
140 + The counter is at: <span>0</span>
141 + </p>
142 + </div>,
143 + );
144 + });
145 +
146 + it('should work with a dispatched state change for a useReducer', () => {
147 + const initialState = {count: 0};
148 +
149 + function reducer(state, action) {
150 + switch (action.type) {
151 + case 'reset':
152 + return initialState;
153 + case 'increment':
154 + return {count: state.count + 1};
155 + case 'decrement':
156 + return {count: state.count - 1};
157 + default:
158 + return state;
159 + }
160 + }
161 +
162 + function SomeComponent({initialCount}) {
163 + const [state, dispatch] = React.useReducer(reducer, {
164 + count: initialCount,
165 + });
166 +
167 + if (state.count === 0) {
168 + dispatch({type: 'increment'});
169 + }
170 +
171 + return (
172 + <div>
173 + <p>
174 + The counter is at: <span>{state.count.toString()}</span>
175 + </p>
176 + </div>
177 + );
178 + }
179 +
180 + const shallowRenderer = createRenderer();
181 + let result = shallowRenderer.render(<SomeComponent initialCount={0} />);
182 +
183 + expect(result).toEqual(
184 + <div>
185 + <p>
186 + The counter is at: <span>1</span>
187 + </p>
188 + </div>,
189 + );
190 + });
191 +
192 + it('should not trigger effects', () => {
193 + let effectsCalled = [];
194 +
195 + function SomeComponent({defaultName}) {
196 + React.useEffect(() => {
197 + effectsCalled.push('useEffect');
198 + });
199 +
200 + React.useLayoutEffect(() => {
201 + effectsCalled.push('useEffect');
202 + });
203 +
204 + return <div>Hello world</div>;
205 + }
206 +
207 + const shallowRenderer = createRenderer();
208 + shallowRenderer.render(<SomeComponent />);
209 +
210 + expect(effectsCalled).toEqual([]);
211 + });
212 +
213 + it('should work with useRef', () => {
214 + function SomeComponent() {
215 + const randomNumberRef = React.useRef({number: Math.random()});
216 +
217 + return (
218 + <div>
219 + <p>The random number is: {randomNumberRef.current.number}</p>
220 + </div>
221 + );
222 + }
223 +
224 + const shallowRenderer = createRenderer();
225 + let firstResult = shallowRenderer.render(<SomeComponent />);
226 + let secondResult = shallowRenderer.render(<SomeComponent />);
227 +
228 + expect(firstResult).toEqual(secondResult);
229 + });
230 +
231 + it('should work with useMemo', () => {
232 + function SomeComponent() {
233 + const randomNumber = React.useMemo(() => {
234 + return {number: Math.random()};
235 + }, []);
236 +
237 + return (
238 + <div>
239 + <p>The random number is: {randomNumber.number}</p>
240 + </div>
241 + );
242 + }
243 +
244 + const shallowRenderer = createRenderer();
245 + let firstResult = shallowRenderer.render(<SomeComponent />);
246 + let secondResult = shallowRenderer.render(<SomeComponent />);
247 +
248 + expect(firstResult).toEqual(secondResult);
249 + });
250 +
251 + it('should work with useContext', () => {
252 + const SomeContext = React.createContext('default');
253 +
254 + function SomeComponent() {
255 + const value = React.useContext(SomeContext);
256 +
257 + return (
258 + <div>
259 + <p>{value}</p>
260 + </div>
261 + );
262 + }
263 +
264 + const shallowRenderer = createRenderer();
265 + let result = shallowRenderer.render(<SomeComponent />);
266 +
267 + expect(result).toEqual(
268 + <div>
269 + <p>default</p>
270 + </div>,
271 + );
272 + });
273 +
274 + it('should not leak state when component type changes', () => {
275 + function SomeComponent({defaultName}) {
276 + const [name] = React.useState(defaultName);
277 +
278 + return (
279 + <div>
280 + <p>
281 + Your name is: <span>{name}</span>
282 + </p>
283 + </div>
284 + );
285 + }
286 +
287 + function SomeOtherComponent({defaultName}) {
288 + const [name] = React.useState(defaultName);
289 +
290 + return (
291 + <div>
292 + <p>
293 + Your name is: <span>{name}</span>
294 + </p>
295 + </div>
296 + );
297 + }
298 +
299 + const shallowRenderer = createRenderer();
300 + let result = shallowRenderer.render(
301 + <SomeComponent defaultName={'Dominic'} />,
302 + );
303 + expect(result).toEqual(
304 + <div>
305 + <p>
306 + Your name is: <span>Dominic</span>
307 + </p>
308 + </div>,
309 + );
310 +
311 + result = shallowRenderer.render(<SomeOtherComponent defaultName={'Dan'} />);
312 + expect(result).toEqual(
313 + <div>
314 + <p>
315 + Your name is: <span>Dan</span>
316 + </p>
317 + </div>,
318 + );
319 + });
320 +});