How to define match router prop at my shallow test - reactjs

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

Related

Detail Image Rendering in React

When I tried to image, image is broken
BASE_URL I want to fetch is composed like this.
{
"renderings": [
{
"_id": "image_file"(string type)
},
...more _id
}
This is a similar topic to the question I posted a few days ago.
But I see the problem seems to arise while implementing the detail page.
// App.tsx
function App() {
return (
<BrowserRouter>
<Route exact path="/">
<Gallery />
</Route>
<Route path="/detail">
<Detail />
</Route>
</BrowserRouter>
);
}
export default App;
// Gallery.tsx
function Gallery() {
const [gallery, setGallery] = useState<any>();
const getGallery = async () => {
const json: GalleriesProps = await (await fetch(BASE_URL)).json();
setGallery(json.renderings);
};
useEffect(() => {
getGallery();
}, []);
return (
<ImgContainer>
<Link to="/detail">
{gallery &&
gallery.map((x: any) => (
<img key={x._id} alt="gallery_logo" src={x._id} />
))}
</Link>
</ImgContainer>
);
}
export default Gallery;
// Detail.tsx
function Detail() {
const [galleryDetail, setGalleryDetail] = useState<any>();
const [clicked, setClicked] = useState(false);
console.log(galleryDetail);
useEffect(() => {
async function fetchGallery() {
const response = await fetch(BASE_URL);
const result: GalleriesProps = await response.json();
setGalleryDetail(result.renderings[0]._id);
}
fetchGallery();
}, []);
const prevPage = () => {
if (clicked) return;
setClicked(true);
};
const nextPage = () => {
if (clicked) return;
setClicked(true);
};
return (
<>
<Container>
<Button onClick={prevPage}>
<FontAwesomeIcon icon={faArrowAltCircleLeft} />
</Button>
<ImageDetail>
{galleryDetail && <img src={galleryDetail} />}
</ImageDetail>
<Button onClick={nextPage}>
<FontAwesomeIcon icon={faArrowAltCircleRight} />
</Button>
</Container>
</>
);
}
export default Detail;
When I click on an image, I go to the detail page, and then I have to implement that image to show,
For example,
If I click the image of the cat.jpeg file, I have to go to the detail page and change the size of cat.jpeg and show the rest as it is,
but I can't quite figure it out.

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

Jest and Enzyme Test Functional Component: Method that returns JSX

Having troubles displaying JSX returned from method.
Error I get is Method “text” is meant to be run on 1 node. 0 found instead.
When I run wrapper.debug() no JSX elements from the condition exists. (I can hit the console.log in that method though)
Thank you in advance!!
component.js
function component(props) {
const [message, setMessage] = useState();
const [open, setOpen] = useState(false);
useEffect(() => {
fetch.then((results)=>(setMessage(fetchedMessage));
});
const displayMessage = () => {
if(condition1) {
return(
<div class="container">
<div id='user'>
Hello user, {message}
{console.log("test if I am hitting inside this statement")}
</div>
</div>
)
};
if(condition2) {
return(
<div class="container">
<div id='admin'>Hello admin, {message}</div>
<button id='modal-button' onClick={() => {setOpen(true}}> Open </button>
</div>
)
};
return null;
};
return (
<>
<div>{message ? displayMessage() : null}</div>
<Modal open={open} />
</>
);
};
component.spec.js
describe('Describe test', () => {
function createMockStore() {
return configureMockStore()({ mockedStore: {some values}})
};
it('displays correct message for condition1', () => {
const mockFetchedData = { message: "Mocked message" };
const mockStore = createMockStore(condition1);
const mockFetchData = jest.fn(() => new Promise((resolve) => resolve({ data: mockFetchedData })));
const wrapper = await mount(
<Provider store={mockStore}>
<Component {...props} />
</Provider>
).find('Component');
expect(wrapper.find('#user').text().toEqual("Mocked message"))); //Failing here. Error is: Method “text” is meant to be run on 1 node. 0 found instead.
});
};

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.

Resources