Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I test a change handler for a file-type input in React using Jest/Enzyme?

I want to test whether my React component can use FileReader to import the contents of a user-selected file from an <input type="file"/> element. My code below shows a working component with a broken test.

In my test I'm attempting to use a blob as a substitute for the file because blobs can also be "read" by FileReader. Is that a valid approach? I also suspect that part of the issue is that reader.onload is asynchronous and that my test needs to take this into consideration. Do I need a promise somewhere? Alternatively, do I perhaps need to mock FileReader using jest.fn()?

I would really prefer to only use the standard React stack. In particular I want to use Jest and Enzyme and not have to use, say, Jasmine or Sinon, etc. However if you know something can't be done with Jest/Enzyme but can be done another way, that might also be helpful.

MyComponent.js:

import React from 'react';
class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {fileContents: ''};
        this.changeHandler = this.changeHandler.bind(this);
    }
    changeHandler(evt) {
        const reader = new FileReader();
        reader.onload = () => {
            this.setState({fileContents: reader.result});
            console.log('file contents:', this.state.fileContents);
        };
        reader.readAsText(evt.target.files[0]);
    }
    render() {
        return <input type="file" onChange={this.changeHandler}/>;
    }
}
export default MyComponent;

MyComponent.test.js:

import React from 'react'; import {shallow} from 'enzyme'; import MyComponent from './MyComponent';
it('should test handler', () => {
    const blob = new Blob(['foo'], {type : 'text/plain'});
    shallow(<MyComponent/>).find('input')
        .simulate('change', { target: { files: [ blob ] } });
    expect(this.state('fileContents')).toBe('foo');
});
like image 350
Andrew Willems Avatar asked Jan 17 '17 17:01

Andrew Willems


People also ask

Can you use jest with React?

If you are new to React, we recommend using Create React App. It is ready to use and ships with Jest! You will only need to add react-test-renderer for rendering snapshots.

What is jest and Enzyme?

Jest and Enzyme are tools which are used in tandem to test React components, and are used in the C#Bot testing framework for unit tests of client-side components. While they are used in this context to test react components, Jest is not specific to React, and can be used in other JavaScript applications.


1 Answers

This answers shows how to access all of the different parts of the code using jest. However, it doesn't necessarily mean that one should test all of these parts this way.

The code-under-test is essentially the same as in the question except that I have substituted addEventListener('load', ... for onload = ..., and I have removed the console.log line:

MyComponent.js:

import React from 'react'; class MyComponent extends React.Component {     constructor(props) {         super(props);         this.state = {fileContents: ''};         this.changeHandler = this.changeHandler.bind(this);     }     changeHandler(evt) {         const reader = new FileReader();         reader.addEventListener('load', () => {             this.setState({fileContents: reader.result});         });         reader.readAsText(evt.target.files[0]);     }     render() {         return <input type="file" onChange={this.changeHandler}/>;     } } export default MyComponent; 

I believe I've managed to test just about everything in the code-under-test (with the one exception noted in the comments and discussed further below) with the following:

MyComponent.test.js:

import React from 'react'; import {mount} from 'enzyme'; import MyComponent from './temp01';  it('should test handler', () => {     const componentWrapper   = mount(<MyComponent/>);     const component          = componentWrapper.get(0);     // should the line above use `componentWrapper.instance()` instead?     const fileContents       = 'file contents';     const expectedFinalState = {fileContents: fileContents};     const file               = new Blob([fileContents], {type : 'text/plain'});     const readAsText         = jest.fn();     const addEventListener   = jest.fn((_, evtHandler) => { evtHandler(); });         // WARNING: But read the comment by Drenai for a potentially serious         // problem with the above test of `addEventListener`.     const dummyFileReader    = {addEventListener, readAsText, result: fileContents};     window.FileReader        = jest.fn(() => dummyFileReader);      spyOn(component, 'setState').and.callThrough();     // spyOn(component, 'changeHandler').and.callThrough(); // not yet working      componentWrapper.find('input').simulate('change', {target: {files: [file]}});      expect(FileReader        ).toHaveBeenCalled    (                             );     expect(addEventListener  ).toHaveBeenCalledWith('load', jasmine.any(Function));     expect(readAsText        ).toHaveBeenCalledWith(file                         );     expect(component.setState).toHaveBeenCalledWith(expectedFinalState           );     expect(component.state   ).toEqual             (expectedFinalState           );     // expect(component.changeHandler).toHaveBeenCalled(); // not yet working }); 

The one thing I haven't explicitly tested yet is whether or not changeHandler was called. This seems like it should be easy but for whatever reason it is still eluding me. It clearly has been called, as other mocked functions within it are confirmed to have been called but I haven't yet been able to check whether it itself was called, either using jest.fn() or even Jasmine's spyOn. I have asked this other question on SO to try to address this remaining problem.

like image 102
Andrew Willems Avatar answered Oct 03 '22 04:10

Andrew Willems