Testing useContext() with react-testing-library - reactjs

I think I found another way to test a component using the useContext hook. I have seen a few tutorials that test if a value can be successfully passed down to a child component from a parent Context Provider but did not find tutorials on the child updating the context value.
My solution is to render the root parent component along with the provider, because the state is ultimately changed in the root parent component and then passed to the provider which then passes it to all child components. Right?
The tests seem to pass when they should and not pass when they shouldn't.
Can someone explain why this is or isn't a good way to test the useContext hook?
The root parent component:
...
const App = () => {
const [state, setState] = useState("Some Text")
const changeText = () => {
setState("Some Other Text")
}
...
<h1> Basic Hook useContext</h1>
<Context.Provider value={{changeTextProp: changeText,
stateProp: state
}} >
<TestHookContext />
</Context.Provider>
)}
The context object:
import React from 'react';
const Context = React.createContext()
export default Context
The child component:
import React, { useContext } from 'react';
import Context from '../store/context';
const TestHookContext = () => {
const context = useContext(Context)
return (
<div>
<button onClick={context.changeTextProp}>
Change Text
</button>
<p>{context.stateProp}</p>
</div>
)
}
And the tests:
import React from 'react';
import ReactDOM from 'react-dom';
import TestHookContext from '../test_hook_context.js';
import {render, fireEvent, cleanup} from '#testing-library/react';
import App from '../../../App'
import Context from '../../store/context';
afterEach(cleanup)
it('Context is updated by child component', () => {
const { container, getByText } = render(<App>
<Context.Provider>
<TestHookContext />
</Context.Provider>
</App>);
console.log(container)
expect(getByText(/Some/i).textContent).toBe("Some Text")
fireEvent.click(getByText("Change Text"))
expect(getByText(/Some/i).textContent).toBe("Some Other Text")
})

