Vitest React - component not "re-rendering" after server request - reactjs

This is my simplifiedReact component:
export const EntryDetail = () => {
const { articleId } = useParams();
const [article, setArticle] = useState({ title: null, body: null, comments: [], likes: [] });
const { title, body, comments, likes } = article;
useEffect(() => {
(async () => {
try {
const response = await getArticleDetail(articleId);
const { title, body, comments, likes } = response.data;
setArticle({ title, body, comments, likes });
} catch (e) {
console.error(e);
}
})();
}, []);
return (
<Container>
{
!article.title
? <div>Loading...</div>
: <>
<h1>{title}</h1>
<p className="body">{body}</p>
</>
}
</Container>
);
};
And this is my test:
import { render, screen } from '#testing-library/react';
import { StateProvider } from '../../config/state';
import { EntryDetail } from './index';
const flushPromises = () => new Promise(resolve => setTimeout(resolve, 0));
vi.mock('react-router-dom', () => ({
useParams: () => ({
articleId: '63d466ca3d00b50db15aed93',
}),
}));
describe("EntryDetail component", () => {
it("should render the EntryDetail component correctly", async () => {
render(
<EntryDetail />
);
await flushPromises();
const element = screen.getByRole("heading");
expect(element).toBeInTheDocument();
});
});
This is what I'm getting in the console:
I was expecting the "await flushPromises()" would actually wait for the response from the call in the useEffect to the "update" the component", but I guess this is kind of "static"? How should this be handled? I actually want to test if the component itself works effectively, I don't want to mock a response, I want to see if the component actually reacts appropriately after the response is back.

Related

fireEvent.click(button) causing error **Call retries were exceeded at ChildProcessWorker.initialize**

One of my unit tests is failing when I'm trying to fire a click event on a component. The component is being rendered and is enabled.
//component
import {makeEncryptedCall} from '../../foo';
const MyComponent = (props) => {
const onRedirection = async () => {
const param = {foo: 'bar'};
return await c(param)
.then((data) => {
history.push('/some-url');
});
};
return (
<>
<button
onClick={onRedirection}
data-testid='my-button'
/>
</>
)
}
// test
it('should fire redirection flow', () => {
jest.mock('../../foo', () => {
return {
makeEncryptedCall: jest.fn(() => {
const response = {
ok: true,
json: () => {
Promise.resolve({
data: 'superEncryptedStuff';
});
}
};
return Promise.resolve(response);
});
}
});
const component = screen.getByTestId('my-button');
expect(component).toBeEnabled();
fireEvent.click(component);
});
I tried finding solutions related to Call retries were exceeded posted before but they are related to setTimeouts, FakeTimers, or async-mock(which I have already implemented).
Note: The test passes when I comment out fireEvent.click. Test only fails when the event is triggered.
The issue was resolved by wrapping the fireEvent in a waitFor function.
it('should fire redirection flow', async () => {
jest.mock('../../foo', () => {
return {
makeEncryptedCall: jest.fn(() => {
const response = {
ok: true,
json: () => {
Promise.resolve({
data: 'superEncryptedStuff';
});
}
};
return Promise.resolve(response);
});
}
});
const component = screen.getByTestId('my-button');
expect(component).toBeEnabled();
await waitFor(() => fireEvent.click(component));
});

react-testing-library and MSW

