Using context in react component tests - reactjs

I am writing tests for components of my projects and one of components is changing context value. How can I check if click actually changes context value? Code I have right now gives error "Invalid hook call. Hooks can only be called inside of the body of a function component." What is the way to actually use hooks in tests?
import { useState, useContext } from "react";
import { IntlProvider } from "react-intl";
import { BrowserRouter } from "react-router-dom";
import { render, screen, fireEvent } from "#testing-library/react";
import { messages } from "../../App/App.const";
import Navbar from "./Navbar";
import { DarkModeContext } from "../../contexts/DarkModeContext";
function NavbarMock() {
const [search, setSearch] = useState("");
const [language, setLanguage] = useState("en");
return (
<IntlProvider
messages={messages[language as keyof typeof messages]}
locale={language}
defaultLocale="en"
>
<BrowserRouter>
<Navbar
setLanguage={setLanguage}
language={language}
setSearch={setSearch}
search={search}
/>
</BrowserRouter>
</IntlProvider>
);
}
describe("testing navbar component", () => {
test("renders logo correctly", async () => {
render(<NavbarMock />);
const logo = screen.getByText(/Todoly/i);
expect(logo).toBeInTheDocument();
});
test("renders mode icon correctly", async () => {
render(<NavbarMock />);
const svgEl = screen.getByTitle("mode icon");
expect(svgEl).toBeInTheDocument();
});
test("mode changes", async () => {
render(<NavbarMock />);
const svgEl = screen.getByTitle("mode icon");
const { isDarkMode } = useContext(DarkModeContext);
fireEvent.click(svgEl);
expect(isDarkMode).toBe(true);
});
test("renders language icon correctly", async () => {
render(<NavbarMock />);
const flagEl = screen.getByAltText("en");
expect(flagEl).toBeInTheDocument();
});
});

Ok, the error isn't caused by any of the provider, instead it's caused by the way you write useContext.
A hook can't be used unless it's referenced inside a Component, this is the RULE by React hooks. Because states defined by hooks are meaningless outside.
const AnotherNavbarMock = () => {
const { isDarkMode } = useContext(DarkModeContext);
return <NavbarMock />
}
The above code is the working example. Basically a test isn't a component, you have to define a component and then use it via <AnotherNavbarMock />, otherwise all the hook code would fail.

Related

How to test component properly using UseState

I am new to tests in react and I don't know how to properly mock the useState value to properly cover the lines that uses the boolean as a parameter to return the component
React Code
import React from "react";
import { InputGroup, Input, Button, Spinner, Center } from "#chakra-ui/react";
import movieAPI from "../../services/movieAPI";
import { useNavigate } from "react-router-dom";
import styles from "./index.module.css";
export const Search = () => {
const navigate = useNavigate();
const [movie, setMovie] = React.useState("");
const [isLoading, setLoading] = React.useState(false);
const handleClick = async () => {
setLoading(true);
const data = await movieAPI.fetchMovieByTitle(movie);
setLoading(false);
navigate(`/movie/${data.Title}`, { state: data });
};
return isLoading ? (
<Center>
<Spinner
data-testid="spinner"
className={styles.verticalCenter}
thickness="6px"
speed="1.0s"
emptyColor="gray.200"
color="green.500"
size="xl"
/>
</Center>
) : (
<InputGroup m={2} p={2}>
<Input onChange={(e) => setMovie(e.target.value)} placeholder="Search" />
<Button onClick={handleClick}>Search</Button>
</InputGroup>
);
};
How can I mock the property loading in order to cover the specific line of the spinner component?
down below is my attempt to test the Spinner code
import { render, fireEvent, RenderResult } from "#testing-library/react";
import { Search } from "../../components/search/search";
import { BrowserRouter as Router, useNavigate } from "react-router-dom";
import React from "react";
describe("search.tsx", () => {
let isLoading = false;
let setLoading = jest.fn();
let container: RenderResult<
typeof import("#testing-library/dom/types/queries"),
HTMLElement,
HTMLElement
>;
jest.mock("react", () => {
return {
...jest.requireActual("react"),
useState: () => ({
isLoading: isLoading,
setLoading: setLoading,
}),
};
});
beforeEach(() => {
container = render(
<Router>
<Search />
</Router>
);
});
it("should render spinner", async () => {
setLoading.mockImplementation((data) => {
isLoading = data;
});
setLoading(true);
console.log(await container.findByTestId("spinner"));
});
});
A component is like a black box for testing.
It has two inputs: props and user interaction. Based on those it renders something. you should not mock useState. Your test would look like this:
You can mock other dependencies like localStorage and or rest api calls. But no internal component implementation.
Your test should look like this, written in pseudo code
it("Should show loader while searching for movies", () => {
// mock the API to return promise which never resolves
// render component
// input some search data
// click the search button
// expect the loader to be visible
});
it("Should reflect text base on user input,", () => {
// render component
// input some search data "Start"
// expect searchInput.text to have value = "Start"
})

