diff --git a/packages/enzyme-adapter-react-16/package.json b/packages/enzyme-adapter-react-16/package.json index 59559385b..266792c50 100644 --- a/packages/enzyme-adapter-react-16/package.json +++ b/packages/enzyme-adapter-react-16/package.json @@ -40,7 +40,8 @@ "object.values": "^1.1.0", "prop-types": "^15.7.2", "react-is": "^16.7.0", - "react-test-renderer": "^16.0.0-0" + "react-test-renderer": "^16.0.0-0", + "semver": "^5.6.0" }, "peerDependencies": { "enzyme": "^3.0.0", diff --git a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js index d1817eb3a..a5053ded0 100644 --- a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js +++ b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js @@ -5,8 +5,10 @@ import ReactDOM from 'react-dom'; import ReactDOMServer from 'react-dom/server'; // eslint-disable-next-line import/no-unresolved import ShallowRenderer from 'react-test-renderer/shallow'; +import { version as testRendererVersion } from 'react-test-renderer/package'; // eslint-disable-next-line import/no-unresolved import TestUtils from 'react-dom/test-utils'; +import semver from 'semver'; import checkPropTypes from 'prop-types/checkPropTypes'; import { isElement, @@ -52,6 +54,8 @@ const is165 = !!TestUtils.Simulate.auxClick; // 16.5+ const is166 = is165 && !React.unstable_AsyncMode; // 16.6+ const is168 = is166 && typeof TestUtils.act === 'function'; +const hasShouldComponentUpdateBug = semver.satisfies(testRendererVersion, '< 16.8'); + // Lazily populated if DOM is available. let FiberTags = null; @@ -303,7 +307,7 @@ class ReactSixteenAdapter extends EnzymeAdapter { onSetState: true, }, getDerivedStateFromProps: { - hasShouldComponentUpdateBug: !is168, + hasShouldComponentUpdateBug, }, getSnapshotBeforeUpdate: true, setState: { diff --git a/packages/enzyme/src/ShallowWrapper.js b/packages/enzyme/src/ShallowWrapper.js index b0165110e..fca3632d6 100644 --- a/packages/enzyme/src/ShallowWrapper.js +++ b/packages/enzyme/src/ShallowWrapper.js @@ -131,6 +131,10 @@ function getAdapterLifecycles({ options }) { }), } : null; + const { getDerivedStateFromProps: originalGDSFP } = lifecycles; + const getDerivedStateFromProps = originalGDSFP ? { + hasShouldComponentUpdateBug: !!originalGDSFP.hasShouldComponentUpdateBug, + } : false; return { ...lifecycles, @@ -142,6 +146,7 @@ function getAdapterLifecycles({ options }) { ...lifecycles.getChildContext, }, ...(componentDidUpdate && { componentDidUpdate }), + getDerivedStateFromProps, }; } @@ -243,6 +248,30 @@ function privateSetChildContext(adapter, wrapper, instance, renderedNode, getChi } } +function mockSCUIfgDSFPReturnNonNull(node, state) { + const { getDerivedStateFromProps } = node.type; + + if (typeof getDerivedStateFromProps === 'function') { + // we try to fix a React shallow renderer bug here. + // (facebook/react#14607, which has been fixed in react 16.8): + // when gDSFP return derived state, it will set instance state in shallow renderer before SCU, + // this will cause `this.state` in sCU be the updated state, which is wrong behavior. + // so we have to wrap sCU to pass the old state to original sCU. + const { instance } = node; + const { restore } = spyMethod( + instance, + 'shouldComponentUpdate', + originalSCU => function shouldComponentUpdate(...args) { + instance.state = state; + const sCUResult = originalSCU.apply(instance, args); + const [, nextState] = args; + instance.state = nextState; + restore(); + return sCUResult; + }, + ); + } +} /** * @class ShallowWrapper @@ -452,6 +481,10 @@ class ShallowWrapper { && instance ) { if (typeof instance.shouldComponentUpdate === 'function') { + const { getDerivedStateFromProps: gDSFP } = lifecycles; + if (gDSFP && gDSFP.hasShouldComponentUpdateBug) { + mockSCUIfgDSFPReturnNonNull(node, state); + } shouldComponentUpdateSpy = spyMethod(instance, 'shouldComponentUpdate'); } if ( @@ -601,6 +634,10 @@ class ShallowWrapper { && lifecycles.componentDidUpdate.onSetState && typeof instance.shouldComponentUpdate === 'function' ) { + const { getDerivedStateFromProps: gDSFP } = lifecycles; + if (gDSFP && gDSFP.hasShouldComponentUpdateBug) { + mockSCUIfgDSFPReturnNonNull(node, state); + } shouldComponentUpdateSpy = spyMethod(instance, 'shouldComponentUpdate'); } if (