diff --git a/packages/react-test-renderer/src/ReactShallowRenderer.js b/packages/react-test-renderer/src/ReactShallowRenderer.js index 7472556c68954..c48f705c4ff7c 100644 --- a/packages/react-test-renderer/src/ReactShallowRenderer.js +++ b/packages/react-test-renderer/src/ReactShallowRenderer.js @@ -8,7 +8,7 @@ */ import React from 'react'; -import {isForwardRef} from 'react-is'; +import {isForwardRef, isMemo} from 'react-is'; import describeComponentFrame from 'shared/describeComponentFrame'; import getComponentName from 'shared/getComponentName'; import shallowEqual from 'shared/shallowEqual'; @@ -500,7 +500,8 @@ class ReactShallowRenderer { element.type, ); invariant( - isForwardRef(element) || typeof element.type === 'function', + isForwardRef(element) || + (typeof element.type === 'function' || isMemo(element.type)), 'ReactShallowRenderer render(): Shallow rendering works only with custom ' + 'components, but the provided element type was `%s`.', Array.isArray(element.type) @@ -514,24 +515,37 @@ class ReactShallowRenderer { return; } + const elementType = isMemo(element.type) ? element.type.type : element.type; this._rendering = true; + this._element = element; - this._context = getMaskedContext(element.type.contextTypes, context); + this._context = getMaskedContext(elementType.contextTypes, context); + + // Inner memo component props aren't currently validated in createElement. + if (isMemo(element.type) && elementType.propTypes) { + currentlyValidatingElement = element; + checkPropTypes( + elementType.propTypes, + element.props, + 'prop', + getComponentName(elementType), + getStackAddendum, + ); + } if (this._instance) { this._updateClassComponent(element, this._context); } else { if (isForwardRef(element)) { - this._rendered = element.type.render(element.props, element.ref); - } else if (shouldConstruct(element.type)) { - this._instance = new element.type( + this._rendered = elementType.render(element.props, element.ref); + } else if (shouldConstruct(elementType)) { + this._instance = new elementType( element.props, this._context, this._updater, ); - - if (typeof element.type.getDerivedStateFromProps === 'function') { - const partialState = element.type.getDerivedStateFromProps.call( + if (typeof elementType.getDerivedStateFromProps === 'function') { + const partialState = elementType.getDerivedStateFromProps.call( null, element.props, this._instance.state, @@ -545,14 +559,13 @@ class ReactShallowRenderer { } } - if (element.type.hasOwnProperty('contextTypes')) { + if (elementType.hasOwnProperty('contextTypes')) { currentlyValidatingElement = element; - checkPropTypes( - element.type.contextTypes, + elementType.contextTypes, this._context, 'context', - getName(element.type, this._instance), + getName(elementType, this._instance), getStackAddendum, ); @@ -563,13 +576,9 @@ class ReactShallowRenderer { } else { const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = this._dispatcher; - this._prepareToUseHooks(element.type); + this._prepareToUseHooks(elementType); try { - this._rendered = element.type.call( - undefined, - element.props, - this._context, - ); + this._rendered = elementType(element.props, this._context); } finally { ReactCurrentDispatcher.current = prevDispatcher; } @@ -604,6 +613,7 @@ class ReactShallowRenderer { this._instance.props = element.props; this._instance.state = this._instance.state || null; this._instance.updater = this._updater; + const elementType = isMemo(element.type) ? element.type.type : element.type; if ( typeof this._instance.UNSAFE_componentWillMount === 'function' || @@ -614,7 +624,7 @@ class ReactShallowRenderer { // In order to support react-lifecycles-compat polyfilled components, // Unsafe lifecycles should not be invoked for components using the new APIs. if ( - typeof element.type.getDerivedStateFromProps !== 'function' && + typeof elementType.getDerivedStateFromProps !== 'function' && typeof this._instance.getSnapshotBeforeUpdate !== 'function' ) { if (typeof this._instance.componentWillMount === 'function') { @@ -637,16 +647,17 @@ class ReactShallowRenderer { } _updateClassComponent(element: ReactElement, context: null | Object) { - const {props, type} = element; + const {props} = element; const oldState = this._instance.state || emptyObject; const oldProps = this._instance.props; + const elementType = isMemo(element.type) ? element.type.type : element.type; if (oldProps !== props) { // In order to support react-lifecycles-compat polyfilled components, // Unsafe lifecycles should not be invoked for components using the new APIs. if ( - typeof element.type.getDerivedStateFromProps !== 'function' && + typeof elementType.getDerivedStateFromProps !== 'function' && typeof this._instance.getSnapshotBeforeUpdate !== 'function' ) { if (typeof this._instance.componentWillReceiveProps === 'function') { @@ -662,8 +673,8 @@ class ReactShallowRenderer { // Read state after cWRP in case it calls setState let state = this._newState || oldState; - if (typeof type.getDerivedStateFromProps === 'function') { - const partialState = type.getDerivedStateFromProps.call( + if (typeof elementType.getDerivedStateFromProps === 'function') { + const partialState = elementType.getDerivedStateFromProps.call( null, props, state, @@ -683,7 +694,10 @@ class ReactShallowRenderer { state, context, ); - } else if (type.prototype && type.prototype.isPureReactComponent) { + } else if ( + elementType.prototype && + elementType.prototype.isPureReactComponent + ) { shouldUpdate = !shallowEqual(oldProps, props) || !shallowEqual(oldState, state); } @@ -692,7 +706,7 @@ class ReactShallowRenderer { // In order to support react-lifecycles-compat polyfilled components, // Unsafe lifecycles should not be invoked for components using the new APIs. if ( - typeof element.type.getDerivedStateFromProps !== 'function' && + typeof elementType.getDerivedStateFromProps !== 'function' && typeof this._instance.getSnapshotBeforeUpdate !== 'function' ) { if (typeof this._instance.componentWillUpdate === 'function') { @@ -727,7 +741,8 @@ function getDisplayName(element) { } else if (typeof element.type === 'string') { return element.type; } else { - return element.type.displayName || element.type.name || 'Unknown'; + const elementType = isMemo(element.type) ? element.type.type : element.type; + return elementType.displayName || elementType.name || 'Unknown'; } } diff --git a/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js b/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js index d83dada160f90..e95508c8d58c2 100644 --- a/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js +++ b/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js @@ -1454,4 +1454,13 @@ describe('ReactShallowRenderer', () => { shallowRenderer.render(); expect(logs).toEqual([undefined]); }); + + it('should handle memo', () => { + const Foo = () => { + return
Foo
; + }; + const MemoFoo = React.memo(Foo); + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + }); }); diff --git a/packages/react-test-renderer/src/__tests__/ReactShallowRendererMemo-test.js b/packages/react-test-renderer/src/__tests__/ReactShallowRendererMemo-test.js new file mode 100644 index 0000000000000..5d668db6e2eb4 --- /dev/null +++ b/packages/react-test-renderer/src/__tests__/ReactShallowRendererMemo-test.js @@ -0,0 +1,1542 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + * @jest-environment node + */ + +'use strict'; + +let createRenderer; +let PropTypes; +let React; + +describe('ReactShallowRendererMemo', () => { + beforeEach(() => { + jest.resetModules(); + + createRenderer = require('react-test-renderer/shallow').createRenderer; + PropTypes = require('prop-types'); + React = require('react'); + }); + + it('should call all of the legacy lifecycle hooks', () => { + const logs = []; + const logger = message => () => logs.push(message) || true; + + const SomeComponent = React.memo( + class SomeComponent extends React.Component { + UNSAFE_componentWillMount = logger('componentWillMount'); + componentDidMount = logger('componentDidMount'); + UNSAFE_componentWillReceiveProps = logger('componentWillReceiveProps'); + shouldComponentUpdate = logger('shouldComponentUpdate'); + UNSAFE_componentWillUpdate = logger('componentWillUpdate'); + componentDidUpdate = logger('componentDidUpdate'); + componentWillUnmount = logger('componentWillUnmount'); + render() { + return
; + } + }, + ); + + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + + // Calling cDU might lead to problems with host component references. + // Since our components aren't really mounted, refs won't be available. + expect(logs).toEqual(['componentWillMount']); + + logs.splice(0); + + const instance = shallowRenderer.getMountedInstance(); + instance.setState({}); + + expect(logs).toEqual(['shouldComponentUpdate', 'componentWillUpdate']); + + logs.splice(0); + + shallowRenderer.render(); + + // The previous shallow renderer did not trigger cDU for props changes. + expect(logs).toEqual([ + 'componentWillReceiveProps', + 'shouldComponentUpdate', + 'componentWillUpdate', + ]); + }); + + it('should call all of the new lifecycle hooks', () => { + const logs = []; + const logger = message => () => logs.push(message) || true; + + const SomeComponent = React.memo( + class SomeComponent extends React.Component { + state = {}; + static getDerivedStateFromProps = logger('getDerivedStateFromProps'); + componentDidMount = logger('componentDidMount'); + shouldComponentUpdate = logger('shouldComponentUpdate'); + componentDidUpdate = logger('componentDidUpdate'); + componentWillUnmount = logger('componentWillUnmount'); + render() { + return
; + } + }, + ); + + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + + // Calling cDU might lead to problems with host component references. + // Since our components aren't really mounted, refs won't be available. + expect(logs).toEqual(['getDerivedStateFromProps']); + + logs.splice(0); + + const instance = shallowRenderer.getMountedInstance(); + instance.setState({}); + + expect(logs).toEqual(['getDerivedStateFromProps', 'shouldComponentUpdate']); + + logs.splice(0); + + shallowRenderer.render(); + + // The previous shallow renderer did not trigger cDU for props changes. + expect(logs).toEqual(['getDerivedStateFromProps', 'shouldComponentUpdate']); + }); + + it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new static gDSFP is present', () => { + const Component = React.memo( + class Component extends React.Component { + state = {}; + static getDerivedStateFromProps() { + return null; + } + componentWillMount() { + throw Error('unexpected'); + } + componentWillReceiveProps() { + throw Error('unexpected'); + } + componentWillUpdate() { + throw Error('unexpected'); + } + render() { + return null; + } + }, + ); + + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + }); + + it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new getSnapshotBeforeUpdate is present', () => { + const Component = React.memo( + class Component extends React.Component { + getSnapshotBeforeUpdate() { + return null; + } + componentWillMount() { + throw Error('unexpected'); + } + componentWillReceiveProps() { + throw Error('unexpected'); + } + componentWillUpdate() { + throw Error('unexpected'); + } + render() { + return null; + } + }, + ); + + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + shallowRenderer.render(); + }); + + it('should not call getSnapshotBeforeUpdate or componentDidUpdate when updating since refs wont exist', () => { + const Component = React.memo( + class Component extends React.Component { + getSnapshotBeforeUpdate() { + throw Error('unexpected'); + } + componentDidUpdate() { + throw Error('unexpected'); + } + render() { + return null; + } + }, + ); + + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + shallowRenderer.render(); + }); + + it('should only render 1 level deep', () => { + const Parent = React.memo(function Parent() { + return ( +
+ +
+ ); + }); + + function Child() { + throw Error('This component should not render'); + } + + const shallowRenderer = createRenderer(); + shallowRenderer.render(React.createElement(Parent)); + }); + + it('should have shallow rendering', () => { + const SomeComponent = React.memo( + class SomeComponent extends React.Component { + render() { + return ( +
+ + +
+ ); + } + }, + ); + + const shallowRenderer = createRenderer(); + const result = shallowRenderer.render(); + + expect(result.type).toBe('div'); + expect(result.props.children).toEqual([ + , + , + ]); + }); + + it('should handle ForwardRef', () => { + const testRef = React.createRef(); + const SomeComponent = React.forwardRef((props, ref) => { + expect(ref).toEqual(testRef); + return ( +
+ + +
+ ); + }); + + const shallowRenderer = createRenderer(); + const result = shallowRenderer.render(); + + expect(result.type).toBe('div'); + expect(result.props.children).toEqual([ + , + , + ]); + }); + + it('should handle Profiler', () => { + const SomeComponent = React.memo( + class SomeComponent extends React.Component { + render() { + return ( + +
+ + +
+
+ ); + } + }, + ); + + const shallowRenderer = createRenderer(); + const result = shallowRenderer.render(); + + expect(result.type).toBe(React.unstable_Profiler); + expect(result.props.children).toEqual( +
+ + +
, + ); + }); + + it('should enable shouldComponentUpdate to prevent a re-render', () => { + let renderCounter = 0; + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + state = {update: false}; + shouldComponentUpdate(nextProps, nextState) { + return this.state.update !== nextState.update; + } + render() { + renderCounter++; + return
{`${renderCounter}`}
; + } + }, + ); + + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + expect(shallowRenderer.getRenderOutput()).toEqual(
1
); + + const instance = shallowRenderer.getMountedInstance(); + instance.setState({update: false}); + expect(shallowRenderer.getRenderOutput()).toEqual(
1
); + + instance.setState({update: true}); + expect(shallowRenderer.getRenderOutput()).toEqual(
2
); + }); + + it('should enable PureComponent to prevent a re-render', () => { + let renderCounter = 0; + const SimpleComponent = React.memo( + class SimpleComponent extends React.PureComponent { + state = {update: false}; + render() { + renderCounter++; + return
{`${renderCounter}`}
; + } + }, + ); + + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + expect(shallowRenderer.getRenderOutput()).toEqual(
1
); + + const instance = shallowRenderer.getMountedInstance(); + instance.setState({update: false}); + expect(shallowRenderer.getRenderOutput()).toEqual(
1
); + + instance.setState({update: true}); + expect(shallowRenderer.getRenderOutput()).toEqual(
2
); + }); + + it('should not run shouldComponentUpdate during forced update', () => { + let scuCounter = 0; + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + state = {count: 1}; + shouldComponentUpdate() { + scuCounter++; + return false; + } + render() { + return
{`${this.state.count}`}
; + } + }, + ); + + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + expect(scuCounter).toEqual(0); + expect(shallowRenderer.getRenderOutput()).toEqual(
1
); + + // Force update the initial state. sCU should not fire. + const instance = shallowRenderer.getMountedInstance(); + instance.forceUpdate(); + expect(scuCounter).toEqual(0); + expect(shallowRenderer.getRenderOutput()).toEqual(
1
); + + // Setting state updates the instance, but doesn't re-render + // because sCU returned false. + instance.setState(state => ({count: state.count + 1})); + expect(scuCounter).toEqual(1); + expect(instance.state.count).toEqual(2); + expect(shallowRenderer.getRenderOutput()).toEqual(
1
); + + // A force update updates the render output, but doesn't call sCU. + instance.forceUpdate(); + expect(scuCounter).toEqual(1); + expect(instance.state.count).toEqual(2); + expect(shallowRenderer.getRenderOutput()).toEqual(
2
); + }); + + it('should rerender when calling forceUpdate', () => { + let renderCounter = 0; + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + render() { + renderCounter += 1; + return
; + } + }, + ); + + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + expect(renderCounter).toEqual(1); + + const instance = shallowRenderer.getMountedInstance(); + instance.forceUpdate(); + expect(renderCounter).toEqual(2); + }); + + it('should shallow render a function component', () => { + function SomeComponent(props, context) { + return ( +
+
{props.foo}
+
{context.bar}
+ + +
+ ); + } + const SomeMemoComponent = React.memo(SomeComponent); + + SomeComponent.contextTypes = { + bar: PropTypes.string, + }; + + const shallowRenderer = createRenderer(); + const result = shallowRenderer.render(, { + bar: 'BAR', + }); + + expect(result.type).toBe('div'); + expect(result.props.children).toEqual([ +
FOO
, +
BAR
, + , + , + ]); + }); + + it('should shallow render a component returning strings directly from render', () => { + const Text = React.memo(({value}) => value); + + const shallowRenderer = createRenderer(); + const result = shallowRenderer.render(); + expect(result).toEqual('foo'); + }); + + it('should shallow render a component returning numbers directly from render', () => { + const Text = React.memo(({value}) => value); + + const shallowRenderer = createRenderer(); + const result = shallowRenderer.render(); + expect(result).toEqual(10); + }); + + it('should shallow render a fragment', () => { + class SomeComponent extends React.Component { + render() { + return
; + } + } + class Fragment extends React.Component { + render() { + return [
, , ]; + } + } + const shallowRenderer = createRenderer(); + const result = shallowRenderer.render(); + expect(result).toEqual([ +
, + , + , + ]); + }); + + it('should shallow render a React.fragment', () => { + class SomeComponent extends React.Component { + render() { + return
; + } + } + class Fragment extends React.Component { + render() { + return ( + +
+ + + + ); + } + } + const shallowRenderer = createRenderer(); + const result = shallowRenderer.render(); + expect(result).toEqual( + +
+ + + , + ); + }); + + it('should throw for invalid elements', () => { + class SomeComponent extends React.Component { + render() { + return
; + } + } + + const shallowRenderer = createRenderer(); + expect(() => shallowRenderer.render(SomeComponent)).toThrowError( + 'ReactShallowRenderer render(): Invalid component element. Instead of ' + + 'passing a component class, make sure to instantiate it by passing it ' + + 'to React.createElement.', + ); + expect(() => shallowRenderer.render(
)).toThrowError( + 'ReactShallowRenderer render(): Shallow rendering works only with ' + + 'custom components, not primitives (div). Instead of calling ' + + '`.render(el)` and inspecting the rendered output, look at `el.props` ' + + 'directly instead.', + ); + }); + + it('should have shallow unmounting', () => { + const componentWillUnmount = jest.fn(); + + class SomeComponent extends React.Component { + componentWillUnmount = componentWillUnmount; + render() { + return
; + } + } + + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + shallowRenderer.unmount(); + + expect(componentWillUnmount).toBeCalled(); + }); + + it('can shallow render to null', () => { + class SomeComponent extends React.Component { + render() { + return null; + } + } + + const shallowRenderer = createRenderer(); + const result = shallowRenderer.render(); + + expect(result).toBe(null); + }); + + it('can shallow render with a ref', () => { + class SomeComponent extends React.Component { + render() { + return
; + } + } + + const shallowRenderer = createRenderer(); + // Shouldn't crash. + shallowRenderer.render(); + }); + + it('lets you update shallowly rendered components', () => { + class SomeComponent extends React.Component { + state = {clicked: false}; + + onClick = () => { + this.setState({clicked: true}); + }; + + render() { + const className = this.state.clicked ? 'was-clicked' : ''; + + if (this.props.aNew === 'prop') { + return ( + + Test link + + ); + } else { + return ( +
+ + +
+ ); + } + } + } + + const shallowRenderer = createRenderer(); + const result = shallowRenderer.render(); + expect(result.type).toBe('div'); + expect(result.props.children).toEqual([ + , + , + ]); + + const updatedResult = shallowRenderer.render(); + expect(updatedResult.type).toBe('a'); + + const mockEvent = {}; + updatedResult.props.onClick(mockEvent); + + const updatedResultCausedByClick = shallowRenderer.getRenderOutput(); + expect(updatedResultCausedByClick.type).toBe('a'); + expect(updatedResultCausedByClick.props.className).toBe('was-clicked'); + }); + + it('can access the mounted component instance', () => { + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + someMethod = () => { + return this.props.n; + }; + + render() { + return
{this.props.n}
; + } + }, + ); + + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + expect(shallowRenderer.getMountedInstance().someMethod()).toEqual(5); + }); + + it('can shallowly render components with contextTypes', () => { + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + static contextTypes = { + name: PropTypes.string, + }; + + render() { + return
; + } + }, + ); + + const shallowRenderer = createRenderer(); + const result = shallowRenderer.render(); + expect(result).toEqual(
); + }); + + it('passes expected params to legacy component lifecycle methods', () => { + const componentDidUpdateParams = []; + const componentWillReceivePropsParams = []; + const componentWillUpdateParams = []; + const setStateParams = []; + const shouldComponentUpdateParams = []; + + const initialProp = {prop: 'init prop'}; + const initialState = {state: 'init state'}; + const initialContext = {context: 'init context'}; + const updatedState = {state: 'updated state'}; + const updatedProp = {prop: 'updated prop'}; + const updatedContext = {context: 'updated context'}; + + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + constructor(props, context) { + super(props, context); + this.state = initialState; + } + static contextTypes = { + context: PropTypes.string, + }; + componentDidUpdate(...args) { + componentDidUpdateParams.push(...args); + } + UNSAFE_componentWillReceiveProps(...args) { + componentWillReceivePropsParams.push(...args); + this.setState((...innerArgs) => { + setStateParams.push(...innerArgs); + return updatedState; + }); + } + UNSAFE_componentWillUpdate(...args) { + componentWillUpdateParams.push(...args); + } + shouldComponentUpdate(...args) { + shouldComponentUpdateParams.push(...args); + return true; + } + render() { + return null; + } + }, + ); + + const shallowRenderer = createRenderer(); + shallowRenderer.render( + React.createElement(SimpleComponent, initialProp), + initialContext, + ); + expect(componentDidUpdateParams).toEqual([]); + expect(componentWillReceivePropsParams).toEqual([]); + expect(componentWillUpdateParams).toEqual([]); + expect(setStateParams).toEqual([]); + expect(shouldComponentUpdateParams).toEqual([]); + + // Lifecycle hooks should be invoked with the correct prev/next params on update. + shallowRenderer.render( + React.createElement(SimpleComponent, updatedProp), + updatedContext, + ); + expect(componentWillReceivePropsParams).toEqual([ + updatedProp, + updatedContext, + ]); + expect(setStateParams).toEqual([initialState, initialProp]); + expect(shouldComponentUpdateParams).toEqual([ + updatedProp, + updatedState, + updatedContext, + ]); + expect(componentWillUpdateParams).toEqual([ + updatedProp, + updatedState, + updatedContext, + ]); + expect(componentDidUpdateParams).toEqual([]); + }); + + it('passes expected params to new component lifecycle methods', () => { + const componentDidUpdateParams = []; + const getDerivedStateFromPropsParams = []; + const shouldComponentUpdateParams = []; + + const initialProp = {prop: 'init prop'}; + const initialState = {state: 'init state'}; + const initialContext = {context: 'init context'}; + const updatedProp = {prop: 'updated prop'}; + const updatedContext = {context: 'updated context'}; + + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + constructor(props, context) { + super(props, context); + this.state = initialState; + } + static contextTypes = { + context: PropTypes.string, + }; + componentDidUpdate(...args) { + componentDidUpdateParams.push(...args); + } + static getDerivedStateFromProps(...args) { + getDerivedStateFromPropsParams.push(args); + return null; + } + shouldComponentUpdate(...args) { + shouldComponentUpdateParams.push(...args); + return true; + } + render() { + return null; + } + }, + ); + + const shallowRenderer = createRenderer(); + + // The only lifecycle hook that should be invoked on initial render + // Is the static getDerivedStateFromProps() methods + shallowRenderer.render( + React.createElement(SimpleComponent, initialProp), + initialContext, + ); + expect(getDerivedStateFromPropsParams).toEqual([ + [initialProp, initialState], + ]); + expect(componentDidUpdateParams).toEqual([]); + expect(shouldComponentUpdateParams).toEqual([]); + + // Lifecycle hooks should be invoked with the correct prev/next params on update. + shallowRenderer.render( + React.createElement(SimpleComponent, updatedProp), + updatedContext, + ); + expect(getDerivedStateFromPropsParams).toEqual([ + [initialProp, initialState], + [updatedProp, initialState], + ]); + expect(shouldComponentUpdateParams).toEqual([ + updatedProp, + initialState, + updatedContext, + ]); + expect(componentDidUpdateParams).toEqual([]); + }); + + it('can shallowly render components with ref as function', () => { + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + state = {clicked: false}; + + handleUserClick = () => { + this.setState({clicked: true}); + }; + + render() { + return ( +
{}} + onClick={this.handleUserClick} + className={this.state.clicked ? 'clicked' : ''} + /> + ); + } + }, + ); + + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + let result = shallowRenderer.getRenderOutput(); + expect(result.type).toEqual('div'); + expect(result.props.className).toEqual(''); + result.props.onClick(); + + result = shallowRenderer.getRenderOutput(); + expect(result.type).toEqual('div'); + expect(result.props.className).toEqual('clicked'); + }); + + it('can initialize state via static getDerivedStateFromProps', () => { + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + state = { + count: 1, + }; + + static getDerivedStateFromProps(props, prevState) { + return { + count: prevState.count + props.incrementBy, + other: 'foobar', + }; + } + + render() { + return ( +
{`count:${this.state.count}, other:${this.state.other}`}
+ ); + } + }, + ); + + const shallowRenderer = createRenderer(); + const result = shallowRenderer.render(); + expect(result).toEqual(
count:3, other:foobar
); + }); + + it('can setState in componentWillMount when shallow rendering', () => { + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + UNSAFE_componentWillMount() { + this.setState({groovy: 'doovy'}); + } + + render() { + return
{this.state.groovy}
; + } + }, + ); + + const shallowRenderer = createRenderer(); + const result = shallowRenderer.render(); + expect(result).toEqual(
doovy
); + }); + + it('can setState in componentWillMount repeatedly when shallow rendering', () => { + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + state = { + separator: '-', + }; + + UNSAFE_componentWillMount() { + this.setState({groovy: 'doovy'}); + this.setState({doovy: 'groovy'}); + } + + render() { + const {groovy, doovy, separator} = this.state; + + return
{`${groovy}${separator}${doovy}`}
; + } + }, + ); + + const shallowRenderer = createRenderer(); + const result = shallowRenderer.render(); + expect(result).toEqual(
doovy-groovy
); + }); + + it('can setState in componentWillMount with an updater function repeatedly when shallow rendering', () => { + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + state = { + separator: '-', + }; + + UNSAFE_componentWillMount() { + this.setState(state => ({groovy: 'doovy'})); + this.setState(state => ({doovy: state.groovy})); + } + + render() { + const {groovy, doovy, separator} = this.state; + + return
{`${groovy}${separator}${doovy}`}
; + } + }, + ); + + const shallowRenderer = createRenderer(); + const result = shallowRenderer.render(); + expect(result).toEqual(
doovy-doovy
); + }); + + it('can setState in componentWillReceiveProps when shallow rendering', () => { + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + state = {count: 0}; + + UNSAFE_componentWillReceiveProps(nextProps) { + if (nextProps.updateState) { + this.setState({count: 1}); + } + } + + render() { + return
{this.state.count}
; + } + }, + ); + + const shallowRenderer = createRenderer(); + let result = shallowRenderer.render( + , + ); + expect(result.props.children).toEqual(0); + + result = shallowRenderer.render(); + expect(result.props.children).toEqual(1); + }); + + it('can update state with static getDerivedStateFromProps when shallow rendering', () => { + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + state = {count: 1}; + + static getDerivedStateFromProps(nextProps, prevState) { + if (nextProps.updateState) { + return {count: nextProps.incrementBy + prevState.count}; + } + + return null; + } + + render() { + return
{this.state.count}
; + } + }, + ); + + const shallowRenderer = createRenderer(); + let result = shallowRenderer.render( + , + ); + expect(result.props.children).toEqual(1); + + result = shallowRenderer.render( + , + ); + expect(result.props.children).toEqual(3); + + result = shallowRenderer.render( + , + ); + expect(result.props.children).toEqual(3); + }); + + it('should not override state with stale values if prevState is spread within getDerivedStateFromProps', () => { + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + state = {value: 0}; + + static getDerivedStateFromProps(nextProps, prevState) { + return {...prevState}; + } + + updateState = () => { + this.setState(state => ({value: state.value + 1})); + }; + + render() { + return
{`value:${this.state.value}`}
; + } + }, + ); + + const shallowRenderer = createRenderer(); + let result = shallowRenderer.render(); + expect(result).toEqual(
value:0
); + + let instance = shallowRenderer.getMountedInstance(); + instance.updateState(); + result = shallowRenderer.getRenderOutput(); + expect(result).toEqual(
value:1
); + }); + + it('should pass previous state to shouldComponentUpdate even with getDerivedStateFromProps', () => { + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + constructor(props) { + super(props); + this.state = { + value: props.value, + }; + } + + static getDerivedStateFromProps(nextProps, prevState) { + if (nextProps.value === prevState.value) { + return null; + } + return {value: nextProps.value}; + } + + shouldComponentUpdate(nextProps, nextState) { + return nextState.value !== this.state.value; + } + + render() { + return
{`value:${this.state.value}`}
; + } + }, + ); + + const shallowRenderer = createRenderer(); + const initialResult = shallowRenderer.render( + , + ); + expect(initialResult).toEqual(
value:initial
); + const updatedResult = shallowRenderer.render( + , + ); + expect(updatedResult).toEqual(
value:updated
); + }); + + it('can setState with an updater function', () => { + let instance; + + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + state = { + counter: 0, + }; + + render() { + instance = this; + return ( + + ); + } + }, + ); + + const shallowRenderer = createRenderer(); + let result = shallowRenderer.render(); + expect(result.props.children).toEqual(0); + + instance.setState((state, props) => { + return {counter: props.defaultCount + 1}; + }); + + result = shallowRenderer.getRenderOutput(); + expect(result.props.children).toEqual(2); + }); + + it('can access component instance from setState updater function', done => { + let instance; + + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + state = {}; + + render() { + instance = this; + return null; + } + }, + ); + + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + + instance.setState(function updater(state, props) { + expect(this).toBe(instance); + done(); + }); + }); + + it('can setState with a callback', () => { + let instance; + + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + state = { + counter: 0, + }; + render() { + instance = this; + return

{this.state.counter}

; + } + }, + ); + + const shallowRenderer = createRenderer(); + const result = shallowRenderer.render(); + expect(result.props.children).toBe(0); + + const callback = jest.fn(function() { + expect(this).toBe(instance); + }); + + instance.setState({counter: 1}, callback); + + const updated = shallowRenderer.getRenderOutput(); + expect(updated.props.children).toBe(1); + expect(callback).toHaveBeenCalled(); + }); + + it('can replaceState with a callback', () => { + let instance; + + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + state = { + counter: 0, + }; + render() { + instance = this; + return

{this.state.counter}

; + } + }, + ); + + const shallowRenderer = createRenderer(); + const result = shallowRenderer.render(); + expect(result.props.children).toBe(0); + + const callback = jest.fn(function() { + expect(this).toBe(instance); + }); + + // No longer a public API, but we can test that it works internally by + // reaching into the updater. + shallowRenderer._updater.enqueueReplaceState( + instance, + {counter: 1}, + callback, + ); + + const updated = shallowRenderer.getRenderOutput(); + expect(updated.props.children).toBe(1); + expect(callback).toHaveBeenCalled(); + }); + + it('can forceUpdate with a callback', () => { + let instance; + + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + state = { + counter: 0, + }; + render() { + instance = this; + return

