因此,我很难使用React光纤门户为模态组件编写测试。因为我的模态安装到<body />
的根目录上的domNode,但由于该domNode不存在,测试失败。
要提供的一些代码,上下文:
的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">×</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;
最后但并非最不重要的是测试: 的 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();
});
});
below,以便您可以看到它的实际效果
答案 0 :(得分:23)
<强> 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();
});
});
一个很小的问题是,酶还没有完全支持反应16,github issue。理论上所有的测试都应该通过,但他们仍然没有解决方案是更改模态的包装器,而不是使用<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>
</div>
);
return ReactDOM.createPortal(ModalMarkup, this.el);
}
您可以找到包含所有代码here
的仓库答案 1 :(得分:4)
可以通过模拟createPortal方法来简单地进行测试。
ReactDOM.createPortal = jest.fn(modal => modal);
let wrapper = shallow(
<Modal visible={true}>Text</Modal>
);
expect(wrapper).toMatchSnapshot();
答案 2 :(得分:1)
对于任何对react测试库有疑问的人,这对我来说都是有效的:
Modal.tsx
const domElement = React.useRef(document.getElementById('modal'));
const jsx = (<Modal>...</Modal>);
return ReactDOM.createPortal(jsx, domElement.current as HTMLElement);
Modal.test.tsx
const element = document.createElement('div');
element.setAttribute('id', 'modal');
element.setAttribute('data-testid', 'modal-test-id');
jest
.spyOn(ReactDOM, 'createPortal')
.mockImplementation((children, c, key) => {
const symbol = Symbol.for('react.portal');
return {
$$typeof: symbol,
key: key == null ? null : '' + key,
children,
containerInfo: element,
implementation: null,
type: symbol.description,
props: null,
} as ReactPortal;
});
我不得不深入研究react-dom库,以了解如何在我的模拟中实现createPortal方法,因为它不允许我只返回任何对象,而必须是ReactPortal对象。
来源
需要使用该符号来确定使用哪种类型的实现来创建元素并为该符号传递帮助。请注意,containerInfo是您在测试中可以使用的元素的传递位置,因此您不必尝试包括整个App模块。