Search code examples
reactjsreact-hooksjestjsreact-hooks-testing-library

React testing custom hook with window.url


I have a custom hook that downloads a file from an url, it works fine and now I trying to test its behaviour.

const useFileDownload = ({ apiResponse, fileName }) => {
  const ref = useRef(null)
  const [url, setUrl] = useState('')
  const [name, setName] = useState('')

  const download = async () => {
    const { data } = await apiResponse()
    const objectUrl = window.URL.createObjectURL(new Blob([data]))

    setUrl(objectUrl)
    setName(fileName)

    ref.current?.click()
    window.URL.revokeObjectURL(objectUrl)
  }

  return { url, ref, name, download }
}

I call like this on my component

  const { ref, url, download, name } = useFileDownload({
    apiResponse: () => axios.get(pdfUrl, { responseType: 'blob' }),
    fileName: 'my_custom.pdf'
  })
...
...
   // this stay hidden on my component
   <a href={url} download={name} ref={ref} />

And my test

describe('useFileDownload', () => {
  it('should create refs', async () => {
    window.URL.createObjectURL = jest.fn()
    const mockRequest = jest.fn().mockResolvedValue({ data: 'url/to/pdf' })
    const { result } = renderHook(() => useFileDownload({ apiResponse: mockRequest, fileName: 'sample.pdf' }))

    act(() => {
      result.current.download()
    })

    expect(result.current.name).toBe('sample.pdf')
    expect(mockRequest).toBeCalledTimes(1)
  })
})

I'm trying to mock createObjectURL, but it doesn't seem to work, and I don't know if its the right way. If the line containing window.URL fails, then the rest of the code fails on the assertion too.


Solution

  • There are two ways:

    Option 1. Using asynchronous version act() function

    Option 2. Using waitForNextUpdate(), see example

    Sometimes, a hook can trigger asynchronous updates that will not be immediately reflected in the result.current value. Luckily, renderHook returns some utilities that allow the test to wait for the hook to update using async/await (or just promise callbacks if you prefer). The most basic async utility is called waitForNextUpdate.

    import { act, renderHook } from '@testing-library/react-hooks';
    import { useFileDownload } from './useFileDownload';
    
    describe('useFileDownload', () => {
      it('should create refs', async () => {
        window.URL.createObjectURL = jest.fn();
        window.URL.revokeObjectURL = jest.fn();
        const mockRequest = jest.fn().mockResolvedValue({ data: 'url/to/pdf' });
        const { result, waitForNextUpdate } = renderHook(() => useFileDownload({ apiResponse: mockRequest, fileName: 'sample.pdf' }));
    
        // option 1
        // await act(async () => {
        //   await result.current.download();
        // });
    
        // option 2
        result.current.download();
        await waitForNextUpdate();
    
        expect(result.current.name).toBe('sample.pdf');
        expect(mockRequest).toBeCalledTimes(1);
      });
    });
    

    Test result:

     PASS  stackoverflow/75393013/useFileDownload.test.ts (10.519 s)
      useFileDownload
        ✓ should create refs (21 ms)
    
    --------------------|---------|----------|---------|---------|-------------------
    File                | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
    --------------------|---------|----------|---------|---------|-------------------
    All files           |     100 |       50 |     100 |     100 |                   
     useFileDownload.ts |     100 |       50 |     100 |     100 | 15                
    --------------------|---------|----------|---------|---------|-------------------
    Test Suites: 1 passed, 1 total
    Tests:       1 passed, 1 total
    Snapshots:   0 total
    Time:        11.205 s
    

    package version:

    "jest": "^26.6.3",
    "react": "^16.14.0",
    "@testing-library/react-hooks": "^8.0.1"
    

    jest.config.js:

    module.exports = {
      preset: 'ts-jest/presets/js-with-ts',
      testEnvironment: 'jsdom',
    }