6

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

Ben Smith
  • 19,589
  • 6
  • 65
  • 93
lbragile
  • 7,549
  • 3
  • 27
  • 64
  • Rather than firing events directly, have you considered simulating actual valid/invalid input with https://testing-library.com/docs/ecosystem-user-event? – jonrsharpe Jun 17 '21 at 07:56
  • Yes, I have, you can see that in the code-sandbox. Same result though unfortunately. I even made sure that the input actually changed value prior to asserting visibility of feedback – lbragile Jun 17 '21 at 14:42
  • Does this help answer your question: [Cannot check expect(elm).not.toBeVisible() for semantic-ui react component](https://stackoverflow.com/questions/52813527/cannot-check-expectelm-not-tobevisible-for-semantic-ui-react-component)? – juliomalves Jun 19 '21 at 23:46
  • It might be correct, but I cannot seem to understand what can be done. I have a feeling this has something to do with stubbing of `.css` and `.scss` files in the `moduleNameMapper` of my `jest.config.js` I added my repository to the description in case it can provide further insight. – lbragile Jun 20 '21 at 00:50

2 Answers2

3

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.

Ben Smith
  • 19,589
  • 6
  • 65
  • 93
  • 1
    Thank you for the awesome advice, I actually ended up doing just this with `Formik` (see my answer above). Would a fix also be using style-components with bootstrap? I think the issue occurs from not being able to "map" bootstrap to the underlying css in the testing environment. However, it seems like `style-components` takes style into account - just not sure how it works with bootstrap still. – lbragile Jun 22 '21 at 04:36
  • Also, how would you apply your solution to multi-input forms. Obviously my MWE contains just one input, but in an application one would have many inputs. In that case you would need to check validity of inputs using a custom function - or schema - which is where Formik/Yup can be really useful. – lbragile Jun 23 '21 at 01:04
0

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();
  });
});
lbragile
  • 7,549
  • 3
  • 27
  • 64
  • 2
    In this answer you are using a completely different framework to what's in your question. People will come to this question looking for a solution to the question which uses react bootstrap not formik. – Ben Smith Jun 22 '21 at 08:01
  • This does use react-bootstrap with Formik. I went with an approach that fixed the issue for me in a manner that I find suitable. Others might find my solution to be a great alternative if they face the same problem. That being said, your approach (which I saw after implementing my solution) is also acceptable - although it probably won’t scale nicely to forms with more than one input. The core of the question is answered by the fact that styling information isn’t actually read in during testing, only class names are (unless you use styled components). – lbragile Jun 23 '21 at 00:59