React Testing Library: How to test components that contain useLocation()? - reactjs

I'm using RedwoodJS which uses React with React Testing Library under the hood. I'm struggling to test a component (and all page components which have this component in the tree) because of the useLocation() hook.
When using a useLocation() hook inside my component, I need to wrap my component under test with a Router that mocks the browser location history to prevent the error Error: Uncaught [TypeError: Cannot read property 'pathname' of undefined].
However when I do that, the Navigation component is no longer fully rendered, so I can't test it... any ideas?
Navigation.js
//import statements
const renderListItems = (pathname) => {
const NavigationItems = [{..},{..},{..}] // example
return NavigationItems.map((item) => {
const selected = pathname.indexOf(item.path) ? false : true
return (
<ListItem
button
key={item.text}
onClick={() => {
navigate(item.route)
}}
selected={selected}
>
<ListItemText primary={item.text} />
</ListItem>
)
})
}
const Navigation = () => {
const { pathname } = useLocation() // this is why I need to wrap the Navigation component in a router for testing; I'm trying to get the current pathname so that I can give a specific navigation item an active state.
return (
<List data-testid="navigation" disablePadding>
{renderListItems(pathname)}
</List>
)
}
export default Navigation
Navigation.test.js
import { screen } from '#redwoodjs/testing'
import { renderWithRouter } from 'src/utilities/testHelpers'
import Navigation from './Navigation'
describe('Navigation', () => {
it('renders successfully', () => {
expect(() => {
renderWithRouter(<Navigation />)
}).not.toThrow()
})
it('has a "Dashboard" navigation menu item', () => {
renderWithRouter(<Navigation />)
expect(
screen.getByRole('button', { text: /Dashboard/i })
).toBeInTheDocument()
})
})
testHelpers.js
This is needed to prevent useLocation() inside Navigation.js from breaking the test.
import { Router, Route } from '#redwoodjs/router'
import { createMemoryHistory } from 'history'
import { render } from '#redwoodjs/testing'
const history = createMemoryHistory()
export const renderWithRouter = (Component) =>
render(
<Router history={history}>
<Route component={Component} />
</Router>
)
Resulting error
Navigation › has a "Dashboard" navigation menu item
TestingLibraryElementError: Unable to find an accessible element with the role "button"
There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the `hidden` option to `true`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole
<body>
<div />
</body>

You can mock useLocation to return the pathname you want. This can apply to any function
Simple
//Put within testing file
jest.mock("router-package", () => ({
...jest.requireActual("router-package"),
useLocation: () => ({
pathname: "customPath/To/Return"
})
}));
Detailed
You can create a helper function where you can pass the path(string) and it automatically mocks it for you as such
random.test.js
import { setUpPageRender } from 'src/utilities/testHelpers'
import Navigation from './Navigation'
describe('Navigation', () => {
//Where we set up our custom path for the describe
const render = setUpPageRender('/customPathForThisDescribe/Foo')
it('renders successfully', () => {
expect(() => {
render(<Navigation />)
}).not.toThrow()
})
})
testHelpers.js
//Mocked Functions
jest.mock('router-package', () => ({
__esModule: true,
...jest.requireActual('router-package'),
useLocation: jest.fn(),
}))
import { useLocation } from 'router-package'
export const setUpPageRender = (location) => {
useLocation.mockReturnValue(location)
beforeEach(() => {
jest.clearAllMocks()
})
return (component) => {
return render( <Router history={history}>
<Route component={Component} />
</Router>)
}
}

Related

Dom does is not updated while testing redirection with react-testing library and react-router v6

