31

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');
});
Andrew Willems
  • 11,880
  • 10
  • 53
  • 70
  • [This discussion](https://github.com/airbnb/enzyme/issues/426) seems to suggest that using `addEventListener` bypasses React's strategy for handling events and thus isn't really supported by, say, enzyme. – Andrew Willems Jan 24 '17 at 17:58
  • The reason I mentioned `addEventListener` in the first comment was because other sites suggest `addEventListener` might be more testable than `onload`. (Links?) If I understand correctly, [that discussion mentioned in my first comment](https://github.com/airbnb/enzyme/issues/426) suggests some other strategies for testing that I haven't yet got to work, but it states that such possible solutions are beyond the regular use of React/enzyme. It did, however, appear to help at least one person test a `mousemove` event on a component that used `addEventListener` but did not give many details. – Andrew Willems Jan 24 '17 at 18:05

1 Answers1

29

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.

Andrew Willems
  • 11,880
  • 10
  • 53
  • 70
  • `spyOn(component, 'changeHandler').and.callThrough()` does not work because you do not have the instance of the wrapper. if you used `spyOn(componentWrapper.Instance(), 'changeHandler')` it would point to the current instance of the react component's method. An alternative would be to use `spyOn(MyComponent.prototype, 'changeHandler')`. The latter would be placed after `mount` – Joshua Rose Oct 11 '18 at 13:46
  • Hello I need such test but just to check if FileReader has been called but I get an error `expect(jest.fn()).toHaveBeenCalled() Expected mock function to have been called.` any help? – jake-ferguson May 30 '19 at 06:33
  • https://www.pastiebin.com/5cef7ae41e03d here is my code for this test... – jake-ferguson May 30 '19 at 06:41
  • 1
    thanks so much this is what was missing for me, your sample helped me, `.simulate('change', {target: {files: [file]}});` – asotog Dec 20 '19 at 22:43
  • 1
    A major drawback to this test approach is that calling `addEventListener` immediately invokes the handler, before `readAsText` code is reached. We miss the async nature of the code, and test the logic in a sequence that never occurs – Drenai Apr 30 '20 at 13:45
  • @Drenai, thanks so much for your insight. Hopefully your comment will be help others not get tripped up by this issue. My original question hinted at the fact that the asynchronous nature of this code might create problems, and it seems that you have highlighted one such problem. I will leave my answer as is, because I think it still helps in other ways. I may try to fix the problem one day, but for the time being I have inserted a warning into my answer/code regarding the issue raised by your comment. – Andrew Willems May 01 '20 at 15:43
  • I'm testing using testing-library/react instead of enzyme, and the part I needed was `{target: {files: [file]}}` for populating the change event properly. – Tom Jan 27 '22 at 21:29