How to isolate tests in React Testing Library? - reactjs

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)

Related

How can I test a redirect call using react-router-dom v6?

I've got a react page:
import { RouterProvider } from "react-router-dom";
export const App: React.FC<{router: any}> = ({router}) => {
return (
<div className="flex flex-col h-full">
<div className="h-1/8 text-center border-brand-primary border-2">Nav bar with login goes here</div>
<div className="h-1/8 text-center border-brand-primary border-2">Pathways overview data goes here</div>
<div className="flex-auto">
<RouterProvider router={router} fallbackElement={<PageNotFound />} />
</div>
<div className="mt-64">
<Footer/>
</div>
</div>
);
}
I have the routes setup thus:
const routes = [
...Routes,
{
path: "*",
element: <Navigate to="/thing/123" replace />
}
]
I'm trying to write a jest test to test the redirect. So far I've gotten this:
import { render, screen, waitFor } from "#testing-library/react";
import React from "react";
import { createMemoryRouter } from "react-router-dom";
...
test('root sends redirect', async () => {
const router = createMemoryRouter(routes);
render(<App router={router} />);
await waitFor(() => {
expect(router.state.location.pathname).toEqual('/thing/123');
}, {timeout: 5000});
});
The problem is, the redirect never seems to happen inside the test. Anyone know what I'm missing here?
I've found lots of examples around react-router v4 and v5 but none of those seem to be working with v6. Worth noting, the redirect does work in the wild, but I can't seem to replicate that functionality in a unit test.

Next/React lazy load not working as expected for components

Hi all I am new in Next/react I am trying to lazy load one component. What my expectation is that component must loads when it is visible in user view port or once a page is fully rendered.
Its html code should not come in the first response. But after debugging I found that it is not working as expected.
I have tried below approaches
index.js
const ProductTabbedWidget = React.lazy(() => import('../../../components/ProductSearch/ProductTabbed/ProductTabbedWidget'));
and
const ProductTabbedWidget = dynamic(() => import('../../../components/ProductSearch/ProductTabbed/ProductTabbedWidget'),{suspense:true,});
<Suspense fallback={<div>Loading</div>}>
<ProductTabWidget vehicleType={type}></ProductTabWidget>
</Suspense>
Component: ProductTabbedWidget
const ProductTabWidget = (props) => {
const getData = () =>{
// fetch the data from api
// ex. locahost:7000/api/get-data
}
useEffect(()=>{
getData()
},[])
return (
<div></div>
)
}
The call to this api is visible in chrome when the page loads.
I am confused if react lazy is the rigt way to do this. I know this can be done using javascript but is there any way to do it ony by react or next.
I have gone through these answers but none of them works.
React suspense/lazy delay?
How to know if React lazy load component is working or not? React js
react
React lazy loading - when to use
In react side use lazy loading like this.
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
In Next JS
import dynamic from 'next/dynamic'
const DynamicComponent = dynamic(() =>
import('../components/hello').then((mod) => mod.Hello)
)
function Home() {
return (
<div>
<Header />
<DynamicComponent />
<p>HOME PAGE is here!</p>
</div>
)
}
export default Home

Query a simple button with jest and

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

Using useHistory on React

I am a beginner in React and trying to learn things by myself. I have this code that I'd like to navigate to the Login page using useHistory but I can't seem to make it work. Hope you can help me. Here is my code below:
import { useHistory } from "react-router-dom";
const App = () => {
let history = useHistory();
const MoveToLogin = () => {
history.push('./container/Login');
}
return (
<div>
<button className='btn' text='User Login' onClick=.
{MoveToLogin}>Login</button>
</div>
);
}
export default App;
First you need to add Provider 'Router' which is imported from 'react-router-dom'
Define routes and corresponding components in routing file.
You can only use history inside the children of Provider Router.
Navigate to route using history.push('/login'). Don't provide relative path of component file here. Use the route you want to show in browser url
I have played around and did more research about useHistory. I was able to make it work. It has navigated to a new component. Please find my solution below. Hope it can help others with the same kind of issue.
import UserLogin from "./pages/UserLogin";
const ButtonLogin = () => {
let history = useHistory();
const MoveToLogin = () => {
history.push('/pages/UserLogin');
}
return (
<div><button className='btn btn-primary' onClick={MoveToLogin}>Login</button></div>
);
}
const App = () => {
return (
<div>
<Router>
<Route path="/pages/UserLogin" exact component={UserLogin} />
<Route path="/" exact component={ButtonLogin} />
</Router>
</div>
);
}
export default App;
Refer to this answer as well for further information.
Cannot read property 'push' of undefined for react use history
I hope you have the routes defined for the path specified
<Router>
<Route path="/container/Login" exact component={LoginComponent} />
</Router>
The dot is not needed in your move to login function.
import { useHistory } from "react-router-dom";
const App = () => {
let history = useHistory();
const MoveToLogin = () => {
history.push('/container/Login'); // Here you don't need dot ./container
}
return (
<div>
<button className='btn' text='User Login' onClick=.
{MoveToLogin}>Login</button>
</div>
);
}
export default App;

How would I test this using Jest & React Testing library?

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

Resources