I'm trying to test the redirection page when the user clicks on the button (I don't want to user jest.mock()).
I created the wrapper according to test-react-library documentation:
import { FC, ReactElement, ReactNode } from "react";
import { render, RenderOptions } from "#testing-library/react";
import { BrowserRouter } from "react-router-dom";
import userEvent from "#testing-library/user-event";
const WrapperProviders: FC<{ children: ReactNode }> = ({ children }) => {
return <BrowserRouter>{children}</BrowserRouter>;
};
const customRender = (
ui: ReactElement,
{ route = "/" } = {},
options?: Omit<RenderOptions, "wrapper">
) => {
window.history.pushState({}, "Home Page", route);
return {
user: userEvent.setup(),
...render(ui, { wrapper: WrapperProviders, ...options })
};
};
export * from "#testing-library/react";
export { customRender as render };
export type RenderType = ReturnType<typeof customRender>;
HomePage.tsx:
import { useNavigate } from "react-router-dom";
export default function HomePage() {
const navigate = useNavigate();
const handleClick = () => navigate("/other");
return (
<>
<h3>HomePage</h3>
<button onClick={handleClick}>Redirect</button>
</>
);
}
Other.tsx:
export default function Other() {
return <h3>Other</h3>;
}
HomePage.test.tsx:
import { render, RenderType } from "./customRender";
import HomePage from "./HomePage";
import "#testing-library/jest-dom/extend-expect";
describe("HomePage", () => {
let wrapper: RenderType;
beforeEach(() => {
wrapper = render(<HomePage />, { route: "/" });
});
test.only("Should redirects to other page", async () => {
const { getByText, user } = wrapper;
expect(getByText(/homepage/i)).toBeInTheDocument();
const button = getByText(/redirect/i);
expect(button).toBeInTheDocument();
user.click(button);
expect(getByText(/other/i)).toBeInTheDocument();
});
});
When I run the test, it fails and the page is not updated in the dom.
Does jest-dom does not support the re-render of the page and update the DOM? Or this test out of scope of the testing-library ?
From the code I can see that you are just rendering the HomePage component and inside of that component you don't have any logic that renders a new component based on the route changes (I suppose that you have that logic on another component). That's why when you click on the button you are not seeing the Other component rendered.
In this case I would suggest you to only make the assertions you need on the window.location object. So after you simulate the click on the button you can do:
expect(window.location.pathname).toBe("/other");
I updated the custom render and add a history for it:
const WrapperProviders: FC<{ children: ReactNode }> = ({ children }) => {
return <BrowserRouter>{children}</BrowserRouter>;
};
const customRender = (
ui: ReactElement,
{ route = "/",
history = createMemoryHistory({initialEntries: [route]}),
} = {},
options?: Omit<RenderOptions, "wrapper">
) => {
return {
user: userEvent.setup(),
...render( <Router location={history.location} navigator={history}>
ui
</Router>
, { wrapper: WrapperProviders, ...options })
};
};
and the test now passes:
describe("HomePage", () => {
let wrapper: RenderType;
beforeEach(() => {
wrapper = render(<HomePage />, { route: "/" });
});
test.only("Should redirects to other page", async () => {
const { getByText, user, history } = wrapper;
expect(getByText(/homepage/i)).toBeInTheDocument();
const button = getByText(/redirect/i);
expect(button).toBeInTheDocument();
await user.click(button);
expect(history.location.pathname).toBe('/other');
});

Testing authentication in react with jest and enzyme

I need to write a test in react using jest and enzyme and I have the following code:
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
const LoggedOutRoute = ({ component: Component, isLoggedIn, ...rest }) => (
<Route render={() => (
!isLoggedIn
? <Component {...rest} />
: <Redirect to='/habits' />
)} />
)
export default LoggedOutRoute;
with the following test:
import LoggedOutRoute from '.';
import { shallow } from 'enzyme';
import { component } from 'react';
describe('LoggedOutRoute', () => {
let component;
beforeEach(() => {
component = shallow(<LoggedOutRoute />);
})
test('it renders', () => {
expect(component.find('Route')).toHaveLength(1)
})
test('it exists', () => {
expect(component.find('LoggedOutRoute').exists()).toBeFalsy();
})
test('it redirects to /habits link', () => {
let links = component.find('Redirect');
expect(links).toHaveLength(0)
})
test('It should return logged in for a false statement', () => {
let LoggedOutRoute = component.find('Route')
expect(LoggedOutRoute).toHaveLength(1)
})
test('')
})
I need to write a test to test for '!isLoggedIn', i'm not sure how to structure this at all, it's for authentication and when i'm running coverage - line 5 is what is letting me down. I'm not sure exactly how to test the ternary.
You should basically mount it with a truthy/falsy prop and test whether it renders accordingly: component = shallow(<LoggedOutRoute isLoggedIn={true} />);

React-router-dom Link does not update history in jest

Example code:
import React, { FC } from 'react';
import { mount, ReactWrapper } from 'enzyme';
import { createMemoryHistory } from 'history';
import { MemoryRouter, Router, Route, Switch, Link } from 'react-router-dom';
type TRoutes = {
[key: string]: {
title?: string;
path: string;
component: FC;
};
};
const routes: TRoutes = {
start: {
title: 'start page',
path: `/`,
component: () => null
},
destination: {
path: `/destination`,
component: () => null
}
};
const initialEntries = [routes.start.path];
const history = createMemoryHistory({
initialEntries
});
let wrapper: ReactWrapper;
describe('Router test', () => {
beforeAll(() => {
wrapper = mount(
<MemoryRouter initialEntries={initialEntries}>
<Router history={history}>
<Link to={routes.destination.path}>Go to destination</Link>
<button
type="button"
onClick={() => {
history.push(routes.destination.path);
}}
>
Go to route
</button>
<Switch>
<Route path={routes.start.path} exact component={routes.start.component} />
<Route path={routes.destination.path} exact component={routes.destination.component} />
</Switch>
</Router>
</MemoryRouter>
);
});
describe('Link', () => {
beforeAll(() => {
history.push(routes.start.path);
});
it(`should render Link - Go to destination`, () => {
expect(wrapper.find('a').at(0).exists).toBeTruthy();
});
it(`should update history from='${routes.start.path}' to location='${routes.destination.path}' on Link click`, () => {
wrapper
.find('a')
.at(0)
.simulate('click');
// wrapper.update();
expect(history.location.pathname).toEqual(routes.destination.path);
});
});
describe('button', () => {
beforeAll(() => {
history.push(routes.start.path);
});
it(`should render button - Go to destination`, () => {
expect(wrapper.find('button').at(0).exists).toBeTruthy();
});
it(`should update history from='${routes.start.path}' to location='${routes.destination.path}' on history.push`, () => {
wrapper
.find('button')
.at(0)
.simulate('click');
// wrapper.update();
expect(history.location.pathname).toEqual(routes.destination.path);
});
});
});
I have 4 tests above.
it should test that the link exists = pass
on click of link it should update the history = fail
it should test that the button exists = pass
on click of button it should update the history = pass
The history updates only when I do history.push, but not when clicking the Link.
From the documents on - react-router-dom, it implies that this approach is valid and should work:
https://reactrouter.com/web/guides/testing
the url bar), you can add a route that updates a variable in the test:// app.test.js
test("clicking filter links updates product query params", () => {
let history, location;
render(
<MemoryRouter initialEntries={["/my/initial/route"]}>
<App />
<Route
path="*"
render={({ history, location }) => {
history = history;
location = location;
return null;
}}
/>
</MemoryRouter>,
node
);
act(() => {
// example: click a <Link> to /products?id=1234
});
// assert about url
expect(location.pathname).toBe("/products");
const searchParams = new URLSearchParams(location.search);
expect(searchParams.has("id")).toBe(true);
expect(searchParams.get("id")).toEqual("1234");
});
Any advice on this?
thanks

How do you mock useLocation() pathname using shallow test enzyme Reactjs?

I have header component like below:
import { useLocation } from "react-router-dom";
const Header = () => {
let route = useLocation().pathname;
return route === "/user" ? <ComponentA /> : <ComponentB />;
}
How will you mock this useLocation() to get the path as user?
I cant simply call the Header component as below in my test file as I am getting an error:
TypeError: Cannot read property 'location' of undefined at useLocation
describe("<Header/>", () => {
it("call the header component", () => {
const wrapper = shallow(<Header />);
expect(wrapper.find(ComponentA)).toHaveLength(1);
});
});
I have tried looking similar to the link How to test components using new react router hooks? but it didnt work.
I have tried like below:
const wrapper = shallow(
<Provider store={store}>
<MemoryRouter initialEntries={['/abc']}>
<Switch>
<AppRouter />
</Switch>
</MemoryRouter>
</Provider>,
);
jestExpect(wrapper.find(AppRouter)
.dive()
.find(Route)
.filter({path: '/abc'})
.renderProp('render', { history: mockedHistory})
.find(ContainerABC)
).toHaveLength(1);
from the link Testing react-router with Shallow rendering but it didnt work.
Please let me know.
Thanks in advance.
I found that I can mock the React Router hooks like useLocation using the following pattern:
import React from "react"
import ExampleComponent from "./ExampleComponent"
import { shallow } from "enzyme"
jest.mock("react-router-dom", () => ({
...jest.requireActual("react-router-dom"),
useLocation: () => ({
pathname: "localhost:3000/example/path"
})
}));
describe("<ExampleComponent />", () => {
it("should render ExampleComponent", () => {
shallow(<ExampleComponent/>);
});
});
If you have a call to useLocation in your ExampleComponent the above pattern should allow you to shallow render the component in an Enzyme / Jest test without error.
I've been struggling with this recently too...
I found this works quite nicely:
import React from "react"
import ExampleComponent from "./ExampleComponent"
import { shallow } from "enzyme"
const mockUseLocationValue = {
pathname: "/testroute",
search: '',
hash: '',
state: null
}
jest.mock('react-router', () => ({
...jest.requireActual("react-router") as {},
useLocation: jest.fn().mockImplementation(() => {
return mockUseLocationValue;
})
}));
describe("<ExampleComponent />", () => {
it("should render ExampleComponent", () => {
mockUseLocationValue.pathname = "test specific path";
shallow(<ExampleComponent/>);
...
expect(...
});
});
this way, I was able to both mock useLocation and provide a value for pathname in specific tests as necessary.
HTH
If you are using react-testing-library:
import React from 'react';
import { Router } from 'react-router-dom';
import { render } from '#testing-library/react';
import { createMemoryHistory } from 'history';
import Component from '../Component.jsx';
test('<Component> renders without crashing', () => {
const history = createMemoryHistory();
render(
<Router history={history}>
<Component />
</Router>
);
});
More info: https://testing-library.com/docs/example-react-router/
I know this isn’t a direct answer to your question, but if what you want is to test the browser location or history, you can use mount and add an extra Route at the end where you can “capture” the history and location objects.
test(`Foobar`, () => {
let testHistory
let testLocation
const wrapper = mount(
<MemoryRouter initialEntries={[`/`]}>
<MyRoutes />
<Route
path={`*`}
render={routeProps => {
testHistory = routeProps.history
testLocation = routeProps.location
return null
}}/>
</MemoryRouter>
)
// Manipulate wrapper
expect(testHistory)...
expect(testLocation)...
)}
Have you tried:
describe("<Header/>", () => {
it("call the header component", () => {
const wrapper = shallow(<MemoryRouter initialEntries={['/abc']}><Header /></MemoryRouter>);
expect(wrapper.find(Header).dive().find(ComponentA)).toHaveLength(1);
});
});
When you use shallow only the first lvl is rendered, so you need to use dive to render another component.
None of the solutions above worked for my use case(unit testing a custom hook). I had to override the inner properties of useLocation which was read-only.
\\ foo.ts
export const useFoo = () => {
const {pathname} = useLocation();
\\ other logic
return ({
\\ returns whatever thing here
});
}
/*----------------------------------*/
\\ foo.test.ts
\\ other imports here
import * as ReactRouter from 'react-router';
Object.defineProperty(ReactRouter, 'useLocation', {
value: jest.fn(),
configurable: true,
writable: true,
});
describe("useFoo", () => {
it(' should do stgh that involves calling useLocation', () => {
const mockLocation = {
pathname: '/path',
state: {},
key: '',
search: '',
hash: ''
};
const useLocationSpy = jest.spyOn(ReactRouter, 'useLocation').mockReturnValue(mockLocation)
const {result} = renderHook(() => useFoo());
expect(useLocationSpy).toHaveBeenCalled();
});
});

How to test the ProtectedRoute component. (Invariant failed: You should not use <Route> outside a <Router>)

I have built this component to create a ProtectedRoute component that can be used to check if the user is Authenticated and be redirected to the routes or be prompted to login.
/* eslint-disable
react/jsx-props-no-spreading,
no-undef, import/no-extraneous-dependencies,
no-unused-expressions,
no-return-assign,
prettier/prettier
*/
import { Redirect, Route } from 'react-router';
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { isValidElementType } from 'react-is';
import toast from '../lib/toast';
import setAuthenticate from '../store/actions/authenticateAction';
export const ProtectedRoute = ({
setAuthState,
component: Component,
...rest
}) => {
setAuthState(true);
const isAuthenticated = !!localStorage.bn_user_data;
!isAuthenticated && toast('error', 'You need to be logged in');
return (
<Route
data-test='protected-route'
render={props =>
isAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to={{ pathname: '/login', state: { from: props.location } }}
/>
// eslint-disable-next-line prettier/prettier
)}
{...rest}
/>
);
};
ProtectedRoute.propTypes = {
component: (props, propName) => {
if (props[propName] && !isValidElementType(props[propName])) {
return new Error(
`Invalid prop 'component' supplied to 'Route':
the prop is not a valid React component`,
);
}
},
location: PropTypes.shape({
pathname: PropTypes.string.isRequired,
}),
setAuthState: PropTypes.func.isRequired,
};
ProtectedRoute.defaultProps = {
location: null,
component: null,
};
export default connect(null, {
setAuthState: setAuthenticate,
})(ProtectedRoute);
And here is my test for to render that component:
import React from "react";
import { ProtectedRoute } from "../../components/ProtectedRoute";
import { render } from "#testing-library/react";
describe("\"ProtectedRoute\"", () => {
beforeAll(() => {
const user_data = `{
"email":"requestero#user.com",
"name":"Requester",
"userId":2,
"verified":true,
"role":"requester",
"lineManagerId":7,
"iat":1578472431,
"exp":1578558831
}`;
global.localStorage = {
bn_user_data: user_data,
};
});
it("should render without error", function() {
const { getByTestId } = render(
<ProtectedRoute setAuthState={jest.fn()}/>
);
});
});
The component works well but with I can't build the test for it, it is failling with this error:
Invariant failed: You should not use <Route> outside a <Router>
So I need some advice on how to implement the test with jest on how to implement this.
I found a workaround of this issue by using enzyme instead of using react-testing-library:
import React from "react";
import { ProtectedRoute } from "../../components/ProtectedRoute";
import { shallow } from "enzyme";
const setUp = (props = {}) => shallow(<ProtectedRoute {...props} />);
describe("\"ProtectedRoute\"", () => {
let wrapper;
const props = {
setAuthState: jest.fn()
};
beforeAll(() => {
const user_data = `{
"email":"requestero#user.com",
"name":"Requester",
"userId":2,
"verified":true,
"role":"requester",
"lineManagerId":7,
"iat":1578472431,
"exp":1578558831
}`;
global.localStorage = {
bn_user_data: user_data
};
wrapper = setUp(props);
});
it("should render without error", function() {
const component = wrapper.find(`[data-test='protected-route']`);
expect(component.length).toBe(1);
});
});

Resources