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();
});
});
Related
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"
})
I've got a React component that returns an iframe and handles sending messages to and receiving messages from the iframe. I am totally stumped as to how I can mock the iframe and simulate a message being posted in the iframe with React Testing Library. Any ideas or examples would be very helpful.
Component:
const ContextualHelpClient: React.VFC<CHCProps> = ({ onAction, data }) => {
const [iframe, setIframe] = React.useState<HTMLIFrameElement | null>(null);
const handleMessage = React.useCallback((event: MessageEvent<ContextualHelpAction>) => {
onAction(event.data);
}, [onAction])
const contentWindow = iframe?.contentWindow;
React.useEffect(() => {
if (contentWindow) {
contentWindow.addEventListener('message', handleMessage);
return () => contentWindow.removeEventListener('message', handleMessage);
}
}, [handleMessage, contentWindow]);
React.useEffect(() => {
if (contentWindow) {
contentWindow.postMessage({ type: CONTEXTUAL_HELP_CLIENT_DATA, data }, "/");
}
}, [data, contentWindow]);
return <iframe src={IFRAME_SOURCE} width={300} height={300} ref={setIframe} title={IFRAME_TITLE} />;
};
Test:
import React from 'react';
import { render, screen, fireEvent } from '#testing-library/react';
import ContextualHelpClient from '../ContextualHelpClient';
import { IFRAME_SOURCE, IFRAME_TITLE } from '../constants';
describe('<ContextualHelpClient />', () => {
it('should call onAction when a message is posted', () => {
const handleAction = jest.fn();
const content = render(<ContextualHelpClient onAction={handleAction} />);
const iframe = content.getByTitle(IFRAME_TITLE);
fireEvent(iframe.contentWindow, new MessageEvent('data'));
expect(handleAction).toBeCalled(); // fails
});
});
In my case, using the message type on fireEvent was enough to test this.
import React from 'react';
import { render, screen, fireEvent } from '#testing-library/react';
import ContextualHelpClient from '../ContextualHelpClient';
import { IFRAME_SOURCE, IFRAME_TITLE } from '../constants';
describe('<ContextualHelpClient />', () => {
it('should call onAction when a message is posted', () => {
const handleAction = jest.fn();
const content = render(<ContextualHelpClient onAction={handleAction} />);
const iframe = content.getByTitle(IFRAME_TITLE);
fireEvent(
window,
new MessageEvent('message', {origin: window.location.origin}),
);
expect(handleAction).toBeCalled(); // fails
});
});
Solved!
it('should call onAction when a message is posted', async () => {
const handleAction = jest.fn();
const content = render(<ContextualHelpClient onAction={handleAction} />);
const iframe = content.getByTitle(IFRAME_TITLE) as HTMLIFrameElement;
const TEST_MESSAGE = 'TEST_MESSAGE';
// https://github.com/facebook/jest/issues/6765
iframe.contentWindow?.postMessage(TEST_MESSAGE, window.location.origin);
await waitFor(() => expect(handleAction).toBeCalledWith(TEST_MESSAGE));
});
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`);
});
im trynna test my code, but it gives an error "TypeError: _react2.fireEvent.submit(...) is not a function", I dont know how to fix it, This error appears when i put the **"({ getByText, getByTestId, getByLabelText } = render(<TechList />)) "**, in the second test.
I need to test if the text "Node.js" stay in the component even when i rerender it.
My test code below:
import '#testing-library/jest-dom/extend-expect'
import React from 'react'
import { render, fireEvent } from '#testing-library/react'
import TechList from '../components/Techlist'
describe('TechList component', () => {
it('should be able to add new Tech', () => {
const { getByText, getByTestId, getByLabelText } = render(<TechList />)
fireEvent.change(getByLabelText('Tech'), { target: { value: 'Node.js' }})
fireEvent.submit(getByTestId('tech-form'))
expect(getByTestId('tech-list')).toContainElement(getByText('Node.js'))
expect(getByLabelText('Tech')).toHaveValue('')
})
it('should store techs in storage', () => {
let { getByText, getByTestId, getByLabelText} = render(<TechList />)
fireEvent.change(getByLabelText('Tech'), { target: { value: 'Node.js' }})
fireEvent.submit(getByTestId('tech-form'))
({ getByText, getByTestId, getByLabelText } = render(<TechList />))
expect(getByTestId('tech-list')).toContainElement(getByText('Node.js'))
})
})
and the Component:
import React, { useState } from 'react';
function Component() {
const [techs, setTechs] = useState([])
const [newTech, setNewTech] = useState('')
function handleAddTech() {
setTechs([...techs, 'Node.js'])
setNewTech('')
}
return (
<form data-testid="tech-form" onSubmit={handleAddTech} >
<ul data-testid="tech-list">
{techs.map(tech => <li key={tech}>{tech}</li>)}
</ul>
<label htmlFor="tech">Tech</label>
<input id="tech" type="text" value={newTech} onChange={e => setNewTech(e.target.value)}/>
<button type="submit" onClick={handleAddTech}>Adicionar</button>
</form>
)
}
export default Component;
In your second test code, the line ({ getByText, getByTestId, getByLabelText } = render(<TechList />)) is breaking it. Delete it and it should be working.
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