简体   繁体   中英

React Testing: Event handlers in React Shallow Rendering unit tests

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

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 .

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');
});

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

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!

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