Search code examples
reactjstestingreact-testing-libraryreact-hooks-testing-library

how to test a hook with async state update in useEffect?


i have a simple hook that fetches the value and sets it to option as follows:

import Fuse from 'fuse.js'
import React from 'react'

// prefetches options and uses fuzzy search to search on that option
// instead of fetching on each keystroke
export function usePrefetchedOptions<T extends {}>(fetcher: () => Promise<T[]>) {
  const [options, setOptions] = React.useState<T[]>([])
  React.useEffect(() => {
    // fetch options initially
    const optionsFetcher = async () => {
      try {
        const data = await fetcher()
        setOptions(data)
      } catch (err) {
        errorSnack(err)
      }
    }
    optionsFetcher()
  }, [])
  // const fuseOptions = {
  //   isCaseSensitive: false,
  //   keys: ['name'],
  // }

  // const fuse = new Fuse(options, fuseOptions)

  // const dataServiceProxy = (options) => (pattern: string) => {
  //   // console.error('options inside proxy call', { options })
  //   const optionsFromSearch = fuse.search(pattern).map((fuzzyResult) => fuzzyResult.item)
  //   return new Promise((resolve) => resolve(pattern === '' ? options : optionsFromSearch))
  // }

  return options
}

i am trying to test it with following code:

import { act, renderHook, waitFor } from '@testing-library/react-hooks'
import { Wrappers } from './test-utils'
import { usePrefetchedOptions } from './usePrefetchedOptions'
import React from 'react'

const setup = ({ fetcher }) => {
  const {
    result: { current },
    waitForNextUpdate,
    ...rest
  } = renderHook(() => usePrefetchedOptions(fetcher), { wrapper: Wrappers })
  return { current, waitForNextUpdate, ...rest }
}

describe('usePrefetchedOptions', () => {
  const mockOptions = [
    {
      value: 'value1',
      text: 'Value one',
    },
    {
      value: 'value2',
      text: 'Value two',
    },
    {
      value: 'value3',
      text: 'Value three',
    },
  ]
  test('searches for appropriate option', async () => {
    const fetcher = jest.fn(() => new Promise((resolve) => resolve(mockOptions)))
    const { rerender, current: options, waitForNextUpdate } = setup({ fetcher })
    await waitFor(() => {
      expect(fetcher).toHaveBeenCalled()
    })
    // async waitForNextUpdate()
    expect(options).toHaveLength(3) // returns initial value of empty options = []
  })
})

the problem is when i am trying to assert the options at the end of the test, it still has the initial value of []. However if I log the value inside the hook, it returns the mockOptions. How do I update the hook after it is update by useEffect but in async manner.

I have also tried using using waitForNextUpdate where it is commented in the code. it times out with following error: Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error:


Solution

  • Couple things, currently you're waiting for fetcher to be called in your tests, but the state update actually happens not after fetcher is called but after the promise that fetcher returns is resolved. So you'd need to wait on the resolution of that promise in your test

    Also, you're destructuring the value of result.current when you first render your hook. That value is just a copy of result.current after that first render and it will not update after that. To query the current value of options, you should query result.current in your assertion instead.

    const fetcherPromise = Promise.resolve(mockOptions);
    const fetch = jest.fn(() => fetcherPromise);
    const { result } = renderHook(() => usePrefetchedOptions(fetcher), { wrappers: Wrappers })
    await act(() => fetcherPromise);
    expect(result.current).toHaveLength(3)