Ошибки ферментов с React Context и Hooks API

Я создал этот 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'));

Корневой контекст:

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 setContext. Но не уверен, связано ли это с Контекстом, react-router или с обоими.

Вы пробовали mount(<BrowserRouter><Login /></BrowserRouter) откуда BrowserRouter импортируется из react-router?

johnny peter 23.06.2019 18:54
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
5
1
1 666
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

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

Edit Protected Route Root Context

источник/корень/index.js

import React from "react";
import { Provider } from "../hooks/useAuthentication";
import Routes from "../routes";

const Root = () => (
  <Provider>
    <Routes />
  </Provider>
);

export default Root;

источник/маршруты/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");
  });
});

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

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

Edit Protected Route Confined Context

источник/маршруты/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;

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

Другие вопросы по теме