jest + enyzme + useRef - reactjs

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

Related

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

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

Focus Trap React and a few component's in 2 popup's

I have 2 popup's(I reuse CloseButton(component) and Modal(component) in 2 popup's) and need to do focus trap at all. I lf answer 4 better way.
1 popup Screen, components: ModalLogin-Modal-CloseButton.
I read about some hooks: useRef() and forwardRef(props, ref)
but i don't undestand why it's not work in my case. I am trying to find a solution. I need help :)
In ModalLogin, I try to do a focus trap. To do this, I mark what should happen with focus when moving to 1 and the last element. I need to pass my ref hook obtained via Modal-CloseButton. I read that you can't just transfer refs to functional components. I try to use the forwardref hook in the necessary components where I transfer it, here's what I do:
All links without focus-trap and hook's!.
https://github.com/j3n4r3v/ligabank-credit/blob/master/src/components/form-login/modal-login.jsx [Modal-login full]
const ModalLogin = () => {
const topTabTrap* = useRef();
const bottomTabTrap* = useRef();
const firstFocusableElement = useRef();
const lastFocusableElement = useRef();
useEffect(() => {
const trapFocus = (event) => {
if (event.target === topTabTrap.current) {
lastFocusableElement.current.focus()
}
if (event.target === bottomTabTrap.current) {
firstFocusableElement.current.focus()
}
}
document.addEventListener('focusin', trapFocus)
return () => document.removeEventListener('focusin', trapFocus)
}, [firstFocusableElement, lastFocusableElement])
return (
<Modal onCloseModal={() => onCloseForm()} ref={lastFocusableElement}>
<form >
<span ref={topTabTrap} tabIndex="0" />
<Logo />
<Input id="email" ref={firstFocusableElement} />
<Input id="password" />
<Button type="submit" />
<span ref={bottomTabTrap} tabIndex="0"/>
</form>
</Modal>
);
};
https://github.com/j3n4r3v/ligabank-credit/blob/master/src/components/modal/modal.jsx [Modal full]
const Modal = forwardRef(({ props, ref }) => {
const { children, onCloseModal, ...props } = props;
const overlayRef = useRef();
useEffect(() => {
const preventWheelScroll = (evt) => evt.preventDefault();
document.addEventListener('keydown', onEscClick);
window.addEventListener('wheel', preventWheelScroll, { passive: false });
return () => {
document.removeEventListener('keydown', onEscClick);
window.removeEventListener('wheel', preventWheelScroll);
};
});
const onCloseModalButtonClick = () => {
onCloseModal();
};
return (
<div className="overlay" ref={overlayRef}
onClick={(evt) => onOverlayClick(evt)}>
<div className="modal">
<CloseButton
ref={ref}
onClick={() => onCloseModalButtonClick()}
{...props}
/>
{children}
</div>
</div>
);
});
https://github.com/j3n4r3v/ligabank-credit/blob/master/src/components/close-button/close-button.jsx [CloseButton full]
const CloseButton = forwardRef(({ props, ref }) => {
const {className, onClick, ...props} = props;
return (
<button className={`${className} close-button`}
onClick={(evt) => onClick(evt)}
tabIndex="0"
ref={ref}
{...props}
>Close</button>
);
});
And now i have a lot of errors just like: 1 - Cannot read properties of undefined (reading 'children') - Modal, 2 - ... className undefined in CloseButton etc.
2 popup Screen, components: Modal(reuse in 1 popup) - InfoSuccess- CloseButton(reuse in 1 popup)
I have only 1 interactive element - button (tabindex) and no more. Now i don't have any idea about 2 popup with focus-trap ((
https://github.com/j3n4r3v/ligabank-credit/blob/master/src/components/success-modal/success-modal.jsx [SuccessModal full]
const SuccessModal = ({ className, onChangeVisibleSuccess }) => {
return (
<Modal onCloseModal={() => onChangeVisibleSuccess(false)}>
<InfoSuccess className={className} />
</Modal>
);
};
https://github.com/j3n4r3v/ligabank-credit/blob/master/src/components/info-block/info-block.jsx [Infoblock full]
const InfoBlock = ({ className, title, desc, type }) => {
return (
<section className={`info-block ${className} info-block--${type}`}>
<h3 className="info-block__title">{title}</h3>
<p className="info-block__desc">{desc}</p>
</section>
);
};
const InfoSuccess = ({ className }) => (
<InfoBlock
title="Спасибо за обращение в наш банк."
desc="Наш менеджер скоро свяжется с вами по указанному номеру телефона."
type="center"
className={className}
/>
);
I know about 3 in 1 = 1 component and no problem in popup with Focus-Trap. But i want understand about my case, it's real to life or not and what best practice.

testing conditional rendering that relies on state using react testing library

