Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to ensure a react-testing-library test waits for a completed promise chain?

In my login form, a successful login initiates a promise chain that ends with the user being redirected to the home screen. In the test below, I hope to ensure my login works by capturing that final step.

I have log statements within the code that tell me every step in the promise chain is executed as I expect, but the assertion still fails. It's clear from my logging that the test completes before the promise chain executes.

I think this might be complicated by the behavior of Formik, which I'm using in my actual form. I have not been able to successfully query and wait for the spinner that is displayed while the login is under way.

I'm at a loss as to how to get this test to wait until the navigation occurs. What promise resolution could be triggering waitFor to complete?

import { act, render, screen, waitFor } from "@testing-library/react"
import userEvent from "@testing-library/user-event"

import React from "react"

import { AuthProvider } from "context/auth-context"
import { rest } from "msw"
import { setupServer } from "msw/node"
import { MemoryRouter as Router } from "react-router-dom"
import { LoginScreen } from "screens/login"
import { handlers } from "test/auth-handlers"
import { buildLoginForm } from "test/generate/auth"
import { deferred } from "test/test-utils"

const Wrapper = ({ children }) => (
  <Router>
    <AuthProvider>{children}</AuthProvider>
  </Router>
)
const serverURL = process.env.REACT_APP_SERVER_URL
const server = setupServer(...handlers)

const mockNav = jest.fn()
jest.mock("react-router-dom", () => ({
  ...jest.requireActual("react-router-dom"),
  useNavigate: () => mockNav,
}))

beforeAll(() => {
  server.listen()
})
afterAll(() => server.close())
afterEach(() => {
  server.resetHandlers()
  jest.clearAllMocks()
})

test("successful login", async () => {
  const { promise, resolve } = deferred()

  render(
    <Wrapper>
      <LoginScreen />
    </Wrapper>,
  )

  expect(screen.getByLabelText(/loading/i)).toBeInTheDocument()

  await act(() => {
    resolve()
    return promise
  })

  const { email, password } = buildLoginForm()

  userEvent.type(screen.getByRole("textbox", { name: /email/i }), email)
  userEvent.type(screen.getByLabelText(/password/i), password)
  userEvent.click(screen.getByRole("button"))

  await waitFor(expect(mockNav).toHaveBeenCalledWith("home"))
})

The login form:

function LoginForm({ onSubmit }) {
  const { isError, isLoading, error, run } = useAsync()

  function handleSubmit(values) {
    // any 400 or 500 is displayed to the user
    run(onSubmit(values)).catch(() => {})
  }

  return (
    <Formik
      initialValues={{ email: "", password: "" }}
      validationSchema={Yup.object({
        email: Yup.string().email("Invalid email address").required("A valid email is required"),
        password: Yup.string().required("Password is required"),
      })}
      onSubmit={(values) => handleSubmit(values)}
    >
      <Form>
        <FormGroup name="email" type="text" label="Email" />
        <FormGroup name="password" type="password" label="Password" />
        <IconSubmitButton loading={isLoading} color="green">
          <MdArrowForward style={{ marginTop: ".6rem" }} />
        </IconSubmitButton>
        {isError ? <ErrorDisplay error={error} /> : null}
      </Form>
    </Formik>
  )
}
like image 226
Stuart Avatar asked Nov 08 '20 14:11

Stuart


People also ask

Is React testing library end-to-end?

What is end-to-end testing in React? End-to-end tests (E2E) simulate actual user actions and are designed to test how a real user would likely use the application. React E2E testing helps ensure that the code you wrote is functional and your app works as intended.

How do you use waitFor in React test library?

In order to use this function, we need to import it from @testing-library/react . import { waitFor } from '@testing-library/react'; As with the other async functions, the waitFor() function returns a Promise, so we have to preface its call with the await keyword.

Is fireEvent asynchronous?

📝 fireEvent is an async method when imported from @testing-library/svelte . This is because it calls tick which tells Svelte to apply any new changes to the DOM.


1 Answers

waitFor is unaware of promises or other implementation details, it works by polling provided assertion in specified intervals until an assertion passes or a timeout occurs.

waitFor works similarly to toThrow in terms of error handling. There's no way how it could catch errors and evaluate an assertion multiple times when it's specified as an argument, expect is called once, throws an error and fails the test:

  await waitFor(expect(mockNav).toHaveBeenCalledWith("home"))

The only way waitFor can work is when it's provided with a function that can be wrapped with try..catch internally and executed multiple times. A correct way to do this is:

  await waitFor(() => expect(mockNav).toHaveBeenCalledWith("home"))
like image 150
Estus Flask Avatar answered Oct 24 '22 10:10

Estus Flask