1

I've written a simple React hook and want to test it with react-hooks-testing-library.

This hook calls an async function once both provider and domain variables, then once it's resolved puts data in state and returns it.

I couldn't find anywhere how to test async hooks but found this: How to test custom async/await hook with react-hooks-testing-library. That question is unanswered but at least I found out that I need to call waitForNextUpdate for testing w/ useEffect.

The problem with my test is that it only runs the hook once (on mount) but not when provider and domain are set. So it always hangs on default value. How do I make it re-trigger the hook to actually call getENS and set the data?

Here's how the hook looks like:

import { useEffect, useState } from 'react'
import { getENS, ResolvedENS } from 'get-ens'
import type { Provider } from '@ethersproject/providers'

const ac = new AbortController()

export const useENS = (provider: Provider, domain: string): ResolvedENS => {
  const [data, set] = useState<ResolvedENS>({ address: null, owner: null, records: { web: {} } })

  useEffect(() => {
    const load = async () => {
      const data = await getENS(provider)(domain, { signal: ac.signal })

      set(data)
    }

    if (provider && domain) load()

    return () => ac.abort()
  }, [domain, provider])

  return data
}

and this is how my test looks like:

import { describe, it, jest } from '@jest/globals'
import { renderHook } from '@testing-library/react-hooks'
import { useENS } from '../packages/use-ens/src/index'
import { providers } from 'ethers'
import fetch from 'fetch-mock'

jest.setTimeout(30000)

describe('use-ens', () => {
  it('should return resolved data for a domain', async () => {
    fetch.mock('*', {
      data: {
        domains: [
          {
            resolvedAddress: { id: '0xf75ed978170dfa5ee3d71d95979a34c91cd7042e' },
            resolver: {
              texts: ['avatar', 'color', 'description', 'email', 'url', 'com.github', 'com.instagram', 'com.twitter']
            },
            owner: { id: '0xf75ed978170dfa5ee3d71d95979a34c91cd7042e' }
          }
        ]
      }
    })

    const provider = new providers.InfuraProvider('homestead', 'INFURA_API_KEY')
    const { result, waitForNextUpdate } = renderHook<[providers.BaseProvider, string], ReturnType<typeof useENS>>(() =>
      useENS(provider, 'foda.eth')
    )

    console.log(result.current)

    await waitForNextUpdate()

    console.log(result.current)
  })
})

when I run the test I get this error:

> NODE_OPTIONS=--experimental-vm-modules pnpx jest tests
  console.log
    { address: null, owner: null, records: { web: {} } }

      at Object.<anonymous> (tests/use-ens.test.ts:30:13)

 FAIL  tests/use-ens.test.ts
  use-ens
    ✕ should return resolved data for a domain (1049 ms)

  ● use-ens › should return resolved data for a domain

    Timed out in waitForNextUpdate after 1000ms.

      30 |     console.log(result.current)
      31 |
    > 32 |     await waitForNextUpdate()
         |     ^
      33 |
      34 |     console.log(result.current)
      35 |   })

      at waitForNextUpdate (node_modules/.pnpm/@testing-library+react-hooks@7.0.1_react-dom@17.0.2+react@17.0.2/node_modules/@testing-library/react-hooks/lib/core/asyncUtils.js:102:13)
      at Object.<anonymous> (tests/use-ens.test.ts:32:5)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        5.215 s
Ran all test suites matching /tests/i.
Jest did not exit one second after the test run has completed.

This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with `--detectOpenHandles` to troubleshoot this issue.
  console.error
    Warning: An update to TestComponent inside a test was not wrapped in act(...).
    
    When testing, code that causes React state updates should be wrapped into act(...):
    
    act(() => {
      /* fire events that update state */
    });
    /* assert on the output */
    
    This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
        at TestComponent (/home/v1rtl/Coding/proj/node_modules/.pnpm/@testing-library+react-hooks@7.0.1_react-dom@17.0.2+react@17.0.2/node_modules/@testing-library/react-hooks/lib/helpers/createTestHarness.js:22:5)

      13 | export const useENS = (provider: Provider, domain: string): ResolvedENS => {
      14 |   const [data, set] = useState<ResolvedENS>({ address: null, owner: null, records: { web: {} } })
    > 15 |
         | ^
      16 |   useEffect(() => {
      17 |     const load = async () => {
      18 |       const data = await getENS(provider)(domain, { signal: ac.signal })

  console.error
    Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
        at TestComponent (/home/v1rtl/Coding/proj/node_modules/.pnpm/@testing-library+react-hooks@7.0.1_react-dom@17.0.2+react@17.0.2/node_modules/@testing-library/react-hooks/lib/helpers/createTestHarness.js:22:5)

      13 | export const useENS = (provider: Provider, domain: string): ResolvedENS => {
      14 |   const [data, set] = useState<ResolvedENS>({ address: null, owner: null, records: { web: {} } })
    > 15 |
         | ^
      16 |   useEffect(() => {
      17 |     const load = async () => {
      18 |       const data = await getENS(provider)(domain, { signal: ac.signal })

      at printWarning (node_modules/.pnpm/react-dom@17.0.2_react@17.0.2/node_modules/react-dom/cjs/react-dom.development.js:67:30)

 ERROR  Test failed. See above for more details.

I have also created a gist containing Jest configuration files: https://gist.github.com/talentlessguy/aad20875b1e4406024e454485a73b1f9

Also here's the source for get-ens library I import from: https://github.com/talentlessguy/get-ens

v1rtl
  • 185
  • 3
  • 8
  • I don't see anything wrong so far. Did you try put breakpoint inside of `useEffect` to check whether it's executed as expected? – skyboyer Aug 21 '21 at 20:11
  • the only thing confuses me - `const ac = new AbortController()` is placed out of the component. So whenever `ac.abort()` is called once, no other hook instance will be able to send request. This should be declared inside of the custom hook - or even better, inside of related `useEffect()` – skyboyer Aug 21 '21 at 20:14
  • @skyboyer, I tried putting a `console.log` inside useEffect and it triggered only once also, here's the logs when abort controller is initialized within useEffect: https://gist.github.com/talentlessguy/82e396dbf9b7ff4da718c0f02b687964 – v1rtl Aug 23 '21 at 13:42

1 Answers1

1

The async/await doesn't properly transpile to execute the useEffect cleanup. I don't know why. I ran into this same problem myself.

If you write it using the Promise style, useEffect cleanup will run properly. Also, you will probably want a new Abort Controller for every useEffect.

Try it like this:

useEffect(() => {
  const ac = new AbortController()
  if (provider && domain) {
    getENS(provider)(domain, { signal: ac.signal }).then((data)=>{
      set(data)
    }
  }
  return () => ac.abort()
}, [domain, provider])
Mary Shaw
  • 79
  • 1
  • 3