How to test state management(Recoil) using react jest? - reactjs

How do I test recoil using react jest?
I expect below test is going to be success but it gives me fail.
Any way to render the status of isLogin: false using jest?
// src/state/user.ts
import { atom } from "recoil";
export type UserType = {
isLogin: boolean;
};
const userState = atom<UserType>({
key: "user",
default: {
isLogin: true,
},
});
export default userState;
// src/pages/user/User.tsx
import { useNavigate, useParams } from "react-router-dom";
import { useRecoilValue } from "recoil";
import userState from "../../state/user";
export default function User() {
const navigate = useNavigate();
const { id } = useParams();
const { isLogin } = useRecoilValue(userState);
if (!isLogin) {
return <div>Login 후 이용 가능합니다.</div>;
}
return (
<div>
{id}
<button
type="button"
onClick={() => {
navigate("/");
}}
>
Go to Home
</button>
</div>
);
}
// src/pages/user/User.test.tsx
import { MemoryRouter } from "react-router-dom";
import { render, screen, renderHook, act } from "#testing-library/react";
import { RecoilRoot, useSetRecoilState } from "recoil";
import User from "./User";
import type { UserType } from "../../state/user";
import userState from "../../state/user";
const userStateMock = (user: UserType) => {
const { result } = renderHook(() => useSetRecoilState(userState), {
wrapper: RecoilRoot,
});
act(() => {
result.current(user);
});
return result;
};
describe("<User />", () => {
const renderUserComponent = () =>
render(
<RecoilRoot>
<MemoryRouter>
<User />
</MemoryRouter>
</RecoilRoot>
);
describe("When user hasn't logged in", () => {
it("Should render warning message", () => {
userStateMock({
isLogin: false,
});
renderUserComponent();
expect(screen.getByText(/Login 후 이용 가능합니다./)).toBeDefined();
});
});
});
Result of the test

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.

expect(jest.fn()).toHaveBeenCalled()

