简体   繁体   中英

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

I am trying to test a React component using Enzyme. 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]"

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

Full component, 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:

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. 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:

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:

  • "@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",
  • "enzyme": "^3.3.0",
  • "enzyme-adapter-react-16": "^1.1.1",
  • "jest": "^25.1.0",
  • "react": "^16.13.0",
  • "react-dom": "^16.13.0"

I'm not finding much documentation for testing a component utilizing the useHistory hook in general. It seems Enzyme only started working with React Hooks a year ago, and only for mock, not for shallow rendering.

Anyone have any idea how I can go about this?

The problem here comes from inside of useHistory hook as you can imagine. 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. 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

    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. If you need to mock it or spy on it, you can overwrite the mock from setupTests file in your specific unit test file.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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