Skip to content

Commit

Permalink
Merge pull request #1966 from trialspark/josh__dive-on-new-context
Browse files Browse the repository at this point in the history
[New] Add ability to dive() and shallow-render-as-root Consumers and Providers
  • Loading branch information
ljharb committed Apr 4, 2019
2 parents 8254e90 + 7360912 commit 2420e2b
Show file tree
Hide file tree
Showing 7 changed files with 494 additions and 31 deletions.
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(),
} = {}) {
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

0 comments on commit 2420e2b

Please sign in to comment.