简体   繁体   English

如何测试使用 React Hooks useHistory hook 和 Enzyme 的 React 组件?

[英]How to test React component that uses React Hooks useHistory hook with Enzyme?

I am trying to test a React component using Enzyme.我正在尝试使用酶测试 React 组件。 Tests worked fine until we converted the component to hooks.测试工作正常,直到我们将组件转换为钩子。 Now I am getting the error, "Error: Uncaught [TypeError: Cannot read property 'history' of undefined]"现在我收到错误消息,“错误:未捕获 [TypeError:无法读取未定义的属性‘历史’]”

I have already read through the following similar issues and wasn't able to solve it:我已经阅读了以下类似的问题,但无法解决:

Also this article: * https://medium.com/7shifts-engineering-blog/testing-usecontext-react-hook-with-enzyme-shallow-da062140fc83还有这篇文章:* https://medium.com/7shifts-engineering-blog/testing-usecontext-react-hook-with-enzyme-shallow-da062140fc83

Full component, AccessBarWithRouter.jsx:完整组件,AccessBarWithRouter.jsx:

/**
 * @description Accessibility bar component to allow user to jump focus to different components on screen. 
 * One dropdown will focus to elements on screen.
 * The other will route you to routes in your navigation bar.
 *  
 */

import React, { useState, useEffect, useRef } from 'react';
import Dropdown from 'react-dropdown-aria';
import { useHistory } from 'react-router-dom';

const AccessBarWithRouter = () => {
  const pathname = useHistory().location.pathname;
  const [sectionInfo, setSectionInfo] = useState(null);
  const [navInfo, setNavInfo] = useState(null);
  const [isHidden, setIsHidden] = useState(true);

  // creating the refs to change focus
  const sectionRef = useRef(null);
  const accessBarRef = useRef(null);


  // sets focus on the current page from the 1st dropdown
  const setFocus = e => {
    const currentLabel = sectionInfo[e];
    const currentElement = document.querySelector(`[aria-labelledBy='${currentLabel}']`);
    currentElement.tabIndex = -1;
    sectionRef.current = currentElement;
    // can put a .click() after focus to focus with the enter button
    // works, but gives error
    sectionRef.current.focus();
  };


  // Changes the page when selecting a link from the 2nd dropdown
  const changeView = e => {
    const currentPath = navInfo[e];
    const accessLinks = document.querySelectorAll('.accessNavLink');
    accessLinks.forEach(el => {
      if (el.pathname === currentPath) {
        el.click();
      };
    });
  };

  // event handler to toggle visibility of AccessBar and set focus to it
  const accessBarHandlerKeyDown = e => {
    if (e.altKey && e.keyCode === 191) {
      if (isHidden) {
        setIsHidden(false)
        accessBarRef.current.focus();
      } else setIsHidden(true);
    }
  }


  /**
   *
   * useEffect hook to add and remove the event handler when 'alt' + '/' are pressed  
   * prior to this, multiple event handlers were being added on each button press 
   * */ 
  useEffect(() => {
    document.addEventListener('keydown', accessBarHandlerKeyDown);
    const navNodes = document.querySelectorAll('.accessNavLink');
    const navValues = {};
    navNodes.forEach(el => {
      navValues[el.text] = el.pathname;
    });
    setNavInfo(navValues);
    return () => document.removeEventListener('keydown', accessBarHandlerKeyDown);
  }, [isHidden]);


  /**
   * @todo figure out how to change the dropdown current value after click
   */
  useEffect(() => {
    //  selects all nodes with the aria attribute aria-labelledby
    setTimeout(() => {
      const ariaNodes = document.querySelectorAll('[aria-labelledby]');
      let sectionValues = {};

      ariaNodes.forEach(node => {
        sectionValues[node.getAttribute('aria-labelledby')] = node.getAttribute('aria-labelledby');
      });

      setSectionInfo(sectionValues);
    }, 500);

  }, [pathname]);



  // render hidden h1 based on isHidden
  if (isHidden) return <h1 id='hiddenH1' style={hiddenH1Styles}>To enter navigation assistant, press alt + /.</h1>;

  // function to create dropDownKeys and navKeys 
  const createDropDownValues = dropDownObj => {
    const dropdownKeys = Object.keys(dropDownObj);
    const options = [];
    for (let i = 0; i < dropdownKeys.length; i++) {
      options.push({ value: dropdownKeys[i]});
    }
    return options;
  };

  const sectionDropDown = createDropDownValues(sectionInfo);
  const navInfoDropDown = createDropDownValues(navInfo);

  return (
    <div className ='ally-nav-area' style={ barStyle }>
        <div className = 'dropdown' style={ dropDownStyle }> 
          <label htmlFor='component-dropdown' tabIndex='-1' ref={accessBarRef} > Jump to section: </label>
          <div id='component-dropdown' >
            <Dropdown
              options={ sectionDropDown }
              style={ activeComponentDDStyle }
              placeholder='Sections of this page'
              ariaLabel='Navigation Assistant'
              setSelected={setFocus} 
            />
          </div>
        </div>
          <div className = 'dropdown' style={ dropDownStyle }> 
          <label htmlFor='page-dropdown'> Jump to page: </label>
          <div id='page-dropdown' >
            <Dropdown
              options={ navInfoDropDown }
              style={ activeComponentDDStyle }
              placeholder='Other pages on this site'
              ariaLabel='Navigation Assistant'
              setSelected={ changeView } 
            />
          </div>
        </div>
      </div>
  );
};

