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.1/src/ReactSixteenOneAdapter.js b/packages/enzyme-adapter-react-16.1/src/ReactSixteenOneAdapter.js index b5f89f1b7..3608f6733 100644 --- a/packages/enzyme-adapter-react-16.1/src/ReactSixteenOneAdapter.js +++ b/packages/enzyme-adapter-react-16.1/src/ReactSixteenOneAdapter.js @@ -187,6 +187,7 @@ class ReactSixteenOneAdapter 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) { @@ -197,7 +198,7 @@ class ReactSixteenOneAdapter 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-react-16.2/src/ReactSixteenTwoAdapter.js b/packages/enzyme-adapter-react-16.2/src/ReactSixteenTwoAdapter.js index 021be7dc7..6e94a9850 100644 --- a/packages/enzyme-adapter-react-16.2/src/ReactSixteenTwoAdapter.js +++ b/packages/enzyme-adapter-react-16.2/src/ReactSixteenTwoAdapter.js @@ -189,6 +189,7 @@ class ReactSixteenTwoAdapter 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) { @@ -199,7 +200,7 @@ class ReactSixteenTwoAdapter 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-react-16.3/src/ReactSixteenThreeAdapter.js b/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js index 5ebbc976b..0524d069d 100644 --- a/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js +++ b/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.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 = []; @@ -133,6 +134,17 @@ function toTree(vnode) { case ContextProviderType: // 13 case ContextConsumerType: // 12 return childrenToTree(node.child); + case ForwardRefType: { + return { + nodeType: 'function', + type: node.type, + props: { ...node.pendingProps }, + 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 +207,7 @@ class ReactSixteenThreeAdapter 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 +218,7 @@ class ReactSixteenThreeAdapter 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-react-16/src/ReactSixteenAdapter.js b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js index b603a2cfb..1cce2322a 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.pendingProps }, + 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..a0c4dd871 100644 --- a/packages/enzyme-adapter-utils/src/createMountWrapper.jsx +++ b/packages/enzyme-adapter-utils/src/createMountWrapper.jsx @@ -3,6 +3,35 @@ import PropTypes from 'prop-types'; /* eslint react/forbid-prop-types: 0 */ +const stringOrFunction = PropTypes.oneOfType([PropTypes.func, PropTypes.string]); +const makeValidElementType = (adapter) => { + if (!adapter) { + return stringOrFunction; + } + + 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 +41,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 +93,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/Debug-spec.jsx b/packages/enzyme-test-suite/test/Debug-spec.jsx index 5de2c84a6..cfc9bd610 100644 --- a/packages/enzyme-test-suite/test/Debug-spec.jsx +++ b/packages/enzyme-test-suite/test/Debug-spec.jsx @@ -14,6 +14,9 @@ import { } from 'enzyme/build/Debug'; import './_helpers/setupAdapters'; +import { + forwardRef, +} from './_helpers/react-compat'; import { describeWithDOM, describeIf, @@ -837,4 +840,44 @@ describe('debug', () => { )); }); }); + + describeIf(is('>= 16.3'), 'forwarded ref Components', () => { + let Parent; + let SomeComponent; + beforeEach(() => { + SomeComponent = forwardRef((props, ref) => ( +
+ +
+ )); + Parent = () => ; + }); + + it('works with a `mount` wrapper', () => { + const wrapper = mount(); + expect(wrapper.debug()).to.equal(( + ` + + +
+ +
+
+
+
` + )); + }); + + it('works with a `mount` `.find` wrapper', () => { + const wrapper = mount(); + const results = wrapper.find(SomeComponent); + expect(results.debug()).to.equal(( + ` +
+ +
+
` + )); + }); + }); }); diff --git a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx index 83ba61811..e9876d79c 100644 --- a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx @@ -23,6 +23,7 @@ import { createPortal, createRef, Fragment, + forwardRef, } from './_helpers/react-compat'; import { describeWithDOM, @@ -80,6 +81,19 @@ describeWithDOM('mount', () => { mount(
); expect(spy).to.have.property('callCount', 1); }); + + describeIf(is('>= 16.3'), 'uses the isValidElementType from the Adapter to validate the prop type of Component', () => { + const Foo = () => null; + const Bar = () => null; + wrap() + .withConsoleThrows() + .withOverride(() => getAdapter(), 'isValidElementType', () => val => val === Foo) + .it('with isValidElementType defined on the Adapter', () => { + expect(() => { + mount(); + }).to.throw('Warning: Failed prop type: Component must be a valid element type!\n in WrapperComponent'); + }); + }); }); describe('context', () => { @@ -210,6 +224,45 @@ describeWithDOM('mount', () => { expect(wrapper.find('span').text()).to.equal('foo'); }); + describeIf(is('>= 16.3'), 'forwarded ref Components', () => { + wrap().withConsoleThrows().it('should mount without complaint', () => { + const SomeComponent = forwardRef((props, ref) => ( +
+ )); + + expect(() => mount()).not.to.throw(); + }); + + 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.lengthOf(1); + expect(results.props()).to.eql({ 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 2d06300ab..2fb84bf8b 100644 --- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx @@ -22,6 +22,7 @@ import { createContext, createRef, Fragment, + forwardRef, } from './_helpers/react-compat'; import { describeIf, @@ -145,7 +146,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', () => { diff --git a/packages/enzyme-test-suite/test/_helpers/setupAdapters.js b/packages/enzyme-test-suite/test/_helpers/setupAdapters.js index 6d5cc4475..9dba418f7 100644 --- a/packages/enzyme-test-suite/test/_helpers/setupAdapters.js +++ b/packages/enzyme-test-suite/test/_helpers/setupAdapters.js @@ -1,4 +1,21 @@ const Enzyme = require('enzyme'); +const wrap = require('mocha-wrap'); + const Adapter = require('./adapter'); Enzyme.configure({ adapter: new Adapter() }); + +const origWarn = console.warn; +const origError = console.error; +wrap.register(function withConsoleThrows() { + return this.withOverrides(() => console, () => ({ + error(msg) { + origError.apply(console, arguments); // eslint-disable-line prefer-rest-params + throw new EvalError(msg); + }, + warn(msg) { + origWarn.apply(console, arguments); // eslint-disable-line prefer-rest-params + throw new EvalError(msg); + }, + })); +});