Need do write unit tests with jest for react app, that uses okta, but getting warning

import { SecureRoute, Security, LoginCallback } from '#okta/okta-react';
import React, { useMemo } from 'react';
import { Route, Switch } from 'react-router-dom';
import { OktaAuth, toRelativeUrl } from '#okta/okta-auth-js';
import Comp from './Comp';
const config = {oidc: {....}};
const AppRouter = () => {
const oktaAuth = useMemo(() => new OktaAuth(config.oidc), []);
const restoreOriginalUri = async (sth, originalUri) => {
history.replace(toRelativeUrl(originalUri || '/', 'some-path-here'));
};
return (
<Security oktaAuth={oktaAuth} restoreOriginalUri={restoreOriginalUri}>
<Switch>
<SecureRoute path="/" exact component={Comp} />
<Route path="/login/callback" component={LoginCallback} />
</Switch>
</Security>
);
};
export default AppRouter;
I have this in my app... how to write unit tests for it if I have the following warning?
Warning: An update to Security inside a test was not wrapped in act(...).
The waitFor thing doesn't seem to work. The okta Security, SecureRoute, and LoginCallback, all trigger test failures (element is undefined, etc).
I got it running this way: I figure I don't need to test any of those elements, I just need to test if my app is rendering. So I mocked everything and this passed the test without undue hackery.
Hope this helps, I see a lot of people out there struggle with this one:
import {cleanup, screen, waitFor} from '#testing-library/react';
import App from './App';
import {render} from "react-dom";
import {BrowserRouter} from "react-router-dom";
jest.mock('#okta/okta-react', () => {
return ({
useOktaAuth: () => ({
authState: {isAuthenticated: true},
authService: {handleAuthentication: jest.fn()},
oktaAuth: {getUser: () => new Promise((resolve, reject) => { resolve('foo')})},
}),
withOktaAuth: (x: any) => x,
Security: () => <div></div>,
SecureRoute: () => <div></div>,
LoginCallback: () => <div></div>
});
});
describe('<App />', () => {
let container: any = null;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(cleanup);
test("Render App", async () => {
render(<BrowserRouter><App /></BrowserRouter>, container);
const comp = await screen.findByTestId('App', container);
expect(comp).toBeInTheDocument();
});
});
The thing is you have to await for Okta asynchronous updates in the component.
You could just wrap your "expects" block with await waitFor(() => { } assuming you're using React Testing Library.
Example:
await waitFor(() => {
expect(getAllByText(/Home/i)[0]).toBeInTheDocument;
expect(getAllByText(/Home/i)[1]).toBeInTheDocument;
});

Passing useState Hook into another function on React

I want to pass in a boolean value in a useState hook that opens and closes a modal on click between two functions. However, I keep getting this error message: Cannot destructure property 'setOpenModal' of 'props' as it is undefined.
Main.js
import React, { useState, useEffect } from "react";
import * as Materials from "../components/Materials"; // <- Material.js
const Main = () => {
const [openModal, setOpenModal] = useState(false); //<- Opens (True) and Closes (False) Modal
const { MaterialContainer } = Materials.MaterialTable(); // <-Calling Function Under MaterialTable
return (
<MaterialContainer
openModal={openModal}
setOpenModal={setOpenModal}
/>
// This is how I am passing in Open/Close useState.
}
Material.js
export const MaterialTable = (props) => {
const { openModal, setOpenModal } = props; // <- Pointed in Error Message.
const openMaterialModal = (item) => {
console.log("Button Clicked");
setOpenModal(true); // <- Where I am passing in a true statement.
};
const MaterialContainer = () => (
<>
<Table>Stuff</Table>
</>
);
return {
MaterialContainer
}
}
Thanks in advance.
The MaterialTable component is entirely malformed from a React perspective, though valid JavaScript. It's just a normal function that defines a couple of constants and then returns nothing. (Well, in the original question it returned nothing. Now it returns an object.)
And when you call that function you indeed don't pass anything to it:
const { MaterialContainer } = Materials.MaterialTable();
So the props will be undefined.
Make MaterialTable itself a React component:
export const MaterialTable = (props) => {
// destructure the props passed to the component
const { openModal, setOpenModal } = props;
// a function I assume you plan to use in the JSX below later?
const openMaterialModal = (item) => {
console.log("Button Clicked");
setOpenModal(true);
};
// the rendering of the component
return (
<>
<Table>Stuff</Table>
</>
);
}
Then just import and use that component, without trying to destructure anything from it or invoke it manually:
import React, { useState, useEffect } from "react";
// import the component
import { MaterialTable } from "../components/Materials";
const Main = () => {
// use the state hook
const [openModal, setOpenModal] = useState(false);
// render the component, passing props
return (
<MaterialTable
openModal={openModal}
setOpenModal={setOpenModal}
/>
);
}

Unit testing error with act and useState, Making an API call in useEffect

I have a react functional component where onload of the App I am smoking an API call which I have wrapped in useEffect and when the data is received I am updating the useState variable.
While writing a unit test case for the below component using jest and react testing library I am receiving act error and useState variable error.
Code:
import React, { useState, useEffect } from "react";
import TextField from "#material-ui/core/TextField";
import Media from "./card";
import Alert from "#material-ui/lab/Alert";
import fetchData from "../API";
const Dashboard = () => {
const [searchInput, setSearchInput] = useState(null);
const [receipeNotFound, setReceipeNotFound] = useState(false);
useEffect(() => {
fetchData(
`https://www.themealdb.com/api/json/v1/1/random.php`,
randomReceipe
);
}, []);
const randomReceipe = (result) => {
result.meals ? setSearchInput(result.meals[0]) : setSearchInput(null);
};
const searchData = (value) => {
if (value) {
setSearchInput(null);
setReceipeNotFound(false);
fetchData(
`https://www.themealdb.com/api/json/v1/1/search.php?s=${value}`,
searchRecepie
);
} else {
setSearchInput(null);
}
};
const notFound = (status) => {
setReceipeNotFound(status);
setSearchInput(null);
};
const searchRecepie = (result) => {
result.meals ? setSearchInput(result.meals[0]) : notFound(true);
};
return (
<div>
<TextField
data-testid="searchInput"
id="standard-basic"
label="Search"
onBlur={(event) => searchData(event.currentTarget.value)}
/>
{receipeNotFound ? (
<Alert data-testid="alert" severity="error">
Receipe not found!
</Alert>
) : null}
{Boolean(searchInput) ? (
<Media data-testid="mediaLoading" loading={false} data={searchInput} />
) : (
<Media data-testid="Media" loading={true} data={{}} />
)}
</div>
);
};
export default Dashboard;
Test:
import React from "react";
import Dashboard from "./dashboard";
import ReactDOM from "react-dom";
import {
render,
screen,
act,
waitFor,
fireEvent,
} from "#testing-library/react";
import { randomMockWithOutVideo, randomMockWithVideo } from "./API.mock";
global.fetch = jest.fn();
let container;
describe("Card ", () => {
test("Should renders data when API request is called with meals data", async (done) => {
jest.spyOn(global, "fetch").mockResolvedValue({
json: jest.fn().mockResolvedValue({ meals: [randomMockWithVideo] }),
});
const { getByTestId, container } = render(<Dashboard />);
expect(global.fetch).toHaveBeenCalledTimes(1);
expect(global.fetch).toHaveBeenCalledWith(
`https://www.themealdb.com/api/json/v1/1/random.php`
);
});
afterEach(() => {
jest.restoreAllMocks();
});
});
error:
Warning: An update to Dashboard inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
19 | result.meals ? setSearchInput(result.meals[0]) : setSearchInput(null);
| ^
This warning indicates that an update has been made to your component (through useState) after it was tear down. You can read a full explanation by Kent C. Dodds here : https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning/.
I would try this syntax to get rid of the warning :
await act(() => {
expect(global.fetch).toHaveBeenCalledTimes(1);
expect(global.fetch).toHaveBeenCalledWith(`https://www.themealdb.com/api/json/v1/1/random.php`);
});

Test react hooks state using Jest and React Hooks Library

I nav component then will toggle state in a sidebar as well as open and close a menu and then trying to get this pass in code coverage. When I log inside my test my state keeps showing up as undefined. Not sure how to tackle this one here.
Component.js:
const Navigation = (props) => {
const {
classes,
...navProps
} = props;
const [anchorEl, setanchorEl] = useState(null);
const [sidebarOpen, setsidebarOpen] = useState(false);
const toggleSidebar = () => {
setsidebarOpen(!sidebarOpen);
};
const toggleMenuClose = () => {
setanchorEl(null);
};
const toggleMenuOpen = (event) => {
setanchorEl(event.currentTarget);
};
return (
<Fragment>
<Button
onClick={toggleMenuOpen}
/>
<SideMenu
toggleSidebar={toggleSidebar}
>
<Menu
onClose={toggleMenuClose}
>
</SideMenu>
</Fragment>
);
};
export default Navigation;
Test.js:
import { renderHook, act } from '#testing-library/react-hooks';
// Components
import Navigation from './navigation';
test('sidebar should be closed by default', () => {
const newProps = {
valid: true,
classes: {}
};
const { result } = renderHook(() => Navigation({ ...newProps }));
expect(result.current.sidebarOpen).toBeFalsy();
});
Author of react-hooks-testing-library here.
react-hooks-testing-library is not for testing components and interrogating the internal hook state to assert their values, but rather for testing custom react hooks and interacting withe the result of your hook to ensure it behaves how you expect. For example, if you wanted to extract a useMenuToggle hook that looked something like:
export function useMenuToggle() {
const [anchorEl, setanchorEl] = useState(null);
const [sidebarOpen, setsidebarOpen] = useState(false);
const toggleSidebar = () => {
setsidebarOpen(!sidebarOpen);
};
const toggleMenuClose = () => {
setanchorEl(null);
};
const toggleMenuOpen = (event) => {
setanchorEl(event.currentTarget);
};
return {
sidebarOpen,
toggleSidebar,
toggleMenuClose,
toggleMenuOpen
}
}
Then you could test it with renderHook:
import { renderHook, act } from '#testing-library/react-hooks';
// Hooks
import { useMenuToggle } from './navigation';
test('sidebar should be closed by default', () => {
const newProps = {
valid: true,
classes: {}
};
const { result } = renderHook(() => useMenuToggle());
expect(result.current.sidebarOpen).toBeFalsy();
act(() => {
result.current.toggleSidebar()
})
expect(result.current.sidebarOpen).toBeTruthy();
});
Generally though, when a hook is only used by a single component and/or in a single context, we recommend you simply test the component and allow the hook to be tested through it.
For testing your Navigation component, you should take a look at react-testing-library instead.
import React from 'react';
import { render } from '#testing-library/react';
// Components
import Navigation from './navigation';
test('sidebar should be closed by default', () => {
const newProps = {
valid: true,
classes: {}
};
const { getByText } = render(<Navigation {...newProps} />);
// the rest of the test
});

Resources