diff --git a/packages/react-test-renderer/src/ReactShallowRenderer.js b/packages/react-test-renderer/src/ReactShallowRenderer.js index 9893ec8696a5e..efd92f42df97a 100644 --- a/packages/react-test-renderer/src/ReactShallowRenderer.js +++ b/packages/react-test-renderer/src/ReactShallowRenderer.js @@ -91,6 +91,17 @@ function areHookInputsEqual( return true; } +function shouldRunEffectsBasedOnInputs( + before: Array, + after: Array | void | null, +) { + if (after == null || before.length !== after.length) { + return true; + } + + return before.some((value, i) => (after: any)[i] !== value); +} + class Updater { constructor(renderer) { this._renderer = renderer; @@ -172,12 +183,21 @@ function basicStateReducer(state: S, action: BasicStateAction): S { return typeof action === 'function' ? action(state) : action; } +type ShallowRenderOptions = { + callEffects: boolean, +}; + +const isEffectHook = Symbol(); + class ReactShallowRenderer { - static createRenderer = function() { - return new ReactShallowRenderer(); + static createRenderer = function( + options: ShallowRenderOptions = {callEffects: false}, + ) { + return new ReactShallowRenderer(options); }; - constructor() { + constructor(options: ShallowRenderOptions) { + this._options = options; this._reset(); } @@ -199,6 +219,7 @@ class ReactShallowRenderer { this._numberOfReRenders = 0; } + _options: ShallowRenderOptions; _context: null | Object; _newState: null | Object; _instance: any; @@ -308,6 +329,54 @@ class ReactShallowRenderer { ); }; + const useGenericEffect = ( + create: () => (() => void) | void, + inputs: Array | void | null, + isLayoutEffect: boolean, + ) => { + this._validateCurrentlyRenderingComponent(); + this._createWorkInProgressHook(); + + if (this._workInProgressHook !== null) { + if (this._workInProgressHook.memoizedState == null) { + this._workInProgressHook.memoizedState = { + isEffectHook, + isLayoutEffect, + create, + inputs, + cleanup: null, + run: true, + }; + } else { + const {memoizedState} = this._workInProgressHook; + this._workInProgressHook.memoizedState = { + isEffectHook, + isLayoutEffect, + create, + inputs, + cleanup: memoizedState.cleanup, + run: + inputs == null || + shouldRunEffectsBasedOnInputs(inputs, memoizedState.inputs), + }; + } + } + }; + + const useEffect = ( + create: () => (() => void) | void, + inputs: Array | void | null, + ) => { + useGenericEffect(create, inputs, false); + }; + + const useLayoutEffect = ( + create: () => (() => void) | void, + inputs: Array | void | null, + ) => { + useGenericEffect(create, inputs, true); + }; + const useMemo = ( nextCreate: () => T, deps: Array | void | null, @@ -374,9 +443,9 @@ class ReactShallowRenderer { return readContext(context); }, useDebugValue: noOp, - useEffect: noOp, + useEffect: this._options.callEffects ? useEffect : noOp, useImperativeHandle: noOp, - useLayoutEffect: noOp, + useLayoutEffect: this._options.callEffects ? useLayoutEffect : noOp, useMemo, useReducer, useRef, @@ -619,6 +688,7 @@ class ReactShallowRenderer { ReactCurrentDispatcher.current = prevDispatcher; } this._finishHooks(element, context); + this._callEffectsIfDesired(); } } } @@ -679,6 +749,36 @@ class ReactShallowRenderer { // because DOM refs are not available. } + _callEffectsIfDesired() { + if (!this._options.callEffects) { + return; + } + + this._callEffects(true); + this._callEffects(false); + } + + _callEffects(callLayoutEffects: boolean) { + for ( + let hook = this._firstWorkInProgressHook; + hook !== null; + hook = hook.next + ) { + const {memoizedState} = hook; + if ( + memoizedState != null && + memoizedState.isEffectHook === isEffectHook && + memoizedState.isLayoutEffect === callLayoutEffects && + memoizedState.run + ) { + if (memoizedState.cleanup) { + memoizedState.cleanup(); + } + memoizedState.cleanup = memoizedState.create(); + } + } + } + _updateClassComponent( elementType: Function, element: ReactElement, diff --git a/packages/react-test-renderer/src/__tests__/ReactShallowRendererHooks-test.js b/packages/react-test-renderer/src/__tests__/ReactShallowRendererHooks-test.js index 6b8a962ad5b14..217f4446916ba 100644 --- a/packages/react-test-renderer/src/__tests__/ReactShallowRendererHooks-test.js +++ b/packages/react-test-renderer/src/__tests__/ReactShallowRendererHooks-test.js @@ -231,7 +231,7 @@ describe('ReactShallowRenderer with hooks', () => { ); }); - it('should not trigger effects', () => { + it('should not trigger effects by default', () => { let effectsCalled = []; function SomeComponent({defaultName}) { @@ -252,6 +252,36 @@ describe('ReactShallowRenderer with hooks', () => { expect(effectsCalled).toEqual([]); }); + describe('when callEffects option is used', () => { + it('should trigger effects after render', () => { + let happenings = []; + + function SomeComponent({defaultName}) { + React.useEffect(() => { + happenings.push('call effect'); + }); + + React.useLayoutEffect(() => { + happenings.push('call layout effect'); + }); + + happenings.push('render'); + + return
Hello world
; + } + + const shallowRenderer = createRenderer({callEffects: true}); + shallowRenderer.render(); + + // Note the layout effect is triggered first. + expect(happenings).toEqual([ + 'render', + 'call layout effect', + 'call effect', + ]); + }); + }); + it('should work with useRef', () => { function SomeComponent() { const randomNumberRef = React.useRef({number: Math.random()});