Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const HostText = 6;
const Mode = 11;
const ContextConsumer = 12;
const ContextProvider = 13;
const ForwardRef = 14;

function nodeAndSiblingsArray(nodeWithSibling) {
const array = [];
Expand Down Expand Up @@ -112,10 +113,11 @@ function toTree(vnode) {
}
case HostText: // 6
return node.memoizedProps;
case Fragment: // 10
case Mode: // 11
case ContextProvider: // 13
case ContextConsumer: // 12
case Fragment:
case ContextProvider:
case ContextConsumer:
case Mode:
case ForwardRef:
return childrenToTree(node.child);
default:
throw new Error(`Enzyme Internal Error: unknown node with tag ${node.tag}`);
Expand Down
7 changes: 6 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,11 @@ import PropTypes from 'prop-types';

/* eslint react/forbid-prop-types: 0 */

// TODO: use react-is?
const specialType = PropTypes.shape({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should use react-is

$$typeof: PropTypes.any.isRequired,
}).isRequired;

/**
* This is a utility component to wrap around the nodes we are
* passing in to `mount()`. Theoretically, you could do everything
Expand Down Expand Up @@ -55,7 +60,7 @@ export default function createMountWrapper(node, options = {}) {
}
}
WrapperComponent.propTypes = {
Component: PropTypes.oneOfType([PropTypes.func, PropTypes.string]).isRequired,
Component: PropTypes.oneOfType([PropTypes.func, PropTypes.string, specialType]).isRequired,
props: PropTypes.object.isRequired,
context: PropTypes.object,
};
Expand Down
48 changes: 47 additions & 1 deletion packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import { ITERATOR_SYMBOL, sym } from 'enzyme/build/Utils';

import './_helpers/setupAdapters';
import { createClass, createContext, createPortal } from './_helpers/react-compat';
import { createClass, createContext, createPortal, forwardRef } from './_helpers/react-compat';
import {
describeWithDOM,
describeIf,
Expand Down Expand Up @@ -181,6 +181,52 @@ describeWithDOM('mount', () => {
expect(wrapper.find('span').text()).to.equal('foo');
});

describeIf(REACT163, '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.called).to.equal(false);

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.length(1);
});

it('should find the forwardRef element itself', () => {
const testRef = () => {};
const OtherComponent = () => (
<div />
);
const SomeComponent = forwardRef((props, ref) => (
<OtherComponent className="dumb" ref={ref} />
));

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

expect(wrapper.find(SomeComponent)).to.have.length(1);
expect(wrapper.find(OtherComponent)).to.have.length(1);
});
});

describeIf(!REACT013, 'stateless components', () => {
it('can pass in context', () => {
const SimpleComponent = (props, context) => (
Expand Down
44 changes: 43 additions & 1 deletion packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { shallow, render, ShallowWrapper, mount } from 'enzyme';
import { ITERATOR_SYMBOL, withSetStateAllowed, sym } from 'enzyme/build/Utils';

import './_helpers/setupAdapters';
import { createClass, createContext } from './_helpers/react-compat';
import { createClass, createContext, forwardRef, createPortal } from './_helpers/react-compat';
import { describeIf, itIf, itWithData, generateEmptyRenderData } from './_helpers';
import { REACT013, REACT014, REACT15, REACT150_4, REACT16, REACT163, is } from './_helpers/version';

Expand Down Expand Up @@ -119,7 +119,19 @@ describe('shallow', () => {

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

itIf(REACT163, 'should find elements through forwardedRef 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(!REACT013, 'stateless function components', () => {
Expand Down Expand Up @@ -2945,6 +2957,36 @@ describe('shallow', () => {
it('should pass through to the debugNodes function', () => {
expect(shallow(<div />).debug()).to.equal('<div />');
});

itIf(REACT163, 'should handle internal types gracefully', () => {
const { Provider, Consumer } = createContext(null);
// eslint-disable-next-line prefer-arrow-callback
const Forwarded = forwardRef(function MyComponent(props) {
return (
<Provider value={5}>
<Forwarded />
<div {...props}>
<React.Fragment>
<Consumer>{() => <div />}</Consumer>
{createPortal(<span />, { nodeType: 1 })}
</React.Fragment>
</div>
</Provider>
);
});

expect(shallow(<Forwarded />).debug()).to.equal(`<ContextProvider value={5}>
<ForwardRef(MyComponent) />
<div>
<Fragment>
<ContextConsumer>
[function child]
</ContextConsumer>
<Portal />
</Fragment>
</div>
</ContextProvider>`);
});
});

describe('.html()', () => {
Expand Down
8 changes: 8 additions & 0 deletions packages/enzyme-test-suite/test/_helpers/react-compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ let createClass;
let renderToString;
let createPortal;
let createContext;
let forwardRef;

if (is('>=15.5 || ^16.0.0-alpha || ^16.3.0-alpha')) {
// eslint-disable-next-line import/no-extraneous-dependencies
Expand Down Expand Up @@ -37,9 +38,16 @@ if (is('^16.3.0-0')) {
createContext = null;
}

if (is('^16.3.0-0')) {
({ forwardRef } = require('react'));
} else {
forwardRef = null;
}

export {
createClass,
renderToString,
createPortal,
createContext,
forwardRef,
};
35 changes: 32 additions & 3 deletions packages/enzyme/src/Debug.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,41 @@ import {
propsOfNode,
childrenOfNode,
} from './RSTTraversal';
import {
typeOf,
AsyncMode,
ContextProvider,
ContextConsumer,
Element,
ForwardRef,
Fragment,
Portal,
StrictMode,
} from './Utils';

const booleanValue = Function.bind.call(Function.call, Boolean.prototype.valueOf);


export function typeName(node) {
return typeof node.type === 'function'
? (node.type.displayName || functionName(node.type) || 'Component')
: node.type;
const { type } = node;
switch (typeOf(node)) {
case AsyncMode: return 'AsyncMode';
case ContextProvider: return 'ContextProvider';
case ContextConsumer: return 'ContextConsumer';
case Portal: return 'Portal';
case StrictMode: return 'StrictMode';
case ForwardRef: {
const name = type.render.displayName || functionName(type.render);
return name ? `ForwardRef(${name})` : 'ForwardRef';
}
case Fragment:
return 'Fragment';
case Element:
default:
return typeof node.type === 'function'
? (type.displayName || functionName(type) || 'Component')
: type || 'unknown';
}
}

export function spaces(n) {
Expand Down Expand Up @@ -66,6 +94,7 @@ function indentChildren(childrenStrs, indentLength) {

export function debugNode(node, indentLength = 2, options = {}) {
if (typeof node === 'string' || typeof node === 'number') return escape(node);
if (typeof node === 'function') return '[function child]';
if (!node) return '';

const childrenStrs = compact(childrenOfNode(node).map(n => debugNode(n, indentLength, options)));
Expand Down
39 changes: 39 additions & 0 deletions packages/enzyme/src/Utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,47 @@ import configuration from './configuration';
import validateAdapter from './validateAdapter';
import { childrenOfNode } from './RSTTraversal';

const hasSymbol = typeof Symbol === 'function' && Symbol.for;

export const ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;

// TODO use react-is when a version is released with no peer dependency on React
export const AsyncMode = hasSymbol ? Symbol.for('react.async_mode') : 0xeacf;
export const ContextProvider = hasSymbol ? Symbol.for('react.provider') : 0xeacd;
export const ContextConsumer = hasSymbol ? Symbol.for('react.context') : 0xeace;
export const Element = hasSymbol ? Symbol.for('react.element') : 0xeac7;
export const ForwardRef = hasSymbol ? Symbol.for('react.forward_ref') : 0xead0;
export const Fragment = hasSymbol ? Symbol.for('react.fragment') : 0xeacb;
export const Portal = hasSymbol ? Symbol.for('react.portal') : 0xeaca;
export const StrictMode = hasSymbol ? Symbol.for('react.strict_mode') : 0xeacc;

export function typeOf(node) {
if (node !== null) {
const { type, $$typeof } = node;

switch (type || $$typeof) {
case AsyncMode:
case Fragment:
case StrictMode:
return type;
default: {
const $$typeofType = type && type.$$typeof;

switch ($$typeofType) {
case ContextConsumer:
case ForwardRef:
case Portal:
case ContextProvider:
return $$typeofType;
default:
return type || $$typeof;
}
}
}
}
return undefined;
}

export function getAdapter(options = {}) {
if (options.adapter) {
validateAdapter(options.adapter);
Expand Down
8 changes: 7 additions & 1 deletion packages/enzyme/src/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
childrenOfNode,
hasClassName,
} from './RSTTraversal';
import { nodeHasType, propsOfNode } from './Utils';
import { nodeHasType, propsOfNode, typeOf, ForwardRef } from './Utils';
// our CSS selector parser instance
const parser = createParser();

Expand Down Expand Up @@ -227,6 +227,12 @@ export function buildPredicate(selector) {
if (hasUndefinedValues) {
throw new TypeError('Enzyme::Props can’t have `undefined` values. Try using ‘findWhere()’ instead.');
}
// the selector could also be a forwardRef
if (typeOf(selector) === ForwardRef) {
// re-build the predicate based on what is wrapped by forwardRef
// rather than the forwardRef itself
return buildPredicate(selector.render().props);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is the right way to do this but maybe i'm misunderstanding. We should copy waht react-test-renderer does and this code should go in the react-16-adapter

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I'm not sure either. Maybe the root cause of this is that enzyme is not generating a corresponding element for forwardRef and we shouldn't defer searching to the next level here. I'll look into react-test-renderer.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the selector module is used for all adapters, so the node type should be opaque in this module.

}
return node => nodeMatchesObjectProps(node, selector);
}
throw new TypeError('Enzyme::Selector does not support an array, null, or empty object as a selector');
Expand Down