Jest Testing an asynchronous image upload in React

Viewed 642

I am trying to test a bunch of functions that asynchronously run after a user uploads a file. These functions are all within a React Component called UploadImageModal. These functions are triggered by this on change function:

const onChange = async () => {
  try {
    const file = document.querySelector('#upload-profile-img-dialog')
      .files[0];
    if (file) {
      const fileData = {
        mimeType: file.type,
        base64: await getBase64(file),
        file
      };

      await hasMinPicDimension(fileData);
      setPicFile(fileData);
    }
  } catch (error_) {
    setError(error_.message);
  }
};

This on change function then calls this hasMinPicDimensions function:

const hasMinPicDimension = ({ mimeType, base64, file }) =>
new Promise((resolve, reject) => {
  const image = new Image();
  image.src = `data:${mimeType};base64,${base64}`;
  image.addEventListener('load', () => {
    if (!['image/gif', 'image/jpeg', 'image/png'].includes(mimeType)) {
      return reject(new Error('Image is not a supported type'));
    }

    if (image.height >= 151 && image.width >= 151) {
      return resolve({ mimeType, base64, file });
    }

    return reject(new Error('Image is too small'));
  });

  image.addEventListener('error', () => {
    reject(new Error('File is not an image'));
  });
});

And after that function resolves it's promise, the setPicFile, which is a react state change that changes a components src to have the data URI of the image. and here is the image upload input component:

<Input
          id="upload-profile-img-dialog"
          name="profileImage"
          type="file"
          accept="image/gif,image/jpeg,image/png"
          onChange={onChange}
          sx={{ visibility: 'hidden' }}
          data-testid="fileinput"
        />

I have tried testing this function by just using Jest to simulate an image upload - but no matter what the functions above do not trigger because Jest does not wait for the promises to resolve.

Does anyone have any advice as to how I can do this with Jest? If I need to use enyzme as well, that will be fine.

1 Answers

We test the component, mock the other modules the component depends on and the functions exported by the module.

E.g.

index.jsx:

import React, { useState } from 'react';
import { getBase64, hasMinPicDimension } from './helpers';

export function UploadImageModal() {
  const [error, setError] = useState('');
  const [picFile, setPicFile] = useState();

  const onChange = async () => {
    try {
      const file = document.querySelector('#upload-profile-img-dialog').files[0];
      if (file) {
        const fileData = {
          mimeType: file.type,
          base64: await getBase64(file),
          file,
        };

        await hasMinPicDimension(fileData);
        setPicFile(fileData);
      }
    } catch (error_) {
      setError(error_.message);
    }
  };

  if (error) return <p>{error}</p>;

  // I use console.log to simulate using picFile
  console.log('picFile: ', picFile);

  return (
    <input
      id="upload-profile-img-dialog"
      name="profileImage"
      type="file"
      accept="image/gif,image/jpeg,image/png"
      onChange={onChange}
      data-testid="fileinput"
    />
  );
}

helpers.js:

export const hasMinPicDimension = async ({ mimeType, base64, file }) =>
  new Promise((resolve, reject) => {
    const image = new Image();
    image.src = `data:${mimeType};base64,${base64}`;
    image.addEventListener('load', () => {
      if (!['image/gif', 'image/jpeg', 'image/png'].includes(mimeType)) {
        return reject(new Error('Image is not a supported type'));
      }

      if (image.height >= 151 && image.width >= 151) {
        return resolve({ mimeType, base64, file });
      }

      return reject(new Error('Image is too small'));
    });

    image.addEventListener('error', () => {
      reject(new Error('File is not an image'));
    });
  });

export async function getBase64(file) {
  console.log('Your getBase64 real implementation');
}

index.test.jsx:

import React from 'react';
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
import { UploadImageModal } from './';
import { getBase64, hasMinPicDimension } from './helpers';

jest.mock('./helpers');

describe('68818166', () => {
  afterEach(() => {
    jest.restoreAllMocks();
  });
  afterAll(() => {
    jest.resetAllMocks();
  });
  test('should upload file', async () => {
    const file = new File(['(⌐□_□)'], 'chucknorris.png', { type: 'image/png' });
    jest.spyOn(document, 'querySelector').mockReturnValueOnce({ files: [file] });
    const logSpy = jest.spyOn(console, 'log');
    getBase64.mockResolvedValueOnce('mocked file base64 string');
    hasMinPicDimension.mockImplementationOnce((file) => file);
    render(<UploadImageModal />);
    fireEvent.change(screen.queryByTestId('fileinput'));
    await waitFor(() =>
      expect(logSpy).toBeCalledWith('picFile: ', {
        mimeType: 'image/png',
        base64: 'mocked file base64 string',
        file,
      })
    );
  });

  test('should handle error if file is invalid', async () => {
    const file = new File(['(⌐□_□)'], 'chucknorris.png', { type: 'image/png' });
    jest.spyOn(document, 'querySelector').mockReturnValueOnce({ files: [file] });
    getBase64.mockResolvedValueOnce('mocked file base64 string');
    hasMinPicDimension.mockRejectedValueOnce(new Error('Image is too small'));
    render(<UploadImageModal />);
    await act(async () => {
      fireEvent.change(screen.queryByTestId('fileinput'));
    });
    await waitFor(() => {
      expect(screen.findByText(/Image is too small/)).toBeTruthy();
    });
  });
});

test result:

  console.log
    picFile:  undefined

      at console.<anonymous> (node_modules/jest-mock/build/index.js:845:25)

  console.log
    picFile:  {
      mimeType: 'image/png',
      base64: 'mocked file base64 string',
      file: File {}
    }

      at console.<anonymous> (node_modules/jest-mock/build/index.js:845:25)

  console.log
    picFile:  undefined

      at UploadImageModal (examples/68818166/index.jsx:29:11)

 PASS  examples/68818166/index.test.jsx
  68818166
    ✓ should upload file (105 ms)
    ✓ should handle error if file is invalid (10 ms)

------------|---------|----------|---------|---------|-------------------
File        | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
------------|---------|----------|---------|---------|-------------------
All files   |   61.76 |       30 |   33.33 |      60 |                   
 helpers.js |   18.75 |        0 |       0 |   14.29 | 2-18,23           
 index.jsx  |     100 |       75 |     100 |     100 | 11                
------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.409 s, estimated 10 s
Related