Skip to content

Commit

Permalink
[enzyme-adapter-utils, enzyme-adapter-react-*] Add support for forwar…
Browse files Browse the repository at this point in the history
…dRef
  • Loading branch information
jquense authored and ljharb committed Mar 23, 2018
1 parent 4d4f7bd commit 6be05e9
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 6 deletions.
3 changes: 2 additions & 1 deletion packages/enzyme-adapter-react-13/src/ReactThirteenAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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') {
Expand Down
3 changes: 2 additions & 1 deletion packages/enzyme-adapter-react-14/src/ReactFourteenAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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') {
Expand Down
3 changes: 2 additions & 1 deletion packages/enzyme-adapter-react-15/src/ReactFifteenAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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') {
Expand Down
16 changes: 15 additions & 1 deletion packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const HostText = 6;
const Mode = 11;
const ContextConsumerType = 12;
const ContextProviderType = 13;
const ForwardRefType = 14;

function nodeAndSiblingsArray(nodeWithSibling) {
const array = [];
Expand Down Expand Up @@ -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) {
Expand All @@ -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}`);
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
Expand Down
29 changes: 28 additions & 1 deletion packages/enzyme-adapter-utils/src/createMountWrapper.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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,
};
Expand Down
46 changes: 46 additions & 0 deletions packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
createContext,
createPortal,
Fragment,
forwardRef,
} from './_helpers/react-compat';
import {
describeWithDOM,
Expand Down Expand Up @@ -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) => (
<div {...props} ref={ref} />
));

mount(<SomeComponent />);

expect(warningStub).to.have.property('callCount', 0);

warningStub.restore();
});

it('should find elements through forwardedRef elements', () => {
const testRef = () => {};
const SomeComponent = forwardRef((props, ref) => (
<div ref={ref}>
<span className="child1" />
<span className="child2" />
</div>
));

const wrapper = mount(<div><SomeComponent ref={testRef} /></div>);

expect(wrapper.find('.child2')).to.have.lengthOf(1);
});

it('should find forwardRef element', () => {
const SomeComponent = forwardRef((props, ref) => (
<div ref={ref}>
<span className="child1" />
</div>
));
const Parent = () => <span><SomeComponent foo="hello" /></span>;

const wrapper = mount(<Parent foo="hello" />);
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) => (
Expand Down
13 changes: 13 additions & 0 deletions packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
createClass,
createContext,
Fragment,
forwardRef,
} from './_helpers/react-compat';
import {
describeIf,
Expand Down Expand Up @@ -144,7 +145,19 @@ describe('shallow', () => {

expect(shallow(<Consumes />).find('span')).to.have.lengthOf(1);
expect(shallow(<Provides />).find(Consumes)).to.have.lengthOf(1);
});

itIf(is('>= 16.3'), 'should find elements through forwarded refs elements', () => {
const SomeComponent = forwardRef((props, ref) => (
<div ref={ref}>
<span className="child1" />
<span className="child2" />
</div>
));

const wrapper = shallow(<SomeComponent />);

expect(wrapper.find('.child2')).to.have.length(1);
});

describeIf(is('> 0.13'), 'stateless function components', () => {
Expand Down

0 comments on commit 6be05e9

Please sign in to comment.