用酶测试 React 门户 [英] Testing React portals with enzyme

查看:20
本文介绍了用酶测试 React 门户的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

所以我很难使用 React Fiber 的门户为模态组件编写测试.因为我的模态挂载到 <body/> 根上的 domNode 但由于该 domNode 不存在,所以测试失败.

提供一些代码,上下文:

index.html

<html lang="zh-cn"><头><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1,shrink-to-fit=no"><meta name="theme-color" content="#000000"><link rel="manifest" href="%PUBLIC_URL%/manifest.json"><link rel="快捷图标" href="%PUBLIC_URL%/favicon.ico"><title>React 应用</title><身体><noscript>您需要启用 JavaScript 才能运行此应用程序.</noscript><div id="modal-root"></div><div id="root"></div>

App.js

import React, { Component } from 'react';从'./logo.svg'导入标志;导入'./App.css';import { Modal, ModalHeader } from './Modal';类 App 扩展组件 {构造函数(道具){超级(道具);this.state = { 显示:假};this.toggleModal = this.toggleModal.bind(this);}切换模态(显示){this.setState({ show: show !== undefined ? show : !this.state.show });}使成为() {返回 (<div className="应用程序"><header className="App-header"><img src={logo} className="App-logo" alt="logo"/><h1 className="App-title">欢迎使用 React</h1></标题><p className="App-intro">首先,编辑 src/App.js;并保存以重新加载.</p><button onClick={() =>this.toggleModal()}>显示模态</button><模态切换={this.toggleModal} show={this.state.show}><ModalHeader><span>我是一个标题</span><button onClick={() =>this.toggleModal(false)}><span aria-hidden="true">&times;</span></ModalHeader><p>模态体!!!</p></模态>

);}}导出默认应用程序;

Modal.js

import React, { Fragment } from 'react';从 'react-dom' 导入 ReactDOM;从 'prop-types' 导入 PropTypes;//下一个组件是样式组件,它们只是添加样式,没有任何逻辑进口 {模态背景,模态内容,模态对话框,模态包装,} 来自'./components';类模态扩展 React.Component {构造函数(道具){超级(道具);this.el = document.createElement('div');this.modalRoot = document.getElementById('modal-root');this.outerClick = this.outerClick.bind(this);}componentDidMount() {this.modalRoot.appendChild(this.el);this.modalRoot.parentNode.style.overflow = '';}componentWillUpdate(nextProps) {如果 (this.props.show !== nextProps.show) {this.modalRoot.parentNode.style.overflow = nextProps.show ?'隐藏':'';}}componentWillUnmount() {this.props.toggle(false);this.modalRoot.removeChild(this.el);}外点击(事件){event.preventDefault();如果 (event.target === event.currentTarget ||event.target.nodeName.toLowerCase() === 'a'){this.props.toggle(false);}}使成为() {const ModalMarkup = (<片段><ModalBackdrop show={this.props.show}/><ModalWrap show={this.props.show} onClick={this.outerClick}><ModalDialog show={this.props.show}><ModalContent>{this.props.children}</ModalContent></ModalDialog></ModalWrap></片段>);返回 ReactDOM.createPortal(ModalMarkup, this.el);}}Modal.defaultProps = {显示:假,切换:() =>{},};Modal.propTypes = {孩子:PropTypes.node.isRequired,显示:PropTypes.bool,切换:PropTypes.func,};导出默认模态;

最后但并非最不重要的测试:Modal.test.js

从'react'导入React;从'./Modal.component'导入模态;进口 {模态背景,模态内容,模态对话框,模态包装,} 来自'./components';描述('模态组件',()=> {const Child = () =><div>Yolo</div>;it('应该渲染所有样式组件和子组件', () => {const 组件 = 安装(<模态><孩子/></模态>);期望(component.find(ModalBackdrop).exists()).toBeTruthy();期望(component.find(ModalWrap).exists()).toBeTruthy();期望(component.find(ModalWrap).contains(ModalDialog)).toBeTruthy();期望(component.find(ModalDialog).contains(ModalContent)).toBeTruthy();期望(component.find(ModalContent).contains(Child)).toBeTruthy();});});

一个 codesandbox,所以你可以看到它的运行

解决方案

所以经过大量的斗争,实验和希望.我设法让测试工作,秘密,在我终于记住这是一种可能性之后很明显,就是修改 jsdom 并添加我们的 domNode,我们就可以每次测试后不要忘记卸载组件.

Modal.test.js