I want to test that specific method/s are called when the user clicks 'sign in with microsoft' button.
Unfortunately, I'm receiving the below error in the image:
[enter image description here][1]
What I want is to mock the one specific method/instance that cause the sign up with microsoft pop up to occur on button click.
Below is my test file:
Login.test.js:
import * as React from 'react';
import {
render, screen, waitFor
} from '#testing-library/react';
import userEvent from '#testing-library/user-event';
import { MuiThemeProvider } from '#material-ui/core';
import { SnackbarProvider } from 'notistack';
import { I18nextProvider } from 'react-i18next';
import Login from '../../pages/Login';
import theme from '../../styles/theme';
import i18n from '../../i18n/index';
const mockLoginWithMicrosoft = jest.fn();
jest.mock('../../config/Firebase', () => ({
__esModule: true,
default: {
getCurrentUserId: jest.fn(),
loginWithMicrosoft: () => mockLoginWithMicrosoft,
}
}));
afterEach(() => {
jest.resetAllMocks();
jest.restoreAllMocks();
});
test('Firebase signInWithPopUp triggered on login button ', async () => {
render(
<MuiThemeProvider theme={theme}>
<SnackbarProvider>
<I18nextProvider i18n={i18n}>
<Login />
</I18nextProvider>
</SnackbarProvider>
</MuiThemeProvider>
);
expect(<Login />).toBeTruthy();
userEvent.click(screen.getByRole('button', { name: /sign in with microsoft/i }));
await waitFor(() => expect(mockLoginWithMicrosoft).toHaveBeenCalled());
});
And here is my firebase setup:
Firebase/index.js:
class Firebase {
constructor() {
app.initializeApp(config);
app.analytics();
this.perf = app.performance();
this.auth = app.auth();
this.db = app.firestore();
this.microsoftProvider = new app.auth.OAuthProvider('microsoft.com');
this.microsoftProvider.setCustomParameters({
tenant: '4b9d21d4-5ce6-4db6-bce6-cfcd1920afbc',
});
this.microsoftProvider.addScope('GroupMember.Read.All');
}
async loginWithMicrosoft() {
try {
const result = await this.auth.signInWithPopup(this.microsoftProvider);
const {
accessToken,
} = result.credential;
await this.getUserRoles(accessToken);
this.refreshRoles(true);
return {
message: 'success',
};
} catch (error) {
return {
message: 'failure',
};
}
}
some code
As you see above, I expect loginWithMicrosoft to be called, when clicking login button as shown below in login file:
Login.js:
import { useSnackbar } from 'notistack';
import { Redirect } from 'react-router-dom';
import Button from '#material-ui/core/Button';
import { useTranslation } from 'react-i18next';
import { DASHBOARD as DASHBOARD_PATH } from '../../navigation/CONSTANTS';
import firebase from '../../config/Firebase';
const Login = (props) => {
const { history } = props;
const classes = useStyles(props);
const { enqueueSnackbar } = useSnackbar();
const { t } = useTranslation();
const [loadState, setLoadState] = useState(false);
if (firebase.getCurrentUserId()) {
return (<Redirect to={DASHBOARD_PATH} />);
}
async function login() {
try {
setLoadState(true);
await firebase.loginWithMicrosoft();
history.replace(DASHBOARD_PATH);
} catch (error) {
enqueueSnackbar(t(translationKeys.snackbar.loginFailed), { variant: 'error' });
setLoadState(false);
}
}
return (
<div>
some code
<Button
data-testid="submitbtn"
disabled={loadState}
type="submit"
fullWidth
variant="contained"
color="primary"
startIcon={<img src={MicrosoftIcon} alt="" />}
className={classes.submit}
onClick={() => { login(); }}
>
{t(translationKeys.button.loginWindows)}
</Button>
</Grid>
<Footer />
</Grid>
</div>
);
};
);
};
Any help would be appreciated.
Okay so someone helped solve this problem,
As it was explained to me,
LoginWithMicrosoft doesn't need to be an arrow function since
jest.fn already returns a function
The other problem is jest hoisting the mock call above the fn declaration
My code now looks like this:
import MockFirebase from '../../config/Firebase';
jest.mock('../../config/Firebase', () => ({
__esModule: true,
default: {
getCurrentUserId: jest.fn(),
loginWithMicrosoft: jest.fn(),
}
}));
test('Firebase signInWithPopUp triggered on login button ', async () => {
render(
<MuiThemeProvider theme={theme}>
<SnackbarProvider>
<I18nextProvider i18n={i18n}>
<Login />
</I18nextProvider>
</SnackbarProvider>
</MuiThemeProvider>
);
userEvent.click(screen.getByRole('button', { name: /sign in with microsoft/i }));
expect(MockFirebase.loginWithMicrosoft).toHaveBeenCalled();
});

How to test React.js page that uses Context and useEffect?

I'm having trouble testing a page that has Context and useEffect using Jest and Testing-library, can you help me?
REPO: https://github.com/jefferson1104/padawan
My Context: src/context/personContext.tsx
import { createContext, ReactNode, useState } from 'react'
import { useRouter } from 'next/router'
import { api } from '../services/api'
type PersonData = {
name?: string
avatar?: string
}
type PersonProviderProps = {
children: ReactNode
}
type PersonContextData = {
person: PersonData
loading: boolean
handlePerson: () => void
}
export const PersonContext = createContext({} as PersonContextData)
export function PersonProvider({ children }: PersonProviderProps) {
const [person, setPerson] = useState<PersonData>({})
const [loading, setLoading] = useState(false)
const router = useRouter()
function checkAvatar(name: string): string {
return name === 'Darth Vader'
? '/img/darth-vader.png'
: '/img/luke-skywalker.png'
}
async function handlePerson() {
setLoading(true)
const promise1 = api.get('/1')
const promise2 = api.get('/4')
Promise.race([promise1, promise2]).then(function (values) {
const data = {
name: values.data.name,
avatar: checkAvatar(values.data.name)
}
setPerson(data)
setLoading(false)
router.push('/battlefield')
})
}
return (
<PersonContext.Provider value={{ person, handlePerson, loading }}>
{children}
</PersonContext.Provider>
)
}
My Page: src/pages/battlefield.tsx
import { useContext, useEffect } from 'react'
import { useRouter } from 'next/router'
import { PersonContext } from '../context/personContext'
import Person from '../components/Person'
const Battlefield = () => {
const { person } = useContext(PersonContext)
const router = useRouter()
useEffect(() => {
if (!person.name) {
router.push('/')
}
})
return <Person />
}
export default Battlefield
My Test: src/tests/pages/Battlefield.spec.tsx
import { render, screen } from '#testing-library/react'
import { PersonContext } from '../../context/personContext'
import Battlefield from '../../pages'
jest.mock('../../components/Person', () => {
return {
__esModule: true,
default: function mock() {
return <div data-test-id="person" />
}
}
})
describe('Battlefield page', () => {
it('renders correctly', () => {
const mockPerson = { name: 'Darth Vader', avatar: 'darth-vader.png' }
const mockHandlePerson = jest.fn()
const mockLoading = false
render(
<PersonContext.Provider
value={{
person: mockPerson,
handlePerson: mockHandlePerson,
loading: mockLoading
}}
>
<Battlefield />
</PersonContext.Provider>
)
expect(screen.getByTestId('person')).toBeInTheDocument()
})
})
PRINSCREEN ERROR
enter image description here
I found a solution:
The error was happening because the path where I call the Battlefield page didn't have the absolute path.

