I've created this RootContext to handle authentication for my small React Hooks app. Everything works as expected except for getting weird errors using Enzyme's shallow
and mount
.
I'm trying to test it like this:
const wrapper = mount(<Login />)
Index:
import RootContext from './RootContext'
function Root() {
return (
<RootContext>
<App />
</RootContext>
)
}
ReactDOM.render(<Root/>, document.getElementById('root'));
RootContext:
import React, { useEffect, useState } from 'react'
export const RootContext = React.createContext()
export default ({ children }) => {
const auth = window.localStorage.getItem('authenticated') || 'false'
const cred = window.localStorage.getItem('credentials') || null
const [authenticated, setAuthenticated] = useState(auth)
const [credentials, setCredentials] = useState(cred)
useEffect(
() => {
window.localStorage.setItem('authenticated', authenticated)
window.localStorage.setItem('credentials', credentials)
},
[authenticated, credentials]
)
const defaultContext = {
authenticated,
setAuthenticated,
credentials,
setCredentials
}
return (
<RootContext.Provider value={defaultContext}>
{children}
</RootContext.Provider>
)
}
Login, Logout and Register all use the useAuthenticate
hook that causes this issue. The BmiForm component works fine.
import AuthenticatedRoute from './AuthenticatedRoute'
export default function App() {
return (
<Router>
<Header />
<Switch>
<Container>
<Row>
<Col md={{ span: 4, offset: 4 }}>
<AuthenticatedRoute exact path="/" component={BmiForm} />
<Route exact path="/login" component={ Login } />
<Route exact path="/logout" component={ Logout } />
<Route exact path="/register" component={ Register } />
</Col>
</Row>
</Container>
</Switch>
</Router>
)
}
The useAuthenticate
hook that's causing the issue:
import useReactRouter from 'use-react-router';
import { RootContext } from './../RootContext'
export default function useAuthenticate() {
const { history } = useReactRouter()
const {
authenticated,
setAuthenticated,
credentials,
setCredentials
} = useContext(RootContext);
Adding the useAuthenticate
hook to the BmiForm, causes its test to fail the same way.
import useAuthenticate from './custom/useAuthenticate'
export default function BmiForm(props) {
const { credentials, setAuthenticated } = useAuthenticate()
First error I get:
TypeError: Cannot read property 'authenticated' of undefined
5 | export default function useAuthenticate() {
6 | const {
> 7 | authenticated,
| ^
8 | setAuthenticated,
9 | credentials,
10 | setCredentials
Second error with stacktrace:
use-react-router may only be used within a react-router context.
4 |
5 | export default function useAuthenticate() {
> 6 | const { history } = useReactRouter()
| ^
7 | const {
8 | authenticated,
9 | setAuthenticated,
at useRouter (node_modules/use-react-router/src/use-react-router.ts:20:11)
at useAuthenticate (src/custom/useAuthenticate.js:6:23)
at BmiForm (src/BmiForm.js:15:45)
at renderWithHooks (node_modules/react-dom/cjs/react-dom.development.js:12839:18)
at mountIndeterminateComponent (node_modules/react-dom/cjs/react-dom.development.js:14816:13)
at beginWork (node_modules/react-dom/cjs/react-dom.development.js:15421:16)
at performUnitOfWork (node_modules/react-dom/cjs/react-dom.development.js:19108:12)
at workLoop (node_modules/react-dom/cjs/react-dom.development.js:19148:24)
at renderRoot (node_modules/react-dom/cjs/react-dom.development.js:19231:7)
at performWorkOnRoot (node_modules/react-dom/cjs/react-dom.development.js:20138:7)
at performWork (node_modules/react-dom/cjs/react-dom.development.js:20050:7)
at performSyncWork (node_modules/react-dom/cjs/react-dom.development.js:20024:3)
at requestWork (node_modules/react-dom/cjs/react-dom.development.js:19893:5)
at scheduleWork (node_modules/react-dom/cjs/react-dom.development.js:19707:5)
at scheduleRootUpdate (node_modules/react-dom/cjs/react-dom.development.js:20368:3)
at updateContainerAtExpirationTime (node_modules/react-dom/cjs/react-dom.development.js:20396:10)
at updateContainer (node_modules/react-dom/cjs/react-dom.development.js:20453:10)
at ReactRoot.Object.<anonymous>.ReactRoot.render (node_modules/react-dom/cjs/react-dom.development.js:20749:3)
at node_modules/react-dom/cjs/react-dom.development.js:20886:14
at unbatchedUpdates (node_modules/react-dom/cjs/react-dom.development.js:20255:10)
at legacyRenderSubtreeIntoContainer (node_modules/react-dom/cjs/react-dom.development.js:20882:5)
at Object.render (node_modules/react-dom/cjs/react-dom.development.js:20951:12)
at Object.render (node_modules/enzyme-adapter-react-16/build/ReactSixteenAdapter.js:382:114)
at new ReactWrapper (node_modules/enzyme/build/ReactWrapper.js:134:16)
at mount (node_modules/enzyme/build/mount.js:21:10)
at test (src/test/bmi_calculator.step.test.js:22:21)
at defineScenarioFunction (node_modules/jest-cucumber/src/feature-definition-creation.ts:155:9)
at test (src/test/bmi_calculator.step.test.js:20:3)
at Suite.<anonymous> (node_modules/jest-cucumber/src/feature-definition-creation.ts:279:9)
at defineFeature (node_modules/jest-cucumber/src/feature-definition-creation.ts:278:5)
at Object.<anonymous> (src/test/bmi_calculator.step.test.js:19:1)
I've tried various solutions involving Enzyme's setContext
. But not sure whether this is related to the Context or react-router
or both.
Since you're testing against context
, ideally, you'll want to test at the root level and make assertions against any DOM changes from there. Also note, that you can't use Route
outside of a router (BrowserRouter
, Router
, StaticRouter
, ...etc) nor history
that hasn't been attached to a router. Although I've never used use-react-router
, taking a look under the hood, it still requires a router. Therefore, your test must include the Provider
, a router, and your page/component.
Here's a working example of testing at the root level:
src/root/index.js
import React from "react";
import { Provider } from "../hooks/useAuthentication";
import Routes from "../routes";
const Root = () => (
<Provider>
<Routes />
</Provider>
);
export default Root;
src/routes/index.js
import React from "react";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import { Container, Header, ProtectedRoutes } from "../components";
import { About, Dashboard, Home } from "../pages";
const Routes = () => (
<BrowserRouter>
<Container>
<Header />
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/about" component={About} />
<ProtectedRoutes>
<Route exact path="/dashboard" component={Dashboard} />
</ProtectedRoutes>
</Switch>
</Container>
</BrowserRouter>
);
export default Routes;
src/root/__tests__/root.test.js
import React from "react";
import { mount } from "enzyme";
import Root from "../index";
describe("Authentication", () => {
let wrapper;
beforeAll(() => {
wrapper = mount(<Root />);
wrapper
.find("Router")
.prop("history")
.push("/dashboard");
wrapper.update();
});
afterAll(() => {
wrapper.unmount();
});
it("initially renders a Login component and displays a message", () => {
expect(wrapper.find("h1").text()).toEqual("Login");
expect(wrapper.find("h3").text()).toEqual(
"You must login before viewing the dashboard!"
);
});
it("authenticates the user and renders the Dashboard", () => {
wrapper.find("button").simulate("click");
expect(wrapper.find("h1").text()).toEqual("Dashboard");
});
it("unauthenticates the user and redirects the user to the home page", () => {
wrapper.find("button").simulate("click");
expect(wrapper.find("h1").text()).toEqual("Home");
});
});
The Dashboard page can be isolated as long as it has access to the authentication functions; however, this can create some repetitive test cases for subsequent pages/components and doesn't make much sense since it still requires the context to be set at the root level and a router (especially if a component/page or the hook is subscribing to history
).
Here's a working example where a Dashboard page has been isolated for testing:
src/routes/index.js
import React from "react";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import { Container, Header, ProtectedRoutes } from "../components";
import { About, Dashboard, Home } from "../pages";
const Routes = () => (
<BrowserRouter>
<Container>
<Header />
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/about" component={About} />
<ProtectedRoutes>
<Route exact path="/dashboard" component={Dashboard} />
</ProtectedRoutes>
</Switch>
</Container>
</BrowserRouter>
);
export default Routes;
components/ProtectedRoutes/index.js
import React from "react";
import { useAuthentication } from "../../hooks";
import Login from "../Login";
const ProtectedRoutes = ({ children }) => {
const { isAuthenticated, login } = useAuthentication();
return isAuthenticated ? children : <Login login={login} />;
};
export default ProtectedRoutes;
pages/Dashboard/index.js
import React, { Fragment, useCallback } from "react";
import { useAuthentication } from "../../hooks";
import { Button, Description, Title } from "../../components";
const Dashboard = ({ history }) => {
const { logout } = useAuthentication();
const unAuthUser = useCallback(() => {
logout();
history.push("/");
}, [history, logout]);
return (
<Fragment>
<Title>Dashboard</Title>
<Description>
Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper
suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem
vel eum iriure dolor in hendrerit in vulputate velit esse molestie
consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et
accumsan et iusto odio dignissim qui blandit praesent luptatum zzril
delenit augue duis dolore te feugait nulla facilisi.
</Description>
<Button onClick={unAuthUser}>Logout</Button>
</Fragment>
);
};
export default Dashboard;
pages/Dashboard/__tests__/Dashboard.test.js
import React from "react";
import { mount } from "enzyme";
import { BrowserRouter, Route } from "react-router-dom";
import { Provider } from "../../../hooks/useAuthentication";
import { ProtectedRoutes } from "../../../components";
import Dashboard from "../index";
describe("Dashboard Page", () => {
let wrapper;
beforeAll(() => {
wrapper = mount(
<Provider>
<BrowserRouter>
<ProtectedRoutes>
<Route exact path="/" component={Dashboard} />
</ProtectedRoutes>
</BrowserRouter>
</Provider>
);
});
afterAll(() => {
wrapper.unmount();
});
it("initially renders a login component and displays a message", () => {
expect(wrapper.find("h1").text()).toEqual("Login");
expect(wrapper.find("h3").text()).toEqual(
"You must login before viewing the dashboard!"
);
});
it("authenticates the user and updates the component", () => {
wrapper.find("button").simulate("click");
expect(wrapper.find("h1").text()).toEqual("Dashboard");
});
it("unauthenticates the user", () => {
wrapper.find("button").simulate("click");
expect(wrapper.find("h1").text()).toEqual("Login");
});
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With