【问题标题】:How to test React component that uses React Hooks useHistory hook with Enzyme?如何测试使用 React Hooks useHistory hook 和 Enzyme 的 React 组件?
【发布时间】:2020-03-04 14:23:39
【问题描述】:

我正在尝试使用 Enzyme 测试 React 组件。在我们将组件转换为钩子之前,测试工作正常。现在我收到错误消息“错误:未捕获 [TypeError: 无法读取未定义的属性‘历史’]”

我已经阅读了以下类似问题,但无法解决:

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

完整组件,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;

这是我的测试,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);
    });
  });
});

我是 React Hooks 的新手,所以我试图围绕它来思考。我的理解是我必须为我的测试提供某种模拟历史值。我尝试像这样创建一个单独的 useContext 文件并在测试中将它包裹在我的组件周围,但这不起作用:

import React, { useContext } from 'react';

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

const defaultValues = { history: '/' };

const AccessBarWithRouterContext = React.createContext(defaultValues);

export default useAccessBarWithRouterContext;

我的 devDependencies 的当前版本:

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

我没有找到很多文档来测试使用 useHistory 钩子的组件。似乎 Enzyme 一年前才开始使用 React Hooks,并且仅用于模拟,而不用于浅渲染。

有人知道我该怎么做吗?

【问题讨论】:

    标签: reactjs react-hooks enzyme


    【解决方案1】:

    您可以想象,这里的问题来自 useHistory 钩子内部。该钩子旨在用于路由器提供者的消费者。如果您知道 Providers 和 Consumers 的结构,那么您会很清楚,这里的消费者 (useHistory) 正在尝试从提供程序访问一些信息,这些信息在您的文本案例中不存在。 有两种可能的解决方案:

    1. 用路由器包装你的测试用例

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

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

    我个人更喜欢第二个,因为您可以将它放在 setupTests 文件中而忘记它。 如果您需要模拟或监视它,您可以在特定的单元测试文件中覆盖 setupTests 文件中的模拟。

    【讨论】:

    • 谢谢!将我已安装的组件包装在 中(当然也将 Router 导入到我的测试中)修复了第一个测试并解决了 history is undefined 错误。还要感谢您提供有关阅读提供者和消费者的建议。现在我只需要对我的第二个测试进行故障排除,这给了我正在阅读的 useState 钩子的问题。
    • 没问题。我在回答时没有检查您的第二个测试,但现在正在调查它,可能 wrapper.update() 会解决问题,因为它会强制使用新状态重新渲染。但该测试的附带说明:这不是一个正确的测试,因为它依赖于组件的实现细节(直接调用 setState)。您应该通过模拟事件(点击、输入、滚动等)来设置隐藏状态。
    • 感谢您的反馈,这是一个很好的观点。问题是我们有一个命令会触发整个 document.body 的状态更改,这使得它比模拟按钮单击等有点棘手。 setState 在测试中似乎不再起作用,因为我们已经切换到使用useState 钩子。它无法识别“this.state.isHidden”。可能暂时忽略该测试。
    • 是的,功能组件的状态不是那么容易达到是有道理的
    猜你喜欢
    • 2020-06-09
    • 2021-06-24
    • 2017-04-13
    • 2021-07-29
    • 2021-01-09
    • 1970-01-01
    • 2017-01-01
    • 2017-02-12
    • 2017-02-12
    相关资源
    最近更新 更多