/** Style for entire AccessBar */
const barStyle =  {
  display: 'flex',
  paddingTop: '.1em',
  paddingBottom: '.1em',
  paddingLeft: '5em',
  alignItems: 'center',
  justifyContent: 'flex-start',
  zIndex: '100',
  position: 'sticky',
  fontSize: '.8em',
  backgroundColor: 'gray',
  fontFamily: 'Roboto',
  color: 'white'
};

const dropDownStyle = {
  display: 'flex',
  alignItems: 'center',
  marginLeft: '1em',
};

/** Style for Dropdown component **/
const activeComponentDDStyle = {
  DropdownButton: base => ({
    ...base,
    margin: '5px',
    border: '1px solid',
    fontSize: '.5em',
  }),
  OptionContainer: base => ({
    ...base,
    margin: '5px',
    fontSize: '.5em',
  }),
};

/** Style for hiddenH1 */
const hiddenH1Styles = {
  display: 'block',
  overflow: 'hidden',
  textIndent: '100%',
  whiteSpace: 'nowrap',
  fontSize: '0.01px',
};

export default AccessBarWithRouter;

Here is my test, AccessBarWithRouter.unit.test.js:这是我的测试,AccessBarWithRouter.unit.test.js:

import React from 'react';
import Enzyme, { mount } from 'enzyme';
import AccessBarWithRouter from '../src/AccessBarWithRouter.jsx';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() });

describe('AccessBarWithRouter component', () => {
  it('renders hidden h1 upon initial page load (this.state.isHidden = true)', () => {
    const location = { pathname: '/' };
    const wrapper = mount(
      <AccessBarWithRouter location={location}/>
  );
    // if AccessBarWithRouter is hidden it should only render our invisible h1
    expect(wrapper.exists('#hiddenH1')).toEqual(true);
  })
  it('renders full AccessBarWithRouter when this.state.isHidden is false', () => {
    // set dummy location within test to avoid location.pathname is undefined error
    const location = { pathname: '/' };
    const wrapper = mount(
        <AccessBarWithRouter location={location} />
    );
    wrapper.setState({ isHidden: false }, () => {
      // If AccessBar is not hidden the outermost div of the visible bar should be there
      // Test within setState waits for state change before running test
      expect(wrapper.exists('.ally-nav-area')).toEqual(true);
    });
  });
});

