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>
)
}
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.
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.
📝 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.
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"))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With