React testing library, firebase, mocked functions

I started studying integration testing and I am struggling with mocking.
I have a signup form that onSubmit calls firebase.auth.createUserWithEmailAndPassword, after that it initializes a document with the returned user uid and initialize it with default values.
I managed to check if createCserWithEmailAndPassword is correctly called but when I try to check if the collection set has been called I receive as error:
expect(received).toHaveBeenCalled()
Matcher error: received value must be a mock or spy function
Received has value: undefined
I am struggling here.
Here is my test implementation:
import React from "react";
import "#testing-library/jest-dom";
import { ThemeProvider } from "styled-components";
import { defaultTheme } from "../../styles";
import { createMemoryHistory } from "history";
import { Router, Switch, Route } from "react-router-dom";
import { render, fireEvent, wait } from "#testing-library/react";
import { SignUp } from "../../sections";
import { User } from "../../types";
import { auth, firestore } from "../../lib/api/firebase";
jest.mock("../../lib/api/firebase", () => {
return {
auth: {
createUserWithEmailAndPassword: jest.fn(() => {
return {
user: {
uid: "fakeuid",
},
};
}),
signOut: jest.fn(),
},
firestore: {
collection: jest.fn(() => ({
doc: jest.fn(() => ({
collection: jest.fn(() => ({
add: jest.fn(),
})),
set: jest.fn(),
})),
})),
},
};
});
const defaultUser: User = {
isActive: false,
accountName: "No balance",
startingBalance: 0.0,
monthlyBudget: 0.0,
};
const history = createMemoryHistory({ initialEntries: ["/signup"] });
const renderWithRouter = () =>
render(
<ThemeProvider theme={defaultTheme}>
<Router history={history}>
<Switch>
<Route exact path="/signup">
<SignUp />
</Route>
<Route exact path="/">
<div data-testid="GenericComponent"></div>
</Route>
</Switch>
</Router>
</ThemeProvider>
);
describe("<SignUp/>", () => {
it("correctly renders the signup form", () => {
const { getByTestId } = renderWithRouter();
const form = getByTestId("SignupForm");
expect(form).toBeInTheDocument();
});
it("correctly renders the signup form", () => {
const { getByTestId } = renderWithRouter();
const form = getByTestId("SignupForm");
expect(form).toBeInTheDocument();
});
it("let user signup with valid credentials", async () => {
const { getByPlaceholderText, getByTestId } = renderWithRouter();
const emailField = getByPlaceholderText("yourname#company.com");
const passwordField = getByPlaceholderText("Password");
const confirmPasswordField = getByPlaceholderText("Confirm Password");
const submitButton = getByTestId("Button");
fireEvent.change(emailField, {
target: { value: "test#test.com" },
});
fireEvent.change(passwordField, { target: { value: "Lampone01!" } });
fireEvent.change(confirmPasswordField, {
target: { value: "Lampone01!" },
});
fireEvent.click(submitButton);
expect(submitButton).not.toBeDisabled();
await wait(() => {
expect(auth.createUserWithEmailAndPassword).toHaveBeenCalled();
expect(
firestore.collection("users").doc("fakeUid").set(defaultUser)
).toHaveBeenCalled();
expect(history.location.pathname).toBe("/dashboard");
});
});
});
What am I doing wrong?
Thanks guys.

Resources