From 34828de2286c2ecae21b4dacddefac4d5f15f222 Mon Sep 17 00:00:00 2001 From: Mulenga Bowa Date: Sat, 14 Nov 2020 00:48:58 +0200 Subject: [PATCH] [enzyme-adapter-react-{16,16.3,16.2,16.1}] [fix] `.setContext()`: calls cWRP Fixes #2258 --- packages/enzyme-adapter-react-14/package.json | 1 + .../src/ReactFourteenAdapter.js | 47 +++++++++ .../enzyme-adapter-react-16.1/package.json | 1 + .../src/ReactSixteenOneAdapter.js | 25 +++++ .../enzyme-adapter-react-16.2/package.json | 1 + .../src/ReactSixteenTwoAdapter.js | 25 +++++ .../enzyme-adapter-react-16.3/package.json | 1 + .../src/ReactSixteenThreeAdapter.js | 28 ++++++ .../src/ReactSixteenAdapter.js | 30 +++++- .../test/ReactWrapper-spec.jsx | 1 - .../test/ShallowWrapper-spec.jsx | 42 ++------ .../test/shared/methods/setContext.jsx | 98 +++++++++++++++++++ 12 files changed, 262 insertions(+), 38 deletions(-) diff --git a/packages/enzyme-adapter-react-14/package.json b/packages/enzyme-adapter-react-14/package.json index 06b4c77a0..760d4fed7 100644 --- a/packages/enzyme-adapter-react-14/package.json +++ b/packages/enzyme-adapter-react-14/package.json @@ -39,6 +39,7 @@ "license": "MIT", "dependencies": { "enzyme-adapter-utils": "^1.14.0", + "enzyme-shallow-equal": "^1.0.4", "object.assign": "^4.1.0", "object.values": "^1.1.1", "prop-types": "^15.7.2", diff --git a/packages/enzyme-adapter-react-14/src/ReactFourteenAdapter.js b/packages/enzyme-adapter-react-14/src/ReactFourteenAdapter.js index 68a2d9836..f5322b1bd 100644 --- a/packages/enzyme-adapter-react-14/src/ReactFourteenAdapter.js +++ b/packages/enzyme-adapter-react-14/src/ReactFourteenAdapter.js @@ -23,7 +23,9 @@ import { getNodeFromRootFinder, wrapWithWrappingComponent, getWrappingComponentMountRenderer, + spyMethod, } from 'enzyme-adapter-utils'; +import shallowEqual from 'enzyme-shallow-equal'; function typeToNodeType(type) { if (typeof type === 'function') { @@ -172,6 +174,51 @@ class ReactFourteenAdapter extends EnzymeAdapter { isDOM = true; } else { isDOM = false; + + const inst = renderer._instance; + if (inst) { + const { restore: restoreUpdateComponent } = spyMethod( + inst, + 'updateComponent', + (originalUpadateComponentMethod) => function updateComponent( + transaction, + prevParentElement, + nextParentElement, + prevUnmaskedContext, + nextUnmaskedContext, + ) { + if (prevParentElement === nextParentElement) { + const { restore: restoreProcessPendingState } = spyMethod( + inst, + '_processPendingState', + (originalProcessPendingStateMethod) => function _processPendingState(nextProps, nextContext) { + if (!shallowEqual(prevUnmaskedContext, nextUnmaskedContext)) { + if (inst._instance.componentWillReceiveProps) { + inst._instance.componentWillReceiveProps(nextProps, nextContext); + } + } + + const result = originalProcessPendingStateMethod.call(inst, nextProps, nextContext); + restoreProcessPendingState(); + return result; + }, + ); + } + + const result = originalUpadateComponentMethod.call( + inst, + transaction, + prevParentElement, + nextParentElement, + prevUnmaskedContext, + nextUnmaskedContext, + ); + restoreUpdateComponent(); + return result; + }, + ); + } + return withSetStateAllowed(() => renderer.render(el, context)); } }, diff --git a/packages/enzyme-adapter-react-16.1/package.json b/packages/enzyme-adapter-react-16.1/package.json index 179ad0f92..4f46b4f09 100644 --- a/packages/enzyme-adapter-react-16.1/package.json +++ b/packages/enzyme-adapter-react-16.1/package.json @@ -39,6 +39,7 @@ "license": "MIT", "dependencies": { "enzyme-adapter-utils": "^1.14.0", + "enzyme-shallow-equal": "^1.0.4", "object.assign": "^4.1.0", "prop-types": "^15.7.2", "react-is": "^16.13.1", diff --git a/packages/enzyme-adapter-react-16.1/src/ReactSixteenOneAdapter.js b/packages/enzyme-adapter-react-16.1/src/ReactSixteenOneAdapter.js index ce7ede8f0..8c15ca3e2 100644 --- a/packages/enzyme-adapter-react-16.1/src/ReactSixteenOneAdapter.js +++ b/packages/enzyme-adapter-react-16.1/src/ReactSixteenOneAdapter.js @@ -16,6 +16,7 @@ import { Portal, } from 'react-is'; import { EnzymeAdapter } from 'enzyme'; +import shallowEqual from 'enzyme-shallow-equal'; import { displayNameOfNode, elementToTree as utilElementToTree, @@ -36,6 +37,7 @@ import { getNodeFromRootFinder, wrapWithWrappingComponent, getWrappingComponentMountRenderer, + spyMethod, } from 'enzyme-adapter-utils'; import { findCurrentFiberUsingSlowPath } from 'react-reconciler/reflection'; @@ -373,6 +375,29 @@ class ReactSixteenOneAdapter extends EnzymeAdapter { return withSetStateAllowed(() => renderer.render({ ...el, type: wrappedEl }, context)); } if (isStateful) { + if ( + renderer._instance + && el.props === renderer._instance.props + && !shallowEqual(context, renderer._instance.context) + ) { + const { restore } = spyMethod( + renderer, + '_updateClassComponent', + (originalMethod) => function _updateClassComponent(...args) { + const { props } = renderer._instance; + const clonedProps = { ...props }; + renderer._instance.props = clonedProps; + + const result = originalMethod.apply(renderer, args); + + renderer._instance.props = props; + restore(); + + return result; + }, + ); + } + // fix react bug; see implementation of `getEmptyStateValue` const emptyStateValue = getEmptyStateValue(); if (emptyStateValue) { diff --git a/packages/enzyme-adapter-react-16.2/package.json b/packages/enzyme-adapter-react-16.2/package.json index f6a89bbce..3ea4dbf16 100644 --- a/packages/enzyme-adapter-react-16.2/package.json +++ b/packages/enzyme-adapter-react-16.2/package.json @@ -39,6 +39,7 @@ "license": "MIT", "dependencies": { "enzyme-adapter-utils": "^1.14.0", + "enzyme-shallow-equal": "^1.0.4", "object.assign": "^4.1.0", "object.values": "^1.1.1", "prop-types": "^15.7.2", diff --git a/packages/enzyme-adapter-react-16.2/src/ReactSixteenTwoAdapter.js b/packages/enzyme-adapter-react-16.2/src/ReactSixteenTwoAdapter.js index 1c0fe065e..9c99e8d7e 100644 --- a/packages/enzyme-adapter-react-16.2/src/ReactSixteenTwoAdapter.js +++ b/packages/enzyme-adapter-react-16.2/src/ReactSixteenTwoAdapter.js @@ -17,6 +17,7 @@ import { } from 'react-is'; import { EnzymeAdapter } from 'enzyme'; import { typeOfNode } from 'enzyme/build/Utils'; +import shallowEqual from 'enzyme-shallow-equal'; import { displayNameOfNode, elementToTree as utilElementToTree, @@ -37,6 +38,7 @@ import { getNodeFromRootFinder, wrapWithWrappingComponent, getWrappingComponentMountRenderer, + spyMethod, } from 'enzyme-adapter-utils'; import { findCurrentFiberUsingSlowPath } from 'react-reconciler/reflection'; @@ -375,6 +377,29 @@ class ReactSixteenTwoAdapter extends EnzymeAdapter { return withSetStateAllowed(() => renderer.render({ ...el, type: wrappedEl }, context)); } if (isStateful) { + if ( + renderer._instance + && el.props === renderer._instance.props + && !shallowEqual(context, renderer._instance.context) + ) { + const { restore } = spyMethod( + renderer, + '_updateClassComponent', + (originalMethod) => function _updateClassComponent(...args) { + const { props } = renderer._instance; + const clonedProps = { ...props }; + renderer._instance.props = clonedProps; + + const result = originalMethod.apply(renderer, args); + + renderer._instance.props = props; + restore(); + + return result; + }, + ); + } + // fix react bug; see implementation of `getEmptyStateValue` const emptyStateValue = getEmptyStateValue(); if (emptyStateValue) { diff --git a/packages/enzyme-adapter-react-16.3/package.json b/packages/enzyme-adapter-react-16.3/package.json index f447f4302..c82428f8e 100644 --- a/packages/enzyme-adapter-react-16.3/package.json +++ b/packages/enzyme-adapter-react-16.3/package.json @@ -39,6 +39,7 @@ "license": "MIT", "dependencies": { "enzyme-adapter-utils": "^1.14.0", + "enzyme-shallow-equal": "^1.0.4", "object.assign": "^4.1.0", "object.values": "^1.1.1", "prop-types": "^15.7.2", diff --git a/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js b/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js index fc6259fce..fc5155980 100644 --- a/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js +++ b/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js @@ -26,6 +26,7 @@ import { } from 'react-is'; import { EnzymeAdapter } from 'enzyme'; import { typeOfNode } from 'enzyme/build/Utils'; +import shallowEqual from 'enzyme-shallow-equal'; import { displayNameOfNode, elementToTree as utilElementToTree, @@ -45,6 +46,7 @@ import { getNodeFromRootFinder, wrapWithWrappingComponent, getWrappingComponentMountRenderer, + spyMethod, } from 'enzyme-adapter-utils'; import { findCurrentFiberUsingSlowPath } from 'react-reconciler/reflection'; @@ -411,6 +413,32 @@ class ReactSixteenThreeAdapter extends EnzymeAdapter { ); return withSetStateAllowed(() => renderer.render({ ...el, type: wrappedEl }, context)); } + + if (isStateful) { + if ( + renderer._instance + && el.props === renderer._instance.props + && !shallowEqual(context, renderer._instance.context) + ) { + const { restore } = spyMethod( + renderer, + '_updateClassComponent', + (originalMethod) => function _updateClassComponent(...args) { + const { props } = renderer._instance; + const clonedProps = { ...props }; + renderer._instance.props = clonedProps; + + const result = originalMethod.apply(renderer, args); + + renderer._instance.props = props; + restore(); + + return result; + }, + ); + } + } + return withSetStateAllowed(() => renderer.render(el, context)); } }, diff --git a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js index d61a60a1e..5ba368da6 100644 --- a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js +++ b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js @@ -57,6 +57,7 @@ import { wrapWithWrappingComponent, getWrappingComponentMountRenderer, compareNodeTypeOf, + spyMethod, } from 'enzyme-adapter-utils'; import findCurrentFiberUsingSlowPath from './findCurrentFiberUsingSlowPath'; import detectFiberTags from './detectFiberTags'; @@ -700,14 +701,39 @@ class ReactSixteenAdapter extends EnzymeAdapter { )); } - if (!isStateful(Component) && typeof Component === 'function') { + const isComponentStateful = isStateful(Component); + + if (!isComponentStateful && typeof Component === 'function') { return withSetStateAllowed(() => renderElement( { ...renderedEl, type: wrapFunctionalComponent(Component) }, context, )); } - if (isStateful) { + if (isComponentStateful) { + if ( + renderer._instance + && el.props === renderer._instance.props + && !shallowEqual(context, renderer._instance.context) + ) { + const { restore } = spyMethod( + renderer, + '_updateClassComponent', + (originalMethod) => function _updateClassComponent(...args) { + const { props } = renderer._instance; + const clonedProps = { ...props }; + renderer._instance.props = clonedProps; + + const result = originalMethod.apply(renderer, args); + + renderer._instance.props = props; + restore(); + + return result; + }, + ); + } + // fix react bug; see implementation of `getEmptyStateValue` const emptyStateValue = getEmptyStateValue(); if (emptyStateValue) { diff --git a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx index 4b51da319..12fdb7381 100644 --- a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx @@ -122,7 +122,6 @@ describeWithDOM('mount', () => { expect(document.createElement('div')).to.be.instanceOf(HTMLElement); const [[firstArg]] = spy.args; - console.log(firstArg); expect(firstArg).to.be.instanceOf(HTMLElement); }); diff --git a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx index bd60a71c5..7e225d0ba 100644 --- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx @@ -2087,6 +2087,10 @@ describe('shallow', () => { } } + Foo.contextTypes = { + foo: PropTypes.string, + }; + const options = { disableLifecycleMethods: true, context: { @@ -2122,7 +2126,7 @@ describe('shallow', () => { ]); }); - describeIf(is('0.13 || 15 || > 16'), 'setContext', () => { + describe('setContext', () => { it('calls expected methods when receiving new context', () => { const wrapper = shallow(, options); expect(spy.args).to.deep.equal([ @@ -2130,43 +2134,11 @@ describe('shallow', () => { ['render'], ]); spy.resetHistory(); - wrapper.setContext({ foo: 'foo' }); - expect(spy.args).to.deep.equal([ - ['componentWillReceiveProps'], - ['shouldComponentUpdate'], - ['componentWillUpdate'], - ['render'], - ]); - }); - }); - describeIf(is('16'), 'setContext', () => { - it('calls expected methods when receiving new context', () => { - const wrapper = shallow(, options); - expect(spy.args).to.deep.equal([ - ['componentWillMount'], - ['render'], - ]); - spy.resetHistory(); - wrapper.setContext({ foo: 'foo' }); - expect(spy.args).to.deep.equal([ - ['shouldComponentUpdate'], - ['componentWillUpdate'], - ['render'], - ]); - }); - }); + wrapper.setContext({ foo: 'bar' }); - describeIf(is('0.14'), 'setContext', () => { - it('calls expected methods when receiving new context', () => { - const wrapper = shallow(, options); - expect(spy.args).to.deep.equal([ - ['componentWillMount'], - ['render'], - ]); - spy.resetHistory(); - wrapper.setContext({ foo: 'foo' }); expect(spy.args).to.deep.equal([ + ['componentWillReceiveProps'], ['shouldComponentUpdate'], ['componentWillUpdate'], ['render'], diff --git a/packages/enzyme-test-suite/test/shared/methods/setContext.jsx b/packages/enzyme-test-suite/test/shared/methods/setContext.jsx index 2918f5960..d37775533 100644 --- a/packages/enzyme-test-suite/test/shared/methods/setContext.jsx +++ b/packages/enzyme-test-suite/test/shared/methods/setContext.jsx @@ -1,9 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import { expect } from 'chai'; +import sinon from 'sinon-sandbox'; import { describeIf, + itIf, } from '../../_helpers'; import { is } from '../../_helpers/version'; @@ -14,6 +16,7 @@ import { export default function describeSetContext({ Wrap, WrapperName, + isShallow, }) { describe('.setContext(newContext)', () => { const SimpleComponent = createClass({ @@ -79,5 +82,100 @@ export default function describeSetContext({ ); }); }); + + it('calls componentWillReceiveProps when context is updated', () => { + const spy = sinon.spy(); + const updatedProps = { foo: 'baz' }; + class Foo extends React.Component { + componentWillReceiveProps() { + spy('componentWillReceiveProps'); + } + + render() { + spy('render'); + const { foo } = this.context; + return
{foo}
; + } + } + Foo.contextTypes = { + foo: PropTypes.string, + }; + + const wrapper = Wrap( + , + { + context: { foo: 'bar' }, + }, + ); + + wrapper.setContext(updatedProps); + + expect(spy.args).to.deep.equal([ + ['render'], + ['componentWillReceiveProps'], + ['render'], + ]); + expect(wrapper.context('foo')).to.equal(updatedProps.foo); + + expect(wrapper.debug()).to.equal(isShallow + ? `
+ baz +
` + : ` +
+ baz +
+
`); + }); + + itIf(is('>= 16.3'), 'calls componentWillReceiveProps and UNSAFE_componentWillReceiveProps when context is updated', () => { + const spy = sinon.spy(); + const updatedProps = { foo: 'baz' }; + class Foo extends React.Component { + componentWillReceiveProps() { + spy('componentWillReceiveProps'); + } + + UNSAFE_componentWillReceiveProps() { // eslint-disable-line camelcase + spy('UNSAFE_componentWillReceiveProps'); + } + + render() { + spy('render'); + const { foo } = this.context; + return
{foo}
; + } + } + Foo.contextTypes = { + foo: PropTypes.string, + }; + + const wrapper = Wrap( + , + { + context: { foo: 'bar' }, + }, + ); + + wrapper.setContext(updatedProps); + + expect(spy.args).to.deep.equal([ + ['render'], + ['componentWillReceiveProps'], + ['UNSAFE_componentWillReceiveProps'], + ['render'], + ]); + expect(wrapper.context('foo')).to.equal(updatedProps.foo); + + expect(wrapper.debug()).to.equal(isShallow + ? `
+ baz +
` + : ` +
+ baz +
+
`); + }); }); }