Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Enzyme errors with React Context and Hooks API

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.

like image 329
dan-klasson Avatar asked Oct 16 '22 06:10

dan-klasson


1 Answers

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:

Edit Protected Route Root Context

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:

Edit Protected Route Confined Context

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");
  });
});
like image 183
Matt Carlotta Avatar answered Oct 27 '22 09:10

Matt Carlotta