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

Add ability to dive() and shallow-render-as-root Consumers and Providers #1966

Merged
merged 3 commits into from Apr 4, 2019
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
51 changes: 49 additions & 2 deletions packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js
Expand Up @@ -15,6 +15,8 @@ import {
Element,
ForwardRef,
Fragment,
isContextConsumer,
isContextProvider,
isElement,
isForwardRef,
isPortal,
Expand Down Expand Up @@ -241,6 +243,18 @@ function nodeToHostNode(_node) {
return mapper(node);
}

function getProviderDefaultValue(Provider) {
// React stores references to the Provider's defaultValue differently across versions.
if ('_defaultValue' in Provider._context) {
return Provider._context._defaultValue;
}
throw new Error('Enzyme Internal Error: can’t figure out how to get Provider’s default value');
}

function makeFakeElement(type) {
return { $$typeof: Element, type };
}

const eventOptions = { animation: true };

class ReactSixteenThreeAdapter extends EnzymeAdapter {
Expand Down Expand Up @@ -357,11 +371,30 @@ class ReactSixteenThreeAdapter extends EnzymeAdapter {
let isDOM = false;
let cachedNode = null;
return {
render(el, context) {
render(el, context, {
providerValues = new Map(),
} = {}) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd like to make the third argument an object. This way we can easily pass additional params as enzyme/react evolves without introducing breaking changes.

cachedNode = el;
/* eslint consistent-return: 0 */
if (typeof el.type === 'string') {
isDOM = true;
} else if (isContextProvider(el)) {
providerValues.set(el.type, el.props.value);
const MockProvider = Object.assign(
props => props.children,
el.type,
);
return withSetStateAllowed(() => renderer.render({ ...el, type: MockProvider }));
} else if (isContextConsumer(el)) {
const Provider = adapter.getProviderFromConsumer(el.type);
const value = providerValues.has(Provider)
? providerValues.get(Provider)
: getProviderDefaultValue(Provider);
const MockConsumer = Object.assign(
props => props.children(value),
el.type,
);
return withSetStateAllowed(() => renderer.render({ ...el, type: MockConsumer }));
} else {
isDOM = false;
const { type: Component } = el;
Expand Down Expand Up @@ -539,20 +572,34 @@ class ReactSixteenThreeAdapter extends EnzymeAdapter {
}

isCustomComponent(type) {
const fakeElement = { $$typeof: Element, type };
const fakeElement = makeFakeElement(type);
return !!type && (
typeof type === 'function'
|| isForwardRef(fakeElement)
|| isContextProvider(fakeElement)
|| isContextConsumer(fakeElement)
);
}

isContextConsumer(type) {
return !!type && isContextConsumer(makeFakeElement(type));
}

isCustomComponentElement(inst) {
if (!inst || !this.isValidElement(inst)) {
return false;
}
return this.isCustomComponent(inst.type);
}

getProviderFromConsumer(Consumer) {
const { Provider } = Consumer || {};
if (Provider) {
return Provider;
}
throw new Error('Enzyme Internal Error: can’t figure out how to get Provider from Consumer');
}

createElement(...args) {
return React.createElement(...args);
}
Expand Down
62 changes: 60 additions & 2 deletions packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js
Expand Up @@ -18,6 +18,8 @@ import {
Element,
ForwardRef,
Fragment,
isContextConsumer,
isContextProvider,
isElement,
isForwardRef,
isMemo,
Expand Down Expand Up @@ -303,6 +305,21 @@ function wrapAct(fn) {
return returnVal;
}

function getProviderDefaultValue(Provider) {
// React stores references to the Provider's defaultValue differently across versions.
if ('_defaultValue' in Provider._context) {
return Provider._context._defaultValue;
}
if ('_currentValue' in Provider._context) {
return Provider._context._currentValue;
}
throw new Error('Enzyme Internal Error: can’t figure out how to get Provider’s default value');
}

function makeFakeElement(type) {
return { $$typeof: Element, type };
}

class ReactSixteenAdapter extends EnzymeAdapter {
constructor() {
super();
Expand Down Expand Up @@ -454,11 +471,30 @@ class ReactSixteenAdapter extends EnzymeAdapter {
};

return {
render(el, unmaskedContext) {
render(el, unmaskedContext, {
providerValues = new Map(),
} = {}) {
cachedNode = el;
/* eslint consistent-return: 0 */
if (typeof el.type === 'string') {
isDOM = true;
} else if (isContextProvider(el)) {
providerValues.set(el.type, el.props.value);
const MockProvider = Object.assign(
props => props.children,
el.type,
);
return withSetStateAllowed(() => renderer.render({ ...el, type: MockProvider }));
} else if (isContextConsumer(el)) {
const Provider = adapter.getProviderFromConsumer(el.type);
const value = providerValues.has(Provider)
? providerValues.get(Provider)
: getProviderDefaultValue(Provider);
const MockConsumer = Object.assign(
props => props.children(value),
el.type,
);
return withSetStateAllowed(() => renderer.render({ ...el, type: MockConsumer }));
} else {
isDOM = false;
const { type: Component } = el;
Expand Down Expand Up @@ -673,20 +709,42 @@ class ReactSixteenAdapter extends EnzymeAdapter {
}

isCustomComponent(type) {
const fakeElement = { $$typeof: Element, type };
const fakeElement = makeFakeElement(type);
return !!type && (
typeof type === 'function'
|| isForwardRef(fakeElement)
|| isContextProvider(fakeElement)
|| isContextConsumer(fakeElement)
);
}

isContextConsumer(type) {
return !!type && isContextConsumer(makeFakeElement(type));
}

isCustomComponentElement(inst) {
if (!inst || !this.isValidElement(inst)) {
return false;
}
return this.isCustomComponent(inst.type);
}

getProviderFromConsumer(Consumer) {
// React stores references to the Provider on a Consumer differently across versions.
if (Consumer) {
let Provider;
if (Consumer.Provider) {
({ Provider } = Consumer);
} else if (Consumer._context) {
({ Provider } = Consumer._context);
}
if (Provider) {
return Provider;
}
}
throw new Error('Enzyme Internal Error: can’t figure out how to get Provider from Consumer');
}

createElement(...args) {
return React.createElement(...args);
}
Expand Down
35 changes: 34 additions & 1 deletion packages/enzyme-test-suite/test/Adapter-spec.jsx
Expand Up @@ -1169,7 +1169,40 @@ Warning: Failed Adapter-spec type: Invalid Adapter-spec \`foo\` of type \`string
});

itIf(is('>=16.3'), 'returns true for forward refs', () => {
expect(adapter.isCustomComponent(React.forwardRef(() => null))).to.equal(true);
expect(adapter.isCustomComponent(forwardRef(() => null))).to.equal(true);
});
});

describeIf(is('>= 16.3'), 'isContextConsumer(type)', () => {
it('returns true for createContext() Consumers', () => {
expect(adapter.isContextConsumer(createContext().Consumer)).to.equal(true);
});

it('returns false for everything else', () => {
expect(adapter.isContextConsumer(null)).to.equal(false);
expect(adapter.isContextConsumer(true)).to.equal(false);
expect(adapter.isContextConsumer(undefined)).to.equal(false);
expect(adapter.isContextConsumer(false)).to.equal(false);
expect(adapter.isContextConsumer(() => <div />)).to.equal(false);
expect(adapter.isContextConsumer(forwardRef(() => null))).to.equal(false);
expect(adapter.isContextConsumer(createContext().Provider)).to.equal(false);
});
});

describeIf(is('>= 16.3'), 'getProviderFromConsumer(Consumer)', () => {
it('gets a createContext() Provider from a Consumer', () => {
const Context = createContext();

expect(adapter.getProviderFromConsumer(Context.Consumer)).to.equal(Context.Provider);
});

it('throws an internal error if something that is not a Consumer is passed', () => {
expect(() => adapter.getProviderFromConsumer(null)).to.throw(
'Enzyme Internal Error: can’t figure out how to get Provider from Consumer',
);
expect(() => adapter.getProviderFromConsumer({})).to.throw(
'Enzyme Internal Error: can’t figure out how to get Provider from Consumer',
);
});
});
});
97 changes: 87 additions & 10 deletions packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
Expand Up @@ -261,6 +261,53 @@ describeWithDOM('mount', () => {
expect(wrapper.text()).to.equal('Context says: I can be set!');
});

describeIf(is('>= 16.3'), 'with createContext()', () => {
let Context1;
let Context2;

function WrappingComponent(props) {
const { value1, value2, children } = props;
return (
<Context1.Provider value={value1}>
<Context2.Provider value={value2}>
{children}
</Context2.Provider>
</Context1.Provider>
);
}

function Component() {
return (
<Context1.Consumer>
{value1 => (
<Context2.Consumer>
{value2 => (
<div>Value 1: {value1}; Value 2: {value2}</div>
)}
</Context2.Consumer>
)}
</Context1.Consumer>
);
}

beforeEach(() => {
Context1 = createContext('default1');
Context2 = createContext('default2');
});

it('renders', () => {
const wrapper = mount(<Component />, {
wrappingComponent: WrappingComponent,
wrappingComponentProps: {
value1: 'one',
value2: 'two',
},
});

expect(wrapper.text()).to.equal('Value 1: one; Value 2: two');
});
});

it('throws an error if the wrappingComponent does not render its children', () => {
class BadWrapper extends React.Component {
render() {
Expand Down Expand Up @@ -437,20 +484,50 @@ describeWithDOM('mount', () => {
expect(wrapper.context('name')).to.equal(context.name);
});

itIf(is('>= 16.3'), 'finds elements through Context elements', () => {
const { Provider, Consumer } = createContext('');
describeIf(is('>= 16.3'), 'createContext()', () => {
let Context;

class Foo extends React.Component {
render() {
return (
<Consumer>{value => <span>{value}</span>}</Consumer>
);
beforeEach(() => {
Context = createContext('hello');
});

it('finds elements through Context elements', () => {
class Foo extends React.Component {
render() {
return (
<Context.Consumer>{value => <span>{value}</span>}</Context.Consumer>
);
}
}
}

const wrapper = mount(<Provider value="foo"><div><Foo /></div></Provider>);
const wrapper = mount(<Context.Provider value="foo"><div><Foo /></div></Context.Provider>);

expect(wrapper.find('span').text()).to.equal('foo');
});

expect(wrapper.find('span').text()).to.equal('foo');
it('can render a <Provider /> as the root', () => {
const wrapper = mount(
<Context.Provider value="cool">
<Context.Consumer>{value => <div>{value}</div>}</Context.Consumer>
</Context.Provider>,
);
expect(wrapper.text()).to.equal('cool');

wrapper.setProps({ value: 'test' });
expect(wrapper.text()).to.equal('test');
});

it('can render a <Consumer /> as the root', () => {
const wrapper = mount(
<Context.Consumer>{value => <div>{value}</div>}</Context.Consumer>,
);
expect(wrapper.text()).to.equal('hello');

wrapper.setProps({
children: value => <div>Value is: {value}</div>,
});
expect(wrapper.text()).to.equal('Value is: hello');
});
});

describeIf(is('>= 16.3'), 'forwarded ref Components', () => {
Expand Down