I have been trying to implement some tests on my project, but I got some blockers.
This error:
Unable to find an element with the text: C-3PO/i. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
I don't know how to fix it. Should I use another query or I am missing something?
import { render, screen } from "#testing-library/react";
import CharacterList from "./index";
import { rest } from "msw";
import { setupServer } from "msw/node";
const server = setupServer(
rest.get("https://swapi.dev/api/people/", (req, res, ctx) => {
return res(
ctx.json({ results: [{ name: "Luke Skywalker", gender: "male" }] })
);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe("render characters", () => {
it("should render character C-3PO when get api response", async () => {
render(<CharacterList />);
const character = await screen.findByText("C-3PO/i");
expect(character).toBeInTheDocument();
});
});
and my component:
import { NavLink } from "react-router-dom";
import { useEffect, useState } from "react";
export default function CharactersList() {
const [data, setData] = useState(undefined);
const [home, setHome] = useState(undefined);
useEffect(() => {
fetch("https://swapi.dev/api/people/")
.then((response) => {
if (response) {
return response.json();
} else {
return Promise.reject(response);
}
})
.then((data) => {
setData(data);
});
}, []);
if (data) {
return data.results.map((item) => {
const id = item.url.slice(29);
return (
<>
<NavLink to={`/character/${id}`}>{item.name}</NavLink>
<p>Gender: {item.gender}</p>
<p>Home planet: {item.homeworld}</p>
</>
);
});
} else {
return <p>Loading...</p>;
}
}
Please Add screen.debug() to see your actual screen, from that you can consider which get method will work to you.
I think the problem is you don't have C-3PO/i text in the DOM
describe("render characters", () => {
it("should render character C-3PO when get api response", async () => {
render(<CharacterList />);
screen.debug(); <------- ADD THIS
const character = await screen.findByText("C-3PO/i");
expect(character).toBeInTheDocument();
});
});

how to call useMutation hook from a function?

I have this functional React component:
// CreateNotification.tsx
import {useMutation} from '#apollo/client';
import resolvers from '../resolvers';
const createNotification = (notification) => {
const [createNotification] = useMutation(resolvers.mutations.CreateNotification);
createNotification({
variables: {
movie_id: notification.movie.id,
actor_id: notification.user.id,
message:
`${notification.user.user_name} has added ${notification.movie.original_title} to their watchlist.`,
},
});
};
export default createNotification;
I call the createNotification component in a function and pass in some variables after a other useMutation hook has been called:
// AddMovie.tsx
const addMovie = async (movie: IMovie) => {
await addUserToMovie({
variables: {...movie, tmdb_id: movie.id},
update: (cache, {data}) => {
cache.modify({
fields: {
moviesFromUser: () => {
return [...data.addUserToMovie];
},
},
});
},
}).then( async () => {
createNotification({movie: movie, user: currentUserVar()});
});
};
When I run the code I get the (obvious) error:
Uncaught (in promise) Error: Invalid hook call. Hooks can only be called inside of the body of a function component
Because I call the createNotification hook in the addMovie function.
If I move the createNotification to the top of the component:
// AddMovie.tsx
const AddMovieToWatchList = ({movie}: {movie: IMovie}) => {
createNotification({movie: movie, user: currentUserVar()});
const [addUserToMovie] = useMutation(resolvers.mutations.AddUserToMovie);
const addMovie = async (movie: IMovie) => {
await addUserToMovie({
variables: {...movie, tmdb_id: movie.id},
update: (cache, {data}) => {
cache.modify({
fields: {
moviesFromUser: () => {
return [...data.addUserToMovie];
},
},
});
},
});
};
}
The code works fine, except that the hook is now called every time the AddMovie component is rendered instead of when the addMovie function is called from the click:
return (
<a className={classes.addMovie} onClick={() => addMovie(movie)}>
Add movie to your watchlist
</a>
);
Figured it out:
// createNotification.tsx
import {useMutation} from '#apollo/client';
import resolvers from '../resolvers';
export const createNotification = () => {
const [createNotification, {data, loading, error}] = useMutation(resolvers.mutations.CreateNotification);
const handleCreateNotification = async (notification) => {
createNotification({
variables: {
movie_id: notification.movie.id,
actor_id: notification.user.id,
message:
`${notification.user.user_name} has added ${notification.movie.original_title} to their watchlist.`,
},
});
console.log(data, loading, error);
};
return {
createNotification: handleCreateNotification,
};
};
If I'm correct then this returns a reference (createNotification) to the function handleCreateNotification
Then in the component I want to use the createNotification helper I import it:
// AddMovie.tsx
import {createNotification} from '../../../../helpers/createNotification';
const AddMovieToWatchList = ({movie}: {movie: IMovie}) => {
const x = createNotification();
const addMovie = async (movie: IMovie) => {
await addUserToMovie({
variables: {...movie, tmdb_id: movie.id},
update: (cache, {data}) => {
cache.modify({
fields: {
moviesFromUser: () => {
return [...data.addUserToMovie];
},
},
});
},
}).then( async () => {
x.createNotification({movie: movie, user: currentUserVar()});
});
}
};
You (kind of) answer your own question by showing the error and saying it's obvious. createNotification is not a React component, and it is not a custom hook, it is just a function. Thus using a hook inside of it breaks the Rules of Hooks.
If you want to keep that logic in it's own function, that's fine, just redefine your component like this:
const AddMovieToWatchList = ({movie}: {movie: IMovie}) => {
const [addUserToMovie] = useMutation(resolvers.mutations.AddUserToMovie);
const [createNotification] = useMutation(resolvers.mutations.CreateNotification);
const addMovie = async (movie: IMovie) => {
await addUserToMovie({
variables: {...movie, tmdb_id: movie.id},
update: (cache, {data}) => {
cache.modify({
fields: {
moviesFromUser: () => {
return [...data.addUserToMovie];
},
},
});
},
});
await createNotification({movie: movie, user: currentUserVar()});
};
return (
<a className={classes.addMovie} onClick={() => addMovie(movie)}>
Add movie to your watchlist
</a>
);
}

Using Axios in a React Function

I am trying to pull data from an Axios Get. The backend is working with another page which is a React component.
In a function however, it doesn't work. The length of the array is not three as it is supposed to be and the contents are empty.
I made sure to await for the axios call to finish but I am not sure what is happening.
import React, { useState, useEffect } from "react";
import { Container } from "#material-ui/core";
import ParticlesBg from "particles-bg";
import "../utils/collagestyles.css";
import { ReactPhotoCollage } from "react-photo-collage";
import NavMenu from "./Menu";
import { useRecoilValue } from "recoil";
import { activeDogAtom } from "./atoms";
import axios from "axios";
var setting = {
width: "300px",
height: ["250px", "170px"],
layout: [1, 3],
photos: [],
showNumOfRemainingPhotos: true,
};
const Collages = () => {
var doggies = [];
//const [dogs, setData] = useState({ dogs: [] });
const dog = useRecoilValue(activeDogAtom);
const getPets = async () => {
try {
const response = await axios.get("/getpets");
doggies = response.data;
//setData(response.data);
} catch (err) {
// Handle Error Here
console.error(err);
}
};
useEffect(() => {
const fetchData = async () => {
getPets();
};
fetchData();
}, []);
return (
<>
<NavMenu />
<ParticlesBg type="circle" margin="20px" bg={true} />
<br></br>
<div>
{doggies.length === 0 ? (
<div>Loading...</div>
) : (
doggies.map((e, i) => {
return <div key={i}>{e.name}</div>;
})
)}
</div>
<Container align="center">
<p> The length of dogs is {doggies.length} </p>
<h1>Knight's Kennel</h1>
<h2> The value of dog is {dog}</h2>
<h2>
Breeders of high quality AKC Miniature Schnauzers in Rhode Island
</h2>
<section>
<ReactPhotoCollage {...setting} />
</section>
</Container>
</>
);
};
export default Collages;
Try doing the following:
const [dogs, setData] = useState([]);
[...]
const getPets = async () => {
try {
const response = await axios.get("/getpets");
doggies = response.data;
setData(response.data);
} catch (err) {
// Handle Error Here
console.error(err);
}
};
const fetchData = async () => {
getPets();
};
useEffect(() => {
fetchData();
}, []);
No idea if it will actually work, but give it a try if you haven't.
If you don't use useState hook to change the array, it won't update on render, so you will only see an empty array on debug.
As far as I can tell you do not return anything from the getPets() function.
Make use of the useState Function to save your doggies entries:
let [doggies, setDoggies ] = useState([]);
const getPets = async () => {
try {
const response = await axios.get("/getpets");
return response.data;
} catch (err) {
// Handle Error Here
console.error(err);
}
return []
};
useEffect(() => {
setDoggies(await getPets());
});
I used setState inside the getPets function. Now it works.
const Collages = () => {
const [dogs, setData] = useState([]);
const dog = useRecoilValue(activeDogAtom);
const getPets = async () => {
try {
const response = await axios.get("/getpets");
setData(response.data);
} catch (err) {
// Handle Error Here
console.error(err);
}
};
useEffect(() => {
const fetchData = async () => {
getPets();
};
fetchData();
}, []);

How to wait for all useEffect Chain using Jest and Enzyme when all of them make asynchronous calls?

I have the following Minimal Component
import React, { useState, useEffect } from "react";
import { API } from "aws-amplify";
export default function TestComponent(props) {
const [appointmentId, setAppointmentId] = useState(props.appointmentId);
const [doctorId, setDoctorId] = useState(null);
useEffect(() => {
const loadDoctor = async () => {
if (doctorId) {
const doctorData = await API.post("backend", "/doctor/get", {
body: {
doctorId: doctorId
}
});
console.log("This does not come", doctorData);
}
}
loadDoctor();
}, [doctorId])
useEffect(() => {
const loadAppointment = async () => {
if (appointmentId) {
const appointmentData = await API.post("backend", "/appointment/get", {
body: {
appointmentId: appointmentId
}
});
console.log("This Loads", appointmentData);
setDoctorId(appointmentData.doctorId);
}
}
loadAppointment();
}, [appointmentId])
return (
<div>Testing Page</div>
)
}
The Following this does not work not load wait for the doctorId useEffect promise.
But the second test this does work waits for both the useEffect
import React from "react";
import { API } from "aws-amplify";
import { mount } from "enzyme";
import { act } from "react-dom/test-utils";
import TestComponent from "./TestComponent.js";
jest.mock("aws-amplify");
beforeEach(() => {
API.post.mockImplementation((api, path, data) => {
if (path === "/appointment/get") {
return Promise.resolve({
doctorId: "10000001"
});
}
if (path === "/doctor/get") {
return Promise.resolve({
doctorName: "Mr. Doctor"
});
}
});
afterEach(() => {
API.post.mockClear();
});
it("this does not work", async () => {
const wrapper = mount(
<TestComponent appointmentId={"2000001"}/>
);
await act(async () => {
await Promise.resolve(wrapper);
wrapper.update();
});
// this does not print the line console.log("This does not come", doctorData);
});
it("this does work", async () => {
const wrapper = mount(
<TestComponent appointmentId={"2000001"}/>
);
await act(async () => {
await Promise.resolve(wrapper);
wrapper.update();
await act(async () => {
await Promise.resolve(wrapper);
wrapper.update();
});
});
// this prints it. This works, but this is not scalable for more complicated code
});
Is there a way I can wait for all the subsequent useEffect and then test ?
I did something similar to preload images. this put all the promisse in a stack and wait for all to resolve
preloadImages(srcs) {
function loadImage(src) {
return new Promise(function(resolve, reject) {
var img = new Image();
img.onload = function() {
resolve(img);
};
img.onerror = img.onabort = function() {
reject(src);
};
img.src = src;
});
}
var promises = [];
for (var i = 0; i < srcs.length; i++) {
//const imgName = srcs[i].substring(15,18)
//promises.push(loadImage(srcs[i]));
promises.push(loadImage(images(`./${srcs[i]}.png`).default));
}
return Promise.all(promises);
}
then.. prelaodImages(...).then(...

Resources