间歇性测试失败,出现“ TypeError:无法读取属性'body'为null”或'TypeError:无法读取属性'createEvent'为null错误

时间:2020-03-30 18:25:13

标签: reactjs jestjs enzyme

我们的测试间歇性失败,错误为TypeError: Cannot read property 'body' of nullTypeError: Cannot read property 'createEvent' of null

  • 有时所有测试都成功,下次随机测试失败
  • 测试始终在本地运行,但在我们的CI管道中间歇性地失败
  • 只有Ax(可访问性)测试失败
  • 所有失败都与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,
};

更新:

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,但感觉像是一种解决方法。

0 个答案:

没有答案