While I'm ultimately trying to write an Enzyme test for the flow in this react router example: https://reacttraining.com/react-router/web/example/auth-workflow
import React from 'react';
import ReactDOM from 'react-dom';
import Enzyme, { shallow, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import { MemoryRouter, Route, Switch, Link } from 'react-router-dom';
const Home = () => <div>Home</div>;
const MockComp = () => (
<div className="protected">
<nav>hi</nav>
Protected
</div>
);
const MockDenied = () => <div className="denied">Denied</div>;
test('Renders visited protected component if authorized', () => {
const wrapper = mount(
<MemoryRouter initialEntries={['/']}>
<div>
<Link to="/foo" />
<Switch>
<Route path="/" component={Home} />
<Route path="/401" component={MockDenied} />
<ProtectedRouteBasic
path="/foo"
auth={{ hasAuth: true }}
component={MockComp}
/>
</Switch>
</div>
</MemoryRouter>
);
wrapper.find('a').simulate('click', { button: 0 });
expect(wrapper.find('.protected').length).toEqual(1);
expect(wrapper.find('.denied').length).toEqual(0);
});
I've found a number of issues and have tried to peel away the complexity and then slowly reintroduce the elements that I've removed.
So I've landed on this test as what I will need to get working to proceed:
test('Clicking link will render component associated with path', () => {
const wrapper = mount(
<MemoryRouter>
<div>
<Link to="/foo" />
<Switch>
<Route path="/" component={Home} />
<Route path="/foo" component={MockComp} />
</Switch>
</div>
</MemoryRouter>
);
wrapper.find('a').simulate('click', { button: 0 });
expect(wrapper.find('.protected')).toHaveLength(1);
});
However, this test isn't working as expected as I expect the test to pass in its current state. I've read this thread to update my simulate call to include the {button: 0} as well as this thread about wrapping the entire router in a functional component, however, that option's not available to me as far as I know, since the framework I'm working with doesn't seem to allow for it. Additionally, I believe that that piece is immaterial to the issue I'm having. That said, any help would be much appreciated.
From the Switch docs:
Renders the first child <Route> or <Redirect> that matches the location.
In this case <Route path="/" component={Home} /> matches when the path is both / and /foo so Home is always rendered.
You can fix this by using either exact so it only matches if the path is exactly /, or moving it to the end of the Route list so other routes match first:
test('Clicking link will render component associated with path', () => {
const wrapper = mount(
<MemoryRouter>
<div>
<Link to="/foo" />
<Switch>
<Route path="/foo" component={MockComp} />
<Route path="/" component={Home} />
</Switch>
</div>
</MemoryRouter>
);
wrapper.find('a').simulate('click', { button: 0 });
expect(wrapper.find('.protected')).toHaveLength(1); // SUCCESS
});
Related
I have a component that I am using in React Router v6 for managing private routes, that does some checks on an auth token, and will either render the Outlet component or will redirect to a login page.
I have -
import { Outlet } from 'react-router-dom';
export const CheckAuth = (props) => {
const valid = ...;
if (!valid) {
window.location.replace(loginUrl);
return null;
}
return <Outlet />;
};
and using it like -
<Route element={<CheckAuth token={authToken} />}>
// ... private routes ...
</Route>
I can mock out window.location.replace with Jest
delete window.location;
window.location = { replace: jest.fn() };
...
render(<CheckAuth token={token} />)
expect(window.location.replace).toHaveBeenCalledWith(loginUrl);
but how can I test the Outlet component using Testing Library?
If it helps anyone, I ended up just wrapping the components in the test with a react router components, and passed a dummy component as a child to Route and asserted that some fake text in that component was or was not rendered
Outside the test block -
const FakeComponent = () => <div>fake text</div>;
and for a failure scenario, where the outlet should not render -
render(
<MemoryRouter initialEntries={['/']}>
<Routes>
<Route element={<CheckAuth />}>
<Route path="/" element={<FakeComponent />} />
</Route>
</Routes>
</MemoryRouter>
);
expect(screen.queryByText('fake text')).not.toBeInTheDocument();
and for a success scenario, assert that the text is present -
render(
<MemoryRouter initialEntries={['/']}>
<Routes>
<Route element={<CheckAuth token={correctToken}/>}>
<Route path="/" element={<FakeComponent />} />
</Route>
</Routes>
</MemoryRouter>
);
expect(screen.queryByText('fake text')).toBeInTheDocument();
I am having an issue when using same component for two different routes, where i am expecting that that component gets destroyed and than get mounted again, but that does not happen:
When i change from /page1 to /page2 by clicking on the button Change to /page2 output in the console should be:
COMPONENT DISMOUNTED
COMPONENT MOUNTED
This means that MyComponent should be destroyed after path changes. This is important because i rely on the fact that change of the path gives me fresh component. I don't want to reset states and other hooks to default values manually.
Codesadnbox example
Is there a React problem or perhaps React router one?
App component
import {
Routes,
Route,
BrowserRouter,
Navigate
} from 'react-router-dom';
const App = () => {
return (
<BrowserRouter>
{/* Routes */}
<Routes>
{/* Route 1 */}
<Route path="/page1" element={<MyComponent someProp="value1" />} />
{/* Route 2 */}
<Route path="/page2" element={<MyComponent someProp="value2" />} />
<Route path="/*" element={<Navigate to={{ pathname: '/page1' }} />} />
</Routes>
</BrowserRouter>
);
};
MyComponent
import type { FunctionComponent } from 'react';
import { useEffect } from 'react';
import {
useNavigate
} from 'react-router-dom';
const MyComponent: FunctionComponent<{ someProp: string }> = ({ someProp }) => {
const history = useNavigate();
const onRouteChange = (route: string) => {
history(route);
};
useEffect(() => {
console.log('COMPONENT MOUNTED');
return () => {
console.log('COMPONENT DISMOUNTED');
};
}, []);
return (
<div>
<button onClick={() => onRouteChange('/page1')}>Change to /page1</button>
<button onClick={() => onRouteChange('/page2')}>Change to /page2</button>
<div>{someProp}</div>
</div>
);
};
React is actually doing its job correctly, since Route component returns same component with changed prop someProp. In any other case where i have a component where i change prop to it, this would happen again.
There is no obvious way to find this out unless you stumble upon this problem. Although thinking in the way React works, this should be obvious.
SOLUTION
Simple key should be added to both MyComponent components. In this way, React will know, because of the different key, that new component returned by Route differs.
Codesandbox to the solution
const App = () => {
return (
<BrowserRouter>
{/* Routes */}
<Routes>
{/* Route 1 */}
<Route
path="/page1"
element={<MyComponent key="/page1" someProp="value1" />}
/>
{/* Route 2 */}
<Route
path="/page2"
element={<MyComponent key="/page2" someProp="value2" />}
/>
<Route path="/*" element={<Navigate to={{ pathname: "/page1" }} />} />
</Routes>
</BrowserRouter>
);
};
Why is rendering the parent component and the child trying to enter the child component
"react-router-dom": "^6.0.1",
when I enter on the route:
http://localhost:3000/dashboard- the view work
http://localhost:3000/dashboard/employee - rendering dashboard and employee view (both views)
http://localhost:3000/dashboard/accounting - rendering dashboard and accounting view (both views)
Documentation:
https://reactrouter.com/docs/en/v6/getting-started/tutorial#nested-routes
index.js
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
);
App.js
import AppRouter from "./routers/AppRouter";
function App() {
return (
<>
<AppRouter />
</>
);
}
export default App;
AppRouter.js
import { Route, Routes } from "react-router-dom";
import Navbar from "../components/template/Navbar";
import AccountingHomeView from "../components/views/accounting/AccountingHomeView";
import DashboardHomeView from "../components/views/dashboard/DashboardHomeView";
import EmployeeHomeView from "../components/views/employee/EmployeeHomeView";
import HomeView from "../components/views/public/HomeView";
import LoginView from "../components/views/public/LoginView";
const AppRouter = () => {
return (
<div>
<Navbar />
<Routes>
<Route path="/" element={<HomeView />} />
<Route path="dashboard" element={<DashboardHomeView />}>
<Route path="employee" element={<EmployeeHomeView />} />
<Route path="accounting" element={<AccountingHomeView />} />
</Route>
<Route path="/login" element={<LoginView />} />
</Routes>
</div>
);
};
export default AppRouter;
DashboardHomeView.js (with outlet)
import { Outlet } from "react-router-dom";
const DashboardHomeView = function () {
return (
<>
<h1>DashboardHomeView</h1>
<Outlet />
</>
);
};
export default DashboardHomeView;
component children Accounting
import React from "react";
const AccountingHomeView = function () {
return (
<div>
<h1> Accountin</h1>
</div>
);
};
export default AccountingHomeView;
I also initially found this a bit confusing, but with nested routes the "parent" route is considered more of a "layout" component in that it is always rendered when its path matches, and renders all its children routes into its outlet.
const AppRouter = () => {
return (
<div>
<Navbar />
<Routes>
<Route path="/" element={<HomeView />} />
<Route
path="dashboard"
element={<DashboardHomeView />} // <-- always matched/rendered at "/dashboard*"
>
<Route
path="employee"
element={<EmployeeHomeView />} // <-- conditionally matched/rendered
/>
<Route
path="accounting"
element={<AccountingHomeView />} // <-- conditionally matched/rendered
/>
</Route>
<Route path="/login" element={<LoginView />} />
</Routes>
</div>
);
};
const DashboardHomeView = function () {
return (
<>
<h1>DashboardHomeView</h1> // <-- always matched/rendered at "/dashboard*"
<Outlet /> // <-- conditionally matched/rendered children
</>
);
};
Nested-Routes
You may have noticed when clicking the links that the layout in App
disappears. Repeating shared layouts is a pain in the neck. We've
learned that most UI is a series of nested layouts that almost always
map to segments of the URL so this idea is baked right in to React
Router.
I believe what you are expecting is what is called an Index Route. It is what would be rendered on a "/dashboard" route when it isn't a layout/wrapper container.
Notice it has the index prop instead of a path. That's because the
index route shares the path of the parent. That's the whole point--it
doesn't have a path.
Maybe you're still scratching your head. There are a few ways we try
to answer the question "what is an index route?". Hopefully one of
these sticks for you:
Index routes render in the parent routes outlet at the parent route's path.
Index routes match when a parent route matches but none of the other children match.
Index routes are the default child route for a parent route.
Index routes render when the user hasn't clicked one of the items in a navigation list yet.
const AppRouter = () => {
return (
<div>
<Navbar />
<Routes>
<Route path="/" element={<HomeView />} />
<Route path="dashboard" element={<DashboardLayout />}>
<Route path="employee" element={<EmployeeHomeView />} />
<Route path="accounting" element={<AccountingHomeView />} />
<Route index element={<DashboardHomeView />} />
</Route>
<Route path="/login" element={<LoginView />} />
</Routes>
</div>
);
};
const DashboardLayout = function () {
return (
<div /* with any layout styling */>
.... other common layout content
<Outlet />
.... more possible common page content
</div>
);
};
const DashboardHomeView = function () {
return (
<>
<h1>DashboardHomeView</h1>
.... dashboard specific content
</>
);
};
How about using the exact prop for the parent Route. Like <Route exact path="dashboard" element={<DashboardHomeView />}>. This may solve the issue.
For some reason, I can't reach router in my tests.
My render in main container:
render() {
return (
<div className="AppEntry-scope">
<BrowserRouter>
<Switch>
<Route exact path="/" component={Dashboard} />
// ...Other routes
<Route component={FourOhFourNothinFound} />
</Switch>
</BrowserRouter>
</div>
);
}
My test:
describe('<Dashboard />', () => {
it('Should see dashboard', () => {
const wrapper = mount(
<MemoryRouter initialEntries={['/']}>
<AppEntry store={store} />
</MemoryRouter>,
);
expect(wrapper.find(Dashboard)).toBePresent();
});
});
Also, I've tried:
expect(wrapper.find(Dashboard).length).toBe(1);
And my test doesn't see Dashboard component.
In my app this route is works, but I want to set up test for routes.
BTW, when I tried to test my notfound component FourOhFourNothinFound - it works and seems my tests always see notfound component.
What is wrong and how I can set up my react-router tests correctly?
Is there a way to nest routes in React Router v4?
This works:
<Router basename='/app'>
<main>
<Route path='/' component={AppBar} />
<Route path='/customers' component={Customers} />
</main>
</Router>
This does not:
<Router basename='/app'>
<Route path='/' component={AppBar}>
<Route path='/customers' component={Customers} />
</Route>
</Router>
Customers Component:
import React, { Component, PropTypes } from 'react'
import styled from 'styled-components'
export default class Customers extends Component {
render () {
return (
<Container>
<h1>Customers</h1>
</Container>
)
}
}
const Container = styled.section`
height: 100%;
padding: 15px;
overflow: auto;
`
Best pattern I have found so far.
// main app
<div>
// not setting a path prop, makes this always render
<Route component={AppShell}/>
<Switch>
<Route exact path="/" component={Login}/>
<Route path="/dashboard" component={AsyncDashboard(userAgent)}/>
<Route component={NoMatch}/>
</Switch>
</div>
I can just keep nesting this inside a component and everything works nice including hmr(If using webpack, dont forget to set output.publicPath to "/")
// dashboard component
<div>
// the same way as before, not setting a path prop
// makes it render on every /dashboard/** request
<Route component={DashboardTAB}/>
<Switch>
// longer path (with same root) than others first
<Route path="/dashboard/graphs/longerpath" component={GraphForm}/>
<Route path="/dashboard/graphs" component={Graphs}/>
<Route path="/dashboard/workers" component={List}/>
<Route path="/dashboard/insert" component={InsertComponent}/>
</Switch>
</div>
I adapted this from the docs, seem to work so far. Probably missing something obvious, and yes it is not the v4 way but we need all the routes defined in one place.
function RouteNest(props){ return (
<Route exact={props.exact} path={props.path} render={ p => <props.component {...p} children={props.children}/> } />
)}
export const MainRoutes = props =>
<div className='content layout'>
<Route exact path="/" component={Landing}/>
<Route path={'/contact'} component={Contact}/>
<RouteNest path={'/thing'} component={CompoWithSub}>
<RouteNest path={'/thing/suba'} component={SubComponentA}/>
<RouteNest path={'/thing/subb'} component={SubComponentB}/>
</RouteNest>
</div>
export const CompoWithSub = props => <div>{props.children)</div>
You're AppBar component is in charge of rendering Customers. For customers to be called, you have to render the children of AppBar. Anything directly nested under AppBar is a child of AppBar.
import React from 'react';
const AppBar = ({ children }) => (
<div>
<header>
<h1> stuff </h1>
</header>
{children}
</div>
);
export default AppBar
Please note that only AppBar will render when you visit "/". AppBar and Customers will render when you visit "/customers".
If someone wants to have nested routes without typing prefix of wrapper route I've created something like this in TSX:
Imports:
import * as React from 'react';
import { Route, RouteComponentProps, RouteProps, Switch } from 'react-router-dom';
import Index from 'views/index';
import Login from 'views/login';
import NoMatch from 'views/no-match';
Interfaces:
interface INestedRoutes {
nested?: string;
}
interface INestedRoute extends RouteProps, INestedRoutes {}
NestedRoute and NestedRoutes wrapper:
class NestedRoutes extends React.Component<INestedRoutes> {
public render() {
const childrenWithProps = React.Children.map(this.props.children, (child) => {
return React.cloneElement(
child as React.ReactElement<any>, { nested: this.props.nested },
);
})
return childrenWithProps;
}
}
const NestedRoute: React.SFC<INestedRoute> = (props: INestedRoute) => {
return <Route path={`${props.nested}${props.path}`} component={props.component} />;
};
And routes with wrapper:
const MultiLanguage: React.SFC<RouteComponentProps<any>> = (props: RouteComponentProps<any>) => {
return (
<NestedRoutes nested={props.match.path} >
<NestedRoute path="/test" component={Login} />
<NestedRoute path="/no-match" component={NoMatch} />
</NestedRoutes>
);
};
export default (
<Switch>
<Route path="/:language" component={MultiLanguage}/>
<Route exact={true} path="/" component={Index} />
<Route path="/login" component={Login} />
<Route component={NoMatch} />
</Switch>
);
For nested routes there is a very simple way which i using.
Example main router is be like that
<Router history={history}>
<Switch >
<Route path="/" component={Home}></Route>
</Switch>
</Router>
Inside Home component using Nested Routing be like:
<div className="App">
<Navbar title="Home" links = { NavbarLinks }/>
{this.renderContentPage()}
</div>
renderContentPage will check the URL and render the nested route.
<Route exact path="/" component={Page1}></Route>
<Route exact path="/page1" component={Page1}></Route>
<Route exact path='/page2' component={Page2} />
So inside Home component page1 and page2 components rendered.
Route expects a single children i.e. a component.
It should not be a new Route.
What you can do is to include your nested routes inside your customers component.
Also make sure to remove exact inside the routes in customers component.