The problem with the approach that you mention is coupling.
Your Context to be tested, depends on <TestHookContext/> and <App/>
As Kent C. Dodds, the author of react-testing-library, has a full article on "Test Isolation with React" if you want to give it a read.
TLDR: Demo repo here
How to test Context
Export a <ContextProvider> component that holds the state and returns <MyContext.Provider value={{yourWhole: "State"}}>{children}<MyContext.Provider/> This is the component that we are going to test for the provider.
On the components that consume that Context, create a MockContextProvider to replace the original one. You want to test the component in isolation.
You can test the whole app workflow to by testing the root component.
Testing Auth Provider
Let's say we have a component that provides Auth using context the following way:
import React, { createContext, useState } from "react";
export const AuthContext = createContext();
const AuthProvider = ({ children }) => {
const [isLoggedin, setIsLoggedin] = useState(false);
const [user, setUser] = useState(null);
const login = (user) => {
setIsLoggedin(true);
setUser(user);
};
const logout = () => {
setIsLoggedin(false);
setUser(null);
};
return (
<AuthContext.Provider value={{ logout, login, isLoggedin, user }}>
{children}
</AuthContext.Provider>
);
};
export default AuthProvider;
The test file would look like:
import { fireEvent, render, screen } from "#testing-library/react";
import AuthProvider, { AuthContext } from "./AuthProvider";
import { useContext } from "react";
const CustomTest = () => {
const { logout, login, isLoggedin, user } = useContext(AuthContext);
return (
<div>
<div data-testid="isLoggedin">{JSON.stringify(isLoggedin)}</div>
<div data-testid="user">{JSON.stringify(user)}</div>
<button onClick={() => login("demo")} aria-label="login">
Login
</button>
<button onClick={logout} aria-label="logout">
LogOut
</button>
</div>
);
};
test("Should render initial values", () => {
render(
<AuthProvider>
<CustomTest />
</AuthProvider>
);
expect(screen.getByTestId("isLoggedin")).toHaveTextContent("false");
expect(screen.getByTestId("user")).toHaveTextContent("null");
});
test("Should Login", () => {
render(
<AuthProvider>
<CustomTest />
</AuthProvider>
);
const loginButton = screen.getByRole("button", { name: "login" });
fireEvent.click(loginButton);
expect(screen.getByTestId("isLoggedin")).toHaveTextContent("true");
expect(screen.getByTestId("user")).toHaveTextContent("demo");
});
test("Should Logout", () => {
render(
<AuthProvider>
<CustomTest />
</AuthProvider>
);
const loginButton = screen.getByRole("button", { name: "logout" });
fireEvent.click(loginButton);
expect(screen.getByTestId("isLoggedin")).toHaveTextContent("false");
expect(screen.getByTestId("user")).toHaveTextContent("null");
});
Testing Component that consumes Context
import React, { useContext } from "react";
import { AuthContext } from "../context/AuthProvider";
const Welcome = () => {
const { logout, login, isLoggedin, user } = useContext(AuthContext);
return (
<div>
{user && <div>Hello {user}</div>}
{!user && <div>Hello Anonymous Goose</div>}
{!isLoggedin && (
<button aria-label="login" onClick={() => login("Jony")}>
Log In
</button>
)}
{isLoggedin && (
<button aria-label="logout" onClick={() => logout()}>
Log out
</button>
)}
</div>
);
};
export default Welcome;
We will mock the AuthContext value by providing one of our own:
import React, { useContext } from "react";
import { render, screen } from "#testing-library/react";
import "#testing-library/jest-dom";
import Welcome from "./welcome";
import userEvent from "#testing-library/user-event";
import { AuthContext } from "../context/AuthProvider";
// A custom provider, not the AuthProvider, to test it in isolation.
// This customRender will be a fake AuthProvider, one that I can controll to abstract of AuthProvider issues.
const customRender = (ui, { providerProps, ...renderOptions }) => {
return render(
<AuthContext.Provider value={providerProps}>{ui}</AuthContext.Provider>,
renderOptions
);
};
describe("Testing Context Consumer", () => {
let providerProps;
beforeEach(
() =>
(providerProps = {
user: "C3PO",
login: jest.fn(function (user) {
providerProps.user = user;
providerProps.isLoggedin = true;
}),
logout: jest.fn(function () {
providerProps.user = null;
providerProps.isLoggedin = false;
}),
isLoggedin: true,
})
);
test("Should render the user Name when user is signed in", () => {
customRender(<Welcome />, { providerProps });
expect(screen.getByText(/Hello/i)).toHaveTextContent("Hello C3PO");
});
test("Should render Hello Anonymous Goose when is NOT signed in", () => {
providerProps.isLoggedin = false;
providerProps.user = null;
customRender(<Welcome />, { providerProps });
expect(screen.getByText(/Hello/i)).toHaveTextContent(
"Hello Anonymous Goose"
);
});
test("Should render Logout button when user is signed in", () => {
customRender(<Welcome />, { providerProps });
expect(screen.getByRole("button", { name: "logout" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "login" })).toBeNull();
});
test("Should render Login button when user is NOT signed in", () => {
providerProps.isLoggedin = false;
providerProps.user = null;
customRender(<Welcome />, { providerProps });
expect(screen.getByRole("button", { name: "login" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "logout" })).toBeNull();
});
test("Should Logout when user is signed in", () => {
const { rerender } = customRender(<Welcome />, { providerProps });
const logout = screen.getByRole("button", { name: "logout" });
expect(logout).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "login" })).toBeNull();
userEvent.click(logout);
expect(providerProps.logout).toHaveBeenCalledTimes(1);
//Technically, re renders are responsability of the parent component, but since we are here...
rerender(
<AuthContext.Provider value={providerProps}>
<Welcome />
</AuthContext.Provider>
);
expect(screen.getByText(/Hello/i)).toHaveTextContent(
"Hello Anonymous Goose"
);
expect(screen.getByRole("button", { name: "login" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "logout" })).toBeNull();
});
test("Should Login when user is NOT signed in", () => {
providerProps.isLoggedin = false;
providerProps.user = null;
const { rerender } = customRender(<Welcome />, { providerProps });
const login = screen.getByRole("button", { name: "login" });
expect(login).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "logout" })).toBeNull();
userEvent.click(login);
expect(providerProps.login).toHaveBeenCalledTimes(1);
//Technically, re renders are responsability of the parent component, but since we are here...
rerender(
<AuthContext.Provider value={providerProps}>
<Welcome />
</AuthContext.Provider>
);
expect(screen.getByText(/Hello/i)).toHaveTextContent("Hello Jony");
expect(screen.getByRole("button", { name: "logout" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "login" })).toBeNull();
});
});

Your example/code is dead on. (Not sure you need to mount the wrapping <App /> - you should just wrap in the Context Provider directly).
To your question:
The tests seem to pass when they should and not pass when they shouldnt. Can someone explain why this is or isnt a good way to test the useContext() hook.
It is a good way to test when using useContext() because it looks like you have abstracted out your context so that your child (consuming) component and its test both use the same context. I don't see any reason why you would mock or emulate what the context provider is doing when (as you do in your example) you use the same context provider.
The React Testing Library docs point out that:
The more your tests resemble the way your software is used, the more confidence they can give you.
Therefore, setting up your tests the same way you set up your components achieves that goal. If you have multiple tests in one app that do need to be wrapped in the same context, this blog post has a neat solution for reusing that logic.

