React Testing: Event handlers in React Shallow Ren

2020-07-05 05:05发布

问题:

Background

I am trying to learn how to use the React Shallow Rendering TestUtil and had the tests passing until I added an onClick event handler to both; It seems that there must be some difference with the Accordion.toggle function I am trying to use in Accordion.test.js vs this.toggle in Accordian.js...but I can't figure it out.

Question

How can I get the two highlighted tests in Accordian.test.js to pass?

Steps to reproduce

  1. Clone https://github.com/trevordmiller/shallow-rendering-testing-playground
  2. npm install
  3. npm run dev - see that component is working when you click "Lorem Ipsum"
  4. npm run test:watch - see that tests are failing

回答1:

There are a number of issues preventing your tests from passing.

Looking at the test "should be inactive by default":

  1. Accordion.toggle in your test is a property of the Accordion class, and this.toggle in your code is a property of a instance of the Accordion class - so in this case you are comparing two different things. To access the 'instance' method in your test you could replace Accordion.toggle with Accordion.prototype.toggle. Which would work if it were not for this.toggle = this.toggle.bind(this); in your constructor. Which leads us to the second point.

  2. When you call .bind() on a function it creates a new function at runtime - so you can't compare it to the original Accordion.prototype.toggle. The only way to work around this is to pull the "bound" function out of the result from render:

    let toggle = result.props.children[0].props.onClick;
    
    assert.deepEqual(result.props.children, [
      <a onClick={toggle}>This is a summary</a>,
      <p style={{display: 'none'}}>This is some details</p>
    ]);
    

As for your second failing test "should become active when clicked":

  1. You try calling result.props.onClick() which does not exist. You meant to call result.props.children[0].props.onClick();

  2. There is a bug in React that requires a global "document" variable to be declared when calling setState with shallow rendering - how to work around this in every circumstance is beyond the scope of this question, but a quick work around to get your tests passing is to add global.document = {}; right before you call the onClick method. In other words where your original test had:

    result.props.onClick();
    

    Should now say:

    global.document = {};
    result.props.children[0].props.onClick();
    

    See the section "Fixing Broken setState()" on this page and this react issue.



回答2:

Marcin Grzywaczewski wrote a great article with a workaround for testing a click handler that works with shallow rendering.

Given a nested element with an onClick prop and a handler with context bound to the component:

render() {
  return (
    <div>
      <a className="link" href="#" onClick={this.handleClick}>
        {this.state.linkText}
      </a>
      <div>extra child to make props.children an array</div>
    </div>
  );
}

handleClick(e) {
  e.preventDefault();
  this.setState({ linkText: 'clicked' });
}

You can manually invoke the function value of the onClick prop, stubbing in the event object:

it('updates link text on click', () => {
  let tree, link, linkText;

  const renderer = TestUtils.createRenderer();
  renderer.render(<MyComponent />);

  tree = renderer.getRenderOutput();
  link = tree.props.children[0];
  linkText = link.props.children;

  // initial state set in constructor
  expect(linkText).to.equal('Click Me');

  // manually invoke onClick handler via props
  link.props.onClick({ preventDefault: () => {} });

  tree = renderer.getRenderOutput();
  link = tree.props.children[0];
  linkText = link.props.children;

  expect(linkText).to.equal('Clicked');
});


回答3:

For testing user events like onClick you would have to use TestUtils.Simulate.click. Sadly:

Right now it is not possible to use ReactTestUtils.Simulate with Shallow rendering and i think the issue to follow should be: https://github.com/facebook/react/issues/1445



回答4:

I have successfully tested my click in my stateless component. Here is how:

My component:

import './ButtonIcon.scss';

import React from 'react';
import classnames from 'classnames';

const ButtonIcon = props => {
  const {icon, onClick, color, text, showText} = props,
    buttonIconContainerClass = classnames('button-icon-container', {
      active: showText
    });

  return (
    <div
      className={buttonIconContainerClass}
      onClick={onClick}
      style={{borderColor: color}}>
      <div className={`icon-container ${icon}`}></div>
      <div
        className="text-container"
        style={{display: showText ? '' : 'none'}}>{text}</div>
    </div>
  );
}

ButtonIcon.propTypes = {
  icon: React.PropTypes.string.isRequired,
  onClick: React.PropTypes.func.isRequired,
  color: React.PropTypes.string,
  text: React.PropTypes.string,
  showText: React.PropTypes.bool
}

export default ButtonIcon;

My test:

it('should call onClick prop when clicked', () => {
  const iconMock = 'test',
    clickSpy = jasmine.createSpy(),
    wrapper = ReactTestUtils.renderIntoDocument(<div><ButtonIcon icon={iconMock} onClick={clickSpy} /></div>);

  const component = findDOMNode(wrapper).children[0];

  ReactTestUtils.Simulate.click(component);

  expect(clickSpy).toHaveBeenCalled();
  expect(component).toBeDefined();
});

The important thing is to wrap the component:

<div><ButtonIcon icon={iconMock} onClick={clickSpy} /></div>

Hope it help!