{this.state.counter}

; + } + }, + ); + + const shallowRenderer = createRenderer(); + const result = shallowRenderer.render(); + expect(result.props.children).toBe(0); + + const callback = jest.fn(function() { + expect(this).toBe(instance); + }); + + instance.forceUpdate(callback); + + const updated = shallowRenderer.getRenderOutput(); + expect(updated.props.children).toBe(0); + expect(callback).toHaveBeenCalled(); + }); + + it('can pass context when shallowly rendering', () => { + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + static contextTypes = { + name: PropTypes.string, + }; + + render() { + return
{this.context.name}
; + } + }, + ); + + const shallowRenderer = createRenderer(); + const result = shallowRenderer.render(, { + name: 'foo', + }); + expect(result).toEqual(
foo
); + }); + + it('should track context across updates', () => { + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + static contextTypes = { + foo: PropTypes.string, + }; + + state = { + bar: 'bar', + }; + + render() { + return
{`${this.context.foo}:${this.state.bar}`}
; + } + }, + ); + + const shallowRenderer = createRenderer(); + let result = shallowRenderer.render(, { + foo: 'foo', + }); + expect(result).toEqual(
foo:bar
); + + const instance = shallowRenderer.getMountedInstance(); + instance.setState({bar: 'baz'}); + + result = shallowRenderer.getRenderOutput(); + expect(result).toEqual(
foo:baz
); + }); + + it('should filter context by contextTypes', () => { + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + static contextTypes = { + foo: PropTypes.string, + }; + render() { + return
{`${this.context.foo}:${this.context.bar}`}
; + } + }, + ); + + const shallowRenderer = createRenderer(); + let result = shallowRenderer.render(, { + foo: 'foo', + bar: 'bar', + }); + expect(result).toEqual(
foo:undefined
); + }); + + it('can fail context when shallowly rendering', () => { + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + static contextTypes = { + name: PropTypes.string.isRequired, + }; + + render() { + return
{this.context.name}
; + } + }, + ); + + const shallowRenderer = createRenderer(); + expect(() => shallowRenderer.render()).toWarnDev( + 'Warning: Failed context type: The context `name` is marked as ' + + 'required in `SimpleComponent`, but its value is `undefined`.\n' + + ' in SimpleComponent (at **)', + ); + }); + + it('should warn about propTypes (but only once)', () => { + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + static propTypes = { + name: PropTypes.string.isRequired, + }; + + render() { + return React.createElement('div', null, this.props.name); + } + }, + ); + + const shallowRenderer = createRenderer(); + expect(() => + shallowRenderer.render(React.createElement(SimpleComponent, {name: 123})), + ).toWarnDev( + 'Warning: Failed prop type: Invalid prop `name` of type `number` ' + + 'supplied to `SimpleComponent`, expected `string`.\n' + + ' in SimpleComponent', + ); + }); + + it('should enable rendering of cloned element', () => { + const SimpleComponent = React.memo( + class SimpleComponent extends React.Component { + constructor(props) { + super(props); + + this.state = { + bar: 'bar', + }; + } + + render() { + return
{`${this.props.foo}:${this.state.bar}`}
; + } + }, + ); + + const shallowRenderer = createRenderer(); + const el = ; + let result = shallowRenderer.render(el); + expect(result).toEqual(
foo:bar
); + + const cloned = React.cloneElement(el, {foo: 'baz'}); + result = shallowRenderer.render(cloned); + expect(result).toEqual(
baz:bar
); + }); + + it('this.state should be updated on setState callback inside componentWillMount', () => { + let stateSuccessfullyUpdated = false; + + const Component = React.memo( + class Component extends React.Component { + constructor(props, context) { + super(props, context); + this.state = { + hasUpdatedState: false, + }; + } + + UNSAFE_componentWillMount() { + this.setState( + {hasUpdatedState: true}, + () => (stateSuccessfullyUpdated = this.state.hasUpdatedState), + ); + } + + render() { + return
{this.props.children}
; + } + }, + ); + + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + expect(stateSuccessfullyUpdated).toBe(true); + }); + + it('should handle multiple callbacks', () => { + const mockFn = jest.fn(); + const shallowRenderer = createRenderer(); + + const Component = React.memo( + class Component extends React.Component { + constructor(props, context) { + super(props, context); + this.state = { + foo: 'foo', + }; + } + + UNSAFE_componentWillMount() { + this.setState({foo: 'bar'}, () => mockFn()); + this.setState({foo: 'foobar'}, () => mockFn()); + } + + render() { + return
{this.state.foo}
; + } + }, + ); + + shallowRenderer.render(); + + expect(mockFn).toHaveBeenCalledTimes(2); + + // Ensure the callback queue is cleared after the callbacks are invoked + const mountedInstance = shallowRenderer.getMountedInstance(); + mountedInstance.setState({foo: 'bar'}, () => mockFn()); + expect(mockFn).toHaveBeenCalledTimes(3); + }); + + it('should call the setState callback even if shouldComponentUpdate = false', done => { + const mockFn = jest.fn().mockReturnValue(false); + + const Component = React.memo( + class Component extends React.Component { + constructor(props, context) { + super(props, context); + this.state = { + hasUpdatedState: false, + }; + } + + shouldComponentUpdate() { + return mockFn(); + } + + render() { + return
{this.state.hasUpdatedState}
; + } + }, + ); + + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + + const mountedInstance = shallowRenderer.getMountedInstance(); + mountedInstance.setState({hasUpdatedState: true}, () => { + expect(mockFn).toBeCalled(); + expect(mountedInstance.state.hasUpdatedState).toBe(true); + done(); + }); + }); + + it('throws usefully when rendering badly-typed elements', () => { + const shallowRenderer = createRenderer(); + + const renderAndVerifyWarningAndError = (Component, typeString) => { + expect(() => { + expect(() => shallowRenderer.render()).toWarnDev( + 'React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite components) ' + + `but got: ${typeString}.`, + ); + }).toThrowError( + 'ReactShallowRenderer render(): Shallow rendering works only with custom ' + + `components, but the provided element type was \`${typeString}\`.`, + ); + }; + + renderAndVerifyWarningAndError(undefined, 'undefined'); + renderAndVerifyWarningAndError(null, 'null'); + renderAndVerifyWarningAndError([], 'array'); + renderAndVerifyWarningAndError({}, 'object'); + }); + + it('should have initial state of null if not defined', () => { + const SomeComponent = React.memo( + class SomeComponent extends React.Component { + render() { + return ; + } + }, + ); + + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + + expect(shallowRenderer.getMountedInstance().state).toBeNull(); + }); + + it('should invoke both deprecated and new lifecycles if both are present', () => { + const log = []; + + const Component = React.memo( + class Component extends React.Component { + componentWillMount() { + log.push('componentWillMount'); + } + componentWillReceiveProps() { + log.push('componentWillReceiveProps'); + } + componentWillUpdate() { + log.push('componentWillUpdate'); + } + UNSAFE_componentWillMount() { + log.push('UNSAFE_componentWillMount'); + } + UNSAFE_componentWillReceiveProps() { + log.push('UNSAFE_componentWillReceiveProps'); + } + UNSAFE_componentWillUpdate() { + log.push('UNSAFE_componentWillUpdate'); + } + render() { + return null; + } + }, + ); + + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + expect(log).toEqual(['componentWillMount', 'UNSAFE_componentWillMount']); + + log.length = 0; + + shallowRenderer.render(); + expect(log).toEqual([ + 'componentWillReceiveProps', + 'UNSAFE_componentWillReceiveProps', + 'componentWillUpdate', + 'UNSAFE_componentWillUpdate', + ]); + }); + + it('should stop the update when setState returns null or undefined', () => { + const log = []; + let instance; + const Component = React.memo( + class Component extends React.Component { + constructor(props) { + super(props); + this.state = { + count: 0, + }; + } + render() { + log.push('render'); + instance = this; + return null; + } + }, + ); + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + log.length = 0; + instance.setState(() => null); + instance.setState(() => undefined); + instance.setState(null); + instance.setState(undefined); + expect(log).toEqual([]); + instance.setState(state => ({count: state.count + 1})); + expect(log).toEqual(['render']); + }); + + it('should not get this in a function component', () => { + const logs = []; + const Foo = React.memo(function Foo() { + logs.push(this); + return
foo
; + }); + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + expect(logs).toEqual([undefined]); + }); +});