I have a component that I would like to test using Jest and React Testing Library. When I say test, I'm basically saying that I want to check if the content shows up on the screen. However, I'm running into a serious problem because I'm dealing with an async operation that updates the state, so the content is not appearing immediately. How would I approach this problem? A code snippet would be much appreciated.
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';
const Home = () => {
const [tv, setTv] = useState([]);
const [tvLoading, setTvLoading] = useState(true);
// Go and fetch popular TV shows
const getPopularTv = async () => {
axios.get( ... )
setTv(data);
setTvLoading(false);
};
// This will run once. As soon as the component gets rendered for the 1st time
useEffect(() => {
getPopularTv();
}, []);
let TvData, loading;
const img_path = 'https://image.tmdb.org/t/p/w500/';
// If we have TV shows, set the 'TvData' variable to a pre-defined block of JSX using it.
if (tv && tv.total_results > 0) {
TvData = (
<div className="row animated fadeIn ">
{tv.results.slice(0, 10).map((show) => {
return (
// I WANT TO TEST IF THIS DIV APPEARS ON THE SCREEN
// SO, ON THIS DIV I'M SETTING UP THE 'data-testid'
// HOWEVER THIS IS A ASYNC OPERATION AND THE CONTENT
// WON'T SHOW UP IMMEDIATELY. HOW WOULD I TEST THIS???
<div
data-testid="home-shows" // HERE'S THE ID THAT I WANT TO USE IN MY TEST
className="col s6 m6 l6"
key={show.id}
>
<Link to={'/tvs/' + show.id}>
<img
className="responsive-img z-depth-3 poster tooltipped"
data-tooltip={show.name}
data-position="top"
src={img_path + show.poster_path}
alt={show.name}
/>
</Link>
</div>
);
})}
</div>
);
}
// Set up the 'loading' screen
loading = (
<div className="progress">
<div className="indeterminate"></div>
</div>
);
return (
<div className="container">
{tvLoading ? loading : TvData}
</div>
);
};
export default Home;
I've tried a combination of act, findByTestId, waitFor, etc. But I can't get it to work properly.
For example, I tried something like this:
it('should display TV shows', async () => {
const { getByText, findByTestId } =
render(
<BrowserRouter>
<Home />
</BrowserRouter>
)
await findByTestId('home-shows')
expect(getByText('More Info')).toBeInTheDocument();
});
My thinking was, if the content appears then it should contain the text of "More Info". If that's not the case the content is not visible, so the test should fail. however, the test fails regards if the content appears or not and I'm getting an error that I should wrap my test inside of an act() callback.
Thanks to #EstusFlask I came to a breakthrough. The solution was to use waitFor.
This is how I solved the problem:
it('should display movies', async () => {
render(
<BrowserRouter>
<Home />
</BrowserRouter>
);
const data = await waitFor(() => screen.findByTestId('home-shows'));
expect(data).toBeTruthy();
});
Related
I am using portal in React and want to get access to elements from other components.
For example, I have components with:
// here I want to put some content via portal
<div id='buttons'></div>
That is my component where I create portal
const elem = document.getElementById('buttons');
return (
<div className="block-mainContent">
{isLoading ?
<Loader />
:
<Table
data={memoCustomers}
headers={headers}
actionRender={actionRender}
showCheckbox
onSelect={onSelect}
onSelectAll={onSelectAll}
selectedRows={Array.from(selectedIds)}
/>}
<Modal
isOpen={isOpen}
onClose={() => dispatch(closeModalAction())}
content={editModalContent()}
/>
{!!elem && createPortal(<button>test</button>, elem)} // !!! here is my portal !!!
</div>
);
My problem is I get such error "Target container is not a DOM element
As I understand, the problem happens because I try to get an element before it is mounted and as result I get null.
How to check if my div elemen exists and then to create portal?
I try to use different approaches. In the code above I use that
{!!elem && createPortal(<button>test</button>, elem)}
// here I check If element exists it will return true and portal will be created
// but it works a bit wrong. Describe it below
It helps to remove error, but my portal starts working only at the second render. I mean, after loading my page there is no portal. If only I make smth on the page, rerender happens and portal appears.
I tried else
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, []);
{mounted && createPortal(<button>test</button>, elem)}
// here I create portal in the first render via useEffect and
But it throws the same error. No one of my ideas works.
If you know how to fix it, thank you in advance!
I think you need to create your buttons element to the parent component which of you need to create portal, or html body in index.html.
For example
// index.html in public folder if
<html>
<body>
<div id="root"></div>
<div id='buttons'></div>
</body>
...
</html>
or in your parent component without using useRef and follow your previous coding style
import React, { useEffect, useState } from "react";
import { createPortal } from "react-dom";
const PortalContainer = () => {
return <div id={"buttons"}></div>;
};
const Home = () => {
const [mounted, setMounted] = useState(false);
const [portalElement, setPortalElement] = useState();
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
const portalContainer = document.getElementById("buttons");
if (portalContainer)
setPortalElement(createPortal(<button>test</button>, portalContainer));
}, [mounted]);
return (
<>
<h2>Portal Testing</h2>
<PortalContainer />
{portalElement}
</>
);
};
export default Home;
Try with useRef for the portal DOM node.
// parent component
const buttonsContainerRef = useRef();
<div ref={buttonsContainerRef}>{/* buttons */}</div>
<MainContent buttonsContainer={buttonsContainerRef.current} />
// main content
{buttonsContainer ? createPortal(<button />, buttonsContainer) : null}
I'm trying to test my component, so here it's :
import React from 'react';
import { useNavigate } from 'react-router-dom';
import '../styles/mainPageStyles.css';
const MainPage = (): React.ReactElement => {
const navigate = useNavigate();
const handleBeginQuiz = () => {
navigate('/quiz');
};
return (
<div className="mainPageContainer">
<div className="mainpageWrapper">
<h2 className="defaultFontSize">Welcome to the Trivia Challenge!</h2>
<p className="defaultFontSize">
You will be presented with 10 True or False questions.
</p>
<p className="defaultFontSize">Can you score 100%?</p>
<button
className="beginButton defaultFontSize"
onClick={handleBeginQuiz}
aria-label="BEGIN"
>
BEGIN
</button>
</div>
</div>
);
};
export default MainPage;
as you see it has only one functionality, to redirect me to another page,
I ended to test on click event on the button it self,
It seems like I can't select it, I always get this error:
Main Page Tests › On Begin Button Click
TestingLibraryElementError: Unable to find an element with the text: BEGIN. 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.
and here are my attempts:
test('On Begin Button Click', () => {
const history = createMemoryHistory();
render(
<MemoryRouter initialEntries={[`/`]}>
<Routes>
<Route element={<MainPage />} />
</Routes>
</MemoryRouter>
);
// I have also tried getByText
const buttonElement = screen.getAllByText('BEGIN', { selector: 'button' });
// fireEvent.click(buttonElement);
// expect(history.location.pathname).toBe('/quiz');
});
try using findByText instead of getByText
this is a component of a mern crud app. Post creation works and were displaying on the browser and mongo DB. but while implementing update, JSX of one of the component is causing the app to crash. Please check the following component
import React from 'react'
import Post from './Post/Post'
import { useSelector } from 'react-redux';
const Posts = ({ setCurrentId }) => {
const posts = useSelector((state) => state.posts);
console.log(posts);
return (
!posts.length ? "No Posts, Make a post and come back" : (
<div className='outer'>
{
posts.map((post)=>(
<div className = 'inner' key= {post._id}>
<Post post = {post} setCurrentId = {setCurrentId} />
</div>
))
}
</div>
)
)
}
I am getting these errors ONLY in console, visual code does not show any error for client and server side.
Error in console
hit and trial :
if I comment out everything inside the outer div, I can see part of the app(one component of two components in the App.js), if I paste it back I can see the whole app, but when I refresh it goes back to black white screen.
guys plese let me know whats causing this, I guessing it is to do with the syntax, but I tried a lot o things but still not working.
Inside a component your return need to be XML (JSX = JS + XML)
So, your return must be begin by a and finish by the close of the same element .
In your case :
return (
<> //this is fragment, see react doc for that
{productsData.map((product) => {
return (
<>
{posts.length === 0 ?
"No Posts, Make a post and come back"
: (
<div className="outer">
{posts.map((post) => (
<div className="inner" key={post._id}>
<Post post={post} setCurrentId={setCurrentId} />
</div>
))}
</div>
)}
</>
);
})}
</>
)
Does state.posts exist in your store?
And you need to return a JSX element, always
To have a safe render, try;
import React from "react";
import Post from "./Post/Post";
import { useSelector } from "react-redux";
import "./Posts.css";
const Posts = ({ setCurrentId }) => {
const posts = useSelector((state) => state.posts);
console.log(posts);
return (
<>
{(posts.map = (post) => {
let obj = {
post: { post },
setCurrentId: { setCurrentId }
};
return <Post obj={obj} />
}
)}
</>
);
};
export default Posts;
Suppose I need to build a home page and I want the h1 and p to be rendered first and if the user scroll to the area of MyComponent, MyComponnet gets rendered or the async call in MyComponent does not prevent h1 or p rendering so that to have a better user experience. Is there a way I can do it?
const Home = () => {
return <div>
<h1>Home Page</h1>
<p>aaaaaaaaaa</p>
<p>aaaaaaaaaa</p>
<p>aaaaaaaaaa</p>
<MyComponent />
</div>;
}
const MyComponent = () => {
const res = await fetch('some url...');
// ... some code process the res
const data = processRes(res);
return <div>data</div>
}
React is evolving for such use cases for enhanced experience and currently it's in experimental phase.
https://reactjs.org/docs/concurrent-mode-intro.html
Having said that, yours can be achieved with minor changes.
const MyComponent = React.lazy(() => import('./MyComponent')); // load lazy
return (
<>
<h1></h1>
<p></p>
<Suspense fallback={<SplashScreen/>}>
<MyComponent/>
</Suspense>
</>);
My tests are affecting each other. I'm using the default create-react-app setup with Typescript. All tests run fine individually but when I run all tests the last one fails (both in IntelliJ and npm test). The assertion that fails finds a value that was caused by the previous test.
Now I have read articles such as Test Isolation with React but I am not sharing any values between my tests. I also read about the cleanUp function and tried adding beforeEach(cleanup) and beforeAll(cleanUp), but I didn't found a working solution yet besides putting each test in a separate file. I feel the solution should be pretty simple.
I've quickly generated a create-react-app with TypeScript to reproduce the issue in a small as possible project: https://github.com/Leejjon/breakingtests
My App.tsx
import React from 'react';
import './App.css';
import {BrowserRouter, Link, Route} from 'react-router-dom';
const About: React.FC = () => {
return (
<div>
<h1 id="pageHeader">About page</h1>
<p>This is the about page</p>
</div>
);
};
const Home: React.FC = () => {
return (
<div>
<h1 id="pageHeader">Home page</h1>
<p>This is the home page</p>
</div>
);
};
const News: React.FC = () => {
return (
<div>
<h1 id="pageHeader">News page</h1>
<p>This is the news page</p>
</div>
);
};
const App: React.FC = () => {
return (
<div className="App">
<BrowserRouter>
<Link id="linkToHome" to="/">Home</Link><br/>
<Link id="linkToNews" to="/news">News</Link><br/>
<Link id="linkToAbout" to="/about">About</Link>
<Route exact path="/" component={Home}/>
<Route exact path="/news" component={News}/>
<Route exact path="/about" component={About}/>
</BrowserRouter>
</div>
);
};
export default App;
My App.test.tsx:
import React from 'react';
import {render, fireEvent, waitForElement} from '#testing-library/react';
import App from './App';
describe('Test routing', () => {
test('Verify home page content', () => {
const {container} = render(<App/>);
const pageHeaderContent = container.querySelector("#pageHeader")
?.firstChild
?.textContent;
expect(pageHeaderContent).toMatch('Home page');
});
test('Navigate to news', async () => {
const {container} = render(<App/>);
const pageHeaderContent = container.querySelector("#pageHeader")
?.firstChild
?.textContent;
expect(pageHeaderContent).toMatch('Home page');
const linkToNewsElement: Element = (container.querySelector('#linkToNews') as Element);
fireEvent.click(linkToNewsElement);
const pageHeaderContentAfterClick = await waitForElement(() => container.querySelector('#pageHeader')?.firstChild?.textContent);
expect(pageHeaderContentAfterClick).toMatch('News page');
});
test('Navigate to about', async () => {
const {container} = render(<App/>);
const pageHeaderContent = container.querySelector("#pageHeader")
?.firstChild
?.textContent;
expect(pageHeaderContent).toMatch('Home page');
const linkToAboutElement: Element = (container.querySelector('#linkToAbout') as Element);
fireEvent.click(linkToAboutElement);
const pageHeaderContentAfterClick = await waitForElement(() => container.querySelector('#pageHeader')?.firstChild?.textContent);
expect(pageHeaderContentAfterClick).toMatch('About page');
});
});
I found out by adding console.log(document.location.href); that the location is not reset. Which makes sense.
The code below resets the url. I could enter any domain to fix my tests, for example http://blabla/ will also work.
beforeEach(() => {
delete window.location;
// #ts-ignore
window.location = new URL('http://localhost/');
});
In TypeScript this gives an error: TS2739: Type 'URL' is missing the following properties from type 'Location': ancestorOrigins, assign, reload, replace. I didn't know how to fix this so I suppressed it it for now.
EDIT:
cleanup Unmounts React trees that were mounted with render, but doesn't reset state from stores/reducers. The solution I took for this situation was to create a reset function in my store and call it at the beginning of each test.
resetStore: () => {
set(initialState);
},
and call it in your test file
beforeEach(() => {
resetStore();
});
If you're using mocha, Jest, or Jasmine, the cleanup will be done automatically, but you need to put your render in a beforeEach to recreate it for every test.
let container;
beforeEach(() => {
const app = render(<App/>);
container = app.container
});
If you use another testing framework, you'll need to cleanup manually like so
import { cleanup, render } from '#testing-library/react'
import test from 'ava'
test.afterEach(cleanup)