diff --git a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
index 831f711bb..c3e736e33 100644
--- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
+++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
@@ -270,6 +270,54 @@ describe('shallow', () => {
expect(wrapper.text()).to.equal('Context says: I can be set!');
});
+ describeIf(is('>= 16.3'), 'with createContext()', () => {
+ let Context1;
+ let Context2;
+ beforeEach(() => {
+ Context1 = createContext('default1');
+ Context2 = createContext('default2');
+ });
+
+ function WrappingComponent(props) {
+ const { value1, value2, children } = props;
+ return (
+
+
+ {children}
+
+
+ );
+ }
+
+ function Component() {
+ return (
+
+ {value1 => (
+
+ {value2 => (
+ Value 1: {value1}; Value 2: {value2}
+ )}
+
+ )}
+
+ );
+ }
+
+ it('renders', () => {
+ const wrapper = shallow(, {
+ wrappingComponent: WrappingComponent,
+ wrappingComponentProps: {
+ value1: 'one',
+ value2: 'two',
+ },
+ });
+ const consumer1 = wrapper.find(Context1.Consumer).dive();
+ const consumer2 = consumer1.find(Context2.Consumer).dive();
+
+ expect(consumer2.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() {
@@ -415,6 +463,145 @@ describe('shallow', () => {
expect(wrapper.find('.child2')).to.have.lengthOf(1);
});
+ describeIf(is('>= 16.3'), 'createContext()', () => {
+ describe('rendering as root:', () => {
+ let Context;
+
+ beforeEach(() => {
+ Context = createContext('cool');
+ });
+
+ describe('', () => {
+ it('can be rendered as the root', () => {
+ const wrapper = shallow(
+
+
+ {value => {value}
}
+
+ ,
+ );
+ expect(wrapper.debug()).to.eql(`
+
+ [function]
+
+ `.trim());
+ });
+
+ it('supports changing the value', () => {
+ const wrapper = shallow(
+
+
+ {value => {value}
}
+
+ ,
+ );
+ wrapper.setProps({ value: 'world' });
+ expect(wrapper.find(Context.Consumer).dive().text()).to.eql('world');
+ });
+ });
+
+ describe('', () => {
+ function DivRenderer({ children }) {
+ return
{children}
;
+ }
+ it('can be rendered as the root', () => {
+ const wrapper = shallow(
+
+ {value => {value}}
+ ,
+ );
+ expect(wrapper.debug()).to.eql(`
+
+ cool
+
+ `.trim());
+ });
+
+ it('supports changing the children', () => {
+ const wrapper = shallow(
+
+ {value => {value}}
+ ,
+ );
+ wrapper.setProps({ children: value => Changed: {value} });
+ expect(wrapper.find(DivRenderer).dive().text()).to.eql('Changed: cool');
+ });
+ });
+ });
+
+ describe('dive() on Provider and Consumer', () => {
+ let Provider;
+ let Consumer;
+
+ beforeEach(() => {
+ ({ Provider, Consumer } = React.createContext('howdy!'));
+ });
+
+ class Consumes extends React.Component {
+ render() {
+ return (
+
+ {value => {value}}
+
+ );
+ }
+ }
+
+ class Provides extends React.Component {
+ render() {
+ const { children } = this.props;
+
+ return (
+
+ );
+ }
+ }
+
+ class MyComponent extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+ }
+
+ it('works on a Provider', () => {
+ const wrapper = shallow();
+ const provides = wrapper.find(Provides).dive();
+ const provider = provides.find(Provider).dive();
+ expect(provider.text()).to.equal('');
+ });
+
+ it('always gives the default provider value if dive()ing directly to a ', () => {
+ // Diving directly on a consumer will give you the default value
+ const wrapper = shallow();
+ const consumes = wrapper.find(Consumes).dive();
+ const consumer = consumes.find(Consumer).dive();
+ expect(consumer.text()).to.equal('howdy!');
+ });
+
+ it('gives the actual value if one dive()s it', () => {
+ const wrapper = shallow();
+ const provides = wrapper.find(Provides).dive();
+ const provider = provides.find(Provider).dive();
+ const consumes = provider.find(Consumes).dive();
+ const consumer = consumes.find(Consumer).dive();
+ expect(consumer.text()).to.equal('foo');
+ });
+
+ it('does not leak values across roots', () => {
+ const wrapper = shallow();
+ const provides = wrapper.find(Provides).dive();
+ const provider = provides.find(Provider).dive();
+ expect(provider).to.have.lengthOf(1);
+
+ const consumes = wrapper.find(Consumes).dive();
+ const consumer = consumes.find(Consumer).dive();
+ expect(consumer.text()).to.equal('howdy!');
+ });
+ });
+ });
+
describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
it('can pass in context', () => {
const SimpleComponent = (props, context) => (
diff --git a/packages/enzyme-test-suite/test/shared/methods/getWrappingComponent.jsx b/packages/enzyme-test-suite/test/shared/methods/getWrappingComponent.jsx
index b90555c1a..c9e37f5f4 100644
--- a/packages/enzyme-test-suite/test/shared/methods/getWrappingComponent.jsx
+++ b/packages/enzyme-test-suite/test/shared/methods/getWrappingComponent.jsx
@@ -183,6 +183,31 @@ export default function describeGetWrappingComponent({
}
});
+ itIf(is('>= 16.3'), 'updates a if it is rendered as root', () => {
+ const Context = React.createContext();
+ function WrappingComponent(props) {
+ const { value, children } = props;
+ return (
+
+ {children}
+
+ );
+ }
+ const wrapper = Wrap((
+
+ {value => {value}
}
+
+ ), {
+ wrappingComponent: WrappingComponent,
+ wrappingComponentProps: { value: 'hello!' },
+ });
+ const wrappingComponent = wrapper.getWrappingComponent();
+ expect(wrapper.text()).to.equal('hello!');
+
+ wrappingComponent.setProps({ value: 'goodbye!' });
+ expect(wrapper.text()).to.equal('goodbye!');
+ });
+
itIf(!isShallow, 'handles a partial prop update', () => {
const wrapper = Wrap(, {
wrappingComponent: MyWrappingComponent,
diff --git a/packages/enzyme/src/ShallowWrapper.js b/packages/enzyme/src/ShallowWrapper.js
index 4058847fc..c358fac71 100644
--- a/packages/enzyme/src/ShallowWrapper.js
+++ b/packages/enzyme/src/ShallowWrapper.js
@@ -45,6 +45,7 @@ const CHILD_CONTEXT = sym('__childContext__');
const WRAPPING_COMPONENT = sym('__wrappingComponent__');
const PRIMARY_WRAPPER = sym('__primaryWrapper__');
const ROOT_FINDER = sym('__rootFinder__');
+const PROVIDER_VALUES = sym('__providerValues__');
/**
* Finds all nodes in the current wrapper nodes' render trees that match the provided predicate
@@ -313,14 +314,18 @@ function deepRender(wrapper, target, adapter) {
* @param {WrappingComponentWrapper} wrapper The `WrappingComponentWrapper` for a
* `wrappingComponent`
* @param {Adapter} adapter An Enzyme adapter
- * @returns {object} The context collected
+ * @returns {object} An object containing an object of legacy context values and a Map of
+ * `createContext()` Provider values.
*/
function getContextFromWrappingComponent(wrapper, adapter) {
const rootFinder = deepRender(wrapper, wrapper[ROOT_FINDER], adapter);
if (!rootFinder) {
throw new Error('`wrappingComponent` must render its children!');
}
- return rootFinder[OPTIONS].context;
+ return {
+ legacyContext: rootFinder[OPTIONS].context,
+ providerValues: rootFinder[PROVIDER_VALUES],
+ };
}
/**
@@ -338,6 +343,7 @@ function getContextFromWrappingComponent(wrapper, adapter) {
function makeShallowOptions(nodes, root, passedOptions, wrapper) {
const options = makeOptions(passedOptions);
const adapter = getAdapter(passedOptions);
+ privateSet(options, PROVIDER_VALUES, passedOptions[PROVIDER_VALUES]);
if (root || !isCustomComponent(options.wrappingComponent, adapter)) {
return options;
}
@@ -347,16 +353,18 @@ function makeShallowOptions(nodes, root, passedOptions, wrapper) {
const { node: wrappedNode, RootFinder } = adapter.wrapWithWrappingComponent(nodes, options);
// eslint-disable-next-line no-use-before-define
const wrappingComponent = new WrappingComponentWrapper(wrappedNode, wrapper, RootFinder);
- const wrappingComponentContext = getContextFromWrappingComponent(
- wrappingComponent, adapter,
- );
+ const {
+ legacyContext: wrappingComponentLegacyContext,
+ providerValues: wrappingComponentProviderValues,
+ } = getContextFromWrappingComponent(wrappingComponent, adapter);
privateSet(wrapper, WRAPPING_COMPONENT, wrappingComponent);
return {
...options,
context: {
...options.context,
- ...wrappingComponentContext,
+ ...wrappingComponentLegacyContext,
},
+ [PROVIDER_VALUES]: wrappingComponentProviderValues,
};
}
@@ -385,10 +393,12 @@ class ShallowWrapper {
privateSet(this, UNRENDERED, nodes);
const renderer = adapter.createRenderer({ mode: 'shallow', ...options });
privateSet(this, RENDERER, renderer);
- this[RENDERER].render(nodes, options.context);
+ const providerValues = new Map(options[PROVIDER_VALUES] || []);
+ this[RENDERER].render(nodes, options.context, { providerValues });
const renderedNode = this[RENDERER].getNode();
privateSetNodes(this, getRootNode(renderedNode));
privateSet(this, OPTIONS, options);
+ privateSet(this, PROVIDER_VALUES, providerValues);
const { instance } = renderedNode;
if (instance && !options.disableLifecycleMethods) {
@@ -415,6 +425,7 @@ class ShallowWrapper {
privateSetNodes(this, nodes);
privateSet(this, OPTIONS, root[OPTIONS]);
privateSet(this, ROOT_NODES, root[NODES]);
+ privateSet(this, PROVIDER_VALUES, null);
}
}
@@ -612,7 +623,9 @@ class ShallowWrapper {
);
}
if (props) this[UNRENDERED] = cloneElement(adapter, this[UNRENDERED], props);
- this[RENDERER].render(this[UNRENDERED], nextContext);
+ this[RENDERER].render(this[UNRENDERED], nextContext, {
+ providerValues: this[PROVIDER_VALUES],
+ });
if (shouldComponentUpdateSpy) {
shouldRender = shouldComponentUpdateSpy.getLastReturnValue();
shouldComponentUpdateSpy.restore();
@@ -1659,14 +1672,16 @@ class ShallowWrapper {
if (!isCustomComponentElement(el, adapter)) {
throw new TypeError(`ShallowWrapper::${name}() can only be called on components`);
}
- return this.wrap(el, null, {
+ const childOptions = {
...this[OPTIONS],
...options,
context: options.context || {
...this[OPTIONS].context,
...this[ROOT][CHILD_CONTEXT],
},
- });
+ };
+ privateSet(childOptions, PROVIDER_VALUES, this[ROOT][PROVIDER_VALUES]);
+ return this.wrap(el, null, childOptions);
});
}
@@ -1686,14 +1701,35 @@ class ShallowWrapper {
* `wrappingComponent` re-renders.
*/
function updatePrimaryRootContext(wrappingComponent) {
- const context = getContextFromWrappingComponent(
- wrappingComponent,
- getAdapter(wrappingComponent[OPTIONS]),
- );
- wrappingComponent[PRIMARY_WRAPPER].setContext({
+ const adapter = getAdapter(wrappingComponent[OPTIONS]);
+ const primaryWrapper = wrappingComponent[PRIMARY_WRAPPER];
+ const primaryRenderer = primaryWrapper[RENDERER];
+ const primaryNode = primaryRenderer.getNode();
+ const {
+ legacyContext,
+ providerValues,
+ } = getContextFromWrappingComponent(wrappingComponent, adapter);
+ const prevProviderValues = primaryWrapper[PROVIDER_VALUES];
+
+ primaryWrapper.setContext({
...wrappingComponent[PRIMARY_WRAPPER][OPTIONS].context,
- ...context,
+ ...legacyContext,
});
+ primaryWrapper[PROVIDER_VALUES] = new Map([...prevProviderValues, ...providerValues]);
+
+ if (typeof adapter.isContextConsumer === 'function' && adapter.isContextConsumer(primaryNode.type)) {
+ const Consumer = primaryNode.type;
+ // Adapters with an `isContextConsumer` method will definitely have a `getProviderFromConsumer`
+ // method.
+ const Provider = adapter.getProviderFromConsumer(Consumer);
+ const newValue = providerValues.get(Provider);
+ const oldValue = prevProviderValues.get(Provider);
+
+ // Use referential comparison like React
+ if (newValue !== oldValue) {
+ primaryWrapper.rerender();
+ }
+ }
}
/**