React Jest test - How to wrap state into an act(...) - reactjs

My tests are passing fine - but I still get the (in)famous *Warning: An update to Movie 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 */*
here:
13 |
14 | const handleSelect = () => {
15 | setSelected(!selected);
| ^
16 | };
17 |
18 | return (
I can't figure how to solve it.
My code below
Movies.tsx
import { useState } from "react";
import { MovieSearchResults } from "../../model/movie";
import Movie from "../Movie/Movie";
import Search from "../Search/Search";
import Pager from "../Pager/Pager";
import "./Movies.css";
const Movies = () => {
const [search, setSearch] = useState<string>("");
const [movies, setMovies] = useState<MovieSearchResults>({
results: [],
total_pages: 1,
page: 1,
});
const handleSearch = () => {
fetch(
`https://api.themoviedb.org/3/search/movie?api_key=${process.env.REACT_APP_API_KEY}&query=${search}`
)
.then((response) => response.json())
.then((json) => setMovies(json));
};
const handlePager = (page: number) => {
fetch(
`https://api.themoviedb.org/3/search/movie?api_key=${process.env.REACT_APP_API_KEY}&query=${search}&page=${page}`
)
.then((response) => response.json())
.then((json) => setMovies(json));
};
return (
<div className="Movies">
<Search search={search} setSearch={setSearch} onSearch={handleSearch} />
{movies && movies.total_pages > 1 ? (
<Pager
{...movies}
total_pages={movies.total_pages}
onPageChange={handlePager}
/>
) : (
<></>
)}
{movies &&
movies.results.map((movie) => (
<Movie
{...movie} /*here I am destructuring movie properties because I don't need to specify all*/
id={movie.id}
poster_path={movie.poster_path}
title={movie.title}
key={movie.id}
overview={movie.overview.substring(0, 250) + "..."}
/>
))}
{movies && movies.total_pages > 1 ? (
<Pager
{...movies}
total_pages={movies.total_pages}
onPageChange={handlePager}
/>
) : (
<div className="message--noResulst">
Sorry. There's no results available for your search...
</div>
)}
</div>
);
};
export default Movies;
Movie.tsx
import { useState } from "react";
import { MovieSearchResult } from "../../model/movie";
import classnames from "classnames";
import "./Movie.css";
const Movie = (movie: MovieSearchResult, key: number) => {
//select Movie on click:
const [selected, setSelected] = useState<boolean>(false);
// use classnames to modify className on select:
const classNames = classnames("Movie", { Movie__selected: selected });
const handleSelect = () => {
setSelected(!selected);
};
return (
<div className={classNames} onClick={handleSelect}>
<div className="Movie__poster-wrap">
<img
className="Movie__poster"
src={`https://image.tmdb.org/t/p/w92${movie.poster_path}`}
alt="Movie poster"
/>
</div>
<div className="Movie__details">
<h2>{movie.title}</h2>
<p>{movie.overview}</p>
</div>
</div>
);
};
export default Movie;
Movie.test.js
import React from "react";
import renderer from "react-test-renderer";
import { act, render, fireEvent } from "#testing-library/react";
import Movie from "./Movie";
describe("Movie", () => {
it(" changes the class of Movie when clicked i.e. selected", async () => {
const movie = {
adult: false,
backdrop_path: "/eM9MZ1dhDCbRzwQg0FWpWWtLPMn.jpg",
genre_ids: [10752, 28, 18],
id: 390054,
original_language: "en",
original_title: "Sand Castle",
overview:
"Set during the occupation of Iraq, a squad of U.S. soldiers try to protect a small village....",
popularity: 38.305,
poster_path: "/c9buG2jVRgAu68E4D4jpwlgqhO1.jpg",
release_date: "2017-04-21",
title: "Sand Castle",
video: false,
vote_average: 6.5,
vote_count: 613,
};
const component = renderer.create(
<Movie
{...movie}
id={movie.id}
poster_path={movie.poster_path}
title={movie.title}
key={movie.id}
overview={movie.overview.substring(0, 250) + "..."}
/>
);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
//trigger the click
tree.props.onClick();
// re-rendering
tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});
I've looked into the act(...) functionality on Jets but couldn't figure it out.
Thanks in advance for the help and explanations

Related

Please how do I add maxFiles properly to this code? I have tried a billion different things, but it just does not work with the dropzone

import { FC } from 'react';
import { useDropzone } from 'react-dropzone';
import { FileIcon } from 'assets/icons';
import Typography from '../Typography';
import { TMultipleDropzoneProps } from './types';
import styles from './MultipleDropzone.module.scss';
const MultipleDropzone: FC<TMultipleDropzoneProps> = ({ title, onDrop }) => {
const { getRootProps, getInputProps, open, isDragAccept, isFocused, isDragReject } = useDropzone({
accept: { 'image/*': ['.jpeg', '.png'], 'video/mp4': ['.mp4', '.MP4'] },
onDrop,
noClick: true,
noKeyboard: true,
maxFiles: 3,
});
const accept = isDragAccept ? 1 : 0;
const focused = isFocused ? 1 : 0;
const rejected = isDragReject ? 1 : 0;
// This is used for warning in console for camel-case attributes to the DOM element and to make it boolean
return (
<div className={styles.wrapper}>
<div
onClick={open}
className={styles.container}
{...getRootProps({ accept, focused, rejected })}
>
<input {...getInputProps({})} />
{rejected === 1 && (
<div className={styles.error}>
Some files were rejected because they did not meet the requirements.
</div>
)}
<div className={styles.container__content}>
<Typography>{title}</Typography>
</div>
<button onClick={open} className={styles.icon}>
<FileIcon />
</button>
</div>
</div>
);
};
export default MultipleDropzone;
the type file:
export type TMultipleDropzoneProps = {
title: string;
onDrop: (e: any, a: any) => void;
isFileUploaded: boolean;
maxFiles?: number;
};
the modal I am using it in:
import { useContext, useState } from 'react';
import { ModalContext } from 'context/Modal';
import { FileExtended } from 'types/global/file';
import { useAppDispatch, useAppSelector } from 'hooks';
import { NewPostTextArea, MultipleDropzone, Typography, Button } from 'components';
import { createActivityPost } from 'store/slices/activitiesSlice/activitiesThunks';
import { CloseCircleIcon } from 'assets/icons';
import { TImages } from './types';
import styles from './NewPost.module.scss';
const NewPost = () => {
const { closeModal } = useContext(ModalContext);
const [images, setImages] = useState<TImages[]>([]);
const [description, setDescription] = useState<string>('');
const dispatch = useAppDispatch();
const { userData } = useAppSelector((state) => state.auth);
const createPost = () => {
const post = {
user_id: userData?.id as number,
description: description,
content_url: 'https://cdn.pixabay.com/photo/2015/04/23/22/00/tree-736885__340.jpg',
content_type: 'string',
};
closeModal();
if (description.trim()) {
dispatch(createActivityPost(post));
}
};
const isFileUploaded = images.length > 0;
const onDrop = (acceptedFiles: FileExtended[], maxFiles: number) => {
acceptedFiles.forEach((file) => {
const reader = new FileReader();
reader.onload = (e) => {
setImages((prev) => [
...prev,
{
id: Date.now(),
type: file.type,
src: e.target && e.target.result,
file: file,
description: file.name,
},
]);
};
reader.readAsDataURL(file);
});
let modifiedAcceptedFiles = acceptedFiles;
if (acceptedFiles.length > maxFiles) {
// If the number of files exceeds the maxFiles limit,
// slice the extra files off the array and show an error message
modifiedAcceptedFiles = acceptedFiles.slice(0, maxFiles);
}
};
const removeImage = (id: number) => {
setImages(images.filter((image) => image.id !== id));
};
const imageFiles = images.map((image) => {
return (
<div key={image.id} className={styles.container__main__case__box}>
<CloseCircleIcon
onClick={() => removeImage(image.id)}
className={styles.container__main__case__box_close}
/>
{image.type.includes('video') ? (
<video src={image.src as string} autoPlay loop />
) : (
<img src={image.src as string} alt={image.description} />
)}
</div>
);
});
return (
<div className={styles.container}>
<Typography className={styles.container__head}>New Post</Typography>
<div className={styles.container__description}>
<NewPostTextArea value={description} setValue={setDescription} />
</div>
<div className={styles.container__main}>
<Typography className={styles.container__main__head}>{images.length} items</Typography>
<div className={styles.container__main__case}>{imageFiles}</div>
</div>
<MultipleDropzone
onDrop={onDrop}
title='Please attach your files here (Max 3)'
isFileUploaded={isFileUploaded}
/>
<div className={styles.container__footer}>
<Button className={styles.container__footer_close} onClick={closeModal}>
Close
</Button>
<Button type='submit' className={styles.container__footer_submit} onClick={createPost}>
Create
</Button>
</div>
</div>
);
};
export default NewPost;
I tried adding maxFiles to every single component, I also tried adding it to the onDrop component. New to React ( 1 week) and I am slowly losing my sanity. I will never forgive Zuckerberg for this apparition he has brought upon coderkin. Even chatGPT could not help my case.

How can i create path with uuid4()

NewsDetails
import React, { useState, useEffect } from 'react'
import { useParams } from 'react-router-dom'
const NewsDetail = ({ state }) => {
const { id } = useParams();
return (
<div>
{
state
.filter((a) => a.id === id)
.map((card, index) => (
<>
<div className="card" key={index}>
<h2>{card.title}</h2>
<h2>{card.content}</h2>
<img src={card.imageUrl} alt="" />
</div>
</>
))
}
</div>
)
}
export default NewsDetail
NewsItem
import React from 'react'
import clock from "../components/assets/img/Clock.svg"
import user from "../components/assets/img/User.svg"
import { Link } from 'react-router-dom'
const NewsItem = (props) => {
const { imageUrl, title, author, content, date, id } = props
return (
<Link className="col-lg-4 p-2" to={`/detail/${id}`}>
<div className="newsItem">
<img src={imageUrl} alt='newsPhoto' />
<h2>{id}</h2>
<div className="itemBody">
<p className='title'>{title}</p>
<div className="line"></div>
<p className='content'>{content}</p>
<div className="itemfooter">
<span><img src={clock} alt='clock' />{date}</span>
<span><img src={user} alt='user' />{author}</span>
</div>
</div>
</div>
</Link>
)
}
export default NewsItem
Home
import React, { useEffect, useState } from "react";
import NewsItem from "./NewsItem";
import SpinnerLoad from "./SpinnerLoad";
import { v4 as uuidv4 } from 'uuid';
const Home = (props) => {
const Category = [
"all",
"business",
"sports",
"world",
"technology",
"entertainment",
"science"
];
const { state, setState} = props;
const [loading, setLoading] = useState(false)
const fetchValue = (category) => {
fetch(`https://inshorts-api.herokuapp.com/news?category=${category}`)
.then(res => res.json())
.then(res => {
setState(res.data)
setLoading(true)
})
.catch((error) => console.log(error))
console.log(state);
setLoading(false);
};
// const fetchValue = async () => {
// try {
// const data = await axios
// .get(`https://inshorts-api.herokuapp.com/news?category=sports`)
// .then(res => {
// console.log(res);
// setState(res.data)
// })
// setLoading(true)
// console.log(loading);
// } catch (e) {
// console.log(e);
// }
// }
const CategoryButton = ({ category }) => (
<button onClick={() => fetchValue(category)} style={{ textTransform: 'capitalize' }}>{category}</button>
);
useEffect(() => {
fetchValue('all')
},[])
return (
<>
<div className="header-bg">
<h1 className="mb-3">News</h1>
<div className="btns ">
{Category.map((value, index) => {
return <CategoryButton category={value} key={index} />;
})}
</div>
</div>
<div className="news">
<div className="container">
<div className="row">
{
!loading
? <SpinnerLoad/>
:
state.map((data,index) => {
return (
<NewsItem
imageUrl={data.imageUrl}
author={data.author}
title={data.title}
content={data.content}
date={data.date}
id={uuidv4()}
key={index}
/>
);
})
}
</div>
</div>
</div>
</>
);
};
export default Home;
I have created a project with api. With categories it is possible to change the incoming data, but there is one thing where I want to get more detailed information when I click on the newsItem card. That api doesn't have id value, so I used uuid. Information corresponding to the id value should come with useParams. But it doesn't work. How can I fix this problem?
The first issue is that you are generating a GUID when rendering the state array which won't necessarily correlate to any data you are trying to match/filter by in the NewsDetail component.
state.map((data,index) => (
<NewsItem
imageUrl={data.imageUrl}
author={data.author}
title={data.title}
content={data.content}
date={data.date}
id={uuidv4()} // <-- new id each render cycle
key={index}
/>
))
You want to inject the id property when the data is fetch so that it's a stable reference that lives as long as the data does. In other words, it should be an intrinsic property of the data.
Example:
const fetchValue = async (category) => {
setLoading(true);
try {
const res = await fetch(`https://inshorts-api.herokuapp.com/news?category=${category}`);
const { data } = await res.json();
setState(data.map(el => ({
...el,
id: uuidv4(), // <-- map and inject id here
})));
} catch(error) {
console.log(error);
} finally {
setLoading(false);
}
};
...
state.map((data) => (
<NewsItem
key={data.id} // <-- use as React key
data={data} // <-- pass entire data object as prop
/>
))
NewsItem
const NewsItem = ({ data }) => {
const { imageUrl, title, author, content, date, id } = data;
return (
...
);
};
NewsDetail
const NewsDetail = ({ state }) => {
const { id } = useParams();
return (
<div>
{state
.filter((card) => card.id === id)
.map((card) => (
<div className="card" key={card.id}>
<h2>{card.title}</h2>
<h2>{card.content}</h2>
<img src={card.imageUrl} alt="" />
</div>
))
}
</div>
);
};

Why the React List component gets rendered twice?

I have a React code as below:
import React, { useState, useCallback } from "react";
const initialUsers = [
{
id: "1",
name: "foo",
},
{
id: "2",
name: "bar",
},
];
const List = React.memo(({ users, onRemove }) => {
console.log("rendering list");
return (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} <span onClick={() => onRemove(user.id)}>X</span>
</li>
))}
</ul>
);
});
const App = () => {
const [users, setUsers] = useState(initialUsers);
const [text, setText] = useState("");
const handleRemove = useCallback(
(userId) => {
console.log("handleRemove", userId);
const filteredUsers = users.filter((user) => user.id !== userId);
setUsers(filteredUsers);
},
[users]
);
const handleText = (event) => {
setText(event.target.value);
};
return (
<div>
<input type="text" value={text} onChange={handleText} />
<List users={users} onRemove={handleRemove} />
</div>
);
};
export default App;
When I first load the page, I am seeing the rendering list got dumped twice in the browser console, see https://codesandbox.io/s/elegant-bash-ic9uqv?file=/src/App.js
Why is that?
That's because you have used StrictMode in your index.js
When the StrictMode is used, the lifecycle methods are called twice.
It happens for detecting unexpected side effects in your lifecycle methods.
Reference: https://reactjs.org/docs/strict-mode.html

