Jest/Enzyme Test Function Call in Callback - javascript

Problem: I am trying to achieve 100% test coverage using Jest with my React component, but it just doesn't seem to detect the function being called. I tried to extract out only the relevant parts below:
LinkedRepos.js:
import { auth } from "../../firebase";
import React, { useEffect } from "react";
import { navigate } from "#reach/router";
const ReposPage = () => {
React.useEffect(() => {
auth.onAuthStateChanged(function (user) {
if (!user) {
console.log("DEBUGGING NO USER");
navigate("/");
}
});
}, []);
};
export default ReposPage;
LinkedRepos.test.js
import React from "react";
import { shallow } from "enzyme";
import ReposPage from "../Components/Pages/LinkedRepos";
import { navigate } from "#reach/router";
jest.mock('#reach/router', () => ({
navigate: jest.fn(),
}))
describe("Linked repos page", () => {
it("not logged in", () => {
jest.clearAllMocks();
jest.spyOn(React, 'useEffect').mockImplementation(f => f());
const repopage = shallow(<ReposPage />);
expect(navigate).toHaveBeenCalledTimes(1);
})
});
The test output:
● Linked repos page › not logged in
expect(jest.fn()).toHaveBeenCalledTimes(expected)
Expected number of calls: 1
Received number of calls: 0
....
console.log src/Components/Pages/LinkedRepos.js:14
DEBUGGING NO USER
I can't seem to find any answers that apply to my problem. I know my code is quite bad, but does anyone know the reason why it won't detect the "navigate" function being called? Clearly, it is reaching that point in the code, since it is printing out my debugging message. I just can't seem to wrap my head around testing.

It turns out I justed needed an await statement for Jest to recognise that navigate was being called. The final code is something like this:
it("should call navigate once if not logged in", async () => {
await mount(<ReposPage />);
expect(navigate).toHaveBeenCalledTimes(1);
});

Related

Getting undefined from state while using useEffect and useState

My issue here is that sometimes my projects state is returning undefined sometimes. I am not sure why. As you can see, in the useEffect I have a function that gets project data from an API call to my backend server. This will then return an array of projects, which I then planned to see in the dom in the return statement. However, for whatever reason, upon the initial render it gives me an undefined and the screen goes white.
Strangely, enough, if I change the return statement to just display a regular string, let's say "hello" for example, save, and then change it back to {projects[0].name} it will then work. Yet on initial render I am getting a Uncaught TypeError: Cannot read properties of undefined (reading 'name');
I will add that I am getting a 304 status from my server in the console but that is because the data has not changed and thus I am receiving the previous UI from local storage if I remember correctly. This is not an issue with other parts of my application so I do not know why it would be an issue here.
import { useEffect, useState } from "react";
import { fetchPage } from "./../store/actions"
import { connect } from "react-redux"
/*import { ProjectCard } from "./../components/ProjectCard"*/
import API from './../api/API'
const Projects = ({ fetchPage }) => {
const [projects, setProjects] = useState([])
useEffect(() => {
const getProjectData = async () => {
try {
const { data } = await API.getAllProjects()
setProjects(data.data)
} catch (err) {
console.log(err);
}
}
fetchPage('Projects', "Here are your projects")
getProjectData()
}, [fetchPage])
return (<div>
{projects[0].name}
</div>)
}
export default connect(null, { fetchPage })(Projects);
Here is a different part of my application that works more or less the same way
const [users, setUsers] = useState([])
useEffect(() => {
const getUserData = async () => {
const { data } = await axios.get('/api/v1/users', {
headers: {
'Content-type': 'application/json'
}
})
setUsers(data.data.data)
}
fetchPage("TEAM", "Here is your team");
getUserData();
}, [fetchPage])
I tried removing the action creator which I expected did not work

How can I write a unit test to check that a React component method calls a Firebase auth method?

