【问题标题】:Unit testing React click outside component单元测试 React 点击外部组件
【发布时间】:2017-12-16 05:38:41
【问题描述】:

使用来自this answer 的代码来解决在组件外点击的问题:

componentDidMount() {
    document.addEventListener('mousedown', this.handleClickOutside);
}

componentWillUnmount() {
    document.removeEventListener('mousedown', this.handleClickOutside);
}

setWrapperRef(node) {
    this.wrapperRef = node;
}

handleClickOutside(event) {
    if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
        this.props.actions.something() // Eg. closes modal
    }
}

我不知道如何对不愉快的路径进行单元测试,这样警报就不会运行,到目前为止我得到了什么:

it('Handles click outside of component', () => {
  props = {
    actions: {
      something: jest.fn(),
    }
  }
  const wrapper = mount(
    <Component {... props} />,
  )
  expect(props.actions.something.mock.calls.length).toBe(0)

  // Happy path should trigger mock

  wrapper.instance().handleClick({
    target: 'outside',
  })

  expect(props.actions.something.mock.calls.length).toBe(1)  //true

  // Unhappy path should not trigger mock here ???

  expect(props.actions.something.mock.calls.length).toBe(1)
})

我试过了:

  • 通过wrapper.html()发送
  • .find一个节点并通过发送(不模拟event.target)
  • .simulateing click 在内部元素上(不触发事件监听器)

我确定我遗漏了一些小东西,但我在任何地方都找不到这样的例子。

【问题讨论】:

标签: javascript unit-testing reactjs jestjs enzyme


【解决方案1】:

使用sinon 来跟踪handleClickOutside 是否被调用。顺便说一句,我刚刚发布了我们的项目,我需要在 Nav 组件中进行此单元测试。实际上,当您单击外部时,所有子菜单都应关闭。

import sinon from 'sinon';
import Component from '../src/Component';

it('handle clicking outside', () => {
     const handleClickOutside = sinon.spy(Component.prototype, 'handleClickOutside');
     const wrapper = mount(
         <div> 
           <Component {... props} />
           <div><a class="any-element-outside">Anylink</a></div>
         </div>
      ); 

      wrapper.find('.any-element-outside').last().simulate('click'); 
      expect(handleClickOutside.called).toBeTruthy(); 
      handleClickOutside.restore(); 
})

【讨论】:

  • 据我所知,这个测试 handleClickOutside 已被调用,但不测试它是否包含在 wrapperRef 中的 if 条件?
  • 这行不通。它的编写方式是在组件内部查找存在于组件外部的类。我想你可以导入一个在组件外部的视图中的组件来测试点击,但感觉应该有更好的方法。
【解决方案2】:
import { mount } from 'enzyme'
import React from 'react'
import ReactDOM from 'react-dom'

it('Should not call action on click inside the component', () => {
  const map = {}

  document.addEventListener = jest.fn((event, cb) => {
    map[event] = cb
  })

  const props = {
    actions: {
      something: jest.fn(),
    }
  }

  const wrapper = mount(<Component {... props} />)

  map.mousedown({
    target: ReactDOM.findDOMNode(wrapper.instance()),
  })

  expect(props.actions.something).not.toHaveBeenCalled()
})

github上this酶问题的解决方案。

【讨论】:

  • 不是问反了吗?如何在单元测试中检测到组件外部的点击?
  • 我认为只需将目标更改为document.body 之类的内容即可完成此答案。
【解决方案3】:

选择的答案没有覆盖handleClickOutside的else路径

我在 ref 元素上添加了 mousedown 事件以触发 handleClickOutside 的 else 路径

import { mount } from 'enzyme'
import React from 'react'
import ReactDOM from 'react-dom'

it('Should not call action on click inside the component', () => {
  const map = {}

  document.addEventListener = jest.fn((event, cb) => {
    map[event] = cb
  })

  const props = {
    actions: {
      something: jest.fn(),
    }
  }
  //test if path of handleClickOutside
  const wrapper = mount(<Component {... props} />)

  map.mousedown({
    target: ReactDOM.findDOMNode(wrapper.instance()),
  })

  //test else path of handleClickOutside
  const refWrapper = mount(<RefComponent />)

  map.mousedown({
    target: ReactDOM.findDOMNode(refWrapper.instance()),
  })

  expect(props.actions.something).not.toHaveBeenCalled()
})

【讨论】:

    【解决方案4】:

    我找到了可以避免使用ReactDOM.findDOMNode 的案例/解决方案。处理以下示例:

    import React from 'react';
    import { shallow } from 'enzyme';
    
    const initFireEvent = () => {
      const map = {};
    
      document.addEventListener = jest.fn((event, cb) => {
        map[event] = cb;
      });
    
      document.removeEventListener = jest.fn(event => {
        delete map[event];
      });
    
      return map;
    };
    
    describe('<ClickOutside />', () => {
      const fireEvent = initFireEvent();
      const children = <button type="button">Content</button>;
    
      it('should call actions.something() when clicking outside', () => {
        const props = {
          actions: {
           something: jest.fn(),
         }
        };
    
        const onClick = jest.fn();
    
        mount(<ClickOutside {...props}>{children}</ClickOutside>);
        fireEvent.mousedown({ target: document.body });
    
        expect(props.actions.something).toHaveBeenCalledTimes(1);
      });
    
      it('should NOT call actions.something() when clicking inside', () => {
        const props = {
          actions: {
           something: jest.fn(),
         }
        };
    
        const wrapper = mount(
          <ClickOutside onClick={onClick}>{children}</ClickOutside>,
        );
    
        fireEvent.mousedown({
          target: wrapper.find('button').instance(),
        });
    
        expect(props.actions.something).not.toHaveBeenCalled();
      });
    });
    

    版本:

    "react": "^16.8.6",
    "jest": "^25.1.0",
    "enzyme": "^3.11.0",
    "enzyme-adapter-react-16": "^1.15.2"
    

    【讨论】:

      【解决方案5】:

      最简单的就是dispatchEvent on body

        mount(<MultiTagSelect {...props} />);
      window.document.body.dispatchEvent(new Event('click'));

      【讨论】:

        猜你喜欢
        • 2019-11-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-10-10
        • 2016-07-10
        • 1970-01-01
        相关资源
        最近更新 更多