React Testing Library can't see result of useState update function

Viewed 641

The problem:

I have a react component with a simple form and an area to display output on submit.

I have manually tested and confirmed that submitting the form outputs the values as expected. However I cannot get React Testing Library to make the same assertion.

From what I can gather it doesn't seem to be seeing the updated page content.

Here's the output of the test failure:

 ● User should be able to submit their first name

    expect(element).toHaveTextContent()

    Expected element to have text content:
      firstName: Matthew
    Received:
      firstName:

      47 |
      48 |     expect(output.firstName).toHaveTextContent("firstName:");
    > 49 |     expect(output.firstName).toHaveTextContent("firstName: Matthew");
         |                              ^
      50 | });
      51 |
      52 | /*

Here is the component:

import React, { useState } from "react";
import './Playground.css';

const Playground = ({
    firstName,
    lastName,
    email,
    subject,
    message
}) => {

    let [myState, setMyState] = useState({
        firstName: firstName ? firstName : "",
        lastName: lastName ? lastName : "",
        email: email ? email : "",
        subject: subject ? subject : "",
        message: message ? message : ""
    });

    function submit(event) {
        event.preventDefault();
        const form = event.target;

        setMyState({
            ...myState,
            firstName: form?.firstName?.value   ? form?.firstName?.value : myState.firstName,
            lastName:  form?.lastName?.value    ? form.lastName?.value   : myState.lastName,
            email:     form?.email?.value       ? form?.email?.value     : myState.email,
            subject:   form?.subject?.value     ? form?.subject?.value   : myState.subject,
            message:   form?.message?.value     ? form?.message?.value   : myState.message
        });
    }

    return (
        <section data-testid="playground">
            <h1>Form</h1>

            <section data-testid="input">
                <form onSubmit={submit}>
                    <label htmlFor="firstName">First Name</label>
                    <input type="text" id="firstName" placeholder="Jane" />

                    <label htmlFor="lastName">Last Name</label>
                    <input type="text" id="lastName" placeholder="Doe" />

                    <label htmlFor="email">Email</label>
                    <input type="text" id="email" placeholder="jane.doe@example.com" />

                    <label htmlFor="subject">Subject</label>
                    <input type="text" id="subject" placeholder="Subject" />

                    <label htmlFor="message">Message</label>
                    <textarea rows="3" id="message" type="text" placeholder="Message"></textarea>

                    <button type="submit" name="update">Update</button>
                </form>
            </section>

            <section data-testid="output">
                <div>firstName: {myState.firstName}</div>
                <div>lastName: {myState.lastName}</div>
                <div>email: {myState.email}</div>
                <div>subject: {myState.subject}</div>
                <div>message: {myState.message}</div>
            </section>
        </section>
    );
};

export default Playground;

Here is the test in question (which fails on the assertion on line 49):

import React from 'react';
import {render, screen} from "@testing-library/react";
import userEvent from '@testing-library/user-event';
import Playground from "./Playground";

function getFormFields (screen) {
    const formFields = {};

    formFields.firstName = screen.getByLabelText(/first name/i);
    formFields.lastName = screen.getByLabelText(/last name/i);
    formFields.email = screen.getByLabelText(/email/i);
    formFields.subject = screen.getByLabelText(/subject/i);
    formFields.message = screen.getByLabelText(/^message$/i);

    return formFields;
}

function getOutput (screen) {
    const output = {};

    output.firstName = screen.getByText(/^firstname:/i);
    output.lastName = screen.getByText(/^lastname:/i);
    output.email = screen.getByText(/^email:/i);
    output.subject = screen.getByText(/^subject:/i);
    output.message = screen.getByText(/^message:/i);

    return output;
}

test('Should have an editable First Name field', () => {
    render(<Playground />)
    const fields = getFormFields(screen);

    userEvent.type(fields.firstName, "Matthew");

    expect(fields.firstName).toHaveValue("Matthew");
});

test('User should be able to submit their first name', async () => {
    render(<Playground />)
    const fields = getFormFields(screen),
          submit = screen.getByRole('button', { name: /update/i }),
          output = getOutput(screen);

    userEvent.type(fields.firstName, "Matthew");
    userEvent.click(submit);

    expect(output.firstName).toHaveTextContent("firstName:");
    expect(output.firstName).toHaveTextContent("Matthew");
});

Things I've tried

  • Using findBy instead of getBy for text content
  • Declaring the target element for the assertion after the update click has been performed
  • Using await
  • Lots of other stuff I've now forgotten

Final thought

This is driving me bonkers as it seems like a really simple issue (I'm not exactly doing rocket surgery). I'll be really grateful for any help I can get!

Thanks!

1 Answers

You need to wait for the UI to change after an event (such as click):

import { render, screen, waitFor } from '@testing-library/react';

...

userEvent.click(submit);

await waitFor(() => {
    // The first assertion may not be necessary, as it never changes?
    // And it's not recommended to have multiple assertions in waitFor.
    expect(output.firstName).toHaveTextContent("firstName:");
    expect(output.firstName).toHaveTextContent("Matthew");
});

Edit

There is another problem, you need to change your submit handler:

function submit(event) {
    event.preventDefault();
    const form = event.target.elements; // <-- note that it uses 'elements'
    ...

Interestngly enough, it worked in "real life", but not in testing context. But with elements it will work in both cases.

You can see this MDN article for more info.

Related