How to test React Router V6 Outlet using #testing-library/react - reactjs

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

Related

How to share data between two components without using higher order functions in React Router and outlet

I have two components, one called <Flights/> and the other called <FlightResults/>.
Flights renders a context provider for the rest of the application.
const Flights = () => {
return (
<FlightSearchContext.Provider
value={{
typeOfTrip,
fromAirport,
departureDate,
returnDate,
toAirport,
outGoingFlights,
searchAirports,
setSearchAirports,
}}
>
<h1>Some UI</h1>
</FlightSearchContext.Provider>
);
};
export default Flights;
const FlightResults = () => {
const { toAirport, outGoingFlights } = useContext(FlightSearchContext);
return (
<div>Flight results</div>
)
}
export default FlightResults
Right now I'm declaring both routes in my index.tsx file like this.
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { Routes, Route } from "react-router";
import { BrowserRouter } from "react-router-dom";
import Flights from "./Components/Flights/Flights";
import FlightResults from "./Components/Flights/FlightResults";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route path="/" element={<App />}>
<Route path="/flights" element={<Flights />} index />
<Route path="/flights/flight-results" element={<FlightResults />} />
</Route>
</Routes>
</BrowserRouter>
</React.StrictMode>
);
FlightResults is a child route of Flights and it needs to access the context data that is declared in the Flights component.
Currently everything works fine but FlightResults cant access the data in the Flights component.
After a lot of searching around i found out that I should wrap my child components in the index.tsx file as below.
<Route path="/" element={<App />}>
<Route path="/flights" element={<Flights />} index>
<Route path="/flight-results" element={<FlightResults />} />
</Route>
<Route path="/hotels" element={<Hotel />} />
<Route path="/taxi" element={<Taxi />} />
</Route>
and whilst this works the FlightResults UI doesn't show under /flights/flightresults, a possible fix was to render an Outlet component on the Flights component like below
const Flights = () => {
return (
<FlightSearchContext.Provider
value={{
typeOfTrip,
fromAirport,
departureDate,
returnDate,
toAirport,
outGoingFlights,
searchAirports,
setSearchAirports,
}}
>
<h1>Some UI</h1>
<Outlet/>
</FlightSearchContext.Provider>
);
};
export default Flights;
and the above does work but now both UIs show up on /flights/flightresults/ .as in both Flights and FlightResults show up on the same URL.
How can I make the right component render on the right URL but also still access the context data?
Issue
The issue is that you're mixing what you want exclusively to be rendered on "/flights" with data you want provided to multiple routes.
Solution
The FlightSearchContext.Provider component of the current Flights component should refactored to a Layout Route leaving the UI portion in Flights to be rendered on its own route.
Example:
const FlightsProvider = () => (
<FlightSearchContext.Provider
value={{
typeOfTrip,
fromAirport,
departureDate,
returnDate,
toAirport,
outGoingFlights,
searchAirports,
setSearchAirports,
}}
>
<Outlet/>
</FlightSearchContext.Provider>
);
<Route path="/flights" element={<FlightsProvider />}>
<Route
index // <-- "/flights"
element={<Flights />}
/>
<Route
path="flight-results" // <-- "/flights/flight-results"
element={<FlightResults />}
/>
</Route>
If you are really wanting to not use an Outlet component then you can simply lift the FlightSearchContext.Provider higher in the ReactTree. In this case the FlightsProvider becomes a normal Wrapper component.
Example:
const FlightsProvider = ({ children }) => (
<FlightSearchContext.Provider
value={{
typeOfTrip,
fromAirport,
departureDate,
returnDate,
toAirport,
outGoingFlights,
searchAirports,
setSearchAirports,
}}
>
{children}
</FlightSearchContext.Provider>
);
root.render(
<React.StrictMode>
<BrowserRouter>
<FlightsProvider>
<Routes>
<Route path="/" element={<App />}>
<Route path="/flights" >
<Route index element={<Flights />} />
<Route path="flight-results" element={<FlightResults />} />
</Route>
</Route>
</Routes>
</FlightsProvider>
</BrowserRouter>
</React.StrictMode>
);
Short answer: you should not declare FlightSearchContext.Provider in Flights, but instead above both Flights and FlightResults.
Adapting this example from the docs, you can declare the context above both Route, and if you need to update data in Flights, be sure to include setWhateverData in the context so you can update it from any children of the context

