我们的测试间歇性失败,错误为TypeError: Cannot read property 'body' of null
或TypeError: Cannot read property 'createEvent' of null
。
this.setState(...
指向同一行以下是其中一个错误的示例:
FAIL src/views/SettingsView/SettingsView.test.js
● <SettingsView /> component › passes aXe test
TypeError: Cannot read property 'createEvent' of null
54 | this.props.read().then((readSettings) => {
55 | if (readSettings.error) {
> 56 | this.setState({ infoMsg: infoMsg.errorRead });
| ^
57 | } else {
58 | // api data name for destinationDirectory is s3address
59 | this.showOnForm(readSettings.s3address, readSettings.email);
at Object.invokeGuardedCallbackDev (node_modules/react-dom/cjs/react-dom.development.js:111:26)
at invokeGuardedCallback (node_modules/react-dom/cjs/react-dom.development.js:256:31)
at commitPassiveEffects (node_modules/react-dom/cjs/react-dom.development.js:18248:9)
at wrapped (node_modules/scheduler/cjs/scheduler-tracing.development.js:207:34)
at flushPassiveEffects (node_modules/react-dom/cjs/react-dom.development.js:18292:5)
at Object.enqueueSetState (node_modules/react-dom/cjs/react-dom.development.js:11040:5)
at SettingsForm.setState (node_modules/react/cjs/react.development.js:335:16)
at src/views/SettingsView/SettingsForm.js:56:22
(node:99) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'body' of null
(node:99) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)
(node:99) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
这里是测试之一:
import React from 'react';
import { mount } from 'enzyme';
import { axe } from 'jest-axe';
import SettingsForm from './SettingsForm';
import SettingsView from './SettingsView';
describe('<SettingsView /> component', () => {
it('renders correctly <SettingsForm/> sub-component', () => {
const component = mount(<SettingsView />);
expect(component.find(SettingsForm)).toHaveLength(1);
});
it('passes aXe test', async () => {
const wrapper = mount(<SettingsView />);
expect(await axe(wrapper.html())).toHaveNoViolations();
});
});
代码如下:
设置视图:
import React from 'react';
import SettingsForm from './SettingsForm';
import validate from './FormValidator';
import validateSimple from './SimpleFormValidator';
import UploadApi from '../../api/UploadApi';
const SettingsView = () => (
<SettingsForm
read={UploadApi.getSettingsApi}
save={UploadApi.setSettingsApi}
send={UploadApi.uploadSettingsApi}
validate={validate}
validateSimple={validateSimple}
/>
);
export default SettingsView;
设置表格:
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Form, Container } from 'react-bootstrap';
import 'views/SettingsView//style.less';
import BannerMessage from './components/BannerMessage';
import InputLocalDirectory from './components/InputLocalDirectory';
import InputDestinationDirectory from './components/InputDestinationDirectory';
import ButtonSubmit from './components/ButtonSubmit';
import ButtonClear from './components/ButtonClear';
import ButtonSave from './components/ButtonSave';
import SavePrompt from './components/SavePrompt';
import InputEmail from './components/InputEmail';
import InfoBox from './components/InfoBox';
import Title from './components/Title';
import Label from './components/Label';
import infoMsg from './InfoMsg';
const initState = {
// controls the save form button
saveButtonEnabled: false,
// controls the submit form button
submitButtonEnabled: false,
// displays a message beneath the page title
message: 'Send files from your local machine to the server',
// indicates what info to display
infoMsg: infoMsg.noInfo,
// form fields:
inputLocalDirectory: '',
inputDestinationDirectory: '',
inputEmail: '',
};
/**
* Settings form component
*
* Props:
* save - function which saves form fields: destination and email
* send - function which sends/submits form data
* read - function which reads saved form fields: destination and email
* validate - function which validates form fields
* validateSimple - function which enforces mandatory form fields
*
*/
export default class SettingsForm extends Component {
constructor(props) {
super(props);
this.state = initState;
}
componentDidMount() {
// Reads saved form fields and display them on the form.
// Displays read error when an error occurred during read.
this.props.read().then((readSettings) => {
if (readSettings.error) {
this.setState({ infoMsg: infoMsg.errorRead });
} else {
// api data name for destinationDirectory is s3address
this.showOnForm(readSettings.s3address, readSettings.email);
}
});
}
onClearClick = () => {
this.setState(initState);
};
/** Validates and saves form fields
* Displays validation error when an error occurred during validation
* @param {object} event - The event
*/
onSaveClick = async (event) => {
event.preventDefault();
// ! form validation accepts a boolean for disabling local directory selection
const validation = this.validateSaveForm(this.state);
if (validation.valid) {
const saveResponse = await this.props.save({
localDirectory: this.state.inputLocalDirectory,
destinationDirectory: this.state.inputDestinationDirectory,
email: this.state.inputEmail,
});
if (saveResponse.ok) {
this.setState({ saveButtonEnabled: false, infoMsg: infoMsg.successUpdate });
} else {
this.setState({ saveButtonEnabled: true, infoMsg: infoMsg.errorUpdate });
}
} else {
this.setState({ saveButtonEnabled: false, infoMsg: validation.infoMsg });
}
};
/** Validates and submits form fields
* Displays validation error when an error occurred during validation
* @param {object} event - The event
*/
onSubmitClick = async (event) => {
event.preventDefault();
const validation = this.validateForm(this.state);
if (validation.valid) {
const submitResponse = await this.props.send({
localDirectory: this.state.inputLocalDirectory,
destinationDirectory: this.state.inputDestinationDirectory,
email: this.state.inputEmail,
});
if (submitResponse.ok) {
this.setState({ submitButtonEnabled: false, infoMsg: infoMsg.successUpload });
} else {
this.setState({ submitButtonEnabled: true, infoMsg: infoMsg.errorUpdate });
}
} else {
this.setState({ submitButtonEnabled: false, infoMsg: validation.infoMsg });
}
};
showOnForm(s3address, email) {
this.setState({
inputDestinationDirectory: s3address || '',
inputEmail: email || '',
});
}
/** Saves form into the state.
* If all fields have non-blank value enable the apply button
* @param {object} event - The event
*/
handleChange = (event) => {
this.setState({
[event.target.name]: event.target.value,
infoMsg: infoMsg.noInfo,
}, () => {
this.setState({ saveButtonEnabled: this.validateSaveFields(this.state) });
this.setState({ submitButtonEnabled: this.validateBasic(this.state) });
});
};
/**
* Validates form for size and format
* @param {object} form - The form values
*/
validateForm(form) {
return this.props.validate(form.inputDestinationDirectory, form.inputEmail, form.inputLocalDirectory);
}
/**
* Validates form for size and format
* @param {object} form - The form values
*/
validateSaveForm(form) {
return this.props.validate(form.inputDestinationDirectory, form.inputEmail);
}
/**
* Validates that save fields have non-empty values
* @param {object} form - The form values
*/
validateSaveFields(form) {
return this.props.validateSimple([form.inputDestinationDirectory, form.inputEmail]);
}
/**
* Validates that all fields have non-empty values
* @param {object} form - The form values
*/
validateBasic(form) {
return this.props.validateSimple([form.inputEmail, form.inputLocalDirectory, form.inputDestinationDirectory]);
}
render() {
return (
<Container>
<Form id="settings-form" className="simple-form">
<Title text="File Uploader" />
<BannerMessage message={this.state.message} />
<InfoBox variant={this.state.infoMsg.variant} show={this.state.infoMsg.show} text={this.state.infoMsg.text} />
<div className="form-rows">
<Label label="FROM" />
<InputLocalDirectory value={this.state.inputLocalDirectory} onChange={this.handleChange} />
</div>
<div className="form-rows">
<Label label="TO" />
<InputDestinationDirectory value={this.state.inputDestinationDirectory} onChange={this.handleChange} />
</div>
<div className="form-rows">
<Label label="NOTIFY" />
<InputEmail value={this.state.inputEmail} onChange={this.handleChange} />
</div>
<div className="form-rows">
<SavePrompt />
<ButtonSave onClick={this.onSaveClick} enable={this.state.saveButtonEnabled} />
</div>
<div className="form-rows, form-buttons">
<ButtonSubmit onClick={this.onSubmitClick} enable={this.state.submitButtonEnabled} />
<ButtonClear onClick={this.onClearClick} />
</div>
</Form>
</Container>
);
}
}
SettingsForm.propTypes = {
save: PropTypes.func.isRequired,
send: PropTypes.func.isRequired,
read: PropTypes.func.isRequired,
validate: PropTypes.func.isRequired,
validateSimple: PropTypes.func.isRequired,
};
更新:
UploadApi.getSettingsApi
是async
并呼叫fetch()
fetch()
:https://www.npmjs.com/package/jest-fetch-mock it('renders correctly <SettingsForm/> sub-component', () => {
const component = mount(<SettingsView />);
expect(component.find(SettingsForm)).toHaveLength(1);
component.unmount();
});
PASS src/views/SettingsView/SettingsView.test.js
● Console
console.error node_modules/react-dom/cjs/react-dom.development.js:506
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
in SettingsForm (created by SettingsView)
in SettingsView (created by WrapperComponent)
in WrapperComponent
我遇到了https://stackoverflow.com/a/52061655/6134928,但感觉像是一种解决方法。