Related

Dom does is not updated while testing redirection with react-testing library and react-router v6

I'm trying to test the redirection page when the user clicks on the button (I don't want to user jest.mock()).
I created the wrapper according to test-react-library documentation:
import { FC, ReactElement, ReactNode } from "react";
import { render, RenderOptions } from "#testing-library/react";
import { BrowserRouter } from "react-router-dom";
import userEvent from "#testing-library/user-event";
const WrapperProviders: FC<{ children: ReactNode }> = ({ children }) => {
return <BrowserRouter>{children}</BrowserRouter>;
};
const customRender = (
ui: ReactElement,
{ route = "/" } = {},
options?: Omit<RenderOptions, "wrapper">
) => {
window.history.pushState({}, "Home Page", route);
return {
user: userEvent.setup(),
...render(ui, { wrapper: WrapperProviders, ...options })
};
};
export * from "#testing-library/react";
export { customRender as render };
export type RenderType = ReturnType<typeof customRender>;
HomePage.tsx:
import { useNavigate } from "react-router-dom";
export default function HomePage() {
const navigate = useNavigate();
const handleClick = () => navigate("/other");
return (
<>
<h3>HomePage</h3>
<button onClick={handleClick}>Redirect</button>
</>
);
}
Other.tsx:
export default function Other() {
return <h3>Other</h3>;
}
HomePage.test.tsx:
import { render, RenderType } from "./customRender";
import HomePage from "./HomePage";
import "#testing-library/jest-dom/extend-expect";
describe("HomePage", () => {
let wrapper: RenderType;
beforeEach(() => {
wrapper = render(<HomePage />, { route: "/" });
});
test.only("Should redirects to other page", async () => {
const { getByText, user } = wrapper;
expect(getByText(/homepage/i)).toBeInTheDocument();
const button = getByText(/redirect/i);
expect(button).toBeInTheDocument();
user.click(button);
expect(getByText(/other/i)).toBeInTheDocument();
});
});
When I run the test, it fails and the page is not updated in the dom.
Does jest-dom does not support the re-render of the page and update the DOM? Or this test out of scope of the testing-library ?
From the code I can see that you are just rendering the HomePage component and inside of that component you don't have any logic that renders a new component based on the route changes (I suppose that you have that logic on another component). That's why when you click on the button you are not seeing the Other component rendered.
In this case I would suggest you to only make the assertions you need on the window.location object. So after you simulate the click on the button you can do:
expect(window.location.pathname).toBe("/other");
I updated the custom render and add a history for it:
const WrapperProviders: FC<{ children: ReactNode }> = ({ children }) => {
return <BrowserRouter>{children}</BrowserRouter>;
};
const customRender = (
ui: ReactElement,
{ route = "/",
history = createMemoryHistory({initialEntries: [route]}),
} = {},
options?: Omit<RenderOptions, "wrapper">
) => {
return {
user: userEvent.setup(),
...render( <Router location={history.location} navigator={history}>
ui
</Router>
, { wrapper: WrapperProviders, ...options })
};
};
and the test now passes:
describe("HomePage", () => {
let wrapper: RenderType;
beforeEach(() => {
wrapper = render(<HomePage />, { route: "/" });
});
test.only("Should redirects to other page", async () => {
const { getByText, user, history } = wrapper;
expect(getByText(/homepage/i)).toBeInTheDocument();
const button = getByText(/redirect/i);
expect(button).toBeInTheDocument();
await user.click(button);
expect(history.location.pathname).toBe('/other');
});

Mocking react custom hook return value as a module with Jest returns wrong value

I need to mock my custom hook when unit testing React component. I've read some stackoverflow answers but haven't succeeded in implementing it correctly.
I can't use useAuth without mocking it as it depends on server request and I'm only writing unit tests at the moment.
//useAuth.js - custom hook
import React, { createContext, useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
const authContext = createContext();
function useProvideAuth() {
const [accessToken, setAccessToken] = useState('');
const [isAuthenticated, setAuthenticated] = useState(
accessToken ? true : false
);
useEffect(() => {
refreshToken();
}, []);
const login = async (loginCredentials) => {
const accessToken = await sendLoginRequest(loginCredentials);
if (accessToken) {
setAccessToken(accessToken);
setAuthenticated(true);
}
};
const logout = async () => {
setAccessToken(null);
setAuthenticated(false);
await sendLogoutRequest();
};
const refreshToken = async () => {
const accessToken = await sendRefreshRequest();
if (accessToken) {
setAccessToken(accessToken);
setAuthenticated(true);
} else setAuthenticated(false);
setTimeout(async () => {
refreshToken();
}, 15 * 60000 - 1000);
};
return {
isAuthenticated,
accessToken,
login,
logout
};
}
export function AuthProvider({ children }) {
const auth = useProvideAuth();
return <authContext.Provider value={auth}>{children}</authContext.Provider>;
}
AuthProvider.propTypes = {
children: PropTypes.any
};
const useAuth = () => {
return useContext(authContext);
};
export default useAuth;
The test I've written
import React from 'react';
import { render, fireEvent, screen } from '#testing-library/react';
import { NavBar } from '../App';
jest.resetAllMocks();
jest.mock('../auth/useAuth', () => {
const originalModule = jest.requireActual('../auth/useAuth');
return {
__esModule: true,
...originalModule,
default: () => ({
accessToken: 'token',
isAuthenticated: true,
login: jest.fn,
logout: jest.fn
})
};
});
describe('NavBar when isAuthenticated', () => {
it('LogOut button is visible when isAuthenticated', () => {
render(<NavBar />);
expect(screen.getByText(/log out/i)).toBeVisible();
});
});
The function I'm writing tests on:
//App.js
import React from 'react';
import cn from 'classnames';
import useAuth, { AuthProvider } from './auth/useAuth';
import './App.css';
import '../node_modules/bootstrap/dist/css/bootstrap.css';
function App() {
return (
<AuthProvider>
<Router>
<NavBar />
</Router>
</AuthProvider>
);
}
const NavBarSignUpButton = () => (
<button className='button info'>
Sign up
</button>
);
const NavBarLogoutButton = () => {
const auth = useAuth();
const handleLogOut = () => {
auth.logout();
};
return (
<button className='button info' onClick={handleLogOut}>
Log out
</button>
);
};
export const NavBar = () => {
const isAuthenticated = useAuth().isAuthenticated;
const loginButtonClassName = cn({
btn: true,
invisible: isAuthenticated
});
return (
<nav className='navbar navbar-expand-lg navbar-light'>
<div className='container'>
<div className='d-flex justify-content-end'>
<div className='navbar-nav'>
<button className={loginButtonClassName}>
Log In
</button>
{isAuthenticated ? <NavBarLogoutButton /> : <NavBarSignUpButton />}
</div>
</div>
</div>
</nav>
);
};
The test code above doesn't throw any errors. However, the test fails as useAuth().isAuthenticated is always false (but I'm mocking it to return true). It doesn't change whether I test App or only NavBar
What am I doing wrong?
I made a super minified example that should show the mocking works. It just features the hook itself and a component returning YES or NO based on the hook. The test
useAuth.js
import {createContext, useContext} from 'react'
const authContext = createContext()
const useAuth = () => {
return useContext(authContext)
}
export default useAuth
component.js
import useAuth from './useAuth'
export const Component = () => {
const isAuthenticated = useAuth().isAuthenticated
return isAuthenticated ? 'YES' : 'NO'
}
component.test.js
import React from 'react'
import {render, screen} from '#testing-library/react'
import {Component} from './component'
jest.mock('./useAuth', () => {
const originalModule = jest.requireActual('./useAuth')
return {
__esModule: true,
...originalModule,
default: () => ({
accessToken: 'token',
isAuthenticated: true,
login: jest.fn,
logout: jest.fn,
}),
}
})
describe('When isAuthenticated', () => {
it('Component renders YES', () => {
render(<Component />)
screen.getByText(/YES/i)
})
})
In this case, the component does in fact render YES and the test passes. This makes me thing there are other things involved. When I change the mock to false, the test fails because it renders NO.

Testing methods exposed through react context

I have a react context like this
UserContext.js
export const UserContext = createContext({
createUser: async () => {
...execute some xhr requests and manipulateData
},
getUser: () => {},
});
const UserProvider ({ children }) => {
const context = {
createUser,
getUser,
};
return (
<UserContext.Provider value={context}>{children}</UserContext.Provider>
);
}
And the following unit test
UserContext.spec.js
import { render } from '#testing-library/react';
import UserProvider, { UserContext } from '#my-contexts/user';
...
let createUser = () => {};
render(
<UserProvider>
<UserContext.Consumer>
{value => {
createUser = value.createUser;
return null;
}}
</UserContext.Consumer>
</UserProvider>
);
await createUser(data);
expect(data).toEqual({ status: 200 });
I am not sure if this way
let createUser = () => {};
render(
<UserProvider>
<UserContext.Consumer>
{value => {
createUser = value.createUser;
return null;
}}
</UserContext.Consumer>
</UserProvider>
);
it is the better technique for "extract" the inner methods exposed in the UserContext (in this case createUser method)
In my App code I use this context like following:
import { useContext } from 'react';
import { UserContext } from '#my-contexts/user';
const someComponent = (props) => {
...
const { createUser } = useContext(UserContext);
const handleCreate = (e) => {
createUser(form);
};
return (
<form>
<label>Username</label>
<input value={form.userName} />
<button type="submit" onClick={handleCreate}> create user </button>
</form>
)
};
But the problem is that I can get the exposed methods from context only if I have a component and get the method using useContext hook.
If I want to test the createUser method in isolation:
Exists a better way for get the methods exposed in context without rendering a provider and consumer and "manually extract" him?

Enzyme/Jest onSubmit not calling Submit Function

I'm having trouble checking if my mock action was called from an onSubmit on a form:
Login Form:
class Login extends Component {
constructor(props) {
super(props);
this.state = {
email: "",
password: "",
};
}
handleSubmit = e => {
e.preventDefault();
this.props.loginUser(this.state.email, this.state.password);
};
handleChange = event => {
const { value } = event.target;
const { name } = event.target;
this.setState({
[name]: value,
});
};
render() {
const { t } = this.props;
return (
<div className="login-container>
<form data-test-id="login-form" onSubmit={this.handleSubmit}>
<TextComponent
label="Email"
testId="login-email"
value={this.state.email}
handleChange={this.handleChange}
/>
<div className="my-3" />
<TextComponent
label="Password"
testId="login-password"
value={this.state.password}
handleChange={this.handleChange}
/>
<button action="submit" data-test-id="login-button">
{t("en.login")}
</button>
</form>
</div>
);
}
}
function mapStateToProps(state) {
return {
authenticated: state.login.authenticated,
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ loginUser }, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(withTranslation()(Login));
Test:
import { mount, shallow, render } from "enzyme";
import React from "react";
import { Provider } from "react-redux";
import { I18nextProvider } from "react-i18next";
import { Router } from "react-router-dom";
import { findByTestAtrr, testStore } from "../../test/utils/utils";
import Login from "../../src/Login/index";
import i18n from "../../src/i18n";
import history from "../../src/history";
const setUp = loginUser => {
const initialState = {
login: {
authenticated: true,
},
};
const store = testStore(initialState);
const wrapper = mount(
<I18nextProvider i18n={i18n}>
<Provider store={store}>
<Router history={history}>
<Login onSubmit={loginUser} />
</Router>
</Provider>
</I18nextProvider>
);
return wrapper;
};
describe("submit button", () => {
let emailInput;
let passwordInput;
let submitButton;
let newWrapper;
let loginAction;
beforeEach(() => {
loginAction = jest.fn();
newWrapper = setUp(loginAction);
emailInput = findByTestAtrr(newWrapper, "login-email");
emailInput.instance().value = "email.com";
emailInput.simulate("change");
passwordInput = findByTestAtrr(newWrapper, "login-password");
passwordInput.instance().value = "password";
passwordInput.simulate("change");
submitButton = findByTestAtrr(newWrapper, "login-button");
});
it("login action is called", () => {
console.log(submitButton.debug());
submitButton.simulate("submit");
expect(loginAction).toHaveBeenCalledTimes(1);
});
});
});
I'm able to simulate adding values to the email and password but I can't simulate the onClick to work. Am I testing the submit function incorrectly?
This is my submit button when I console.log
console.log __tests__/integration_tests/login.test.js:97
<button action="submit" data-test-id="login-button">
Log in
</button>
error:
expect(jest.fn()).toHaveBeenCalledTimes(expected)
Expected number of calls: 1
Received number of calls: 0
As raised from 2016 on github: Simulated click on submit button in form does not submit form #308, this issue still persists on my project, using Jest + Enzyme (2020).
There are some workarounds such as using tape or sino.
The other solution is to get the DOM element and trigger the click without using simulate (go to given link, scroll down the thread to learn more).
If anyone ever found a solution to simulate form submit, kindly tag me here. Thank you.
As explained in the below mentioned link, you can use done and setTimeout.
https://github.com/ant-design/ant-design/issues/21272#issuecomment-628607141
it('login action is called', (done) => {
submitButton.simulate("submit");
setTimeout(() => {
expect(loginAction).toHaveBeenCalledTimes(1);
done();
}, 0);
});
it('should call onSubmit method', ()=>{
const spy = jest.spyOn(wrapper.instance(), "onSubmit");
wrapper.find('button[type="submit"]').simulate('click');
expect(spy).toHaveBeenCalledTimes(1);
});
Check the name of method to be called on simulate click. In my case it was onSubmit.
No need to import sinon or spy from sinon jest does everything.

How can I test functions that are provided by context using jest and react-testing-library?

I have a <UserProvider /> component that provides context which has a login method in its value. In my test I want to check if that method has been called. How can I check to make sure this method is being called in my test if it comes from context? I've tried using the spyOn and mock methods but I can't get them to work.
Here's my UserProvider component which provides the context.
import React, { useState, useContext, createContext } from 'react'
const UserContext = createContext()
function UserProvider({ children }) {
const [user, setUser] = useState(null)
const login = user => setUser(user)
return <UserContext.Provider value={{ user, login }}>{children}</UserContext.Provider>
}
const useUser = () => useContext(UserContext)
export { UserProvider, useUser }
Here's my Login component using the context from UserProvider (via useUser)
import React from 'react'
import { useUser } from './UserProvider'
function Login() {
const { login } = useUser()
const handleSubmit = async e => {
e.preventDefault()
// I need to make sure this method is being called in my test.
login({ name: 'John Doe' })
}
return (
<form onSubmit={handleSubmit}>
<button>Login</button>
</form>
)
}
export default Login
Here's my Login test
import React from 'react'
import { render, fireEvent } from '#testing-library/react'
import Login from './Login'
import { UserProvider, useUser } from './UserProvider'
// Is this correct?
jest.mock('./UserProvider', () => ({
useUser: jest.fn(() => ({
login: jest.fn()
}))
}))
it('should log a user in', () => {
const { getByText } = render(
<UserProvider>
<Login />
</UserProvider>
)
const submitButton = getByText(/login/i)
fireEvent.click(submitButton)
// How can I make this work?
const { login } = useUser()
expect(login).toHaveBeenCalledTimes(1)
})
I have a codesandbox but it's erroring out about jest.mock not being a function so I don't know if it's very useful.
I can't get it to work without slightly change the source code a little bit.
First, I have to export the actual context
export { UserProvider, useUser, UserContext }
We can re create provider with a mocked login function and the following test will serve you purpose.
import React from 'react'
import { render, fireEvent } from '#testing-library/react'
import Login from './Login'
import { UserProvider, useUser, UserContext } from './UserProvider'
it('should log a user in', () => {
const login = jest.fn();
const { getByText } = render(
<UserContext.Provider value={{ login }}>
<Login />
</UserContext.Provider>
);
const submitButton = getByText('Login');
fireEvent.click(submitButton);
expect(login).toHaveBeenCalledTimes(1)
});
It is entirely possible that this is not the best approach. I hope it helps.
I see 2 more approaches here:
Inject Context.Consumer alongside your component under test. Then so you would be able to verify against it
const contextCallback = jest.fn();
const { getByText } = render(
<UserProvider>
<Login />
<UserContext.Consumer>{contextCallback}</UserContext.Consumer>
</UserProvider>
);
const submitButton = getByText('Login');
fireEvent.click(submitButton);
expect(contextCallback.mock.calls[0][0]).toEqual({
user: "John Doe"
});
Mock calls behind UserContext.Consumer(let it be speculative LoginAPI call in UserContext)
jest.mock("../LoginAPI.js");
...
const { getByText } = render(
<UserProvider>
<Login />
</UserProvider>
);
const submitButton = getByText('Login');
fireEvent.click(submitButton);
expect(LoginAPI.login).toHaveBeenCalledTimes(1);
To me 2nd one is better while 1st rely on implementation details(context data's structure).
PS to 1st approach you may declare consumer component right in the test to avoid exporting UserContext:
function ContextHelper({ spy }) {
const contextData = useUser();
spy(contextData);
return null;
}
...
const contextCallback = jest.fn();
const { getByText } = render(
<UserProvider>
<Login />
<ContextHelper spy={contextCallback} />
</UserProvider>
);
But keep in mind that spy may be called more times that you'd expect.

Resources