Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Forward ref #1592

Merged
merged 6 commits into from Aug 16, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/enzyme-adapter-react-13/src/ReactThirteenAdapter.js
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
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
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
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
Expand Up @@ -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) {
Expand All @@ -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)
Expand Down
Expand Up @@ -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) {
Expand All @@ -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)
Expand Down
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 @@ -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}`);
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
Expand Down
16 changes: 15 additions & 1 deletion packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js
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.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}`);
}
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
33 changes: 32 additions & 1 deletion packages/enzyme-adapter-utils/src/createMountWrapper.jsx
Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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,
};
Expand Down
43 changes: 43 additions & 0 deletions packages/enzyme-test-suite/test/Debug-spec.jsx
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,44 @@ describe('debug', () => {
));
});
});

describeIf(is('>= 16.3'), 'forwarded ref Components', () => {
let Parent;
let 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 foo="hello">
<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 foo="hello">
<div>
<span className="child1" />
</div>
</ForwardRef>`
));
});
});
});
53 changes: 53 additions & 0 deletions packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
Expand Up @@ -23,6 +23,7 @@ import {
createPortal,
createRef,
Fragment,
forwardRef,
} from './_helpers/react-compat';
import {
describeWithDOM,
Expand Down Expand Up @@ -80,6 +81,19 @@ describeWithDOM('mount', () => {
mount(<div ref={spy} />);
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(<Bar />);
}).to.throw('Warning: Failed prop type: Component must be a valid element type!\n in WrapperComponent');
});
});
});

describe('context', () => {
Expand Down Expand Up @@ -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) => (
<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