diff --git a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js
index bcc1f48aa..896668a8f 100644
--- a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js
+++ b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js
@@ -32,6 +32,7 @@ const HostText = 6;
const Mode = 11;
const ContextConsumer = 12;
const ContextProvider = 13;
+const ForwardRef = 14;
function nodeAndSiblingsArray(nodeWithSibling) {
const array = [];
@@ -112,10 +113,11 @@ function toTree(vnode) {
}
case HostText: // 6
return node.memoizedProps;
- case Fragment: // 10
- case Mode: // 11
- case ContextProvider: // 13
- case ContextConsumer: // 12
+ case Fragment:
+ case ContextProvider:
+ case ContextConsumer:
+ case Mode:
+ case ForwardRef:
return childrenToTree(node.child);
default:
throw new Error(`Enzyme Internal Error: unknown node with tag ${node.tag}`);
diff --git a/packages/enzyme-adapter-utils/src/createMountWrapper.jsx b/packages/enzyme-adapter-utils/src/createMountWrapper.jsx
index 0bc127eed..6f44d5368 100644
--- a/packages/enzyme-adapter-utils/src/createMountWrapper.jsx
+++ b/packages/enzyme-adapter-utils/src/createMountWrapper.jsx
@@ -3,6 +3,11 @@ import PropTypes from 'prop-types';
/* eslint react/forbid-prop-types: 0 */
+// TODO: use react-is?
+const specialType = PropTypes.shape({
+ $$typeof: PropTypes.any.isRequired,
+}).isRequired;
+
/**
* This is a utility component to wrap around the nodes we are
* passing in to `mount()`. Theoretically, you could do everything
@@ -55,7 +60,7 @@ export default function createMountWrapper(node, options = {}) {
}
}
WrapperComponent.propTypes = {
- Component: PropTypes.oneOfType([PropTypes.func, PropTypes.string]).isRequired,
+ Component: PropTypes.oneOfType([PropTypes.func, PropTypes.string, specialType]).isRequired,
props: PropTypes.object.isRequired,
context: PropTypes.object,
};
diff --git a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
index cc48122ac..f285be0da 100644
--- a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
+++ b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
@@ -11,7 +11,7 @@ import {
import { ITERATOR_SYMBOL, sym } from 'enzyme/build/Utils';
import './_helpers/setupAdapters';
-import { createClass, createContext, createPortal } from './_helpers/react-compat';
+import { createClass, createContext, createPortal, forwardRef } from './_helpers/react-compat';
import {
describeWithDOM,
describeIf,
@@ -181,6 +181,52 @@ describeWithDOM('mount', () => {
expect(wrapper.find('span').text()).to.equal('foo');
});
+ describeIf(REACT163, 'forwarded ref Components', () => {
+
+ it('should mount without complaint', () => {
+ const warningStub = sinon.stub(console, 'error');
+
+ const SomeComponent = forwardRef((props, ref) => (
+
+ ));
+
+ mount();
+
+ expect(warningStub.called).to.equal(false);
+
+ warningStub.restore();
+ });
+
+ it('should find elements through forwardedRef elements', () => {
+ const testRef = () => {};
+ const SomeComponent = forwardRef((props, ref) => (
+
+
+
+
+ ));
+
+ const wrapper = mount(
);
+
+ expect(wrapper.find('.child2')).to.have.length(1);
+ });
+
+ it('should find the forwardRef element itself', () => {
+ const testRef = () => {};
+ const OtherComponent = () => (
+
+ );
+ const SomeComponent = forwardRef((props, ref) => (
+
+ ));
+
+ const wrapper = mount(
);
+
+ expect(wrapper.find(SomeComponent)).to.have.length(1);
+ expect(wrapper.find(OtherComponent)).to.have.length(1);
+ });
+ });
+
describeIf(!REACT013, 'stateless components', () => {
it('can pass in context', () => {
const SimpleComponent = (props, context) => (
diff --git a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
index 8aaf196e1..b1416147f 100644
--- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
+++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
@@ -6,7 +6,7 @@ import { shallow, render, ShallowWrapper, mount } from 'enzyme';
import { ITERATOR_SYMBOL, withSetStateAllowed, sym } from 'enzyme/build/Utils';
import './_helpers/setupAdapters';
-import { createClass, createContext } from './_helpers/react-compat';
+import { createClass, createContext, forwardRef, createPortal } from './_helpers/react-compat';
import { describeIf, itIf, itWithData, generateEmptyRenderData } from './_helpers';
import { REACT013, REACT014, REACT15, REACT150_4, REACT16, REACT163, is } from './_helpers/version';
@@ -119,7 +119,19 @@ describe('shallow', () => {
expect(shallow().find('span')).to.have.length(1);
expect(shallow().find(Consumes)).to.have.length(1);
+ });
+
+ itIf(REACT163, 'should find elements through forwardedRef elements', () => {
+ const SomeComponent = forwardRef((props, ref) => (
+
+
+
+
+ ));
+
+ const wrapper = shallow();
+ expect(wrapper.find('.child2')).to.have.length(1);
});
describeIf(!REACT013, 'stateless function components', () => {
@@ -2945,6 +2957,36 @@ describe('shallow', () => {
it('should pass through to the debugNodes function', () => {
expect(shallow().debug()).to.equal('');
});
+
+ itIf(REACT163, 'should handle internal types gracefully', () => {
+ const { Provider, Consumer } = createContext(null);
+ // eslint-disable-next-line prefer-arrow-callback
+ const Forwarded = forwardRef(function MyComponent(props) {
+ return (
+
+
+
+
+ {() => }
+ {createPortal(, { nodeType: 1 })}
+
+
+
+ );
+ });
+
+ expect(shallow().debug()).to.equal(`
+
+
+
+
+ [function child]
+
+
+
+
+`);
+ });
});
describe('.html()', () => {
diff --git a/packages/enzyme-test-suite/test/_helpers/react-compat.js b/packages/enzyme-test-suite/test/_helpers/react-compat.js
index a63fbc15a..c5c628f15 100644
--- a/packages/enzyme-test-suite/test/_helpers/react-compat.js
+++ b/packages/enzyme-test-suite/test/_helpers/react-compat.js
@@ -10,6 +10,7 @@ let createClass;
let renderToString;
let createPortal;
let createContext;
+let forwardRef;
if (is('>=15.5 || ^16.0.0-alpha || ^16.3.0-alpha')) {
// eslint-disable-next-line import/no-extraneous-dependencies
@@ -37,9 +38,16 @@ if (is('^16.3.0-0')) {
createContext = null;
}
+if (is('^16.3.0-0')) {
+ ({ forwardRef } = require('react'));
+} else {
+ forwardRef = null;
+}
+
export {
createClass,
renderToString,
createPortal,
createContext,
+ forwardRef,
};
diff --git a/packages/enzyme/src/Debug.js b/packages/enzyme/src/Debug.js
index 27ccb372a..5cc23f400 100644
--- a/packages/enzyme/src/Debug.js
+++ b/packages/enzyme/src/Debug.js
@@ -12,13 +12,41 @@ import {
propsOfNode,
childrenOfNode,
} from './RSTTraversal';
+import {
+ typeOf,
+ AsyncMode,
+ ContextProvider,
+ ContextConsumer,
+ Element,
+ ForwardRef,
+ Fragment,
+ Portal,
+ StrictMode,
+} from './Utils';
const booleanValue = Function.bind.call(Function.call, Boolean.prototype.valueOf);
+
export function typeName(node) {
- return typeof node.type === 'function'
- ? (node.type.displayName || functionName(node.type) || 'Component')
- : node.type;
+ const { type } = node;
+ switch (typeOf(node)) {
+ case AsyncMode: return 'AsyncMode';
+ case ContextProvider: return 'ContextProvider';
+ case ContextConsumer: return 'ContextConsumer';
+ case Portal: return 'Portal';
+ case StrictMode: return 'StrictMode';
+ case ForwardRef: {
+ const name = type.render.displayName || functionName(type.render);
+ return name ? `ForwardRef(${name})` : 'ForwardRef';
+ }
+ case Fragment:
+ return 'Fragment';
+ case Element:
+ default:
+ return typeof node.type === 'function'
+ ? (type.displayName || functionName(type) || 'Component')
+ : type || 'unknown';
+ }
}
export function spaces(n) {
@@ -66,6 +94,7 @@ function indentChildren(childrenStrs, indentLength) {
export function debugNode(node, indentLength = 2, options = {}) {
if (typeof node === 'string' || typeof node === 'number') return escape(node);
+ if (typeof node === 'function') return '[function child]';
if (!node) return '';
const childrenStrs = compact(childrenOfNode(node).map(n => debugNode(n, indentLength, options)));
diff --git a/packages/enzyme/src/Utils.js b/packages/enzyme/src/Utils.js
index c479f6313..c9da05f63 100644
--- a/packages/enzyme/src/Utils.js
+++ b/packages/enzyme/src/Utils.js
@@ -7,8 +7,47 @@ import configuration from './configuration';
import validateAdapter from './validateAdapter';
import { childrenOfNode } from './RSTTraversal';
+const hasSymbol = typeof Symbol === 'function' && Symbol.for;
+
export const ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
+// TODO use react-is when a version is released with no peer dependency on React
+export const AsyncMode = hasSymbol ? Symbol.for('react.async_mode') : 0xeacf;
+export const ContextProvider = hasSymbol ? Symbol.for('react.provider') : 0xeacd;
+export const ContextConsumer = hasSymbol ? Symbol.for('react.context') : 0xeace;
+export const Element = hasSymbol ? Symbol.for('react.element') : 0xeac7;
+export const ForwardRef = hasSymbol ? Symbol.for('react.forward_ref') : 0xead0;
+export const Fragment = hasSymbol ? Symbol.for('react.fragment') : 0xeacb;
+export const Portal = hasSymbol ? Symbol.for('react.portal') : 0xeaca;
+export const StrictMode = hasSymbol ? Symbol.for('react.strict_mode') : 0xeacc;
+
+export function typeOf(node) {
+ if (node !== null) {
+ const { type, $$typeof } = node;
+
+ switch (type || $$typeof) {
+ case AsyncMode:
+ case Fragment:
+ case StrictMode:
+ return type;
+ default: {
+ const $$typeofType = type && type.$$typeof;
+
+ switch ($$typeofType) {
+ case ContextConsumer:
+ case ForwardRef:
+ case Portal:
+ case ContextProvider:
+ return $$typeofType;
+ default:
+ return type || $$typeof;
+ }
+ }
+ }
+ }
+ return undefined;
+}
+
export function getAdapter(options = {}) {
if (options.adapter) {
validateAdapter(options.adapter);
diff --git a/packages/enzyme/src/selectors.js b/packages/enzyme/src/selectors.js
index 8342dde1a..358f056e7 100644
--- a/packages/enzyme/src/selectors.js
+++ b/packages/enzyme/src/selectors.js
@@ -13,7 +13,7 @@ import {
childrenOfNode,
hasClassName,
} from './RSTTraversal';
-import { nodeHasType, propsOfNode } from './Utils';
+import { nodeHasType, propsOfNode, typeOf, ForwardRef } from './Utils';
// our CSS selector parser instance
const parser = createParser();
@@ -227,6 +227,12 @@ export function buildPredicate(selector) {
if (hasUndefinedValues) {
throw new TypeError('Enzyme::Props can’t have `undefined` values. Try using ‘findWhere()’ instead.');
}
+ // the selector could also be a forwardRef
+ if (typeOf(selector) === ForwardRef) {
+ // re-build the predicate based on what is wrapped by forwardRef
+ // rather than the forwardRef itself
+ return buildPredicate(selector.render().props);
+ }
return node => nodeMatchesObjectProps(node, selector);
}
throw new TypeError('Enzyme::Selector does not support an array, null, or empty object as a selector');