Mock Router in React testing library and jest - reactjs

I'm writing unit test with React testing library and Jest and wants to check if my React Component is successfully able to navigate to next Page.
import { fireEvent, render, screen } from "#testing-library/react";
import React from 'react';
import { Provider } from 'react-redux';
import '#testing-library/jest-dom';
import appStore from '../../src/app/redux/store';
import { MemoryRouter, Route } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { Router } from 'react-router';
const setup = (initialEntries = []) => {
let inMemHistory = createMemoryHistory({ initialEntries });
const utils = render(
<Router history={inMemHistory}>
<Provider store={appStore}>
<Component-1 />
</Provider>
</Router>
);
const saveButtonElem = screen.getByRole('button', { name: "Save and Continue" });
return {
saveButtonElem,
inMemHistory,
...utils,
}
};
Test:
test('should be able to navigate', async () => {
const {
saveButtonElem,
inMemHistory,
getByText,
queryByText,
queryAllByText,
} = setup(["/component_add"]);
// Content of Test
// Saving Config
fireEvent.click(saveButtonElem);
console.info("Current Path", inMemHistory.location.pathname);
// Got /component_add on console
// Expected /component_data after clicking on save button
})
I've tried waiting for 5 second after clicking save button and then tried to print path, but results are same.

Assuming you use react-router, You can use the Memory router for the testing which is easier and performant. I might have typos or syntax errors as I type without IDE support. But it should help you with idea on what I propose.
Option 1:
it("should route to the expected page", () => {
let mockHistory, mockLocation;
render(
<MemoryRouter initialEntries={["/currentUri"]}>
<Component1 />
// Dummy route that routes for all urls
<Route
path="*"
render={({ history, location }) => {
mockHistory= history;
mockLocation= location;
return null;
}}
/>
</MemoryRouter>
);
// navigate here on event
userEvent.click(screen.getByRole('button', {name: /Save/}));
expect(mockLocation.pathname).toBe("/expectedUri");
});
Option 2:
import { createMemoryHistory } from 'history';
import { Router } from 'react-router';
const renderWithHistory = (initialEntries= [], Component) => {
let inMemHistory = createMemoryHistory({
initialEntries
});
return {
...render(
<Router history={inMemHistory}>
<Component />
</Router >
), history };
};
it("should route to the expected page", () => {
const { history } = renderWithHistory(['/currentUri'], Component1);
// navigate here on event
userEvent.click(screen.getByRole('button', {name: /Save/}));
expect(history.location.pathname).toBe("/expectedUri");
});

Related

React lazy/Suspens + React Router dont change route until component is fetched

I am developing an application that uses the default React code spltting using the Lazy/Suspense approach and the React Router for component rendering. Currently, when I navigate to another path, if the network speed is slow, the path is updated and the fallback component is rendered while the component is fetched, is there any way to wait on the current path until the component package is completely downloaded?
Yes, in concurrent mode, where useTransition() is enabled, you can create a custom router to wrap each of the navigation methods on your history object in a suspense transition:
import { useState, unstable_useTransition as useTransition } from 'react';
import { Router } from 'react-router-dom';
const SuspenseRouter = ({ children, history, ...config }) => {
const [startTransition, isPending] = useTransition(config);
const [suspenseHistory] = useState(() => {
const { push, replace, go } = history;
history.push = (...args) => {
startTransition(() => { push.apply(history, args); });
};
history.replace = (...args) => {
startTransition(() => { replace.apply(history, args); });
};
history.go = (...args) => {
startTransition(() => { go.apply(history, args); });
};
});
suspenseHistory.isPending = isPending;
return (
<Router history={suspenseHistory}>
{children}
</Router>
);
};
export default SuspenseRouter;
Example usage might look something like this:
import { Suspense, lazy, unstable_createRoot as createRoot } from 'react';
import { Switch, Route } from 'react-router-dom';
import { createBrowserHistory } from 'history';
import SuspenseRouter from './components/SuspenseRouter';
const history = createBrowserHistory();
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const App = () => (
<SuspenseRouter history={history} timeoutMs={2000}>
<Suspense fallback="Loading...">
<Switch>
<Route path="/" exact={true} component={Home} />
<Route path="/about" component={About} />
</Switch>
</Suspense>
</SuspenseRouter>
);
createRoot(document.getElementById('root')).render(<App />);
Set timeoutMs to Infinity if you want to wait indefinitely on the previous route. In the example above, setting it to 2000 should wait on the previous route for up to 2 seconds, then display the fallback if the code for the requested route hasn't downloaded by then.
Here is another option: instead of suspending url change you can suspend screen change.
Package react-router-loading allows to show loading bar and fetch some data before switching the screen.
Just use Switch and Route from this package instead of react-router-dom:
import { Switch, Route } from "react-router-loading";
Add loading props to the Route where you want to wait something:
<Route path="/my-component" component={MyComponent} loading/>
And then somewhere at the end of fetch logic in MyComponent add loadingContext.done();:
import { LoadingContext } from "react-router-loading";
const loadingContext = useContext(LoadingContext);
const loading = async () => {
//fetching some data
//call method to indicate that fetching is done and we are ready to switch
loadingContext.done();
};