从'react'导入React;从'酶'导入{安装};从'./Modal.component'导入模态;进口 {模态背景,模态内容,模态对话框,模态包装,} 来自'./components';描述('模态组件',()=> {const Child = () =><div>Yolo</div>;让组件;//添加一个带有#modal-root id 的 div 到全局主体const modalRoot = global.document.createElement('div');modalRoot.setAttribute('id', 'modal-root');const body = global.document.querySelector('body');body.appendChild(modalRoot);afterEach(() => {组件卸载();});it('应该渲染所有样式组件和子组件', () => {组件 = 安装(<模态><孩子/></模态>,);期望(component.find(ModalBackdrop).exists()).toBeTruthy();期望(component.find(ModalWrap).exists()).toBeTruthy();期望(component.find(ModalWrap).contains(ModalDialog)).toBeTruthy();期望(component.find(ModalDialog).contains(ModalContent)).toBeTruthy();期望(component.find(ModalContent).contains(Child)).toBeTruthy();});it('点击时应该触发切换', () => {const 切换 = jest.fn();组件 = 安装(<模态切换={toggle}><孩子/></模态>,);component.find(ModalWrap).simulate('click');期望(toggle.mock.calls).toHaveLength(1);期望(toggle.mock.calls[0][0]).toBeFalsy();});it('应该在 id 为 modal-root 的 div 上挂载 modal', () => {const modalRoot = global.document.querySelector('#modal-root');期望(modalRoot.hasChildNodes()).toBeFalsy();组件 = 安装(<模态><孩子/></模态>,);期望(modalRoot.hasChildNodes()).toBeTruthy();});it('应该在卸载时清除 id 为 modal-root 的 div', () => {const modalRoot = global.document.querySelector('#modal-root');组件 = 安装(<模态><孩子/></模态>,);期望(modalRoot.hasChildNodes()).toBeTruthy();组件卸载();期望(modalRoot.hasChildNodes()).toBeFalsy();});it('应该设置溢出隐藏在主体元素上', () => {const body = global.document.querySelector('body');期望(body.style.overflow).toBeFalsy();组件 = 安装(<模态><孩子/></模态>,);component.setProps({ show: true });期望(body.style.overflow).toEqual('隐藏');component.setProps({ show: false });期望(body.style.overflow).toBeFalsy();});});

一件大事,是酶尚未完全支持 react 16,github问题.理论上所有测试都应该通过,但他们仍然失败了解决方案是更改模式上的包装器,而不是使用 <Fragment/> 我们需要使用旧的普通 <div/>

Modal.js 渲染方法:

render() {const ModalMarkup = (<div><ModalBackdrop show={this.props.show}/><ModalWrap show={this.props.show} onClick={this.outerClick}><ModalDialog show={this.props.show}><ModalContent>{this.props.children}</ModalContent></ModalDialog></ModalWrap>

);返回 ReactDOM.createPortal(ModalMarkup, this.el);}

您可以在此处

找到包含所有代码的存储库

So I'm having a hard time writing tests for a modal component using React fiber's portal. Because my modal mounts to a domNode on the root of the <body /> but because that domNode doesn't exist, the test fails.

Some code to give, context:

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
    <title>React App</title>
  </head>
  <body>
    <noscript>
      You need to enable JavaScript to run this app.
    </noscript>
    <div id="modal-root"></div>
    <div id="root"></div>
  </body>
</html>

App.js

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import { Modal, ModalHeader } from './Modal';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = { show: false };
    this.toggleModal = this.toggleModal.bind(this);
  }

  toggleModal(show) {
    this.setState({ show: show !== undefined ? show : !this.state.show });
  }

  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
        <button onClick={() => this.toggleModal()}>show modal</button>
        <Modal toggle={this.toggleModal} show={this.state.show}>
          <ModalHeader>
            <span>I'm a header</span>
            <button onClick={() => this.toggleModal(false)}>
              <span aria-hidden="true">&times;</span>
            </button>
          </ModalHeader>
          <p>Modal Body!!!</p>
        </Modal>
      </div>
    );
  }
}

export default App;

Modal.js

import React, { Fragment } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
// the next components are styled components, they are just for adding style no logic at all
import {
  ModalBackdrop,
  ModalContent,
  ModalDialog,
  ModalWrap,
} from './components';

class Modal extends React.Component {
  constructor(props) {
    super(props);
    this.el = document.createElement('div');
    this.modalRoot = document.getElementById('modal-root');
    this.outerClick = this.outerClick.bind(this);
  }

  componentDidMount() {
    this.modalRoot.appendChild(this.el);
    this.modalRoot.parentNode.style.overflow = '';
  }

  componentWillUpdate(nextProps) {
    if (this.props.show !== nextProps.show) {
      this.modalRoot.parentNode.style.overflow = nextProps.show ? 'hidden' : '';
    }
  }

  componentWillUnmount() {
    this.props.toggle(false);
    this.modalRoot.removeChild(this.el);
  }

  outerClick(event) {
    event.preventDefault();
    if (
      event.target === event.currentTarget ||
      event.target.nodeName.toLowerCase() === 'a'
    ) {
      this.props.toggle(false);
    }
  }

  render() {
    const ModalMarkup = (
      <Fragment>
        <ModalBackdrop show={this.props.show} />
        <ModalWrap show={this.props.show} onClick={this.outerClick}>
          <ModalDialog show={this.props.show}>
            <ModalContent>{this.props.children}</ModalContent>
          </ModalDialog>
        </ModalWrap>
      </Fragment>
    );
    return ReactDOM.createPortal(ModalMarkup, this.el);
  }
}