I'm building a React web app with a Firebase backend. I'm cool with React and Firebase, and I'm able to implement all the functionality I need without issue.
However, I'm also using this project as an opportunity to learn unit testing, currently using React Testing Library and Jest.
I can successfully test for rendered UI elements. What I'm struggling to get my head around is how I can test for a successful call of the Firebase API, for example with the below form component which allows a user to request a password reset email.
So, as of now, I'm able to successfully test:
if the form is rendered correctly
if the correct UI (warning) elements are rendered when the form is submitted with an invalid or missing email address
What I also want to be able to test for is:
if the sendPasswordResetEmail method is called when the form is submitted with a valid email address
I've stripped the component back to its most basic form (ignoring validation, etc, for now) to demonstrate. Users enter an email address, submit the form, and the Firebase method sendPasswordResetEmail is called.
My question is: how can I write a test that checks whether the sendPasswordResetEmail method is called successfully on form submit?
Is it even possible to test for this case?
Is this even the right question to ask? I'm super new to unit testing. :o)
Any suggestions?
Thanks!
import React, { useState } from "react";
import { auth } from "../../lib/firebaseApp";
import { sendPasswordResetEmail } from "firebase/auth";
const PasswordResetForm = (props) => {
const [email, setEmail] = useState(null);
const handleChange = (e) => {
setEmail(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
sendPasswordResetEmail(auth, email)
.then(() => {
//Email sent
})
.catch((error) => {
console.log(error);
});
};
return (
<form noValidate onSubmit={handleSubmit}>
<input type="text" name="email" value={email || ""} onChange={handleChange} />
<SubmitButton buttonText="Reset password" />
</form>
);
};
export default PasswordResetForm;
Here's a test I've written for one of the UI elements (for info only):
import { render, fireEvent, screen } from "#testing-library/react";
import { BrowserRouter } from "react-router-dom";
import userEvent from "#testing-library/user-event";
import "#testing-library/jest-dom";
//Component
import PasswordResetForm from "../../../components/forms/passwordresetform";
describe("Password reset form", () => {
it("Should render an email field", () => {
render(<PasswordResetForm />, { wrapper: BrowserRouter });
const input = screen.getByRole("textbox", { name: "email" });
expect(input).toBeInTheDocument();
});
});
UPDATE:
I've also now attempted to write a test to check whether the sendPasswordResetEmail method is called successfully:
import React from "react";
import { render, fireEvent, screen } from "#testing-library/react";
import { BrowserRouter } from "react-router-dom";
import userEvent from "#testing-library/user-event";
import "#testing-library/jest-dom";
import { getAuth, sendPasswordResetEmail } from "firebase/auth";
//Component
import PasswordResetForm from "../../../components/forms/passwordresetform";
//Mock firebase
jest.mock('firebase/auth', () => {
return {
getAuth: () => jest.fn(),
sendPasswordResetEmail: () => jest.fn().mockResolvedValue({}),
}
});
//UI tests
describe("Password reset form", () => {
it("Should allow good email addresses and call sendPasswordResetEmail", async () => {
render(<PasswordResetForm />, { wrapper: BrowserRouter });
const input = screen.getByRole("textbox", { name: "email" });
const button = screen.getByRole("button", { name: "submit" });
const value = "user#domain.com";
userEvent.type(input, value);
fireEvent.click(button);
expect(sendPasswordResetEmail).toBeCalledWith(getAuth(), value);
});
});
However, this is giving me the following error:
TypeError: (0 , _auth.sendPasswordResetEmail)(...).then is not a function
42 | if (dataValid) {
43 | sendPasswordResetEmail(auth, email)
> 44 | .then(() => {
| ^
So, the question I now have is am I using jest.fn().mockResolvedValue({}) in the right way?
And if so, why isn't it working?

Keep getting error message about avoiding wrapping code in act after wrapping code in act

I'm writing a test for a component that uses a search form. This is in search.js and it imports a Formik form from another file searchForm.js.
I've written this unit test which passes. I've based it off an example in the react testing docs here.
Every time I run it I get this error, which confuses me since I've wrapped all the code in act?
console.error
Warning: An update to Formik 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 */
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
import React from "react";
import { render, screen, act, fireEvent } from "#testing-library/react";
import Search from "./search.js";
import { ChakraProvider } from "#chakra-ui/react";
test("Search form input", () => {
act(() => {
const setup = () => {
const utils = render(
<ChakraProvider>
<Search />
</ChakraProvider>
);
const searchTermsInput = utils.getByLabelText("search terms input");
return {
searchTermsInput,
...utils,
};
};
const { searchTermsInput } = setup();
fireEvent.change(searchTermsInput, { target: { value: "23" } });
});
expect(screen.getByLabelText("search terms input").value).toBe("23");
});
As per the comment above, fixed this by rewriting the test as follows to allow for some other changes to state that happen in the search.js component which weren't part of this specific test.
import React from "react";
import { render, screen, act, fireEvent, waitFor } from "#testing-library/react";
import Search from "./search.js";
import { ChakraProvider } from "#chakra-ui/react";
test("Search form input", async () => {
act(() => {
const setup = () => {
const utils = render(
<ChakraProvider>
<Search />
</ChakraProvider>
);
const searchTermsInput = utils.getByLabelText("search terms input");
return {
searchTermsInput,
...utils,
};
};
const { searchTermsInput } = setup();
fireEvent.change(searchTermsInput, { target: { value: "23" } });
});
expect(screen.getByLabelText("search terms input").value).toBe("23");
// This removes a code not wrapped in act warning triggered by other state events
// namely setQuery which is called after adding search term inputs
await waitFor(() => screen.queryByText(/Find results/))});

How to reset redux store after each test using #testing-library/react in Next.js?

I'm trying to get #testing-framework/react integrated into my Next.js workflow. To do that, I created a test-utils.js file as suggested in the documentation, where I re-export the render method with all my providers:
import React from 'react';
import { render } from '#testing-library/react';
import { ChakraProvider, ColorModeProvider } from '#chakra-ui/react';
import { Provider as ReduxStore } from 'react-redux';
import { useStore } from './lib/init-store';
import theme from './lib/theme';
const providersWrapper = ({ children }) => {
// As pageProps.initialReduxState is undefined in _app.js
const store = useStore(undefined);
return (
<ReduxStore store={store}>
<ChakraProvider resetCSS theme={theme}>
<ColorModeProvider
options={{
useSystemColorMode: true,
}}>
{children}
</ColorModeProvider>
</ChakraProvider>
</ReduxStore>
);
};
const customRender = (ui, options) =>
render(ui, { wrapper: providersWrapper, ...options });
// re-export everything
export * from '#testing-library/react';
// override render method
export { customRender as render };
On the other hand, I have a Counter component and its tests:
import React from 'react';
import { render, fireEvent, act, cleanup } from '../../test-utils';
import Counter from './index';
describe('Counter works properly', () => {
test('it should increment count when +1 button is clicked', async () => {
await act(async () => {
const { findByText } = render(<Counter />);
const initialCount = await findByText('0');
expect(initialCount).toBeInTheDocument();
const incrementButton = await findByText('+');
fireEvent.click(incrementButton);
const incrementedCount = await findByText('1');
expect(incrementedCount).toBeInTheDocument();
cleanup();
});
});
test('it should decrement count when -1 button is clicked', async () => {
await act(async () => {
const { findByText } = render(<Counter />);
const initialCount = await findByText('0');
expect(initialCount).toBeInTheDocument();
const decrementButton = await findByText('-');
fireEvent.click(decrementButton);
const decrementedCount = await findByText('-1');
expect(decrementedCount).toBeInTheDocument();
cleanup();
});
});
});
The jest setup works perfectly, but the second test, can't find a 0 text, because the state of the first test persists. I've confirmed this by swapping them resulting in the same: The first test passes, but the second fails.
This is weird, since the Testing Library documentation explicitly says that cleanup is called after each test automatically and by default (https://testing-library.com/docs/react-testing-library/setup/?fbclid=IwAR0CgDKrHalIhEUAEuP5S355uVYkTScMBATSIMgMPFcOz4ntsNCqgRA3Jyc#skipping-auto-cleanup).
So I'm a little bit lost here, I even tried to pull the celanup function out of render in both tests, and adding a cleanup() at the end of each test, having no different result.
Because of that, I thing that testing library is not the problem, instead, the redux store is not resetting after each test, causing the second test to read the state of the first. But I can't 100% prove it, and even if I could, I don't know how i'd reset it manually, since cleanup is supposed to happen automatically.
If you are curious, you can read the code of the whole project in this repo: https://github.com/AmetAlvirde/testing-library-redux-next-question/
I really hope you can help me, since being unable to test my code is something I really don't want to live with.
Thank you so much in advance.

How can I test a component created by asynchronous call in componentDidMount?

I'm making a GET request to my API http://localhost:3001/api/cards from the componentDidMount function of a component, so that the api request is made only after the component is rendered for the first time (As suggested by react official guide).
This API sets the state of an array data. In render function, I call data.map function to render multiple components from this array. How should I test whether the desired number of components have been rendered?
My component:
//CardGrid.js
import React from 'react';
import { Card, Col, Row } from 'antd';
import 'antd/dist/antd.css';
import { parseJSON } from './commonfunction';
import './CardGrid.css';
export default class extends React.Component {
constructor()
{
super();
this.state = {
data: {},
};
}
fetchData = async () => {
try
{
const data = await parseJSON(await fetch('http://localhost:3001/api/cards'));
console.log('data');
console.log(data);
this.setState({ data });
}
catch (e)
{
console.log('error is: ');
console.log(e);
}
}
componentDidMount() {
this.fetchData();
}
render() {
return (
<div style={{ background: '#ECECEC', padding: '30px' }}>
<Row gutter={16}>
{Object.keys(this.state.data).map((title) => {
return (<Col span="6" key={title}>
<Card title={title} bodyStyle={{
'fontSize': '6em',
}}>{this.state.data[title]}</Card>
</Col>);
})}
</Row>
</div>
);
}
};
Now I want to check if there are as many Card components being rendered as specified by my API.
I tried this by first mocking the fetch function to return 1 element. Then I use Full DOM Rendering of enzyme and mount the above component and expect it to contain 1 element.
Test case:
// It fails
import React from 'react';
import { Card } from 'antd';
import { mount } from 'enzyme';
import CardGrid from './CardGrid';
it('renders 1 Card element', () => {
fetch = jest.fn().mockImplementation(() =>
Promise.resolve(mockResponse(200, null, '{"id":"1234"}')));
const wrapper = mount(<CardGrid />);
expect(fetch).toBeCalled();
expect(wrapper.find(CardGrid).length).toEqual(1);
expect(wrapper.find(Card).length).toEqual(1);
});
All the tests are passing except that it can't find Card element. Even the fetch mock function is called. It fails until I put a setTimeout function before I try to find Card component.
//It succeeds
import React from 'react';
import { Card } from 'antd';
import { mount } from 'enzyme';
import sinon from 'sinon';
import CardGrid from './CardGrid';
it('renders 1 Card elements', async () => {
fetch = jest.fn().mockImplementation(() =>
Promise.resolve(mockResponse(200, null, '{"id":"1234"}')));
const wrapper = mount(<CardGrid />);
expect(fetch).toBeCalled();
expect(wrapper.find(CardGrid).length).toEqual(1);
await setTimeoutP();
expect(wrapper.find(Card).length).toEqual(1);
});
function setTimeoutP () {
return new Promise(function (resolve, reject) {
setTimeout(() => {
console.log('111111111');
resolve();
}, 2000);
});
}
Is there any concept I'm failing to understand? How should I ideally test such asynchronously loading components? How can I better design them to be easily testable? Any help would be greatly appreciated. Thanks
You have to wait for the resolved promise of your fetch result and for the promise from the parseJSON. Therefor we need to mock parseJSON and let it return a resolved promise as well. Please note that the path needs to be relative to the test file.
import {parseJSON} from './commonfunction'
jest.mock('./commonfunction', () => {parseJSON: jest.fn()}) //this will replace parseJSON in the module by a spy were we can later on return a resolved promise with
it('renders 1 Card elements', async () => {
const result = Promise.resolve(mockResponse(200, null, '{"id":"1234"}'))
parsedResult = Promise.resolve({"id":"1234"})
parseJSON.mockImplementation(()=>parsedResult)
fetch = jest.fn(() => result)
const wrapper = mount(<CardGrid />);
await result;
await parsedResult;
expect(fetch).toBeCalled();
expect(wrapper.find(CardGrid).length).toEqual(1);
expect(wrapper.find(Card).length).toEqual(1);
});

Categories

Resources