React Testing Library / Redux - How to mock cookies?

Issue
I'm able to mock the cookie in Jest, but it won't change the state of my components once the cookie is mocked.
For example, when a user visits my app, I want to check if a cookie of ACCESS_TOKEN exists, if it exists, render a saying "Hi, Username".
When testing, I'm able to create the cookie and get the values with console.log(), but my component won't render the because the test does not think redux-store has the cookie.
Here's what my redux-store looks like (Redux store is not the problem, all my tests that does not rely on cookies and soley relies on store are working):
Root.tsx
export const store = createStore(
reducers,
{ authStatus: { authenticated: Cookies.get("ACCESS_TOKEN") } },
//if our inital state (authStauts) has a cookie, keep them logged in
composeWithDevTools(applyMiddleware(reduxThunk))
);
const provider = ({ initialState = {}, children }) => {
return <Provider store={store}>{children}</Provider>;
};
export default provider
App.tsx
import Root from "./Root"; //root is provider variable in Root.tsx
ReactDOM.render(
<React.StrictMode>
<Root>
<App />
</Root>
</React.StrictMode>,
document.getElementById("root")
);
Welcome.tsx
const Welcome =(props) => {
return(
<div>
{props.authStatus && <h3> Hello USERNAME</h3>}
</div>
}
}
const mapStateToProps = (state) => {
return {
authStatus: state.authStatus.authenticated,
};
};
export default connect(mapStateToProps, {})(Welcome);
Here's my test:
import Cookies from "js-cookie"
beforeEach(async () => {
//Before the component renders, create a cookie of ACCESS_TOKEN.
//Method 1 (Works, console.log() below would show the value, but component won't render):
//jest.mock("js-cookie", () => ({ get: () => "fr" }));
//Method 2 (Works, console.log() below would show the value, but component won't render):
//Cookies.get = jest.fn().mockImplementation(() => "ACCESS_TOKEN");
//Method 3 (Works, console.log() below would show the value, but component won't render)):
// Object.defineProperty(window.document, "cookie", {
// writable: true,
// value: "myCookie=omnomnom",
// });
app = render(
<Root>
<MemoryRouter initialEntries={["/"]} initialIndex={0}>
<Routes />
</MemoryRouter>
</Root>
);
console.log("Cookie Val", Cookies.get());
app.debug(); //I want to see that the <h3> is rendered, but it's not being rendered.
});
Why is this occurring?
Resources used:
How to mock Cookie.get('language') in JEST
Use Jest to test secure cookie value
I'm not exactly sure how did you combine things together but I'm gonna drop you a full example that you can follow and fix your code as bellow, please check inline comments:
// Provider.jsx
import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import Cookies from "js-cookie";
// the reducer I assume as same as you created
const authReducer = (state = {}, action) => {
return {
...state,
...action.payload,
}
}
const store = createStore(
authReducer,
{ authStatus: { authenticated: Cookies.get("ACCESS_TOKEN") } }
);
export default ({ initialState = {}, children }) => {
return <Provider store={store}>{children}</Provider>;
};
// Routes.jsx
// should be the same as you did
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import Welcome from "./Welcome";
export default (props) => {
return (
<Switch>
<Route exact path="/" component={Welcome} />
</Switch>
)
}
Finally the test file:
// index.test.js
import React from 'react';
import Cookies from "js-cookie"
import "#testing-library/jest-dom"
import { screen, render } from '#testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import Routes from "./Routes";
import Provider from "./Provider";
// Mock your cookie as same you did
// but should be the same level with `import` things
jest.mock("js-cookie", () => ({ get: () => "fr" }), {
// this just for being lazy to install the module :)
virtual: true
});
it('should pass', () => {
render(
<Provider>
<MemoryRouter initialEntries={["/"]} initialIndex={0}>
<Routes />
</MemoryRouter>
</Provider>
);
expect(screen.queryByText('Hello USERNAME')).toBeInTheDocument()
})
PS: The link I created the test for you: https://repl.it/#tmhao2005/js-cra (ref at src/Redux to see full example)

