react-hooks-testing-library: Async callback was not invoked within the 5000ms timeout with waitForNextUpdate()

Helo! I could not solve the problem in 8 hours. Need help 😃

Hook:

const useQuery = <T extends Object>(query: string, options: OptionsType = {}): ReturnType<T> => {
  const initialState: InitialStateType<T> = { isFetching: false, data: null, errors: null }
  const [ state, setState ] = useState(initialState)

  const updatedOptions = useDeepMemo(() => ({
    ...options, query
  }), [ options, query ])

  const handleFetch = useCallback(() => {
    const { variables, name = 'noName' } = updatedOptions

    setState({ data: null, errors: null, isFetching: true })

    return request.post(`graphql?name=${name}`, {
      body: {
        query,
        variables,
      },
    })
      .then(
        (data: T) => {
          console.log('RESOLVE ok') // Log for test
          setState(() => ({ data, isFetching: false, errors: null, }))
        },
        ({ errors }) => setState(() => ({ errors, isFetching: false, data: null })),
      )
  }, [ updatedOptions ])

  useEffect(() => {
    handleFetch()
  }, [ updatedOptions ])

  console.log('RENDER hook') // Log for test

  return { ...state, fetch: handleFetch }
}

Test:

import { renderHook } from "@testing-library/react-hooks"

import useQuery from './useQuery'


jest.mock("sb-request", () => ({
  post: jest.fn(async () => new Promise((r) => {
    console.log('POST ok') // Log for test
    setTimeout(() => r('success'), 1000)
  })),
}))

describe("sb-hooks/useQuery", () => {

  it("should return data after fetching", async () => {
    const { result, waitForNextUpdate } = renderHook(() => useQuery('gql query', {
      variables: {},
      name: '1'
    }))

    await waitForNextUpdate();

    expect(result.current.data).toEqual('success');
  })
})

Console screen: https://prnt.sc/puclgu

“@testing-library/react-hooks”: “2.0.3”, (and used 3.2.1) “react-test-renderer”: “16.8.6”, “react”: 16.8.6

