Skip to content

Promise handling in waitFor #686

Open
@mpeyper

Description

@mpeyper

Describe the feature you'd like:

This feature has been mentioned previously in #631 and in discord

The ability to use a Promise with waitFor to wrap async behaviour in act . This is inline with waitFor from @testing-library/dom, but I'm also thinking it would be great to support all of the following:

// currently supported

await waitFor(() => {
  expect(actual).toBe(expected)
})

await waitFor(() => booleanValue)

// proposed new values supported by `@testing-library/dom`

await waitFor(new Promise<void>(resolve => setTimeout(resolve, 100)))

// proposed new values not supported by `@testing-library/dom`

await waitFor( new Promise<boolean>(resolve => setTimeout(() => resolve(true), 100))) // would repeat if resolved to `false`

await waitFor(() => new Promise<void>(resolve => setTimeout(resolve, 100)))

await waitFor(async () => {
  await new Promise<void>(resolve => setTimeout(resolve, 100))
})

await waitFor(() => new Promise<boolean>(resolve => setTimeout(() => resolve(true), 100))) // would repeat if resolved to `false`

await waitFor(async () => {
  await new Promise<void>(resolve => setTimeout(resolve, 100))
  return true // would repeat if returning to `false`
})

Essentially the waitFor would look something like this:

function waitFor(callback: (() => boolean | void | Promise<boolean | void>) | Promise<boolean | void>, options?: WaitOptions) {
 // ...
}

The reason I want to support returning promises from the callback as well as instead of the callback is so that we can ensure any state updates that happen before the first await in a async function occur within the act block, e.g.

function useAuth() {
  const [user, setUser] = useState<User | null>(null)
  const [isAuthenticated, setIsAuthenticated] = useState(false)

  const signIn = async ({ email, password }:{ email: string; password: string; }) => {
    const user = await login(email, password)

    setUser(user)
    setIsAuthenticated(true)
  }
  
  const signOut = async () => {
    setUser(null)
    setIsAuthenticated(false)

    await logout(user)
  }

  return { user, isAuthenticated, signIn, signOut }
}
test('example', async () => {
  const { result, waitFor } = renderHook(() => useAuth());

  await waitFor(result.current.signIn({ email: '[email protected]', password: 'valid-password' }))

  expect(result.current.user).toBeDefined();
  expect(result.current.isAuthenticated).toEqual(true);

  // If we just pass `await waitFor(result.current.signOut())` here, there is an `act` warning because
  // `setUser` and `setIsAuthenticated` are called synchronously before the promise is returned
  await waitFor(() => result.current.signOut())

  expect(result.current.user).toEqual(null);
  expect(result.current.isAuthenticated).toEqual(false);
})

Suggested implementation:

No idea yet. It's probably more complicated than I think it is right now.

Describe alternatives you've considered:

None.

Teachability, Documentation, Adoption, Migration Strategy:

Updating the API docs and possibly adding more examples in the async hooks section. There is also another ticket to take a more holistic look at the async docs already (#625).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestrequest for commentDiscussion about changes to the library

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions