diff --git a/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js b/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js index 3617703aa..a243eddde 100644 --- a/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js +++ b/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js @@ -11,6 +11,7 @@ import TestUtils from 'react-dom/test-utils'; import { isElement, isPortal, + isForwardRef, isValidElementType, AsyncMode, Fragment, @@ -483,6 +484,13 @@ class ReactSixteenThreeAdapter extends EnzymeAdapter { return typeOfNode(fragment) === Fragment; } + isCustomComponentElement(inst) { + if (!inst || !this.isValidElement(inst)) { + return false; + } + return typeof inst.type === 'function' || isForwardRef(inst); + } + createElement(...args) { return React.createElement(...args); } diff --git a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js index ac2941fab..e18030688 100644 --- a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js +++ b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js @@ -11,6 +11,7 @@ import TestUtils from 'react-dom/test-utils'; import { isElement, isPortal, + isForwardRef, isValidElementType, AsyncMode, Fragment, @@ -525,6 +526,13 @@ class ReactSixteenAdapter extends EnzymeAdapter { return typeOfNode(fragment) === Fragment; } + isCustomComponentElement(inst) { + if (!inst || !this.isValidElement(inst)) { + return false; + } + return typeof inst.type === 'function' || isForwardRef(inst); + } + createElement(...args) { return React.createElement(...args); } diff --git a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx index 5f427aa1e..84a78810b 100644 --- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx @@ -6923,6 +6923,33 @@ describe('shallow', () => { expect(underwater.is(RendersDOM)).to.equal(true); }); + describeIf(is('>=16.3.0'), 'forwardRef Elements', () => { + const ForwardRefWrapsRendersDOM = forwardRef && forwardRef(() => ); + const NestedForwarRefsWrapsRendersDom = forwardRef + && forwardRef(() => ); + + if (forwardRef) { + NestedForwarRefsWrapsRendersDom.contextTypes = { foo: PropTypes.string }; + ForwardRefWrapsRendersDOM.contextTypes = { foo: PropTypes.string }; + } + + it('dives + shallow-renders a forwardRef component', () => { + const wrapper = shallow(); + expect(wrapper.is(WrapsRendersDOM)).to.equal(true); + + const underwater = wrapper.dive(); + expect(underwater.is(RendersDOM)).to.equal(true); + }); + + it('dives + shallow-renders a with nested forwardRefs component', () => { + const wrapper = shallow(); + expect(wrapper.is(ForwardRefWrapsRendersDOM)).to.equal(true); + + const underwater = wrapper.dive(); + expect(underwater.is(WrapsRendersDOM)).to.equal(true); + }); + }); + it('merges and pass options through', () => { const wrapper = shallow(, { context: { foo: 'hello' } }); expect(wrapper.context()).to.deep.equal({ foo: 'hello' }); diff --git a/packages/enzyme-test-suite/test/Utils-spec.jsx b/packages/enzyme-test-suite/test/Utils-spec.jsx index bfa0d4fdc..1c3d7ad2d 100644 --- a/packages/enzyme-test-suite/test/Utils-spec.jsx +++ b/packages/enzyme-test-suite/test/Utils-spec.jsx @@ -9,6 +9,7 @@ import { displayNameOfNode, spyMethod, nodeHasType, + isCustomComponentElement, } from 'enzyme/build/Utils'; import getAdapter from 'enzyme/build/getAdapter'; import { @@ -639,6 +640,151 @@ describe('Utils', () => { }); }); + describe('isCustomComponentElement()', () => { + const adapter = getAdapter(); + + wrap() + .withOverride(() => adapter, 'isCustomComponentElement', () => undefined) + .describe('with an adapter lacking `.isCustomComponentElement`', () => { + describe('given a valid CustomComponentElement', () => { + it('returns true', () => { + class Foo extends React.Component { + render() { return
; } + } + expect(isCustomComponentElement(, adapter)).to.equal(true); + }); + + describeIf(is('> 0.13'), 'stateless function elements', () => { + it('returns true', () => { + const Foo = () =>
; + + expect(isCustomComponentElement(, adapter)).to.equal(true); + }); + }); + + describeIf(is('>=16.3.0'), 'forwardRef Elements', () => { + it('returns false', () => { + const Foo = React.forwardRef(() =>
); + expect(isCustomComponentElement(, adapter)).to.equal(false); + }); + }); + }); + + describe('given an invalid CustomComponentElement', () => { + it('returns false for HTML elements', () => { + expect(isCustomComponentElement(
, adapter)).to.equal(false); + }); + + it('returns false for non-Components', () => { + [ + class Foo {}, + {}, + () => {}, + 'div', + 'Foo', + null, + ].forEach((nonComponent) => { + expect(isCustomComponentElement(nonComponent, adapter)).to.equal(false); + }); + }); + }); + }); + + wrap() + .withOverride(() => adapter, 'isCustomComponentElement', () => () => false) + .describe('with an adapter that has `.isCustomComponentElement` that always returns false', () => { + describe('given a valid CustomComponentElement', () => { + it('returns false', () => { + class Foo extends React.Component { + render() { return
; } + } + expect(isCustomComponentElement(, adapter)).to.equal(false); + }); + + describeIf(is('> 0.13'), 'stateless function elements', () => { + it('returns false', () => { + const Foo = () =>
; + + expect(isCustomComponentElement(, adapter)).to.equal(false); + }); + }); + + describeIf(is('>=16.3.0'), 'forwardRef Elements', () => { + it('returns false', () => { + const Foo = React.forwardRef(() =>
); + expect(isCustomComponentElement(, adapter)).to.equal(false); + }); + }); + }); + + describe('given an invalid CustomComponentElement', () => { + it('returns false for HTML elements', () => { + expect(isCustomComponentElement(
, adapter)).to.equal(false); + }); + + it('returns false for non-Components', () => { + [ + class Foo {}, + {}, + () => {}, + 'div', + 'Foo', + null, + ].forEach((nonComponent) => { + expect(isCustomComponentElement(nonComponent, adapter)).to.equal(false); + }); + }); + }); + }); + + wrap() + .withOverride(() => adapter, 'isCustomComponentElement', () => () => true) + .describe('with an adapter that has `.isCustomComponentElement` that always returns true', () => { + describe('given a valid CustomComponentElement', () => { + it('returns true', () => { + class Foo extends React.Component { + render() { return
; } + } + expect(isCustomComponentElement(, adapter)).to.equal(true); + }); + + describeIf(is('> 0.13'), 'stateless function elements', () => { + it('returns true', () => { + const Foo = () =>
; + + expect(isCustomComponentElement(, adapter)).to.equal(true); + }); + }); + + describeIf(is('>=16.3.0'), 'forwardRef Elements', () => { + it('returns true', () => { + const Foo = React.forwardRef(() =>
); + expect(isCustomComponentElement(, adapter)).to.equal(true); + }); + }); + }); + + describe('given an invalid CustomComponentElement', () => { + it('returns true for HTML elements', () => { + expect(isCustomComponentElement(
, adapter)).to.equal(true); + }); + + it('returns true for non-Components', () => { + [ + class Foo {}, + {}, + () => {}, + 'div', + 'Foo', + null, + ].forEach((nonComponent) => { + expect(isCustomComponentElement(nonComponent, adapter)).to.equal(true); + }); + }); + }); + }); + }); + wrap() .withOverride(() => getAdapter(), 'displayNameOfNode', () => undefined) .describe('nodeHasType', () => { diff --git a/packages/enzyme/src/Utils.js b/packages/enzyme/src/Utils.js index 63448a777..b1a8c93d4 100644 --- a/packages/enzyme/src/Utils.js +++ b/packages/enzyme/src/Utils.js @@ -42,6 +42,9 @@ export function makeOptions(options) { } export function isCustomComponentElement(inst, adapter) { + if (adapter.isCustomComponentElement) { + return !!adapter.isCustomComponentElement(inst); + } return !!inst && adapter.isValidElement(inst) && typeof inst.type === 'function'; }