How to test component properly using UseState - reactjs

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"
})

Related

Using context in react component tests

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.

Router.push makes page flash and changes url to localhost:3000/?

After pushing a route in NextJS the path seems to be valid for a split of a second http://localhost:3000/search?query=abc and then changes to http://localhost:3000/?. Not sure why this is happening.
I have tried it with both import Router from 'next/router' and import { useRouter } from 'next/router'. Same problem for both import types.
Here's my component and I use the route.push once user submits a search form.
import React, { useEffect, useState } from "react";
import Router from 'next/router';
const SearchInput = () => {
const [searchValue, setSearchValue] = useState("");
const [isSearching, setIsSearching] = useState(false);
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isSearching) {
Router.push({
pathname: "/search",
query: { query: searchValue },
});
setIsSearching(false);
}
}, [isSearching, searchValue]);
const handleSearch = () => {
if (searchValue) {
setIsSearching(true);
}
};
return (
<form onSubmit={handleSearch}>
<input
value={searchValue}
onChange={(event) => setSearchValue(event.target.value)}
placeholder="Search"
/>
</form>
);
};
The default behavior of form submissions to refresh the browser and render a new HTML page.
You need to call e.preventDefault() inside handleSearch.
const handleSearch = (e) => {
e.preventDefault()
if (searchValue) {
setIsSearching(true);
}
};

Testing debounced function React - React-testing-library

I have the following component
import React, { useState, useEffect } from 'react';
import { FiSearch } from 'react-icons/fi';
import { useProducts } from '../../hooks';
export default function SearchBar() {
const [query, setQuery] = useState('');
const [debounced, setDebounced] = useState('');
useEffect(() => {
const timeout = setTimeout(() => {
setDebounced(query);
}, 300);
return () => {
clearTimeout(timeout);
};
}, [query]);
const handleChange = (e) => {
e.preventDefault();
setQuery(e.target.value);
};
useProducts(debounced);
return (
<div className="search-form">
<FiSearch className="search-form__icon" />
<input
type="text"
className="search-form__input"
placeholder="Search for brands or shoes..."
onChange={handleChange}
value={query}
/>
</div>
);
}
I want to test if useProducts(debounced); is actually called 300ms later after typing. I sadly have no Idea where to begin and was hoping someone could help.
I'd recommend using #testing-library/user-event for typing on the <input> element, as it more closely simulates a user's triggered events.
As for the test, you should mock useProducts implementation to assert that it's called properly.
import React from 'react';
import { render, screen, waitFor } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
import SearchBar from '<path-to-search-bar-component>'; // Update this accordingly
import * as hooks from '<path-to-hooks-file>'; // Update this accordingly
describe('Test <SearchBar />', () => {
it('should call useProducts after 300ms after typing', async () => {
const mockHook = jest.fn();
jest.spyOn(hooks, 'useProducts').mockImplementation(mockHook);
render(<SearchBar />);
const input = screen.getByPlaceholderText('Search for brands or shoes...');
userEvent.type(input, 'A');
expect(mockHook).not.toHaveBeenCalledWith('A'); // It won't be called immediately
await waitFor(() => expect(mockHook).toHaveBeenCalledWith('A'), { timeout: 350 }); // But will get called within 350ms
jest.clearAllMocks();
});
});

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`);
});

How to mock out a response triggered by a function on the same component in a wrapper?

My component has a function which is triggered when a save button is clicked. Then based on that a fetch is done in the wrapper and the fetch response is then again passed down as a prop. So the putFn property accepts a function, the putResponse accepts a Promise.
I would like to mock the wrapper and focus in this test just on the component, in this example "myComponent".
Given the following test setup:
./MyComponent.test.js
function setup() {
let mockPutResponse;
const putMockFn = jest.fn(() => {
mockPutResponse = Promise.resolve(
JSON.stringify({ success: true, loading: false })
);
});
render(<MyComponent putFn={putMockFn} putResponse={mockPutResponse} />);
return { putMockFn };
}
test("MyComponent saves the stuff", async () => {
const { putMockFn } = setup();
const button = screen.getByRole("button", { name: /save changes/i });
userEvent.click(button);
// this passes
expect(putMockFn).toHaveBeenCalled();
// this does not pass since the component shows this message
// based on the putResponse property
expect(await screen.findByText(/saved successfully/i)).toBeInTheDocument();
});
How can I mock the return value passed into the putResponse property?
The component I want to test is something in the line of this:
./MyComponent.js
import React from "react";
const MyComponent = ({ putFn, putResponse }) => {
return (
<form onSubmit={putFn}>
{putResponse?.loading && <p>Loading...</p>}
{putResponse?.success && <p>saved succesfully</p>}
<label htmlFor="myInput">My input</label>
<input name="myInput" id="myInput" type="text" />
<button>Save changes</button>
</form>
);
};
export default MyComponent;
Which is used by a kind of wrapper, something similar to:
./App.js (arbitrary code)
import React, { useState } from "react";
import MyComponent from "./MyComponent";
export default function App() {
const [wrapperPutResponse, setWrapperPutResponse] = useState();
const handlePut = e => {
e.preventDefault();
setWrapperPutResponse({ loading: true });
// timeout, in the actual app this is a fetch
setTimeout(function() {
setWrapperPutResponse({ success: true, loading: false });
}, 3000);
};
return <MyComponent putFn={handlePut} putResponse={wrapperPutResponse} />;
}
Created a sandbox: codesandbox.io/s/bold-cloud-2ule8?file=/src/MyComponent.test.js
You can create a Wrapper component to render and control MyComponent
import React, { useState, useEffect } from "react";
import { screen, render } from "#testing-library/react";
import userEvent from "#testing-library/user-event";
import MyComponent from "./MyComponent";
const mockPutResponse = jest.fn()
function setup() {
const Wrapper = () => {
const [clicked, setClicked] = useState(false)
const response = clicked ? { success: true, loading: false} : null
useEffect(() => {
mockPutResponse.mockImplementation(() => {
setClicked(true)
})
}, [])
return <MyComponent putFn={mockPutResponse} putResponse={response} />
}
render(<Wrapper />);
}
test("MyComponent saves the stuff", async () => {
setup()
// expect(await screen.findByText(/loading.../i)).toBeInTheDocument();
const button = screen.getByRole("button", { name: /save changes/i });
userEvent.click(button);
// this passes
expect(mockPutResponse).toHaveBeenCalled();
// had some issue with expect acting up when using the toBeInDocument assertion
// so I used this one instead
const text = await screen.findByText(/saved succesfully/i)
expect(text).toBeTruthy()
});
Codesandbox

Resources