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';
}