Ad

How Do I Test A Method Defined Within A Functional Component, That Interacts With DOM Elements And Has No Arguments

- 1 answer

I have been having trouble getting 100% test coverage on one of my buttons (A React functional components.) Basically when it is clicked, it executes some code and then also calls another method from within this onClick called resetButtons. This method will find all the buttons like it in the app and remove a class. This is a preemptive behavior so that only one button at a time can be active.

So far I have tested the click using .simulate, passing in a mocked domElement. And then test that the domElement.classList.add method is called with 'active'.

Obviously this being a DOM centered operation, I am finding it very difficult to test the resetButtons method that lies within the component. especially considering it doesn't have any methods.

I have tried defining the resetButtons method outside of the component and then exported it so the jest test could import it. However I have been unable to test the method as it seems to want it to be a spy or mock, and not the method itself. (Matcher error: received value must be a mock or spy function )

Here is the react Functional Component:

import React from 'react';
import PropTypes from 'prop-types';
import classes from './MainButton.module.scss';

const MainButton = (props) => {
  const resetButtons = () => {
    const elements = document.getElementsByClassName('mainButton');
    for (let i = 0; i < elements.length; i += 1) {
      elements[i].classList.remove('active');
    }
  };

  const handleClick = (event) => {
    if (!event.target.classList.contains('active')) {
      resetButtons();
      event.target.classList.add('active');
      props.setVisualState(props.className.split('-')[0]);
    }
  };

  return (
    <button
      onClick={handleClick}
      type="button"
      className={`${classes.mainButton} ${props.className}`}
    >
      {props.children}
    </button>
  );
};

MainButton.propTypes = {
  children: PropTypes.node,
  className: PropTypes.string,
  setVisualState: PropTypes.func.isRequired,
};

MainButton.defaultProps = {
  children: 'Button',
  className: '',
};

export default MainButton;

Here is the Test

import React from 'react';
import { shallow } from 'enzyme';
import MainButton from './MainButton';

describe('MainButton', () => {
  const domElement = { classList: { contains: jest.fn(), remove: jest.fn(), add: jest.fn() } };
  const setVisualStateMock = jest.fn();
  const mainButton = shallow(<MainButton setVisualState={setVisualStateMock} />);

  it(' is rendered properly', () => {
    expect(mainButton).toMatchSnapshot();
  });

  describe('when clicked', () => {
    beforeEach(() => {
      mainButton.find('button').simulate('click', { target: domElement });
    });

    it('it runs `classlist.add` to assign `active` class', () => {
      expect(domElement.classList.add).toHaveBeenCalledWith('active');
    });

    it('it runs set visual state to update `Allergen` container `state`', () => {
      expect(setVisualStateMock).toHaveBeenCalled();
    });
  });
});

Currently the coverage report is reporting 92% coverage, but the branch is at 50 and the line that is causing the trouble is on line 9 (the elements[i].classList.remove('active'); line.

I know at 90% I should probably just move on but this is something I want to be able to figure out. Feel like getting head around this will make me a better tested.

Hope you guys can help!

Ad

Answer

Fumbling around in the DOM yourself is an anti-pattern. That's React's job. Instead of manipulating the dom with target.classList.add you should have a state property that holds the status which of your inputs is currently active. Then, while rendering you can say className={isActiveInput ? "active": null}.

Because the state is not specific to your MainButton component you would lift the state up. If you have the state somewhere in the parent you don't have to crudely search for DOM elements by classname and manipulate the dom yourself.

Simply put, the rule of React is: You define how things are supposed to look like, React takes care that your definition becomes reality in the dom. If you manipulate the DOM yourself - you're doing it wrong.

When all of this is done, you will have no problem at all with tests, because all you have to do is provide the proper state and props, which is easy, and check that your callback is triggered onClick.

EDIT: Advanced version would be to use Context, but I'd go with state lifting first.

Ad
source: stackoverflow.com
Ad