6

I'm trying to test an input type=file component, using the react testing-library.

The component is a standard <input> element, with the following function to handle an image submission:

export default function ImageUpload(props) {
  const { image, setImage } = props;
  const handleImageChange = e => {
    e.preventDefault();
    let reader = new FileReader();
    const imageFile = e.target.files[0];
    reader.onloadend = () => {
      const image = reader.result;
      setImage(image);
    };
    reader.readAsDataURL(imageFile);
  };
  // etc.
}

As I wanted to simulate the uploading of an image, I went about testing it this way:

test("ImageUpload shows two buttons after an image has been uploaded", () => {
    const setImageSpy = jest.fn();
    const image = "data:image/jpeg;base64,/9j/4AAQSkZJ//20==";
    const file = new File([image], "chucknorris.jpg", { type: "image/jpeg" });

    const readAsDataURL = jest.fn();
    const onloadend = jest.fn();
    jest.spyOn(global, "FileReader")
      .mockImplementation(function() {
        this.readAsDataURL = readAsDataURL;
        this.onloadend = onloadend;
      });

    const { getByTestId } = render(
      <ImageUpload image={image} setImage={setImageSpy} />
    );
    fireEvent.change(getByTestId("ImageUpload"), {
      target: {
        files: [file]
      }
    });
    expect(setImageSpy).toHaveBeenCalledWith(image);  // this fails
    expect(readAsDataURL).toHaveBeenCalledTimes(1);
    expect(readAsDataURL).toHaveBeenCalledWith(file);
  });

The problem is that setImageSpy never gets called. If I understand it correctly, this is because onloadend never gets triggered.

How can I fire that event?

skyboyer
  • 22,209
  • 7
  • 57
  • 64
Jir
  • 2,985
  • 8
  • 44
  • 66
  • `readAsDataURL` will be called with the actual `File` and not the base64 one.. so you need to change the expect accordingly. For `setImageSpy` to be called, not sure but you probably need to mock `onloadend` along with `readAsDataURL`? – tanmay Jul 06 '20 at 11:54
  • Good point on `readAsDataURL`: however, the main problem is that it gets called but ... with *no arguments*. I've tried mocking out `onloadend` but the result is the same unfortunately. – Jir Jul 06 '20 at 12:43
  • Not sure yet about your jest test, but in your actual code you shouldn't be using a FileReader here, create a blob:// URL from the File using URL.createObjectURL(blob) – Kaiido Jul 06 '20 at 22:47
  • Thanks for the tip @Kaiido! I wasn't aware of `createObjectURL()` and I've just finished reading about the difference in [another answer here on SO](https://stackoverflow.com/a/31743665/222529). I think you're right, the one you propose is the neater way to go. – Jir Jul 07 '20 at 05:22

1 Answers1

8

According to the expected behaviour, readAsDataURL mock is supposed to provide result rather than being a stub.

this.onloadend = onloadend is a step in wrong direction. onloadend shouldn't be mocked because it's assigned in tested code. It needs to be manually called in the test:

jest.spyOn(global, "FileReader")
  .mockImplementation(function() {
    this.readAsDataURL = jest.fn(() => this.result = image);
  });

...

expect(FileReader).toHaveBeenCalledTimes(1);

const reader = FileReader.mock.instances[0];

expect(reader.readAsDataURL).toHaveBeenCalledTimes(1);
expect(reader.readAsDataURL).toHaveBeenCalledWith(file);
expect(reader.onloadend).toEqual(expect.any(Function));

expect(setImageSpy).not.toHaveBeenCalled();

act(() => reader.onloadend());

expect(setImageSpy).toHaveBeenCalledWith(image);
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • Good point on not mocking `onloadend`. Your solution is almost correct: there are just two things that I'm not sure about. First, the mock for readAsDataURL shouldn't be `image => this.result = image` otherwise it'll store in `result` the argument passed (the file) - I fixed it in your code. Second, once I run the test I get the following error: `console.error node_modules/react-dom/cjs/react-dom.development.js:545` `Warning: An update to ImageUpload inside a test was not wrapped in act(...).` What could this be due to? – Jir Jul 13 '20 at 21:21
  • 1
    Thanks for the fix, indeed, this was a mistake. As for the error, it makes sense, RTL uses act internally for its own functions but since the state was updated without using its API, it needs to be wrapped with act. – Estus Flask Jul 13 '20 at 22:03
  • I see! Just perfect. Thanks! Can't assign the bounty points just yet for some reason. Will do as soon as SO allows me (~5 hrs). – Jir Jul 14 '20 at 05:05