Jest 在 React 中测试异步图片上传

Jest Testing an asynchronous image upload in React

我正在尝试测试一组在用户上传文件后异步 运行 的函数。这些功能都在一个名为 UploadImageModal 的 React 组件中。这些函数由 this on change 函数触发:

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);
  }
};

这个 on change 函数然后调用这个 hasMinPicDimensions 函数:

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'));
  });
});

在该函数解析它的 promise 之后,setPicFile,这是一个反应状态更改,它更改组件 src 以具有图像的数据 URI。 这是图片上传输入组件:

<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"
        />

我已经尝试通过仅使用 Jest 来模拟图像上传来测试此功能 - 但无论上面的功能是什么都不会触发,因为 Jest 不会等待承诺解决。

有人对我如何使用 Jest 执行此操作有任何建议吗?如果我也需要用enyzme就可以了。

我们测试组件,模拟组件依赖的其他模块和模块导出的函数。

例如

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();
    });
  });
});

测试结果:

  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