React-Bootstrap Invalid Form Feedback is always visible how to test?

Viewed 2892

I am not able to test for proper validation of a form with React-bootstrap. I want to see that when the input pattern is not valid, the invalid feedback text is displayed once the form is validated.

Working codesandbox with tests: https://codesandbox.io/s/flamboyant-cerf-7t7jq

import React, { useState } from "react";

import { Form, Button, InputGroup } from "react-bootstrap";

export default function App(): JSX.Element {
  const [validated, setValidated] = useState<boolean>(false);

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setValidated(true);
  };

  return (
    <Form
      className="col-12 col-lg-5 trans-form"
      noValidate
      validated={validated}
      onSubmit={handleSubmit}
    >
      <InputGroup className="my-2">
        <InputGroup.Prepend>
          <InputGroup.Text>Receiver Public Key</InputGroup.Text>
        </InputGroup.Prepend>
        <Form.Control
          role="textbox"
          className="text-truncate rounded-right"
          type="text"
          pattern="[A-Za-z0-9]{5}"
          required
        />
        <Form.Control.Feedback
          className="font-weight-bold"
          type="invalid"
          role="alert"
        >
          Length or format are incorrect!
        </Form.Control.Feedback>
      </InputGroup>

      <Button
        role="button"
        className="mt-2 font-weight-bold"
        variant={"primary"}
        type="submit"
        block
      >
        Sign
      </Button>
    </Form>
  );
}

Tests

import React from "react";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom";

import App from "../src/App";

describe("form validation", () => {
  test("invalid receiver public key length", async () => {
    render(<App />);
    userEvent.click(screen.getByRole("button"));
    userEvent.type(screen.getByRole("textbox"), "invalid");
    expect(screen.getByRole("textbox")).toHaveValue("invalid");
    expect(
      await screen.findByText("Length or format are incorrect!")
    ).toBeVisible();
  });

  // this test fails, making it seem like the invalid-feedback is always present
  test("valid receiver public key length", async () => {
    render(<App />);
    userEvent.click(screen.getByRole("button"));
    userEvent.type(screen.getByRole("textbox"), "valid");
    expect(screen.getByRole("textbox")).toHaveValue("valid");
    await waitFor(() => {
      expect(
        screen.queryByText("Length or format are incorrect!")
      ).not.toBeVisible(); // ← FAILS
    });
  });
});

Result

Second test fails with

Test Result

Repository

https://github.com/lbragile/LibraCoin/tree/develop

2 Answers

So it looks as though you are getting this issue due to the use of SCSS for styling and React Testing Library not being able to interpret the underlying styles.

One way of getting around this issue is to introduce a property on the Feedback component (i.e. add an extra level of indirection) to record the result of the validation:

    import React, { useState } from "react";
    
    import { Form, Button, InputGroup } from "react-bootstrap";
    
    export default function App(): JSX.Element {
      const [validated, setValidated] = useState<boolean>(false);
      // Hook to store the result of the validation
      const [validity, setValidity] = useState<boolean>(false);
    
      const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
    
        const form = e.currentTarget;
        // Persist the result of the validation
        setValidity(form.checkValidity());
        setValidated(true);
      };
    
      return (
        <Form
          className="col-12 col-lg-5 trans-form"
          noValidate
          validated={validated}
          onSubmit={handleSubmit}
        >
          <InputGroup className="my-2">
            <InputGroup.Prepend>
              <InputGroup.Text>Receiver Public Key</InputGroup.Text>
            </InputGroup.Prepend>
            <Form.Control
              role="textbox"
              className="text-truncate rounded-right"
              type="text"
              pattern="[A-Za-z0-9]{5}"
              required
            />
            <Form.Control.Feedback
              className="font-weight-bold"
              type="invalid"
              role="alert"
              data-validity={validity}
            >
              Length or format are incorrect!
            </Form.Control.Feedback>
          </InputGroup>
    
          <Button
            role="button"
            className="mt-2 font-weight-bold"
            variant={"primary"}
            type="submit"
            block
          >
            Sign
          </Button>
        </Form>
      );
    }

Once you have this you can then test for a valid validation result as follows:

    test("valid receiver public key length", async () => {
        const { container } = render(<App />);
        userEvent.type(screen.getByRole("textbox"), "valid");
        userEvent.click(screen.getByRole("button"));
        let validationFeedback;
        await waitFor(() => {
          validationFeedback = container.querySelector('[data-validity="true"]');
        });
        expect(validationFeedback).toBeTruthy();
      });

I forked your example and got it working with the above code here.

I ended up using Formik to have the same (but better) functionality which also allowed me to conditionally render the error message:

Updated codesandbox

// App.js
import React from "react";

import * as yup from "yup";
import { Formik, ErrorMessage, Field } from "formik";
import { Form, Button, InputGroup } from "react-bootstrap";

export default function App(): JSX.Element {
  return (
    <Formik
      validationSchema={yup.object().shape({
        from: yup
          .string()
          .matches(/^[A-Za-z0-9]{5}$/, "invalid format")
          .required("from field is required")
      })}
      onSubmit={async (data, { setSubmitting }) => {
        setSubmitting(true);
        alert("submitted: " + data.from);
        setSubmitting(false);
      }}
      initialValues={{ from: "" }}
    >
      {({ handleSubmit, isSubmitting, touched, errors }) => (
        <Form
          className="col-12 col-lg-5 trans-form"
          noValidate
          onSubmit={handleSubmit}
        >
          <InputGroup className="my-2">
            <InputGroup.Prepend>
              <InputGroup.Text>Label</InputGroup.Text>
            </InputGroup.Prepend>
            <Field
              as={Form.Control}
              role="textbox"
              aria-label="from input"
              type="text"
              name="from"
              required
              isInvalid={!!touched.from && !!errors.from}
              isValid={!!touched.from && !errors.from}
            />

            <ErrorMessage
              name="from"
              render={(errorMessage) => (
                <Form.Control.Feedback
                  className="font-weight-bold"
                  type="invalid"
                  role="alert"
                  aria-label="from feedback"
                >
                  {errorMessage}
                </Form.Control.Feedback>
              )}
            />
          </InputGroup>

          <Button
            role="button"
            className="mt-2 font-weight-bold"
            variant={"primary"}
            type="submit"
            block
            disabled={isSubmitting}
          >
            Sign
          </Button>
        </Form>
      )}
    </Formik>
  );
}



// App.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom";

import App from "../src/App";

describe("form validation", () => {
  test("empty", async () => {
    render(<App />);

    const input = screen.getByRole("textbox", { name: /From Input/i });
    input.focus();
    input.blur(); // cause an error

    expect(input).toHaveValue("");

    const alert = await screen.findByRole("alert", { name: /From Feedback/i });
    expect(alert).toBeInTheDocument();
    expect(alert).toHaveTextContent("from field is required");
  });

  test("invalid length", async () => {
    render(<App />);

    const input = screen.getByRole("textbox", { name: /From Input/i });
    const text = "aaaaaa";
    userEvent.type(input, text);
    input.blur(); // cause an error

    expect(input).toHaveValue(text);

    const alert = await screen.findByRole("alert", { name: /From Feedback/i });
    expect(alert).toBeInTheDocument();
    expect(alert).toHaveTextContent("invalid format");
  });

  test("valid length", async () => {
    render(<App />);

    const input = screen.getByRole("textbox", { name: /From Input/i });
    const text = "bbbbb";
    userEvent.type(input, text);
    input.blur();

    expect(input).toHaveValue(text);

    expect(
      screen.queryByRole("alert", { name: /From Feedback/i })
    ).not.toBeInTheDocument();
  });
});
Related