No idea why waitForNextUpdate not see render(

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 19 (9 by maintainers)

Most upvoted comments

Actually @JayantDaruvuri, I thought about this some more and the version of react-dom shouldn’t affect your test (although it should match the others in a real app).

I even confirmed my dependency suspicion by removing react-dom altogether and the test still passed after reloading the sandbox:

Screenshot_20191231-122741

Screenshot_20191231-122749

I’m not sure if you just needed to reload your sandbox (I’ve had issue when changing dependency versions) but the test passes for me with the same versions you’ve got specified:

Screenshot_20191231-122850

Screenshot_20191231-122901

@mpeyper I managed to solve the issue, here is my solution:

My custom hook:

import { useEffect, useState } from 'react';
import { preloadImages } from '../utils/preloadImages';

interface ImageDimensions {
  [imageId: string]: {
    width: number;
    height: number;
    imageURL: string;
  };
}

export interface ImageObject {
  imageId: string;
  imageURL: string;
  width?: number;
  height?: number;
}

const getImageDimensions = (images: ImageObject[]) => {
  return images.reduce<ImageDimensions>((finalImages, currentImage) => {
    finalImages[currentImage.imageId] = {
      width: currentImage.width ?? 200,
      height: currentImage.height ?? 200,
      imageURL: currentImage.imageURL,
    };
    return finalImages;
  }, {});
};

export const usePreloadImages = (images: ImageObject[]) => {
  if (!Array.isArray(images)) {
    throw new Error('Please provide an array of images');
  }

  const [loading, setLoading] = useState(true);
  const [imagesDimensions, setImagesDimensions] = useState<ImageDimensions>(
    () => getImageDimensions(images)
  );

  const cacheImages = async () => {
    try {
      const results = await preloadImages(images);
      setImagesDimensions(getImageDimensions(results));
      setLoading(false);
    } catch (err) {
      Promise.resolve(err);
    }
  };

  useEffect(() => {
    cacheImages();
  }, []);

  return { loading, imagesDimensions };
};

My new preloadImages util:

import { ImageObject } from '../hooks/usePreloadImages';

export const preloadImages = async (images: ImageObject[]) => {
  try {
    const promises = images.map((image) => {
      return new Promise(
        (
          resolve: (value: ImageObject) => void,
          reject: (reason: string | Event) => void
        ) => {
          const img = new Image();

          img.src = image.imageURL!;
          img.onload = (e: Event) => {
            resolve({
              imageId: image.imageId,
              width: (e.currentTarget as HTMLImageElement).naturalWidth,
              height: (e.currentTarget as HTMLImageElement).naturalHeight,
              imageURL: image.imageURL,
            });
          };
          img.onerror = (e: string | Event) => {
            reject(e);
          };
        }
      );
    });
    const result = await Promise.all(promises);
    return result;
  } catch (e) {
    throw new Error(e);
  }
};

My (finally) passing tests:

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

import { usePreloadImages } from './usePreloadImages';
import { preloadImages } from '../utils/preloadImages';

jest.mock('../utils/preloadImages');

describe('usePreloadImages', () => {
  beforeEach(() => {
    ((preloadImages as unknown) as jest.Mock).mockReset();
  });
  it('returns loading true and image with default dimensions when promise rejects', async () => {
    await ((preloadImages as unknown) as jest.Mock).mockRejectedValueOnce(
      'Error'
    );
    const { result } = renderHook(() =>
      usePreloadImages([
        {
          imageId: 'landing',
          imageURL: '/landing.jpg',
        },
      ])
    );

    expect(result.current.loading).toBe(true);
    expect(result.current.imagesDimensions).toEqual(
      expect.objectContaining({
        'landing': {
          imageURL: '/landing.jpg',
          width: 200,
          height: 200,
        },
      })
    );
  });

  it('returns loading false when the promise resolves', async () => {
    ((preloadImages as unknown) as jest.Mock).mockResolvedValue([
      {
        imageId: 'landing',
        imageURL: '/landing.jpg',
        width: 350,
        height: 700,
      },
    ]);
    const { result, waitForNextUpdate } = renderHook(() =>
      usePreloadImages([
        {
          imageId: 'landing',
          imageURL: '/landing.jpg',
        },
      ])
    );
    await waitForNextUpdate();
    expect(result.current.loading).toBe(false);
    expect(result.current.imagesDimensions).toEqual(
      expect.objectContaining({
        'proton-mail-landing': {
          imageURL: '/landing.jpg',
          width: 350,
          height: 700,
        },
      })
    );
  });
});

I ended up extracting the new Image() logic into its own util, that I can mock during tests.

~It seems that this can happen with ^16.10.0 and "@testing-library/react-hooks": "^3.2.1",~

Update: turns out I was actually testing a default context that had no value set, so was hitting noops, so it seemed that my updates were not happening, but when I added my wrapper that included the real implementation of the context, my updates were working correctly.

Ok, so wasn’t that bad to track down. I actually got my versions confused as to when we started requiring react@6.19.0 as the minimum peer dependency.

TL;DR; reverting @testing-library/react-hooks to ^1.0.0 also passes the test, but with warnings about updates not wrapped in act (what the react@6.19.0 release enabled us to fix) -> https://codesandbox.io/s/zen-waterfall-blq6f

The longer version isn’t much longer to be honest. In version 2.0.0 we wrapped waitForNextUpdate in the new async act utility which allows you to await the promise and the updates get batched in the same act handler, removing the warning. Unfortunately, the previously supported act(() => {}) calls also return a “promise like” (presumably for debugging purposes if the name in the type definitions mean anything), which is what your tests ends up awaiting (I’m not sure if it never resolves or how the promise like is intended to be used, but it definitely isn’t waiting for your hook to rerender like you want it to.

So your options are to roll react (and friends) forward to ^16.9.0 (recommended) or roll @testing-library/react-hooks backwards to ^1.0.0 and live with the hideous warning it produces (not recommended).