What would be the way to test a component that relies on the initial state for conditional rendering ?
For example showLessFlag is dependent on state, and testing state in react-testing-library is counter productive.
so how would i test this condition in the CommentList component
{showLessFlag === true ? (
// will show most recent comments below
showMoreComments()
) : (
<Fragment>
{/* filter based on first comment, this shows by default */}
{filterComments.map((comment, i) => (
<div key={i} className="comment">
<CommentListContainer ref={ref} comment={comment} openModal={openModal} handleCloseModal={handleCloseModal} isBold={isBold} handleClickOpen={handleClickOpen} {...props} />
</div>
))}
</Fragment>
)}
Should it be test like the following
it("should check more comments", () => {
const { getByTestId } = render(<CommentList {...props} />);
const commentList = getByTestId("comment-show-more");
expect(commentList).toBeNull();
});
But im getting this error because of the conditional rendering
TestingLibraryElementError: Unable to find an element by:
[data-testid="comment-show-more"]
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 data-testid="comment-show-more" key={i} className="comment">
<CommentListContainer ref={ref} comment={comment} openModal={openModal} handleCloseModal={handleCloseModal} isBold={isBold} handleClickOpen={handleClickOpen} {...props} />
</div>
));
};
return (
<Grid data-testid="comment-list-div">
<Fragment>
<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} className="comment">
<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>;
CommentList.test.tsx
import "#testing-library/jest-dom";
import React, { Ref } from "react";
import CommentList from "./CommentList";
import { render, getByText, queryByText, getAllByTestId } from "#testing-library/react";
const props = {
user: {},
postId: null,
userId: null,
currentUser: {},
ref: {
current: undefined,
},
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();
});
it("should render first comment", () => {
const { getByTestId } = render(<CommentList {...props} />);
const commentList = getByTestId("comment-list-div");
expect(commentList.firstChild).toBeTruthy();
});
it("should render second child", () => {
const { getByTestId } = render(<CommentList {...props} />);
const commentList = getByTestId("comment-list-div");
expect(commentList.lastChild).toBeTruthy();
});
it("should check comments", () => {
const rtl = render(<CommentList {...props} />);
expect(rtl.getByTestId("comment-list-div")).toBeTruthy();
expect(rtl.getByTestId("comment-list-div")).toBeTruthy();
expect(rtl.getByTestId("comment-list-div").querySelectorAll(".comment").length).toEqual(2);
});
it("should match snapshot", () => {
const rtl = render(<CommentList {...props} />);
expect(rtl).toMatchSnapshot();
});
it("should check more comments", () => {
const { getByTestId } = render(<CommentList {...props} />);
const commentList = getByTestId("comment-show-more");
expect(commentList).toBeNull();
});
});
Any getBy* query in react-testing-library will throw an error if no match is found. If you want to test/assert the absence of an element then you want to use any of the queryBy* queries, they return null if no match is found.
Queries
it("should check more comments", () => {
const { queryByTestId } = render(<CommentList {...props} />);
const commentList = queryByTestId("comment-show-more");
expect(commentList).toBeNull();
});
To better answer this question, being that i have more experience with using react testing library now.
When we go about testing for conditions, we need to trigger the action that makes the change to the state.
For example in this situation
We have a condition like showLessFlag
{showLessFlag === true ? (
// will show most recent comments below
showMoreComments()
) : (
<Fragment>
{/* filter based on first comment, this shows by default */}
{filterComments.map((comment, i) => (
<div key={i} className="comment">
<CommentListContainer ref={ref} comment={comment} openModal={openModal} handleCloseModal={handleCloseModal} isBold={isBold} handleClickOpen={handleClickOpen} {...props} />
</div>
))}
</Fragment>
)}
In order to properly test this, we need to trigger the event that will change showLessFlag to false.
So we can do something like
<OurSecondaryButton
onClick={(e) => showLessComments(e)}
data-testid="_test-show-less"
component="span"
color="secondary"
>
Show Less Comments
</OurSecondaryButton>
test
it("should trigger showLessComments ", () => {
const { getByTestId } = render(<CommentList {...props} />);
const showLessButton = getByTestId("__test-show-less");
fireEvent.click(showLessButton);
expect(...) // whatever to be called, check for the existence of a div tag, or whatever you want
});
Testing for conditions improves code coverage :)

React eslint error:Component definition is missing display name

