Testing authentication in react with jest and enzyme - reactjs

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

Related

Jest mock function not being called

My component code is as below. Not an expert in Jest mocking. referred How to mock useHistory hook in jest? and mocked useHistory.push. But the mock function is not being hit. I would appreciate any suggestions
const ReviseAction = ({
plans,
template,
coveragePercentage,
territoryName,
existingTemplateId,
}) => {
const history = useHistory();
const handleRevise = () => {
history.push({
pathname: "/xxx",
state: {
plans: plans,
template: template,
coveragePercentage: coveragePercentage,
territoryName: territoryName,
existingTemplateId: existingTemplateId,
},
});
};
return (
<button
data-testid="revise-button"
onClick={handleRevise}
key="revise-button"
>
Revise
</button>
);
};
Here is my test:
import React from "react";
import { render, screen, fireEvent } from "#testing-library/react";
import ReviseAction from "./ReviseAction";
import { HashRouter as Router } from "react-router-dom";
describe("ReviseAction", () => {
jest.mock("react-router-dom");
const pushMock = jest.fn();
//reactRouterDom.useHistory = jest.fn().mockReturnValue({push: pushMock});
jest.mock("react-router-dom", () => ({
...jest.requireActual("react-router-dom"),
useHistory: () => ({
push: jest.fn()
})
}));
it("Renders component", async () => {
render(
<Router>
<ReviseAction
plans={[]}
template={{}}
coveragePercentage={"12"}
territoryName={"Name"}
existingTemplateId={"1234"}
/>
</Router>
);
fireEvent.click(screen.queryByTestId("revise-button"));
expect(pushMock).toHaveBeenCalled();
});
});
and getting
Expected number of calls: >= 1
Received number of calls: 0
This fixed it.
What I did:
Changed HashRouter import to router
passed the mock history as props to router.
import React from "react";
import { render, screen, fireEvent } from "#testing-library/react";
import ReviseAction from "./ReviseAction";
import { Router } from "react-router-dom";
describe("ReviseAction", () => {
const mockPush = jest.fn();
jest.mock("react-router-dom", () => ({
useHistory: () => ({
push: mockPush,
}),
}));
const mockHistory = { push: mockPush, location: {}, listen: jest.fn() };
it("Renders component", async () => {
render(
<Router history={mockHistory}>
<ReviseAction
plans={[]}
template={{}}
coveragePercentage={"12"}
territoryName={"Name"}
existingTemplateId={"1234"}
/>
</Router>
);
fireEvent.click(screen.queryByTestId("revise-button"));
expect(mockPush).toHaveBeenCalled();
});
});

Jest mock variables of imported component

I am trying to mock a variable (auth) inside my App component as it is doing conditional rendering. How should I do it without trying to export the variable itself? Been trying for a few days with various solutions but I can't seem to cover it, and now I am stuck.
App.js
import React from "react";
import { useRoutes } from "react-router-dom";
import Routing from "./routes";
import useAuth from "./hooks/useAuth";
import SplashScreen from "./components/splashScreen/SplashScreen";
const App = () => {
const content = useRoutes(Routing());
const auth = useAuth();
return (
<>
{auth.isInitialized ? content : <SplashScreen />}
</>
);
};
export default App;
App.test.js
import React from "react";
import { mount } from "enzyme";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
describe("App Unit Tests", () => {
let wrapper;
beforeEach(() => {
wrapper = mount(
<BrowserRouter>
<App />
</BrowserRouter>
);
});
it("App should render", () => {
expect(wrapper.length).toEqual(1);
});
//Below fails
it("should render splashscreen", () => {
jest.mock("./hooks/useAuth", () => ({
isInitialized: false,
}));
expect(wrapper.length).toEqual(1);
});
it("should render content", () => {
jest.mock("./hooks/useAuth", () => ({
isInitialized: true,
}));
expect(wrapper.length).toEqual(1);
});
});
You could do something like this:
jest.mock('./hooks/use-auth', () => ({
isInitialized: true
});
This basically means that use-auth returns an object which has a inInitialized property
Instead of auth, the useAuth hook should be mocked into an object (say mockUseAuth) that has the isInitialized getter. The getter should return a mockIsInitialized value, that can be changed on per test case basis. Something like this :
let mockIsInitialized = true;
let mockUseAuth = {
isAuthenticated: true
};
Object.defineProperty(mockUseAuth, 'isInitialized', {
get: jest.fn(() => mockIsInitialized)
});
jest.mock('./hooks/use-auth', () => {
return jest.fn(() => (mockUseAuth))
})
describe("App Unit Tests", () => {
let wrapper;
beforeEach(() => {
wrapper = mount(
<BrowserRouter>
<App />
</BrowserRouter>
);
});
it("App should render", () => {
expect(wrapper.length).toEqual(1);
});
it("should render splashscreen", () => {
mockIsInitialized = false;
expect(wrapper.length).toEqual(1);
});
it("should render content", () => {
mockIsInitialized = true;
expect(wrapper.length).toEqual(1);
});
});

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

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

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