React + Redux + Storybook: How to use connected react router's useParams when writing storybook stories?

I have a react component that grabs an id from the route and uses that to load some data and populate the redux state.
I am using useParams from 'react-router' to do this.
import { useParams } from 'react-router'
import { usePreload } from './hooks'
import Display from './Display'
const Overview = () => {
const { id } = useParams()
const { data } = usePreload(id) // uses useEffect to preload the data with the given id
return <Display data={data} />
}
export default Overview
I've got a story
import Overview from './Overview'
import preloadData from './decorators/preloadData'
export default {
title: 'Redux/scenes/Overview',
decorators: [preloadData()],
component: Overview,
argTypes: {}
}
const Template = args => <Overview {...args} />
export const Default = Template.bind({})
The preloadData decorator is simply
import { usePreload } from '../hooks'
import { data } from './fixtures'
const Loaded = ({ children }) => {
useSubmissionsPreload(data.id) // loads the site data into the state
return <>{children}</>
}
const preloadData = () => Story => (
<Loaded>
<Story />
</Loaded>
)
export default preloadData
The code all works fine when actually running in the site but when running within a story there is no :id in the path for useParams to pick up.
For now I am just going to skip this story and just test the Display component, but the completist in me demands to know how to get this to work.
I also had the problem and the comment from De2ev pointed me in the right direction. It did however not work directly and I had to make slight changes. In the end it worked with the following code:
import React from "react";
import { Meta } from "#storybook/react";
import MyComponent from "./MyComponent";
import { MemoryRouter, Route} from "react-router-dom";
export default {
title: "My Title",
component: MyComponent,
decorators: [(Story) => (
<MemoryRouter initialEntries={["/path/58270ae9-c0ce-42e9-b0f6-f1e6fd924cf7"]}>
<Route path="/path/:myId">
<Story />
</Route>
</MemoryRouter>)],
} as Meta;
export const Default = () => <MyComponent />;
I've faced the same problem with Storybook 6.3+ and React Router 6.00-beta and had to wrap the <Route> with <Routes></Routes> for it to work.
import React from "react";
import { Meta } from "#storybook/react";
import MyComponent from "./MyComponent";
import { MemoryRouter, Routes, Route} from "react-router";
export default {
title: "My Title",
component: MyComponent,
decorators: [(Story) => (
<MemoryRouter initialEntries={["/path/58270ae9-c0ce-42e9-b0f6-f1e6fd924cf7"]}>
<Routes>
<Route path="/path/:myId" element={<Story />}/>
</Routes>
</MemoryRouter>)],
} as Meta;
export const Default = () => <MyComponent />;
We have faced similar challenge when trying to create storybook for one of the pages. We found solution published on Medium -> link. All credits and special thanks to the author.
Solution is using MemoryRouter available in react-router.
In our solution we used storybook Decorators which return the story wrapped by MemoryRouter and Router ->
return ( <MemoryRouter initialEntries={["/routeName/param"]} <Route component={(routerProps) => <Story {...routerProps} />} path="/routeName/:paramName"/> </MemoryRouter>)
I hope this helps everyone who experienced the same challenge.
Faced the same issue and completed as below
export default {
title: 'Common/Templates/Template Rendering',
component: CasePage
}
// 👇 We create a “template” of how args map to rendering
const Template: Story<any> = (args: any) => {
const { path } = args
return (
<MemoryRouter initialEntries={path}>
<Route
component={(routerProps: any) => <CasePage {...routerProps} />}
path="/dcp/:caseId"
/>
</MemoryRouter>
)
}
export const TemplateBoxesRendering = Template.bind({})
TemplateBoxesRendering.args = { path: ['/dcp/FX77777'] }
export const TemplateBoxes = Template.bind({})
TemplateBoxes.args = { path: ['/dcp/FX22222'] }