Using Reac.memo to wrap my functional component, and it can run smoothly, but the eslint always reminded me two errors:
error Component definition is missing display name react/display-name
error 'time' is missing in props validation react/prop-types
Here is my code:
type Data = {
time: number;
};
const Child: React.FC<Data> = React.memo(({ time }) => {
console.log('child render...');
const newTime: string = useMemo(() => {
return changeTime(time);
}, [time]);
return (
<>
<p>Time is {newTime}</p>
{/* <p>Random is: {children}</p> */}
</>
);
});
My whole code:
import React, { useState, useMemo } from 'react';
const Father = () => {
const [time, setTime] = useState(0);
const [random, setRandom] = useState(0);
return (
<>
<button type="button" onClick={() => setTime(new Date().getTime())}>
getCurrTime
</button>
<button type="button" onClick={() => setRandom(Math.random())}>
getCurrRandom
</button>
<Child time={time} />
</>
);
};
function changeTime(time: number): string {
console.log('changeTime excuted...');
return new Date(time).toISOString();
}
type Data = {
time: number;
};
const Child: React.FC<Data> = React.memo(({ time }) => {
console.log('child render...');
const newTime: string = useMemo(() => {
return changeTime(time);
}, [time]);
return (
<>
<p>Time is {newTime}</p>
{/* <p>Random is: {children}</p> */}
</>
);
});
export default Father;
It's because you have eslint config which requries you to add displayName and propTypes
Do something like
const Child: React.FC<Data> = React.memo(({ time }) => {
console.log('child render...');
const newTime: string = useMemo(() => {
return changeTime(time);
}, [time]);
return (
<>
<p>Time is {newTime}</p>
{/* <p>Random is: {children}</p> */}
</>
);
});
Child.propTypes = {
time: PropTypes.isRequired
}
Child.displayName = 'Child';
If you are working with React and TypeScript, you can turn off the react/prop-types rule.
This is because TypeScript interfaces/props are good enough to replace React's prop types.

How to define match router prop at my shallow test

I'm trying to do jest+enzyme tests with my react app but i'm getting an error at the route component. I'm using router v4.
I did try to use MemoryRouter Wrapper to the shallow component, or use mount instead of shallow. Everything didn't work.
My test:
describe('Movie Page tests', () => {
const wrapper = shallow(<Movie />).toJSON()
it('should call Movie snapshot correctly', () => {
const tree = renderer
.create(wrapper)
.toJSON();
expect(tree).toMatchSnapshot();
});
})
Full component:
export const Movie = ({
match,
searchMovieAction,
movies,
totalResults,
pending,
error,
}, props) => {
const [ showModal, setShowModal ] = useState(false);
//const { searchMovieAction } = useActions();
const { values, handleInputChange } = useFormInput({
searchValue: '',
});
console.log('PROPES ------>', error,);
/* const {
movie: {
movie,
totalResults,
pending,
error,
},
} = useSelector(state => state); */
const handleSubmit = (e) => {
e.preventDefault()
const { searchValue } = values;
if(searchValue) {
searchMovieAction(searchValue);
}
}
const toggleModal = () => {
setShowModal(!showModal);
}
return (
<div className='movies'>
<div>
<StyledForm onSubmit={handleSubmit}>
<DefaultInput
name='searchValue'
value={values.searchValue}
placeholder='Search a movie...'
handleChange={handleInputChange}
/>
<Button solid rounded right >Search</Button>
</StyledForm>
</div>
{ pending && <LoadingSpinner medium />}
{ error && <Error message={error} /> }
{ movies && movies.length > 0 && !pending && !error && (
<p>
We found <strong>{ totalResults }</strong>
{totalResults == 1 ? 'result!' : ' results!'}
</p>
)}
<StyledMovies>
{movies && movies.map((m) => {
const { Title, Poster, Plot, imdbID } = m
return(
<StyledMovieItem key={uniqueId()}>
<Link to={`${match.url}/${imdbID}`} onClick={setShowModal}>
<MovieSummary data={{Title, Poster, Plot}} />
</Link>
</StyledMovieItem>
)
})
}
<Modal handleClose={toggleModal} show={showModal}>
<Route
exact
path={`${ match.path }/:imdbID`}
render={(props) => <MovieDetail {...props} /> }
/>
</Modal>
</StyledMovies>
</div>
)
}
The error:
TypeError: Cannot read property 'path' of undefined
99 | <Route
100 | exact
101 | path={`${ match.path }/:imdbID`}
| ^
102 | rende
The application is working, but at the test the match param is empty. Does someone knows what can be?
You need to wrap your component into any <MemoryRouter> and <Router> like
const wrapper = shallow(<MemoryRouter><Route component={Movie} /></MemoryRouter>).dive().dive()
dive() is needed because otherwise only <MemoryRouter> itself is rendered by shallow().
See article with more detailed explanation: https://medium.com/#antonybudianto/react-router-testing-with-jest-and-enzyme-17294fefd303
If you don't want to wrap your component with a router in your tests, you can mock match like any other props:
const mockMatch = {
path: 'my-path/some-value',
url: 'my-path/some-value'
}
const wrapper = shallow(<Movie match={mockMatch}/>).toJSON()

Resources