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 460ade7 commit 7fc51c1
Show file tree
Hide file tree
Showing 10 changed files with 162 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
42 changes: 42 additions & 0 deletions packages/enzyme-test-suite/test/Debug-spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import {
} from 'enzyme/build/Debug';

import './_helpers/setupAdapters';
import {
forwardRef,
} from './_helpers/react-compat';
import {
describeWithDOM,
describeIf,
Expand Down Expand Up @@ -837,4 +840,43 @@ describe('debug', () => {
));
});
});

describeIf(is('>= 16.3'), 'forwarded ref Components', () => {
let Parent, SomeComponent;
beforeEach(() => {
SomeComponent = forwardRef((props, ref) => (
<div ref={ref}>
<span className="child1" />
</div>
));
Parent = () => <span><SomeComponent foo="hello" /></span>;
});

it('works with a `mount` wrapper', () => {
const wrapper = mount(<Parent foo="hello" />);
expect(wrapper.debug()).to.equal((
`<Parent foo="hello">
<span>
<ForwardRef>
<div>
<span className="child1" />
</div>
</ForwardRef>
</span>
</Parent>`
));
});

it('works with a `mount` `.find` wrapper', () => {
const wrapper = mount(<Parent foo="hello" />);
const results = wrapper.find(SomeComponent);
expect(results.debug()).to.equal((
`<ForwardRef>
<div>
<span className="child1" />
</div>
</ForwardRef>`
));
});
});
});
40 changes: 40 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,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) => (
<div {...props} ref={ref} />
));

expect(() => mount(<SomeComponent />)).not.to.throw();
});

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.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) => (
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
16 changes: 16 additions & 0 deletions packages/enzyme-test-suite/test/_helpers/setupAdapters.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,19 @@ const Enzyme = require('enzyme');
const Adapter = require('./adapter');

Enzyme.configure({ adapter: new Adapter() });

const wrap = require('mocha-wrap');
const origWarn = console.warn;
const origError = console.error;
wrap.register(function withConsoleThrows() {
return this.withOverrides(() => console, () => ({
error(msg) {
origError.apply(console, arguments);
throw new EvalError(msg);
},
warn(msg) {
origWarn.apply(console, arguments);
throw new EvalError(msg);
},
}));
});

0 comments on commit 7fc51c1

Please sign in to comment.