Home > Blockchain >  Test react custom hook with mocking blob response
Test react custom hook with mocking blob response

Time:04-27

I have a created a hook as following:

import { AxiosResponse } from 'axios';
import { DEFAULT_EXPORT_FILE_NAME } from 'constants';
import { useRef, useState } from 'react';

export interface ExportFileProps {
  readonly apiDefinition: () => Promise<AxiosResponse<Blob | string>>;
  readonly preExport: () => void;
  readonly postExport: () => void;
  readonly one rror: () => void;
}

export interface ExportedFileInfo {
  readonly exportHandler: () => Promise<void>;
  readonly ref: React.MutableRefObject<HTMLAnchorElement | null>;
  readonly name: string | undefined;
  readonly url: string | undefined;
}

export const useExportFile = ({
  apiDefinition,
  preExport,
  postExport,
  one rror,
}: ExportFileProps): ExportedFileInfo => {
  const ref = useRef<HTMLAnchorElement | null>(null);
  const [url, setFileUrl] = useState<string>();
  const [name, setFileName] = useState<string>();

  const exportHandler = async () => {
    try {
      preExport();
      const response = await apiDefinition();
      const objectURL = URL.createObjectURL(
        new Blob([response.data], { type: response.headers['content-type'] }),
      );
      setFileUrl(objectURL);
      const fileName =
        response.headers['content-disposition'].match(/filename="(. )"/)[1] ??
        DEFAULT_EXPORT_FILE_NAME;
      setFileName(fileName);
      ref.current?.click();
      postExport();
      if (url) URL.revokeObjectURL(url);
    } catch (error) {
      one rror();
    }
  };

  return { exportHandler, ref, url, name };
};

This hook is used by this compoenent:

interface ExportFileProps {
  readonly apiDefinition: () => Promise<AxiosResponse<Blob | string>>;
}

const ExportFile: React.FC<ExportFileProps> = (props: ExportFileProps): JSX.Element => {
  const { apiDefinition } = props;
  const [buttonState, setButtonState] = useState<ExportButtonState>(ExportButtonState.Primary);

  const preExport = () => setButtonState('loading');
  const postExport = () => setButtonState('primary');


  const one rrorDownloadFile = () => {
    setButtonState('primary');
  };

  const { ref, url, exportHandler, name } = useExportFile({
    apiDefinition,
    preExport,
    postExport,
    one rror: one rrorDownloadFile,
  });

  return (
    <div>
      <Box sx={{ display: 'none' }}>
        <a href={url} download={name} ref={ref}>
          &nbsp;
        </a>
      </Box>
      <ExportButton
        clickHandler={exportHandler}
        buttonState={buttonState}
      >
        Download XLS
      </ExportButton>
    </div>
  );
};

export default ExportFile;

So what this hook does is it creates an anchor element and click it to download the blob response from apiDefinition.

What I'm trying to do is to test this hook, so I need to mock a response with a blob file with headers['content-disposition'] and response.headers['content-type'] defined.

And then test the returned value of exportHandler, ref, url and name.

This is what I tried:

import { renderHook } from '@testing-library/react-hooks';
import { useExportFile } from './useExportFile';

describe('useExportFile', () => {
  const apiDefinition = jest.fn();
  const preExport = jest.fn();
  const postExport = jest.fn();
  const one rror = jest.fn();

  test('is initialized', async () => {
    const { result } = renderHook(() =>
      useExportFile({
        apiDefinition,
        preExport,
        postExport,
        one rror,
      }),
    );

    const exportHandler = result?.current?.exportHandler;
    expect(typeof exportHandler).toBe('function');
  });
});

The issue I'm having is that I don't know how to mock the api call in this case, since it should return a blob with headers defined.

How can I solve this ?

CodePudding user response:

You can try mocking apiDefinition as below:

const blob: Blob = new Blob(['']);

const axiosResponse: AxiosResponse = {
  data: blob,
  status: 200,
  statusText: "OK",
  config: {},
  headers: {
    "content-disposition": 'filename="some-file"',
    "content-type": "application/json"
  }
};

const apiDefinition = () => Promise.resolve(axiosResponse);

Test Code:

import { act } from 'react-dom/test-utils';
import { AxiosResponse } from 'axios';
import { renderHook } from '@testing-library/react-hooks';
import { useExportFile } from './Export';

describe('useExportFile', () => {
  const FILE_NAME = "some-file"
  const URL = "some-url";
  const blob: Blob = new Blob(['']);

  const axiosResponse: AxiosResponse = {
    data: blob,
    status: 200,
    statusText: "OK",
    config: {},
    headers: {
      "content-disposition": `filename="${FILE_NAME}"`,
      "content-type": "text"
    }
  };

  global.URL.createObjectURL = () => URL;
  global.URL.revokeObjectURL = () => null;
  const apiDefinition = () => Promise.resolve(axiosResponse);
  const preExport = jest.fn();
  const postExport = jest.fn();
  const one rror = jest.fn();

  test('is initialized', async () => {
    const { result } = renderHook(() =>
      useExportFile({
        apiDefinition,
        preExport,
        postExport,
        one rror,
      }),
    );

    const exportHandler = result?.current?.exportHandler;
    expect(typeof exportHandler).toBe('function');
    await act(async () => {
      await exportHandler();
    });
    expect(result?.current?.ref).toEqual({ current: null });
    expect(result?.current?.url).toEqual(URL);
    expect(result?.current?.name).toEqual(FILE_NAME);
\  });
});

CodePudding user response:

Since the code is async I would suggest testing it with an async mock of Axios like this one: https://www.npmjs.com/package/axios-mock-adapter

This way the test will be closer to the real world.

You will have to implement the ApiDefinition interface but the test will be more real. Also, you can add delays with such an approach and test retries/timeouts if needed.

  • Related