Ошибки фермента с React Context и Hooks API - PullRequest
0 голосов
/ 04 марта 2019

Я создал этот RootContext для обработки аутентификации для моего небольшого приложения React Hooks.Все работает, как и ожидалось, за исключением странных ошибок с использованием Enzyme shallow и mount.

Я пытаюсь проверить это так:

const wrapper = mount(<Login />)

Индекс:

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

Для входа, выхода и регистрации используется ловушка useAuthenticate, вызывающая эту проблему.Компонент BmiForm работает нормально.

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

Хук useAuthenticate, вызывающий проблему:

import useReactRouter from 'use-react-router';
import { RootContext } from './../RootContext'

export default function useAuthenticate() {
  const { history } = useReactRouter()
  const {
    authenticated,
    setAuthenticated,
    credentials,
    setCredentials
  } = useContext(RootContext);

Добавление хука useAuthenticate к BmiForm приводит к сбою его тестаТаким же образом.

import useAuthenticate from './custom/useAuthenticate'

export default function BmiForm(props) {
  const { credentials, setAuthenticated } = useAuthenticate()

Первая ошибка, которую я получаю:

    TypeError: Cannot read property 'authenticated' of undefined

       5 | export default function useAuthenticate() {
       6 |   const {
    >  7 |     authenticated,
         |     ^
       8 |     setAuthenticated,
       9 |     credentials,
      10 |     setCredentials

Вторая ошибка с помощью трассировки стека:

   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)

Я пробовал различные решения, включающие Enzyme's setContext.Но не уверен, связано ли это с Контекстом или react-router или с обоими.

1 Ответ

0 голосов
/ 23 июня 2019

Так как вы тестируете против context, в идеале вам нужно протестировать на корневом уровне и сделать утверждения против любых изменений DOM оттуда.Также обратите внимание, что вы не можете использовать Route вне маршрутизатора (BrowserRouter, Router, StaticRouter, ... и т. Д.), А также history, который не был подключен к маршрутизатору.Хотя я никогда не пользовался use-react-router, заглядывая под капот, все же требуется роутер.Следовательно, ваш тест должен включать Provider, маршрутизатор и вашу страницу / компонент.

Вот рабочий пример тестирования на корневом уровне :

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

Страница панели инструментов может быть изолирована, если она имеет доступ к функциям аутентификации;однако, это может создать некоторые повторяющиеся тестовые случаи для последующих страниц / компонентов и не имеет особого смысла, поскольку все еще требует установки контекста на корневом уровне и маршрутизатора (особенно, если компонент / страница или ловушка подписываются наhistory).

Вот рабочий пример, когда страница панели инструментов была изолирована для тестирования :

Edit Protected Route Confined Context

src / route / 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;

компонентов / 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;

страниц/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;

страницы / Панель инструментов / __ тесты __ / 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");
  });
});
...