diff --git a/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js b/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js
index 6645a781e..d4a119ec8 100644
--- a/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js
+++ b/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js
@@ -251,7 +251,9 @@ class ReactSixteenThreeAdapter extends EnzymeAdapter {
componentDidUpdate: {
onSetState: true,
},
- getDerivedStateFromProps: true,
+ getDerivedStateFromProps: {
+ hasShouldComponentUpdateBug: true,
+ },
getSnapshotBeforeUpdate: true,
setState: {
skipsComponentDidUpdateOnNullish: true,
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 1ef9d011e..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,
@@ -50,6 +52,9 @@ import detectFiberTags from './detectFiberTags';
const is164 = !!TestUtils.Simulate.touchStart; // 16.4+
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;
@@ -280,7 +285,7 @@ function getEmptyStateValue() {
}
function wrapAct(fn) {
- if (typeof TestUtils.act !== 'function') {
+ if (!is168) {
return fn();
}
let returnVal;
@@ -301,7 +306,9 @@ class ReactSixteenAdapter extends EnzymeAdapter {
componentDidUpdate: {
onSetState: true,
},
- getDerivedStateFromProps: true,
+ getDerivedStateFromProps: {
+ hasShouldComponentUpdateBug,
+ },
getSnapshotBeforeUpdate: true,
setState: {
skipsComponentDidUpdateOnNullish: true,
diff --git a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
index 9c84ddd8c..d23c3df93 100644
--- a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
+++ b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
@@ -5774,7 +5774,7 @@ describeWithDOM('mount', () => {
]);
});
- it('calls GDSFP when expected', () => {
+ it('calls gDSFP when expected', () => {
const prevProps = { a: 1 };
const state = { state: true };
const wrapper = mount();
@@ -5821,6 +5821,35 @@ describeWithDOM('mount', () => {
}],
]);
});
+
+ it('cDU’s nextState differs from `this.state` when gDSFP returns new state', () => {
+ class SimpleComponent extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { value: props.value };
+ }
+
+ static getDerivedStateFromProps(props, state) {
+ return props.value === state.value ? null : { value: props.value };
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return nextState.value !== this.state.value;
+ }
+
+ render() {
+ const { value } = this.state;
+ return ();
+ }
+ }
+ const wrapper = mount();
+
+ expect(wrapper.find('input').prop('value')).to.equal('initial');
+
+ wrapper.setProps({ value: 'updated' });
+
+ expect(wrapper.find('input').prop('value')).to.equal('updated');
+ });
});
describeIf(is('>= 16'), 'componentDidCatch', () => {
diff --git a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
index e134abe72..86459e854 100644
--- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
+++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
@@ -6114,7 +6114,7 @@ describe('shallow', () => {
]);
});
- it('calls GDSFP when expected', () => {
+ it('calls gDSFP when expected', () => {
const prevProps = { a: 1 };
const state = { state: true };
const wrapper = shallow();
@@ -6161,6 +6161,35 @@ describe('shallow', () => {
}],
]);
});
+
+ it('cDU’s nextState differs from `this.state` when gDSFP returns new state', () => {
+ class SimpleComponent extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { value: props.value };
+ }
+
+ static getDerivedStateFromProps(props, state) {
+ return props.value === state.value ? null : { value: props.value };
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return nextState.value !== this.state.value;
+ }
+
+ render() {
+ const { value } = this.state;
+ return ();
+ }
+ }
+ const wrapper = shallow();
+
+ expect(wrapper.find('input').prop('value')).to.equal('initial');
+
+ wrapper.setProps({ value: 'updated' });
+
+ expect(wrapper.find('input').prop('value')).to.equal('updated');
+ });
});
describeIf(is('>= 16'), 'componentDidCatch', () => {
diff --git a/packages/enzyme-test-suite/test/Utils-spec.jsx b/packages/enzyme-test-suite/test/Utils-spec.jsx
index 2b9dc3f16..c4245efd2 100644
--- a/packages/enzyme-test-suite/test/Utils-spec.jsx
+++ b/packages/enzyme-test-suite/test/Utils-spec.jsx
@@ -787,6 +787,27 @@ describe('Utils', () => {
spy.restore();
expect(Object.getOwnPropertyDescriptor(obj, 'method')).to.deep.equal(descriptor);
});
+
+ it('accepts an optional `getStub` argument', () => {
+ const obj = {};
+ const descriptor = {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: () => {},
+ };
+ Object.defineProperty(obj, 'method', descriptor);
+ let stub;
+ let original;
+ spyMethod(obj, 'method', (originalMethod) => {
+ original = originalMethod;
+ stub = () => { throw new EvalError('stubbed'); };
+ return stub;
+ });
+ expect(original).to.equal(descriptor.value);
+ expect(obj).to.have.property('method', stub);
+ expect(() => obj.method()).to.throw(EvalError);
+ });
});
describe('isCustomComponentElement()', () => {
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 (
diff --git a/packages/enzyme/src/Utils.js b/packages/enzyme/src/Utils.js
index 505f9666d..54f1e3674 100644
--- a/packages/enzyme/src/Utils.js
+++ b/packages/enzyme/src/Utils.js
@@ -282,7 +282,7 @@ export function cloneElement(adapter, el, props) {
);
}
-export function spyMethod(instance, methodName) {
+export function spyMethod(instance, methodName, getStub = () => {}) {
let lastReturnValue;
const originalMethod = instance[methodName];
const hasOwn = has(instance, methodName);
@@ -293,7 +293,7 @@ export function spyMethod(instance, methodName) {
Object.defineProperty(instance, methodName, {
configurable: true,
enumerable: !descriptor || !!descriptor.enumerable,
- value(...args) {
+ value: getStub(originalMethod) || function spied(...args) {
const result = originalMethod.apply(this, args);
lastReturnValue = result;
return result;