React Router Invariant Violation test fails with <Link> outside <Router> [duplicate]

I'm using jest to test a component with a <Link> from react-router v4.
I get a warning that <Link /> requires the context from a react-router <Router /> component.
How can I mock or provide a router context in my test? (Basically how do I resolve this warning?)
Link.test.js
import React from 'react';
import renderer from 'react-test-renderer';
import { Link } from 'react-router-dom';
test('Link matches snapshot', () => {
const component = renderer.create(
<Link to="#" />
);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
The warning when the test is run:
Warning: Failed context type: The context `router` is marked
as required in `Link`, but its value is `undefined`.
You can wrap your component in the test with the StaticRouter to get the router context into your component:
import React from 'react';
import renderer from 'react-test-renderer';
import { Link } from 'react-router-dom';
import { StaticRouter } from 'react-router'
test('Link matches snapshot', () => {
const component = renderer.create(
<StaticRouter location="someLocation" context={context}>
<Link to="#" />
</StaticRouter>
);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
Have a look at the react router docs about testing
I had the same issue and using StaticRouter would still require the context which needed more configuration to have it available in my test, so I ended up using the MemoryRouter which worked very well and without any issues.
import React from 'react';
import renderer from 'react-test-renderer';
import { MemoryRouter } from 'react-router-dom';
// SampleComponent imports Link internally
import SampleComponent from '../SampleComponent';
describe('SampleComponent', () => {
test('should render', () => {
const component = renderer
.create(
<MemoryRouter>
<SampleComponent />
</MemoryRouter>
)
.toJSON();
expect(component).toMatchSnapshot();
});
});
The answer of #Mahdi worked for me! In 2023 if you want to test a component that includes <Link> or <NavLink>, we just need to wrap it with the <MemoryRouter> in the test file:
// App.test.js
import { render, screen } from "#testing-library/react";
import MyComponent from "./components/MyComponent";
import { MemoryRouter } from "react-router-dom"; // <-- Import MemoryRouter
test("My test description", () => {
render(
<MemoryRouter> // <-- Wrap!
<MyComponent />
</MemoryRouter>
);
});
my test like this:
import * as React from 'react'
import DataBaseAccout from '../database-account/database-account.component'
import { mount } from 'enzyme'
import { expect } from 'chai'
import { createStore } from 'redux'
import reducers from '../../../reducer/reducer'
import { MemoryRouter } from 'react-router'
let store = createStore(reducers)
describe('mount database-account', () => {
let wrapper
beforeEach(() => {
wrapper = mount(
< MemoryRouter >
<DataBaseAccout store={store} />
</MemoryRouter >
)
})
afterEach(() => {
wrapper.unmount()
wrapper = null
})
})
but I don't konw why MemoryRouter can solve this。
Above solutions have a common default defact:
Can't access your component's instance! Because the MemoryRouter or StaticRouter component wrapped your component.
So the best to solve this problem is mock a router context, code as follows:
import { configure, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
describe('YourComponent', () => {
test('test component with react router', () => {
// mock react-router context to avoid violation error
const context = {
childContextTypes: {
router: () => void 0,
},
context: {
router: {
history: createMemoryHistory(),
route: {
location: {
hash: '',
pathname: '',
search: '',
state: '',
},
match: { params: {}, isExact: false, path: '', url: '' },
}
}
}
};
// mount component with router context and get component's instance
const wrapper = mount(<YourComponent/>, context);
// access your component as you wish
console.log(wrapper.props(), wrapper.state())
});
beforeAll(() => {
configure({ adapter: new Adapter() });
});
});

React + Redux Server initialize with store and history

I've the following app:
Client
index.js
import React from "react";
import ReactDOM from "react-dom";
import Root from "./containers/Root";
import configureStore from "./store/configureStore";
import { browserHistory } from "react-router";
import { loginUserSuccess } from "./actions/auth";
import { syncHistoryWithStore } from "react-router-redux";
const target = document.getElementById("root");
const store = configureStore(browserHistory, window.__INITIAL_STATE__);
// Create an enhanced history that syncs navigation events with the store
const history = syncHistoryWithStore(browserHistory, store)
const node = (
<Root store={store} history={history} />
);
let token = localStorage.getItem("token");
if (token !== null) {
store.dispatch(loginUserSuccess(token));
}
ReactDOM.render(node, target);
Root.js
import React from "react";
import {Provider} from "react-redux";
import AppRouter from "../routes/appRouter";
export default class Root extends React.Component {
static propTypes = {
store: React.PropTypes.object.isRequired,
history: React.PropTypes.object.isRequired
};
render () {
return (
<Provider store={this.props.store}>
<AppRouter history={this.props.history}>
</AppRouter>
</Provider>
);
}
}
AppRouter (routes/appRouter.js
import React from "react";
import routes from "./routes";
import { Router } from "react-router";
export default (
<Router routes={routes} history={this.props.history}></Router>
)
routes (routes/routes.js)
import React from "react";
import {Route, IndexRoute} from "react-router";
import { App } from "../containers";
import {HomeView, LoginView, ProtectedView, NotFoundView} from "../views";
import {requireAuthentication} from "../components/core/AuthenticatedComponent";
export default (
<Route path='/' component={App} name="app" >
<IndexRoute component={requireAuthentication(HomeView)}/>
<Route path="login" component={LoginView}/>
<Route path="protected" component={requireAuthentication(ProtectedView)}/>
<Route path="*" component={NotFoundView} />
</Route>
)
requireAuthentication (/component/core/AuthenticatedComponent.js)
import React from "react";
import {connect} from "react-redux";
import { push } from "react-router-redux";
export function requireAuthentication(Component) {
class AuthenticatedComponent extends React.Component {
componentWillMount () {
this.checkAuth(this.props.isAuthenticated);
}
componentWillReceiveProps (nextProps) {
this.checkAuth(nextProps.isAuthenticated);
}
checkAuth (isAuthenticated) {
if (!isAuthenticated) {
let redirectAfterLogin = this.props.location.pathname;
this.props
.dispatch(push(`/login?next=${redirectAfterLogin}`));
}
}
render () {
return (
<div>
{this.props.isAuthenticated === true
? <Component {...this.props}/>
: null
}
</div>
)
}
}
const mapStateToProps = (state) => ({
token: state.auth.token,
userName: state.auth.userName,
isAuthenticated: state.auth.isAuthenticated
});
return connect(mapStateToProps)(AuthenticatedComponent);
}
configureStore.js
import rootReducer from "../reducers";
import thunkMiddleware from "redux-thunk";
import {createStore, applyMiddleware, compose} from "redux";
import {routerMiddleware} from "react-router-redux";
import {persistState} from "redux-devtools";
import createLogger from "redux-logger";
import DevTools from "../dev/DevTools";
const loggerMiddleware = createLogger();
const enhancer = (history) =>
compose(
// Middleware you want to use in development:
applyMiddleware(thunkMiddleware, loggerMiddleware, routerMiddleware(history)),
// Required! Enable Redux DevTools with the monitors you chose
DevTools.instrument(),
persistState(getDebugSessionKey())
);
function getDebugSessionKey() {
if(typeof window == "object") {
// You can write custom logic here!
// By default we try to read the key from ?debug_session=<key> in the address bar
const matches = window.location.href.match(/[?&]debug_session=([^&#]+)\b/);
return (matches && matches.length > 0)? matches[1] : null;
}
return;
}
export default function configureStore(history, initialState) {
// Add the reducer to your store on the `routing` key
const store = createStore(rootReducer, initialState, enhancer(history))
if (module.hot) {
module
.hot
.accept("../reducers", () => {
const nextRootReducer = require("../reducers/index");
store.replaceReducer(nextRootReducer);
});
}
return store;
}
Server
server.js
import path from "path";
import { Server } from "http";
import Express from "express";
import React from "react";
import {Provider} from "react-redux";
import { renderToString } from "react-dom/server";
import { match, RouterContext } from "react-router";
import routes from "./src/routes/routes";
import NotFoundView from "./src/views/NotFoundView";
import configureStore from "./src/store/configureStore";
import { browserHistory } from "react-router";
// import { syncHistoryWithStore } from "react-router-redux";
// initialize the server and configure support for ejs templates
const app = new Express();
const server = new Server(app);
app.set("view engine", "ejs");
app.set("views", path.join(__dirname, "./"));
// define the folder that will be used for static assets
app.use("/build", Express.static(path.join(__dirname, "build")));
// // universal routing and rendering
app.get("*", (req, res) => {
const store = configureStore(browserHistory);
match(
{ routes, location: req.url },
(err, redirectLocation, renderProps) => {
// in case of error display the error message
if (err) {
return res.status(500).send(err.message);
}
// in case of redirect propagate the redirect to the browser
if (redirectLocation) {
return res.redirect(302, redirectLocation.pathname + redirectLocation.search);
}
// generate the React markup for the current route
let markup;
if (renderProps) {
// if the current route matched we have renderProps
// markup = renderToString(<Provider store={preloadedState} {...renderProps}/>);
markup = renderToString(
<Provider store={store} >
<RouterContext {...renderProps} />
</Provider>
);
} else {
// otherwise we can render a 404 page
markup = renderToString(<NotFoundView />);
res.status(404);
}
// render the index template with the embedded React markup
const preloadedState = store.getState()
return res.render("index", { markup, preloadedState });
}
);
});
// start the server
const port = process.env.PORT || 3000;
const env = process.env.NODE_ENV || "production";
server.listen(port, err => {
if (err) {
return console.error(err);
}
console.info(`Server running on http://localhost:${port} [${env}]`);
});
I don't know if I'm initializing correctly my store on the server-side.
How do I know this? Because renderProps is always null and thereby it returns NotFoundView.
However I made some modifications since I understood that my routes were being initialized incorrectly.
This made me use my configureStore(...) (which I use on client-side and it's working) on the server-side.
Now I'm getting the following error:
TypeError:Cannot read property 'push' of undefined
This error happens on AuthenticatedComponent in the following line:
this.props
.dispatch(push(`/login?next=${redirectAfterLogin}`));
It's strange since this works on client-side and on server-side it just throws this error.
Any idea?
PS
Am I doing it correctly by using the same Routes.js in client and server?
And the same for my configureStore(...)?
All the examples I see, they use a different approach to create the server-side store, with createStore from redux and not their store configuration from client-side.
PS2
I now understand that push may not work on server, only client (right?). Is there any workaround for this?
PS3
The problem I was facing was happening because I was rendering <Router /> into server, and my routes.js should only contain <Route /> nodes.
However, after a while I discovered that history shouldn't be configured in server and I just configured my store and passed it to the <Provider /> being rendered.
But now I need to compile my JSX and it throws:
Error: Module parse failed: D:\VS\Projects\Tests\OldDonkey\OldDonkey.UI\server.js Unexpected token (46:20)
You may need an appropriate loader to handle this file type.
| // markup = renderToString(<Provider store={preloadedState} {...renderProps}/>);
| markup = renderToString(
| <Provider store={store} >
| <RouterContext {...renderProps} />
| </Provider>

Resources