Upgrading react-router-dom from Switch to Route

This worked in "react-router-dom": "^5.3.0"
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import Api from "./Api";
const api = new Api();
const App = () => {
return (
...
<Router basename="/my-app">
<Switch>
<Route
path="/complete"
render={(props) => <ConfirmationPage {...props} api={api} />}
/>
...
</Switch>
</Router>
After upgrading to "react-router-dom": "^6.4.3"
We've tried:
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Api from "./Api";
const api = new Api();
const App = () => {
return (
...
<Router basename="/my-app">
<Routes>
<Route
path="/complete"
element={(props) => <ConfirmationPage {...props} api={api} />}
/>
...
</Routes>
</Router>
But that doesn't work. We've read through https://reactrouter.com/en/6.4.3/upgrading/v5
but do not see how to handle passing in props.
In react-router-dom#6 the route components are passed as JSX to the element prop, and passing props to the routed component works just like it does anywhere else in React.
Example:
<Router basename="/my-app">
<Routes>
<Route
path="/complete"
element={(
<ConfirmationPage
api={api} // <-- props passed to component
/>
)}
/>
...
</Routes>
</Router>
There are no longer any route props, so if the routed components need access to what was previously provided they will need to use the React hooks, i.e. useLocation, useNavigate, useParams, etc.
Additional documentation:
Why does <Route> have an element prop instead of render or component?
Advantages of <Route element>

React router show only route without components defined in my browser

Hello in my directions file I set my struct
header
navbar
and my switch
foote
const AppRouter = () => (
<BrowserRouter>
<Route path="/login" component={AuthPage} exact={true} />
<Route path="/dashboard/addProduct" component={AddProduct} exact={true} />
<div>
<Header/>
<Navigation/>
<Container maxWidth="lg" >
<Switch>
<Route path="/" component={LandingPage} exact={true} />
<Route path="/xd" component={AuthPage} exact={true} />
<Route component={NotFoundPage} />
</Switch>
</Container>
</div>
</BrowserRouter>
);
But I have two routes where I didn't want to show my header
footer
and nav bar
which are the login and addproduct routes
how could i do that?
This is a little bit hacky (just to match your current approach) since I'm assuming you just want to hide those components in only those 2 routes, You can use withRouter in your Header and Navigation components, withRouter will provide to you the location prop and you can have a condition like this:
import React from "react";
import { withRouter } from "react-router-dom";
const Navigation = props => {
const { location } = props;
const { pathname } = location;
if (pathname !== "/login" || pathname !== "/dashboard/addProduct" ) {
return (
// Component logic goes here
);
}
// if we're on those routes we should return null to not render anything
return null;
};
export default withRouter(Navigation);
If you want a more robust long term approach this answer could help:
How to hide navbar in login page in react router

testing click behavior on React Router Links with Enzyme

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

Can we include normal react component inside <Route />?

I want to do something like:
<Route>
<MyComponent someCondition={true/false}>
<Route1 />
....
</MyComponent>
</Route
To handle some conditional rendering. However, <MyComponent /> seems not mounted upon rendering.
My question is: can we include normal react component within <Route>? If not, is there a better way to handle conditional routing?
What exactly do you mean by conditional routing? Assuming you mean something like not letting a user hit a route if they aren't authenticated, you can use react-router's onEnter hooks . You can make a parent <Route> that doesn't have a component prop and just handles routing checks. I used some simple onEnter checks in this example.
// onEnter hooks for login and home page to redirect if necessary
const checkAuth = function (nextState, replace) {
const { user } = store.getState()
if (isEmpty(user)) {
replace('/')
}
}
const checkSkipAuth = function (nextState, replace) {
const { user } = store.getState()
if (!isEmpty(user)) {
replace('/home')
}
}
var Index = () => {
return (
<Provider store={store}>
<Router history={history}>
<Route path='/' component={Container}>
<IndexRoute component={Login} onEnter={checkSkipAuth} />
<Route path='home' component={Home} onEnter={checkAuth} />
<Route path='*' component={NoMatch} />
</Route>
</Router>
</Provider>
)
}

Resources