enzyme
enzyme copied to clipboard
useContext hook not working with shallow
The useContext hook does not work with shallow rendering and passing in context. Are there any workarounds for now?
Current behavior
The value returned by useContext in a function component appears to be empty when using shallow and passing in a context argument. (The same problem also occurs when wrapping the component in the appropriate context Provider directly and then calling .dive()).
Here is a minimal test case, also reproduced at https://codesandbox.io/s/nice-blackwell-yz7tn in the index.spec.js file.
import React, { useContext } from "react";
import PropTypes from "prop-types";
import Enzyme, { shallow } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
Enzyme.configure({ adapter: new Adapter() });
const MyContext = React.createContext({});
export const MyComponent = () => {
const { myVal } = useContext(MyContext);
return <div data-test="my-component">{myVal}</div>;
};
it("renders the correct text", () => {
MyComponent.contextTypes = {
myVal: PropTypes.any
};
const wrapper = shallow(
<MyComponent />,
{context: {myVal: 'foobar'}}
);
expect(wrapper.text()).toEqual("foobar"); // expected "foobar" received ""
});
Expected behavior
The text in the example above is expected to be "foobar", but it's actually "".
In general, the value returned from useContext appears to be undefined.
Note that using mount instead of shallow causes the test to pass.
Also note: the codesandbox above has a second file (class.spec.js) in which a hack is employed that makes the test pass, which uses the legacy contextTypes. But this only appears to work with classes and this.context, not with useContext.
Your environment
enzyme 3.10.0 enzyme-adapter-react-16 1.14.0 react 16.8.6 react-dom 16.8.6
API
- [x] shallow
- [ ] mount
- [ ] render
Version
| library | version |
|---|---|
| enzyme | |
| react | |
| react-dom | |
| react-test-renderer | |
| adapter (below) |
Adapter
- [x] enzyme-adapter-react-16
- [ ] enzyme-adapter-react-16.3
- [ ] enzyme-adapter-react-16.2
- [ ] enzyme-adapter-react-16.1
- [ ] enzyme-adapter-react-15
- [ ] enzyme-adapter-react-15.4
- [ ] enzyme-adapter-react-14
- [ ] enzyme-adapter-react-13
- [ ] enzyme-adapter-react-helper
- [ ] others ( )
enzyme's context options only work with "legacy" context; I believe useContext uses a different mechanism. React, and react's shallow renderer, don't seem to provide any way for us to hook into hooks.
I also am having this problem, and would MEGA appreciate any workarounds or fixes that might come :)
for the sake of explicitness, here's an example of directly using a context provider and calling .dive(), which also doesn't work:
https://codesandbox.io/s/elastic-zhukovsky-w5pjc
index.spec.js
import React, { useContext } from "react";
import Enzyme, { shallow } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
Enzyme.configure({ adapter: new Adapter() });
const MyContext = React.createContext({});
export const MyComponent = () => {
const { myVal } = useContext(MyContext);
return <div data-test="my-component">{myVal}</div>;
};
it("renders the correct text", () => {
const wrapper = shallow(
<MyContext.Provider value={{ myVal: "foobar" }}>
<MyComponent />
</MyContext.Provider>
).dive();
expect(wrapper.text()).toEqual("foobar"); // expected "foobar" received ""
});
@ljharb is the reason why this variation also doesn't work the same as you mentioned above?
It'd be because .dive() needs to access the context in order to forward it along. However, if you try this, it might work:
it("renders the correct text", () => {
const wrapper = shallow(
<MyComponent />,
{
wrappingComponent: MyContext.Provider,
wrappingComponentProps: { value: { myVal: 'foobar' }
}
);
expect(wrapper.text()).toEqual("foobar"); // expected "foobar" received ""
});
Unfortunately this also results in the context value being undefined.
https://codesandbox.io/s/amazing-noether-bv6u7
import React, { useContext } from "react";
import Enzyme, { shallow } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
Enzyme.configure({ adapter: new Adapter() });
const MyContext = React.createContext({});
export const MyComponent = () => {
const { myVal } = useContext(MyContext);
return <div data-test="my-component">{myVal}</div>;
};
it("renders the correct text", () => {
const wrapper = shallow(<MyComponent />, {
wrappingComponent: MyContext.Provider,
wrappingComponentProps: { value: { myVal: "foobar" } }
});
console.log(wrapper.debug()); // <div data-test="my-component" />
console.log(wrapper.text()); // ""
});
shallow()just use wrappingComponentoption in makeShallowOptions() but not in adapter.createRenderer.So it just use wrappingComponent to create context param of option but never use wrappingComponent to wrap the node.
The evidence is that if you use snapshot test, you will not find wrappingComponent in the result.
ReactSixteenAdapter.createShallowRenderer() use react-test-renderer/shallow to create real component.
createShallowRenderer(options = {}) {
const adapter = this;
const renderer = new ShallowRenderer();
...
return {
render(el, unmaskedContext, {
providerValues = new Map(),
} = {}) {
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 }));
}
...
},
...
}
}
Maybe createShallowRenderer() could use wrappingComponent to wrap the result of withSetStateAllowed()?
The workaround I'm using for now is the one described in this article; wrap useContext in a function and then use jest to mock the implementation.
https://codesandbox.io/s/crimson-mountain-vo7sk
MyContext.js
import React, { useContext } from "react";
const MyContext = React.createContext({});
export default MyContext;
export const useMyContext = () => useContext(MyContext);
index.spec.js
import React from "react";
import Enzyme, { shallow } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import MyContext, { useMyContext } from "./MyContext";
import * as MyContextModule from "./MyContext";
Enzyme.configure({ adapter: new Adapter() });
export const MyComponent = () => {
const { myVal } = useMyContext(); // instead of useContext(MyContext)
return <div data-test="my-component">{myVal}</div>;
};
it("renders the correct text", () => {
jest.spyOn(MyContextModule, "useMyContext").mockImplementation(() => ({
myVal: "foobar"
}));
const wrapper = shallow(
<MyContext.Provider>
<MyComponent />
</MyContext.Provider>
).dive();
expect(wrapper.text()).toEqual("foobar");
});
I'm having same issue with using react-redux 7.1, but I can't change the way how react-redux use the useContext(). Is there any other workaround?
The workaround I'm using for now is the one described in this article; wrap
useContextin a function and then usejestto mock the implementation.https://codesandbox.io/s/crimson-mountain-vo7sk
MyContext.jsimport React, { useContext } from "react"; const MyContext = React.createContext({}); export default MyContext; export const useMyContext = () => useContext(MyContext);
index.spec.jsimport React from "react"; import Enzyme, { shallow } from "enzyme"; import Adapter from "enzyme-adapter-react-16"; import MyContext, { useMyContext } from "./MyContext"; import * as MyContextModule from "./MyContext"; Enzyme.configure({ adapter: new Adapter() }); export const MyComponent = () => { const { myVal } = useMyContext(); // instead of useContext(MyContext) return <div data-test="my-component">{myVal}</div>; }; it("renders the correct text", () => { jest.spyOn(MyContextModule, "useMyContext").mockImplementation(() => ({ myVal: "foobar" })); const wrapper = shallow( <MyContext.Provider> <MyComponent /> </MyContext.Provider> ).dive(); expect(wrapper.text()).toEqual("foobar"); });
I'm having same issue with using react-redux 7.1, but I can't change the way how react-redux use the useContext(). Is there any other workaround?
The workaround I'm using for now is the one described in this article; wrap
useContextin a function and then usejestto mock the implementation. https://codesandbox.io/s/crimson-mountain-vo7skMyContext.jsimport React, { useContext } from "react"; const MyContext = React.createContext({}); export default MyContext; export const useMyContext = () => useContext(MyContext);
index.spec.jsimport React from "react"; import Enzyme, { shallow } from "enzyme"; import Adapter from "enzyme-adapter-react-16"; import MyContext, { useMyContext } from "./MyContext"; import * as MyContextModule from "./MyContext"; Enzyme.configure({ adapter: new Adapter() }); export const MyComponent = () => { const { myVal } = useMyContext(); // instead of useContext(MyContext) return <div data-test="my-component">{myVal}</div>; }; it("renders the correct text", () => { jest.spyOn(MyContextModule, "useMyContext").mockImplementation(() => ({ myVal: "foobar" })); const wrapper = shallow( <MyContext.Provider> <MyComponent /> </MyContext.Provider> ).dive(); expect(wrapper.text()).toEqual("foobar"); });
I have the same problem. Did you solve it?
This is broken until https://github.com/facebook/react/pull/15589 gets though. As hooks do not really work in shallow without it.
that react PR is only for getting useEffect to work. It doesn't address context, though something similar may be doable.
I'm having same issue with using react-redux 7.1, but I can't change the way how react-redux use the useContext(). Is there any other workaround?
The workaround I'm using for now is the one described in this article; wrap
useContextin a function and then usejestto mock the implementation. https://codesandbox.io/s/crimson-mountain-vo7skMyContext.jsimport React, { useContext } from "react"; const MyContext = React.createContext({}); export default MyContext; export const useMyContext = () => useContext(MyContext);
index.spec.jsimport React from "react"; import Enzyme, { shallow } from "enzyme"; import Adapter from "enzyme-adapter-react-16"; import MyContext, { useMyContext } from "./MyContext"; import * as MyContextModule from "./MyContext"; Enzyme.configure({ adapter: new Adapter() }); export const MyComponent = () => { const { myVal } = useMyContext(); // instead of useContext(MyContext) return <div data-test="my-component">{myVal}</div>; }; it("renders the correct text", () => { jest.spyOn(MyContextModule, "useMyContext").mockImplementation(() => ({ myVal: "foobar" })); const wrapper = shallow( <MyContext.Provider> <MyComponent /> </MyContext.Provider> ).dive(); expect(wrapper.text()).toEqual("foobar"); });
I have the same problem with using styled-components@next (v5) :'(
Same problem (I think?) here with react-intl’s useIntl
@lensbart all custom hooks are built on top of builtin hooks; useIntl presumably uses useContext.
Yes it does
@ljharb Is there something more complex preventing this? Or is it just nobody has stepped up to do the work?
Would something similar to https://github.com/facebook/react/pull/16168 fix this? If you could point me in the right direction, I'm happy to make the change. I am not well acquainted with React shallow renderer internals though, so any advice would be appreciated.
I found some workarounds for shallow rendering with React.useContext, Material UI's makeStyles, and react-redux v7.x.x. See below for some high level notes and my codesandbox for examples.
Components that consume context using React.useContext()
- use
just.spyOn(React, 'useContext').mockImplementation((context) => 'context_value' )to return a context value. (Thanks to @mstorus's example above for the idea of mocking module implementations). @garyyeap, you can do this to mock useContext for code you don't control, but that won't help you with react-redux 7.1. See the Redux section below for the redux workaround. - Wrapping your component in
<Context.Provider value={myContextValue}/>does not work. - Using the wrappingComponent option does not work.
Components that consume context using <Context.Consumer />
- Wrapping your component in
<Context.Provider value={myContextValue}/>works. - Using the wrappingComponent option works.
Material UI's makeStyles
- Uses the React.useContext() hook. You'll have to mock return values for React.useContext() for both ThemeContext and StylesContext to get this to work. Yeah, this is pretty gross, but it's all I got.
Redux - Redux has some fancy logic for using context. It looks like you can optionally pass the store or a custom context into a connected component.
- Inject store into connected component. E.G.
<MyConnectedComponent store={mockStore}/> - Inject custom context into connected component. E.G.
<MyConnectedComponent context={MockReduxContext}/>
It's pretty disappointing that shallow rendering's been broken for so long and that our options are limited to switching to mount or finding brittle workarounds like these.
@Alphy11 yes - the change either needs to be made directly in react's shallow renderer, or, enzyme would need to rebuild from scratch react's entire hooks system. Certainly someone submitting a PR to either react or enzyme would be great, but it's not simple, fast, or straightforward.
For me, just.spyOn(React, 'useContext').mockImplementation(() => {...} ) will work if my component has const ctx = React.useContext(MyContext) but not with const ctx = useContext(MyContext).
@twharmon, try this for named exports.
E.G.
import * as ReactAll from 'react';
// React is ReactAll.default
// useContext is ReactAll.useContext
jest.spyOn(ReactAll, 'useContext').mockImplementation(() => {...}
I spent a long time trying a bunch of things because the docs for shallow made it sound like wrappingComponent would work. CodeSandbox of my attempts
The docs for shallow say:
options.wrappingComponent: (ComponentType [optional]): A component that will render as a parent of the node. It can be used to provide context to the node, among other things. See the getWrappingComponent() docs for an example. Note: wrappingComponent must render its children.
In an app context, rendering a Context.Provider as the parent of a node would make the provided values available to any useContext hooks in the child node. But this isn't the case with shallow. The linked getWrappingComponent docs show that this works if the provider is a react-redux Provider - what's different about that vs. a React Context Provider?
Is there a better way the docs could describe what providing wrappingComponent will actually do, so others don't run into this same thing?
useContext isn't the same as actual React context; the wrappingComponent approach works for this.context, or an FC's context arg, not for useContext.
@ljharb yes, you know that from looking at React internals, and I know that because you've said it in this issue. But the React docs for useContext don't imply that at all; in fact, they imply the opposite:
If you’re familiar with the context API before Hooks, useContext(MyContext) is equivalent to static contextType = MyContext in a class, or to <MyContext.Consumer>.
But since it is different in a way that matters to Enzyme, what can we add to the documentation to make other people know it, too? If this issue was archived tomorrow and the discussion disappeared, what guardrails would be in place to stop someone else from opening a new issue to report the apparent bug? (And thereby wasting the maintainer's time, as well as their own.)
A coworker pointed out the code example in the docs on getWrappingComponent also doesn't work now, since it uses react-redux, which now also uses useContext under the hood.
I added a test to my code sandbox to demonstrate this.
I'd be happy to review a PR that clarifies the docs (including the unfortunately-now-incorrect react-redux example).
I could update the docs to remove the incorrect example and add caveats about useContext, but I fear that would be rather incomplete - I haven't figured out what the use case is for wrappingComponent that would make people need it.
It allows you to wrap a component in a (legacy) context provider, but directly be wrapping the actual component, so you don't have to dive down to the thing you're actually trying to test.
For me,
just.spyOn(React, 'useContext').mockImplementation(() => {...} )will work if my component hasconst ctx = React.useContext(MyContext)but not withconst ctx = useContext(MyContext).
@twharmon I don't have to do that if I do
jest.mock("react", () => ({
...jest.requireActual("react"),
useContext: () => 'context_value'
}));
instead of
jest.spyOn(React, 'useContext').mockImplementation(() => 'context_value')
@splybon Weird, this one does not work for me but the version with jest.mock('react', ... does. I have made sure my custom hook with context is written like this:
import React from 'react'
import MyContext from '../real-app-context'
...
const myCustomHook = () => {
const context = React.useContext(MyContext)
const { dependency } = MyContext
....
And in tests I do
const testContext = {
dependency: mockDependency
}
jest.spyOn(React, 'useContext').mockImplementation(() => testContext)
For some reason it's still using the context from ./real-app-context path.