Modal.defaultProps = {
  show: false,
  toggle: () => {},
};

Modal.propTypes = {
  children: PropTypes.node.isRequired,
  show: PropTypes.bool,
  toggle: PropTypes.func,
};

export default Modal;

And last but not least the test: Modal.test.js

import React from 'react';
import Modal from './Modal.component';
import {
  ModalBackdrop,
  ModalContent,
  ModalDialog,
  ModalWrap,
} from './components';

describe('Modal component', () => {
  const Child = () => <div>Yolo</div>;

  it('should render all the styled components and the children', () => {
    const component = mount(
      <Modal>
        <Child />
      </Modal>
    );
    expect(component.find(ModalBackdrop).exists()).toBeTruthy();
    expect(component.find(ModalWrap).exists()).toBeTruthy();
    expect(component.find(ModalWrap).contains(ModalDialog)).toBeTruthy();
    expect(component.find(ModalDialog).contains(ModalContent)).toBeTruthy();
    expect(component.find(ModalContent).contains(Child)).toBeTruthy();
  });
});

A codesandbox so you can see it in action

解决方案

So after a lot of fighting, experiment and hope. I managed to get the test working, the secret, which is kind obvious after I finally remember that is a possibility, is to modify jsdom and add our domNode, we just can't forget to unmount the component after each test.

Modal.test.js

import React from 'react';
import { mount } from 'enzyme';
import Modal from './Modal.component';
import {
  ModalBackdrop,
  ModalContent,
  ModalDialog,
  ModalWrap,
} from './components';

describe('Modal component', () => {
  const Child = () => <div>Yolo</div>;
  let component;

  // add a div with #modal-root id to the global body
  const modalRoot = global.document.createElement('div');
  modalRoot.setAttribute('id', 'modal-root');
  const body = global.document.querySelector('body');
  body.appendChild(modalRoot);

  afterEach(() => {
    component.unmount();
  });

  it('should render all the styled components and the children', () => {
    component = mount(
      <Modal>
        <Child />
      </Modal>,
    );
    expect(component.find(ModalBackdrop).exists()).toBeTruthy();
    expect(component.find(ModalWrap).exists()).toBeTruthy();
    expect(component.find(ModalWrap).contains(ModalDialog)).toBeTruthy();
    expect(component.find(ModalDialog).contains(ModalContent)).toBeTruthy();
    expect(component.find(ModalContent).contains(Child)).toBeTruthy();
  });

  it('should trigger toggle when clicked', () => {
    const toggle = jest.fn();
    component = mount(
      <Modal toggle={toggle}>
        <Child />
      </Modal>,
    );

    component.find(ModalWrap).simulate('click');
    expect(toggle.mock.calls).toHaveLength(1);
    expect(toggle.mock.calls[0][0]).toBeFalsy();
  });

  it('should mount modal on the div with id modal-root', () => {
    const modalRoot = global.document.querySelector('#modal-root');
    expect(modalRoot.hasChildNodes()).toBeFalsy();

    component = mount(
      <Modal>
        <Child />
      </Modal>,
    );

    expect(modalRoot.hasChildNodes()).toBeTruthy();
  });

  it('should clear the div with id modal-root on unmount', () => {
    const modalRoot = global.document.querySelector('#modal-root');

    component = mount(
      <Modal>
        <Child />
      </Modal>,
    );

    expect(modalRoot.hasChildNodes()).toBeTruthy();
    component.unmount();
    expect(modalRoot.hasChildNodes()).toBeFalsy();
  });

  it('should set overflow hidden on the boddy element', () => {
    const body = global.document.querySelector('body');
    expect(body.style.overflow).toBeFalsy();

    component = mount(
      <Modal>
        <Child />
      </Modal>,
    );

    component.setProps({ show: true });

    expect(body.style.overflow).toEqual('hidden');

    component.setProps({ show: false });
    expect(body.style.overflow).toBeFalsy();
  });
});

One big small thing, is that enzyme doesn't have full support for react 16 yet, github issue. And theoretically all tests should pass, but they were still failing the solution was to change the wrapper on the modal, instead of using <Fragment /> we need to use the old plain <div />

Modal.js render method:

render() {
    const ModalMarkup = (
      <div>
        <ModalBackdrop show={this.props.show} />
        <ModalWrap show={this.props.show} onClick={this.outerClick}>
          <ModalDialog show={this.props.show}>
            <ModalContent>{this.props.children}</ModalContent>
          </ModalDialog>
        </ModalWrap>
      </div>
    );
    return ReactDOM.createPortal(ModalMarkup, this.el);
  }

You can find a repo with all the code here

这篇关于用酶测试 React 门户的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
其他开发最新文章
热门教程
热门工具
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