diff --git a/packages/enzyme-adapter-react-13/src/ReactThirteenAdapter.js b/packages/enzyme-adapter-react-13/src/ReactThirteenAdapter.js index 0d7a7e2db..0c2dd7c88 100644 --- a/packages/enzyme-adapter-react-13/src/ReactThirteenAdapter.js +++ b/packages/enzyme-adapter-react-13/src/ReactThirteenAdapter.js @@ -118,6 +118,7 @@ class ReactThirteenAdapter extends EnzymeAdapter { assertDomAvailable('mount'); const domNode = options.attachTo || global.document.createElement('div'); let instance = null; + const adapter = this; return { render(el, context, callback) { if (instance === null) { @@ -128,7 +129,7 @@ class ReactThirteenAdapter extends EnzymeAdapter { context, ...(ref && { ref }), }; - const ReactWrapperComponent = createMountWrapper(el, options); + const ReactWrapperComponent = createMountWrapper(el, { ...options, adapter }); const wrappedEl = React.createElement(ReactWrapperComponent, wrapperProps); instance = React.render(wrappedEl, domNode); if (typeof callback === 'function') { diff --git a/packages/enzyme-adapter-react-14/src/ReactFourteenAdapter.js b/packages/enzyme-adapter-react-14/src/ReactFourteenAdapter.js index d87e25f19..41a1625ac 100644 --- a/packages/enzyme-adapter-react-14/src/ReactFourteenAdapter.js +++ b/packages/enzyme-adapter-react-14/src/ReactFourteenAdapter.js @@ -91,6 +91,7 @@ class ReactFourteenAdapter extends EnzymeAdapter { assertDomAvailable('mount'); const domNode = options.attachTo || global.document.createElement('div'); let instance = null; + const adapter = this; return { render(el, context, callback) { if (instance === null) { @@ -101,7 +102,7 @@ class ReactFourteenAdapter extends EnzymeAdapter { context, ...(ref && { ref }), }; - const ReactWrapperComponent = createMountWrapper(el, options); + const ReactWrapperComponent = createMountWrapper(el, { ...options, adapter }); const wrappedEl = React.createElement(ReactWrapperComponent, wrapperProps); instance = ReactDOM.render(wrappedEl, domNode); if (typeof callback === 'function') { diff --git a/packages/enzyme-adapter-react-15.4/src/ReactFifteenFourAdapter.js b/packages/enzyme-adapter-react-15.4/src/ReactFifteenFourAdapter.js index 472932d36..81897c7ae 100644 --- a/packages/enzyme-adapter-react-15.4/src/ReactFifteenFourAdapter.js +++ b/packages/enzyme-adapter-react-15.4/src/ReactFifteenFourAdapter.js @@ -124,6 +124,7 @@ class ReactFifteenFourAdapter extends EnzymeAdapter { assertDomAvailable('mount'); const domNode = options.attachTo || global.document.createElement('div'); let instance = null; + const adapter = this; return { render(el, context, callback) { if (instance === null) { @@ -134,7 +135,7 @@ class ReactFifteenFourAdapter extends EnzymeAdapter { context, ...(ref && { ref }), }; - const ReactWrapperComponent = createMountWrapper(el, options); + const ReactWrapperComponent = createMountWrapper(el, { ...options, adapter }); const wrappedEl = React.createElement(ReactWrapperComponent, wrapperProps); instance = ReactDOM.render(wrappedEl, domNode); if (typeof callback === 'function') { diff --git a/packages/enzyme-adapter-react-15/src/ReactFifteenAdapter.js b/packages/enzyme-adapter-react-15/src/ReactFifteenAdapter.js index db557717f..0c9deaed7 100644 --- a/packages/enzyme-adapter-react-15/src/ReactFifteenAdapter.js +++ b/packages/enzyme-adapter-react-15/src/ReactFifteenAdapter.js @@ -124,6 +124,7 @@ class ReactFifteenAdapter extends EnzymeAdapter { assertDomAvailable('mount'); const domNode = options.attachTo || global.document.createElement('div'); let instance = null; + const adapter = this; return { render(el, context, callback) { if (instance === null) { @@ -134,7 +135,7 @@ class ReactFifteenAdapter extends EnzymeAdapter { context, ...(ref && { ref }), }; - const ReactWrapperComponent = createMountWrapper(el, options); + const ReactWrapperComponent = createMountWrapper(el, { ...options, adapter }); const wrappedEl = React.createElement(ReactWrapperComponent, wrapperProps); instance = ReactDOM.render(wrappedEl, domNode); if (typeof callback === 'function') { diff --git a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js index b603a2cfb..6d6ff18b6 100644 --- a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js +++ b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js @@ -47,6 +47,7 @@ const HostText = 6; const Mode = 11; const ContextConsumerType = 12; const ContextProviderType = 13; +const ForwardRefType = 14; function nodeAndSiblingsArray(nodeWithSibling) { const array = []; @@ -111,6 +112,7 @@ function toTree(vnode) { instance: null, rendered: childrenToTree(node.child), }; + case HostComponent: { // 5 let renderedNodes = flatten(nodeAndSiblingsArray(node.child).map(toTree)); if (renderedNodes.length === 0) { @@ -133,6 +135,17 @@ function toTree(vnode) { case ContextProviderType: // 13 case ContextConsumerType: // 12 return childrenToTree(node.child); + case ForwardRefType: { + return { + nodeType: 'function', + type: node.type, + props: { ...node.memoizedProps }, + key: ensureKeyOrUndefined(node.key), + ref: node.ref, + instance: null, + rendered: childrenToTree(node.child), + }; + } default: throw new Error(`Enzyme Internal Error: unknown node with tag ${node.tag}`); } @@ -195,6 +208,7 @@ class ReactSixteenAdapter extends EnzymeAdapter { const { attachTo, hydrateIn } = options; const domNode = hydrateIn || attachTo || global.document.createElement('div'); let instance = null; + const adapter = this; return { render(el, context, callback) { if (instance === null) { @@ -205,7 +219,7 @@ class ReactSixteenAdapter extends EnzymeAdapter { context, ...(ref && { ref }), }; - const ReactWrapperComponent = createMountWrapper(el, options); + const ReactWrapperComponent = createMountWrapper(el, { ...options, adapter }); const wrappedEl = React.createElement(ReactWrapperComponent, wrapperProps); instance = hydrateIn ? ReactDOM.hydrate(wrappedEl, domNode) diff --git a/packages/enzyme-adapter-utils/src/createMountWrapper.jsx b/packages/enzyme-adapter-utils/src/createMountWrapper.jsx index 9fa7774b2..42757f949 100644 --- a/packages/enzyme-adapter-utils/src/createMountWrapper.jsx +++ b/packages/enzyme-adapter-utils/src/createMountWrapper.jsx @@ -3,6 +3,31 @@ import PropTypes from 'prop-types'; /* eslint react/forbid-prop-types: 0 */ +const stringOrFunction = PropTypes.oneOfType([PropTypes.func, PropTypes.string]); +const makeValidElementType = (adapter) => { + function validElementType(props, propName, ...args) { + if (!adapter.isValidElementType) { + return stringOrFunction(props, propName, ...args); + } + const propValue = props[propName]; + if (propValue == null || adapter.isValidElementType(propValue)) { + return null; + } + return new TypeError(`${propName} must be a valid element type!`); + } + validElementType.isRequired = function validElementTypeRequired(props, propName, ...args) { + if (!adapter.isValidElementType) { + return stringOrFunction.isRequired(props, propName, ...args); + } + const propValue = props[propName]; // eslint-disable-line react/destructuring-assignment + if (adapter.isValidElementType(propValue)) { + return null; + } + return new TypeError(`${propName} must be a valid element type!`); + }; + return validElementType; +}; + /** * This is a utility component to wrap around the nodes we are * passing in to `mount()`. Theoretically, you could do everything @@ -12,6 +37,8 @@ import PropTypes from 'prop-types'; * pass new props in. */ export default function createMountWrapper(node, options = {}) { + const { adapter } = options; + class WrapperComponent extends React.Component { constructor(...args) { super(...args); @@ -62,7 +89,7 @@ export default function createMountWrapper(node, options = {}) { } } WrapperComponent.propTypes = { - Component: PropTypes.oneOfType([PropTypes.func, PropTypes.string]).isRequired, + Component: makeValidElementType(adapter).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 4592068bb..835044aa4 100644 --- a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx @@ -22,6 +22,7 @@ import { createContext, createPortal, Fragment, + forwardRef, } from './_helpers/react-compat'; import { describeWithDOM, @@ -209,6 +210,51 @@ describeWithDOM('mount', () => { expect(wrapper.find('span').text()).to.equal('foo'); }); + describeIf(is('>= 16.3'), 'forwarded ref Components', () => { + it('should mount without complaint', () => { + const warningStub = sinon.stub(console, 'error'); + + const SomeComponent = forwardRef((props, ref) => ( +
+ )); + + mount(); + + expect(warningStub).to.have.property('callCount', 0); + + 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.lengthOf(1); + }); + + it('should find forwardRef element', () => { + const SomeComponent = forwardRef((props, ref) => ( +
+ +
+ )); + const Parent = () => ; + + const wrapper = mount(); + const results = wrapper.find(SomeComponent); + + expect(results).to.have.length(1); + expect(results.props()).to.deep.equal({ foo: 'hello' }); + }); + }); + describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => { 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 edff45b3f..e5a03c22c 100644 --- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx @@ -21,6 +21,7 @@ import { createClass, createContext, Fragment, + forwardRef, } from './_helpers/react-compat'; import { describeIf, @@ -144,7 +145,19 @@ describe('shallow', () => { expect(shallow().find('span')).to.have.lengthOf(1); expect(shallow().find(Consumes)).to.have.lengthOf(1); + }); + + itIf(is('>= 16.3'), 'should find elements through forwarded refs elements', () => { + const SomeComponent = forwardRef((props, ref) => ( +
+ + +
+ )); + + const wrapper = shallow(); + expect(wrapper.find('.child2')).to.have.length(1); }); describeIf(is('> 0.13'), 'stateless function components', () => {