When I test the first case when a component is routed, and test another case, the previous case still appears in the test DOM.
After the test case : "renders properly" rendered a route to the Header component,
and the test case: "navigates to / on header title click" fails because the Header component is appearing more than once because the test DOM didn't clear the first case. I hope this image helps.
import * as React from 'react'
import {vi, describe, it, expect} from 'vitest'
import { fireEvent, waitFor} from '#testing-library/react'
import { Header } from "./Header"
import { renderWithRouter } from '../testHelpers'
vi.mock("./../CartWidget", ()=> ({
CartWidget: ()=> <div>Hello Widget</div>
}))
describe('Header Shared Component', ()=> {
it('renders properly', ()=> {
const {container} = renderWithRouter(()=><Header/>)
expect(container.innerHTML).toMatch('Goblin Store')
expect(container.innerHTML).toMatch('Hello Widget')
})
it('navigates to / on header title click', async()=> {
const {getByText, history} = renderWithRouter(() => <Header />);
fireEvent.click(getByText("Goblin Store"));
await waitFor (()=>expect(history.location.pathname).toEqual("/"))
})
})
// renderWithRouter
import * as React from 'react'
import * as ReactDom from 'react-router-dom';
import {createMemoryHistory, MemoryHistory} from 'history'
import {render, RenderResult} from '#testing-library/react'
const {Router} = ReactDom;
export const renderWithRouter: RenderWithRouter = (renderComponent, route)=>{
const history = createMemoryHistory()
if(route){
history.push(route)
}
return {
...render(
<Router history={history}>
{renderComponent()}
</Router>),
history
}
}
type RenderWithRouter = (
renderComponent: ()=> React.ReactNode,
route?: string
)=> RenderResult & { history: MemoryHistory }
export const Header = () => (
<header>
<div className="container">
<div className="nav-brand">
<Link to="/">Goblin Store</Link>
<p>Everything for your Typescript adventure</p>
</div>
...
</div>
</header>
);
I have read the react router official DOC with testing for react-router-dom v5, but it appears to be same on my end.
Here are my dependencies
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^5.3.4",
"#testing-library/jest-dom": "^5.16.5",
"#testing-library/react": "^13.4.0",
"vite": "^4.0.0",
"vitest": "^0.26.3"
Related
I have a problem with a new nextjs application, I have configured Material UI
Each time I refresh the page (hard) the style is not correct anymore, I have to redo modifications in each file to find my previous style...
I show you my _app and _document files
Thanks in advance for your help
_app.js
import React from "react";
import { CacheProvider } from "#emotion/react";
import { CssBaseline } from "#mui/material";
import { ThemeProvider } from "#mui/styles";
import createEmotionCache from "../styles/createEmotionCache";
import theme from "../config/MUITheme";
const clientSideEmotionCache = createEmotionCache();
const MyApp = ({ Component, emotionCache = clientSideEmotionCache, pageProps }) => (
<CacheProvider value={emotionCache}>
<ThemeProvider theme={theme}>
<CssBaseline />
<Component {...pageProps} />
</ThemeProvider>
</CacheProvider>
);
export default MyApp;
_document.js
import * as React from "react";
import Document, { Html, Head, Main, NextScript } from "next/document";
import createEmotionServer from "#emotion/server/create-instance";
import createEmotionCache from "../styles/createEmotionCache";
export default class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
MyDocument.getInitialProps = async (ctx) => {
const originalRenderPage = ctx.renderPage;
const cache = createEmotionCache();
const { extractCriticalToChunks } = createEmotionServer(cache);
/* eslint-disable */
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) =>
function EnhanceApp(props) {
return <App emotionCache={cache} {...props} />;
},
});
/* eslint-enable */
const initialProps = await Document.getInitialProps(ctx);
const emotionStyles = extractCriticalToChunks(initialProps.html);
const emotionStyleTags = emotionStyles.styles.map((style) => (
<style
data-emotion={${style.key} ${style.ids.join(" ")}}
key={style.key}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: style.css }}
/>
));
return {
...initialProps,
// Styles fragment is rendered after the app and page rendering finish.
styles: [...React.Children.toArray(initialProps.styles), ...emotionStyleTags],
};
};
Before refresh :
After refresh :
Environment :
"next": "12.1.0",
"react": "17.0.2",
"#emotion/cache": "^11.7.1",
"#emotion/react": "^11.8.1",
"#emotion/server": "^11.4.0",
"#emotion/styled": "^11.8.1",
"#fontsource/roboto": "^4.5.3",
"#mui/icons-material": "^5.4.4",
"#mui/material": "^5.4.4",
"#mui/styles": "^5.4.2"
You can reproduce with : https://codesandbox.io/s/throbbing-rain-72v10e
I had the same problem for months and I realized even though your app.js and document.js is fine by hard refreshing it takes a bit to read theme(I still don't know why). My workaround for this was fully load the components that had this issue on client side. Components that don't need to be SSR you can turned off SSR feature for them. you can do this in two ways
turn off SSR when importing:
export const component = dynamic(
() => import('../components/component'),
{
ssr: false,
}
);
turn off ssr with a wrapper around component:
import React from 'react';
import dynamic from 'next/dynamic';
const NoSSRWrapper = props => <React.Fragment>{props.children}</React.Fragment>;
export default dynamic(() => Promise.resolve(NoSSRWrapper), {
ssr: false,
});
return (
<NoSSRWrapper>
//your component code
</NoSSRWrapper>
)
this way everything Waites and loads fully on client side therefore refreshing/hard refreshing doesn't affect the load of CSS.
this link was really helpful for me
How to cover the lazy load component in react testing library.
import React, {lazy} from 'react';
const ownerInfo = lazy(() => import('../abc'))
const compone = () => {
return <Suspense><abc /></Suspense>
}
export default compone
test.spec.js
import React from 'react'
import {render, fireEvent} from '#testing-library/react'
import configureStore from 'redux-mock-store'
...
After watching the video, I am able to figure out how to cover the lazy load. Let assume that you have lazyload component.
LazyLoad.jsx:
import React, {lazy} from 'react'
const LazyComponent = lazy(() => import('./LazyComponent'))
const LazyLoad = () => {
return (
<div>
<div> Lazy component is here: </div>
<React.Suspense fallback={null}>
<LazyComponent />
</React.Suspense>
</div>
)
}
export default LazyLoad
LazyComponent.jsx:
import React from 'react'
export default () => <div>I am lazy ! </div>
LazyLoad.spec.jsx:
import React from 'react'
import {render, waitFor } from 'react-testing-library'
import LazyLoad from 'LazyLoad'
test('renders lazy component', async () => {
const { getByText } = render(<LazyLoad />)
await waitFor(() => expect(getByText('I am lazy !' )).toBeInTheDocument())
})
According to Kent Dodds, the React Testing Library creator, you should prefer using findByText rather than waitFor + expect.
Those two bits of code are basically equivalent (find* queries use waitFor under the hood), but the second is simpler and the error message you get will be better.
With that in mind, I suggest to refactor your test like this
import React from 'react'
import {render, waitFor } from 'react-testing-library'
import LazyLoad from 'LazyLoad'
test('renders lazy component', async () => {
const { getByText } = render(<LazyLoad />)
expect(
await screen.findByText('I am lazy !')
).toBeInTheDocument();
})
The below test continues to fail. On button click it calls #reach/router navigate with a query string /?searchString=abcde however the history object is not updated in the test. How do you test programmatic navigation updates with react-testing-library and reach-router without mocking.
import { createHistory, createMemorySource, LocationProvider, navigate } from "#reach/router"
import { render, waitFor } from "#testing-library/react"
import userEvent from "#testing-library/user-event"
import React from 'react'
test('Can us search path', async () => {
const history = createHistory(createMemorySource('/'))
const page = render(
<LocationProvider history={history}>
<div>
<button onClick={() => navigate('/?searchString=abcde')}>navigateButton</button>
</div>)
</LocationProvider>
)
expect(history.location.search).toEqual('')
userEvent.click(page.getByText('navigateButton')) //click button to call navigate
await waitFor(() => expect(history.location.search).toEqual('searchString=abcde')) //FAILS but Should PASS
})
dependencies
"#testing-library/react": "^11.2.3",
"#testing-library/user-event": "^12.6.0",
"#testing-library/jest-dom": "^5.11.9",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"#reach/router": "^1.3.4",
I am new to React and Redux, I am actually creating a code of myself but I got stuck with a router kind of thing inside stateless component.
So, Actually I need to route to a component by using this.props.history.push('/somepath'). This is not happening inside a stateless component.
My Stateless component is
import React from "react"; // eslint-disable-line no-unused-vars
import "bootstrap/dist/css/bootstrap.min.css";
import "./header.css";
const Header = () => ({
handleAuthor() {
this.props.history('/somepath')// this is not working
},
render(){
return (
<div className = "header">
<h1 onClick = {this.handleAuthor.bind(this)}>{this.props.headerText}</h1>
</div>
);
}
});
export default Header;
I am calling this inside another stateless component like mainlayout
import React from "react"; // eslint-disable-line no-unused-vars
import Header from "../header/Header"; // eslint-disable-line no-unused-vars
import Footer from "../footer/Footer"; // eslint-disable-line no-unused-vars
import "./mainLayout.css";
const MainLayout = props => ({
render() {
return (
<div className="App">
<Header headerText = "RK Boilerplate"/>
<div className = "mainLayout">
<main>{props.children}</main>
</div>
<Footer />
</div>
);
}
});
export default MainLayout;
My main file index.js looks like this
import React from "react"; // eslint-disable-line no-unused-vars
import ReactDOM from "react-dom"; // eslint-disable-line no-unused-vars
import { matchRoutes, renderRoutes } from "react-router-config"; // eslint-disable-line no-unused-vars
import { Router } from "react-router-dom"; // eslint-disable-line no-unused-vars
import { Switch } from "react-router"; // eslint-disable-line no-unused-vars
import { Provider } from "react-redux"; // eslint-disable-line no-unused-vars
import store from "./store"; // eslint-disable-line no-unused-vars
import routes from "./routes"; // eslint-disable-line no-unused-vars
import MainLayout from "./components/mainLayout/MainLayout"; // eslint-disable-line no-unused-vars
import createHistory from "history/createBrowserHistory";
let history = createHistory();
const App = document.getElementById("app");
export default App;
ReactDOM.render(
<Provider store={store}>
<MainLayout>
<Router history= {history}>
<Switch>
{renderRoutes(routes)}
</Switch>
</Router>
</MainLayout>
</Provider>,
App);
SO what i need is I have to route from the header to another component where this component and path is given in a router file
router.js
import Home from "../containers/Home";
import About from "../containers/About";
import CreateUser from "../containers/CreateUser";
import Layout from "../containers/Layout";
const routes = [
{ path: "/",
exact: true,
component: Layout
},
{ path: "/home",
exact: true,
component: Home
},
{
path:"/About",
exact: true,
component: About
},
{
path:"/createUser",
exact: true,
component: CreateUser
}
];
export default routes;
I got an error like push of undefined , when i tried to route from header.
Am I missing something is there any change that i should do here.
Thanks in advance.
In 2019, React Router v4 has now added the useHistory hook for stateless / functional components:
https://reacttraining.com/react-router/web/api/Hooks/usehistory
From the docs:
import { useHistory } from "react-router-dom";
function HomeButton() {
let history = useHistory();
function handleClick() {
history.push("/home");
}
return (
<button type="button" onClick={handleClick}>
Go home
</button>
);
}
It worked for me by using withRouter, (plus ignoring typescript warning):
import { withRouter } from 'react-router-dom';
...
type Props = { myProp: boolean };
// #ts-ignore
export const MyComponent: FC<Props> = withRouter(({ myProp, history }) => {
...
})
You can directly import the history object and push from that. For example try below code.
import React from "react"; // eslint-disable-line no-unused-vars
import "bootstrap/dist/css/bootstrap.min.css";
import "./header.css";
import history from "/your/history/path" //try this
const Header = () => ({
handleAuthor() {
history.push('/somepath')// change to this
},
render(){
return (
<div className = "header">
<h1 onClick = {this.handleAuthor.bind(this)}>{this.props.headerText}</h1>
</div>
);
}
});
export default Header;
Create browser history like below and use it every where by importing.
import createBrowserHistory from 'history/createBrowserHistory';
export default createBrowserHistory({});
Hope this doesn't come in too late. I successfully could redirect from a stateful component but I wanted to convert my component to a stateless component since the only thing that made it stateful was the redirect bit. I'm fairly new to react and most of the resources out there were not as clear. This is how I went about it.
import React from "react"; // eslint-disable-line no-unused-vars
import "bootstrap/dist/css/bootstrap.min.css";
import "./header.css";
const Header = (props) => {
return (
<div className = "header">
<h1 onClick = {props.history.push('/some-path')}>{props.headerText}</h1>
</div>
);
}
export default Header;
I have 2 containers, both import a component called PageHeader this is a functional component and is imported / exported as like so....
( this happens with any component I import, not just PageHeader )
components/PageHeader/PageHeader.jsx
import React from 'react';
import PropTypes from 'prop-types';
import './PageHeader.sass';
const PageHeader = ({ title, description, image }) => (
<div className="section is-paddingless is-hidden-touch">
<div className="header header__desktop" style={{ backgroundImage: `url('${image}')` }}>
<h3 className="is-size-3 has-text-weight-semibold">{title}</h3>
<h5>{description}</h5>
</div>
</div>
);
PageHeader.propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
image: PropTypes.string.isRequired,
};
export default PageHeader;
components/PageHeader/index.js
import PageHeader from './PageHeader';
export default PageHeader;
In the root of my components folder I then have....
components/index.js
......
export { default as PageHeader } from './PageHeader';
......
This allows me to import components into a container like so....
import { DelayedComponent, Loading, Page, PageHeader, ClientList, RecentlyViewed, ClientFilter } from '../../components';
When I only had 1 container, this was working fine, I have now introduced a second container, this is essentially a second page.
My Containers are as follows....
containers/Clients/Clients.jsx
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { DelayedComponent, Loading, Page, PageHeader, ClientList, RecentlyViewed, ClientFilter } from '../../components';
import headerImage from '../../assets/images/client-search-banner.jpg';
class Clients extends Component {
state = { selector: 'ALL' };
static propTypes = {
fetchClientListAction: PropTypes.func.isRequired,
selectClientAction: PropTypes.func.isRequired,
isLoading: PropTypes.bool.isRequired,
clients: PropTypes.array.isRequired,
};
componentDidMount() {
this.props.fetchClientListAction();
}
onClientSelect = client => this.props.selectClientAction(client);
onFilterTenantList = selector => this.setState({ selector });
render() {
const { isLoading, clients } = this.props;
const { selector } = this.state;
if (isLoading)
return (
<DelayedComponent>
<Loading />
</DelayedComponent>
);
return (
<Fragment>
<PageHeader
title="Choose a client"
description="Bacon ipsum dolor amet ribeye biltong tongue, pig brisket venison fatback pork bacon kielbasa burgdoggen salami strip steak."
image={headerImage}
/>
<Page>
<Page.Columns>
<Page.Column modifiers="is-paddingless">
<ClientFilter clients={clients} selector={selector} filterByLetter={this.onFilterTenantList} />
</Page.Column>
</Page.Columns>
<Page.Columns modifiers="is-multiline">
<Page.Column modifiers="is-two-thirds">
<ClientList clients={clients} selector={selector} onClientSelect={this.onClientSelect} />
</Page.Column>
<Page.Column modifiers="is-one-third">
<RecentlyViewed />
</Page.Column>
</Page.Columns>
</Page>
</Fragment>
);
}
}
export default Clients;
containers/EmployeeSearch/EmployeeSearch.jsx
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { PageHeader } from '../../components';
import headerImage from '../../assets/images/client-search-banner.jpg';
class EmployeeSearch extends Component {
render() {
return (
<PageHeader
title="Search"
description="Bacon ipsum dolor amet ribeye biltong tongue, pig brisket venison fatback pork bacon kielbasa burgdoggen salami strip steak."
image={headerImage}
/>
);
}
}
export default EmployeeSearch;
Doing this causes Webpack crash with the following error...
/Users/xxxxx/Code/xxxxx/search-webapp/node_modules/toposort/index.js:29
throw new Error('Cyclic dependency: '+JSON.stringify(node))
^
TypeError: Converting circular structure to JSON
at JSON.stringify (<anonymous>)
at visit (/Users/xxxxx/Code/xxxxx/search-webapp/node_modules/toposort/index.js:29:50)
at visit (/Users/xxxxx/Code/xxxxx/search-webapp/node_modules/toposort/index.js:47:9)
at visit (/Users/xxxxx/Code/xxxxx/search-webapp/node_modules/toposort/index.js:47:9)
at Function.toposort [as array] (/Users/xxxxx/Code/xxxxx/search-webapp/node_modules/toposort/index.js:22:22)
at Object.module.exports.dependency (/Users/xxxxx/Code/xxxxx/search-webapp/node_modules/html-webpack-plugin/lib/chunksorter.js:50:35)
at HtmlWebpackPlugin.sortChunks (/Users/xxxxx/Code/xxxxx/search-webapp/node_modules/html-webpack-plugin/index.js:364:35)
at /Users/xxxxx/Code/xxxxx/search-webapp/node_modules/html-webpack-plugin/index.js:113:21
at AsyncSeriesHook.eval [as callAsync] (eval at create (/Users/xxxxx/Code/xxxxx/search-webapp/node_modules/tapable/lib/HookCodeFactory.js:24:12), <anonymous>:7:1)
at AsyncSeriesHook.lazyCompileHook [as _callAsync] (/Users/xxxxx/Code/xxxxx/search-webapp/node_modules/tapable/lib/Hook.js:35:21)
error An unexpected error occurred: "Command failed.
However, if I change the import in EmployeeSearch.jsx so it looks like this....
import PageHeader from '../../components/PageHeader';
It works.
I am at a complete loss and would love some input as to why / how this happening and how I can fix this without adding multiple import statements for components.
My Containers are rendered as follows....
import React from 'react';
import Loadable from 'react-loadable';
import { Switch, Route } from 'react-router-dom';
import Loading from './components/Loading';
const AsyncRoute = loader =>
Loadable({
loader,
loading: Loading,
delay: 300,
});
const Clients = AsyncRoute(() => import(/* webpackChunkName: "clients" */ './containers/Clients'));
const EmployeeSearch = AsyncRoute(() => import(/* webpackChunkName: "employee-search" */ './containers/EmployeeSearch'));
const Routes = () => (
<Switch>
<Route exact path="/" component={Clients} />
<Route path="/employee-search/:client" component={EmployeeSearch} />
</Switch>
);
export default Routes;
It turns out I had included App within my components index.js file.
My routes are wrapped in App, so that was my circular dependancy.