jest + enyzme + useRef

I have a component that looks like:
import React, { useRef } from 'react'
import Modal from '#example-component-library/modal'
import ModalHeader from '#example-component-library/modal/header'
import ModalFooter from '#example-component-library/modal/footer'
const ExampleModal = () => {
const headerRef = useRef(null)
...
return (
<Modal
headerRef={headerRef}
isOpen={showModal}
header={
<ModalHeader closeModal={handleCloseModal} headerRef={headerRef} content="Modal Header"/>
}
footer={<ModalFooter closeModal={handleCloseModal} />}
>
Modal body stuff
</Modal>
)
}
Then I have a test:
it('renders as expected', () => {
const wrapper = mount(
<TestWrapper>
<ExampleModal />
</TestWrapper>
)
expect(wrapper.exists()).toBe(true)
})
})
and then I have an error
TypeError: Cannot read property 'style' of null
28 |
29 | it('renders as expected', () => {
> 30 | const wrapper = mount(
| ^
31 | <TestWrapper>
32 | <ExampleModal />
33 | </TestWrapper>
If I change ExampleModal prop header to:
<Modal header={<>HEADER</>} ...>
The test works without issues - so I believe it has something to do with the headerRef I've tried jest.spyOn and a few other solutions - however I always get the same error.
Modal Component Markup
const ModalHeader = ({ headerRef, content, ...}) => (
<div>
<h5 tabIndex={-1} ref={headerRef}> {content}</h5>
...
</div>
)
const Modal = ({ id, isOpen, header, headerRef, children, ...}) => {
useEffect(() => {
const selectorId = `#${id}`
const selectedElement: HTMLElement = document.querySelector(selectorId)
// set focus to the header when modal is opened
if (isOpen && headerRef.current) {
headerRef.current.focus()
// React Ref wasn't working for this case
selectedElement.style.right = '0px'
document.body.style.overflow = 'hidden'
setPostAnimationState(true)
}
if (!isOpen && document.querySelector(selectorId)) {
// React Ref wasn't working for this case
document.body.style.overflow = 'auto'
selectedElement.style.right = '-768px'
setTimeout(() => {
setPostAnimationState(false)
}, 400)
}
}, [isOpen, headerRef, id])
return (
<div>
...
{!!header && header}
<div className="modal-content" ref={!header ? headerRef : null}>
{children}
</div>
...
</div>
)
}
It actually had nothing to do with ref, it had to do with the document.body. So here is the test I have that works now:
it('renders as expected', () => {
const wrapper = mount(
<TestWrapper>
<ExampleModal />
</TestWrapper>,
{ attachTo: document.body }
)
expect(wrapper.exists()).toBe(true)
})
})

How to unit test a component that has a ref prop

How do you unit test a component that has a ref prop ? i'm getting this error
● Should render › should render
TypeError: Cannot add property current, object is not extensible
I looked at another question Unit testing React component ref, but there is no solution for that question.
this is the test
CommentList.test.tsx
import "#testing-library/jest-dom";
import React from "react";
import { CommentListComponent as CommentList } from "./CommentList";
import { render, getByText, queryByText, getAllByTestId } from "#testing-library/react";
const props = {
user: {},
postId: null,
userId: null,
currentUser: {},
ref: {},
comments: [
{
author: { username: "barnowl", gravatar: "https://api.adorable.io/avatars/400/bf1eed82fbe37add91cb4192e4d14de6.png", bio: null },
comment_body: "fsfsfsfsfs",
createdAt: "2020-05-27T14:32:01.682Z",
gifUrl: "",
id: 520,
postId: 28,
updatedAt: "2020-05-27T14:32:01.682Z",
userId: 9,
},
{
author: { username: "barnowl", gravatar: "https://api.adorable.io/avatars/400/bf1eed82fbe37add91cb4192e4d14de6.png", bio: null },
comment_body: "fsfsfsfsfs",
createdAt: "2020-05-27T14:32:01.682Z",
gifUrl: "",
id: 519,
postId: 27,
updatedAt: "2020-05-27T14:32:01.682Z",
userId: 10,
},
],
deleteComment: jest.fn(),
};
describe("Should render <CommentList/>", () => {
it("should render <CommentList/>", () => {
const commentList = render(<CommentList {...props} />);
expect(commentList).toBeTruthy();
});
});
This is the component.
CommentList.tsx
import React, { Fragment, useState, Ref } from "react";
import Grid from "#material-ui/core/Grid";
import OurSecondaryButton from "../../../common/OurSecondaryButton";
import CommentListContainer from "../commentListContainer/commentListContainer";
function CommentList(props: any, ref: Ref<HTMLDivElement>) {
const [showMore, setShowMore] = useState<Number>(2);
const [openModal, setOpenModal] = useState(false);
const [showLessFlag, setShowLessFlag] = useState<Boolean>(false);
const the_comments = props.comments.length;
const inc = showMore as any;
const min = Math.min(2, the_comments - inc);
const showComments = (e) => {
e.preventDefault();
if (inc + 2 && inc <= the_comments) {
setShowMore(inc + 2);
setShowLessFlag(true);
} else {
setShowMore(the_comments);
}
};
const handleClickOpen = () => {
setOpenModal(true);
};
const handleCloseModal = () => {
setOpenModal(false);
};
const showLessComments = (e) => {
e.preventDefault();
setShowMore(2);
setShowLessFlag(false);
};
const isBold = (comment) => {
return comment.userId === props.userId ? 800 : 400;
};
// show comments by recent, and have the latest comment at the bottom, with the previous one just before it.
const filterComments = props.comments
.slice(0)
.sort((a, b) => {
const date1 = new Date(a.createdAt) as any;
const date2 = new Date(b.createdAt) as any;
return date2 - date1;
})
.slice(0, inc)
.reverse();
const showMoreComments = () => {
return filterComments.map((comment, i) => (
<div key={i}>
<CommentListContainer ref={ref} comment={comment} openModal={openModal} handleCloseModal={handleCloseModal} isBold={isBold} handleClickOpen={handleClickOpen} {...props} />
</div>
));
};
console.log(ref);
return (
<Grid>
<Fragment>
<div data-testid="comment-list-div" style={{ margin: "30px 0px" }}>
{props.comments.length > 2 ? (
<Fragment>
{min !== -1 && min !== -2 ? (
<Fragment>
{min !== 0 ? (
<OurSecondaryButton onClick={(e) => showComments(e)} component="span" color="secondary">
View {min !== -1 && min !== -2 ? min : 0} More Comments
</OurSecondaryButton>
) : (
<OurSecondaryButton onClick={(e) => showLessComments(e)} component="span" color="secondary">
Show Less Comments
</OurSecondaryButton>
)}
</Fragment>
) : (
<OurSecondaryButton onClick={(e) => showLessComments(e)} component="span" color="secondary">
Show Less Comments
</OurSecondaryButton>
)}
</Fragment>
) : null}
</div>
</Fragment>
{showLessFlag === true ? (
// will show most recent comments below
showMoreComments()
) : (
<Fragment>
{/* filter based on first comment */}
{filterComments.map((comment, i) => (
<div key={i}>
<CommentListContainer ref={ref} comment={comment} openModal={openModal} handleCloseModal={handleCloseModal} isBold={isBold} handleClickOpen={handleClickOpen} {...props} />
</div>
))}
</Fragment>
)}
</Grid>
);
}
export default React.forwardRef(CommentList) as React.RefForwardingComponent<HTMLDivElement, any>;
I fixed it, i had to import
import CommentList from "./CommentList";
instead of
import { CommentListComponent as CommentList } from "./CommentList";
and do this to the props
ref: {
current: undefined,
},
and comment/remove this line of code from commentList
// // prevents un-necesary re renders
// // export default React.memo(CommentList);
// // will be useful for unit testing.
// export { CommentList as CommentListComponent };

Resources