I am new to React Hooks so trying to wrap my mind around it.我是 React Hooks 的新手,所以我试图围绕它进行思考。 My understanding is that I have to provide some sort of mock history value for my test.我的理解是我必须为我的测试提供某种模拟历史值。 I tried creating a separate useContext file as so and wrapping it around my component in the test, but that didn't work:我尝试创建一个单独的 useContext 文件,并在测试中将其包裹在我的组件周围,但这不起作用:

import React, { useContext } from 'react';

export const useAccessBarWithRouterContext = () => useContext(AccessBarWithRouterContext);

const defaultValues = { history: '/' };

const AccessBarWithRouterContext = React.createContext(defaultValues);

export default useAccessBarWithRouterContext;

My current versions of my devDependencies:我当前的 devDependencies 版本:

  • "@babel/cli": "^7.8.4", "@babel/cli": "^7.8.4",
  • "@babel/core": "^7.8.6", "@babel/core": "^7.8.6",
  • "@babel/polyfill": "^7.0.0-beta.51", "@babel/polyfill": "^7.0.0-beta.51",
  • "@babel/preset-env": "^7.8.6", "@babel/preset-env": "^7.8.6",
  • "@babel/preset-react": "^7.8.3", "@babel/preset-react": "^7.8.3",
  • "babel-core": "^7.0.0-bridge.0", "babel-core": "^7.0.0-bridge.0",
  • "babel-jest": "^25.1.0", "babel-jest": "^25.1.0",
  • "enzyme": "^3.3.0", "酶": "^3.3.0",
  • "enzyme-adapter-react-16": "^1.1.1", "enzyme-adapter-react-16": "^1.1.1",
  • "jest": "^25.1.0", "jest": "^25.1.0",
  • "react": "^16.13.0", “反应”:“^ 16.13.0”,
  • "react-dom": "^16.13.0" "react-dom": "^16.13.0"

I'm not finding much documentation for testing a component utilizing the useHistory hook in general.我没有找到很多文档来测试一般使用 useHistory 钩子的组件。 It seems Enzyme only started working with React Hooks a year ago, and only for mock, not for shallow rendering.似乎 Enzyme 一年前才开始使用 React Hooks,并且只用于模拟,而不是用于浅层渲染。

Anyone have any idea how I can go about this?任何人都知道我该怎么做?

The problem here comes from inside of useHistory hook as you can imagine.正如您可以想象的那样,这里的问题来自 useHistory 钩子内部。 The hook is designed to be used in consumers of a router provider.该钩子旨在用于路由器提供商的消费者。 If you know the structure of Providers and Consumers, it'll make perfect sense to you that, here the consumer (useHistory) is trying to access some information from provider, which doesn't exist in your text case.如果您知道 Providers 和 Consumers 的结构,那么对您来说很有意义,这里的消费者 (useHistory) 试图从提供者访问一些信息,而这些信息在您的文本案例中不存在。 There are two possible solutions:有两种可能的解决方案:

  1. Wrap your test case with a router用路由器包装你的测试用例

    it('renders hidden h1 upon initial page load (this.state.isHidden = true)', () => { const location = { pathname: '/' }; const wrapper = mount( <Router> <AccessBarWithRouter location={location}/> </Router> ) });
  2. Mock useHistory hook with a fake history data使用假历史数据模拟 useHistory 钩子

    jest.mock('react-router-dom', () => { const actual = require.requireActual('react-router-dom') return { ...actual, useHistory: () => ({ methods }), } })

I personally prefer the 2nd one as you can place it in setupTests file and forget about it.我个人更喜欢第二个,因为您可以将它放在 setupTests 文件中并忘记它。 If you need to mock it or spy on it, you can overwrite the mock from setupTests file in your specific unit test file.如果您需要模拟或监视它,您可以在您的特定单元测试文件中覆盖 setupTests 文件中的模拟。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM