Search code examples
reactjsreact-testing-libraryreact-final-formfinal-formtesting-library

Cannot find alert by accessible name


I am writing a test to satisfy the following business rule:

If we select Canada from the country dropdown, show error messages on blur for the province and postal code fields if they are blank.

My React Final Form under test is:

<Form
  onSubmit={onSubmit}
  validate={values => {
    const errors: ValidationErrors = {};

    if (!values.country) {
      errors.country = "Country must be selected";
    }

    if (values.country === "Canada" && !values.province) {
      errors.province = "Province must be provided";
    }

    if (values.country === "Canada" && !values.postalCode) {
      errors.postalCode = "Postal code must be provided";
    }
    return errors;
  }}
  render={({
    ...
  }) => (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="country">Country</label>
        <br />
        <Field<string> id="country" name="country" component="select">
          <option />
          <option value="Canada">Canada</option>
          <option value="US">US</option>
        </Field>
      </div>

      <Condition when="country" is="Canada">
        <div>
          <label htmlFor="province">Province</label>
          <br />
          <Field id="province" name="province" component="input" />
          <Error name="province" />
        </div>
        <div>
          <label htmlFor="postalCode">Postal Code</label>
          <br />
          <Field id="postalCode" name="postalCode" component="input" />
          <Error name="postalCode" />
        </div>
      </Condition>
      ...
    </form>
  )}

The test I am writing looks like this:

describe("Contact form", () => {
test("should show errors if Canada is seledted but province and postal code are blank", async () => {
const { getByLabelText, findByRole } = render(<ContactForm />);

fireEvent.change(getByLabelText("Country"), {
  target: { value: "Canada" }
});

fireEvent.change(getByLabelText("Province"), {
  target: { value: "" }
});

fireEvent.change(getByLabelText("Postal Code"), {
  target: { value: "" }
});

fireEvent.blur(getByLabelText("Postal Code"));

const postalAlert = await findByRole("alert", {
  name: "Postal code must be provided"
});
expect(postalAlert).toBeInTheDocument();

const provinceAlert = await findByRole("alert", {
  name: "Province must be provided"
});
expect(provinceAlert).toBeInTheDocument();

I am attempting to trigger the error messages - which get rendered out with role="alert" - but my test fails with: Unable to find role="alert"

Now, if I remove the name property I am attempting to filter on, then the alert is found:

const postalAlert = await findByRole("alert"); // SUCCESS

I could retrieve the alerts with findAllByRole and iterate over them, but I'd really just like to query for each one explicitly with their accessible name and assert they are in the document.

I see the element when the screen is debugged, I just want to figure out how to query for it directly with its role and name:

<span
      role="alert"
      style="color: rgb(153, 0, 0);"
    >
      Postal code must be provided
</span>

Sample form: https://codesandbox.io/s/react-ts-unit-test-example-6scjc?file=/src/ContactForm.test.tsx


Solution

  • There are some reasons that you can not pass the test

    Seem like span doesn't apply text content to be the accessible name, you can use aria-label to set the accessible name for it, more information here

    export const Error = (props: Props) => {
      const {
        meta: { touched, error }
      } = useField(props.name, { subscription: { touched: true, error: true } });
      return touched && error ? (
        // add aria-label to set accessiable name for span
        <span aria-label={error} role="alert" style={{ color: "#900" }}>
          {error}
        </span>
      ) : null;
    };
    

    You only call blur "Postal Code", but your test include Province error too, the implementation is that you show Province error when you blur Province input, so you need to add that to your test

    import * as React from "react";
    import { render, fireEvent } from "@testing-library/react";
    import { ContactForm } from "./ContactForm";
    
    describe("Contact form", () => {
      test("should show errors if Canada is seledted but province and postal code are blank", async () => {
        const { getByLabelText, findByRole } = render(<ContactForm />);
    
        fireEvent.change(getByLabelText("Country"), {
          target: { value: "Canada" }
        });
    
        fireEvent.change(getByLabelText("Province"), {
          target: { value: "" }
        });
    
        fireEvent.change(getByLabelText("Postal Code"), {
          target: { value: "" }
        });
    
        // you only blur "Postal Code"
        fireEvent.blur(getByLabelText("Postal Code"));
    
        const postalAlert = await findByRole("alert", {
          name: "Postal code must be provided"
        });
        expect(postalAlert).toBeInTheDocument();
        
        // This will not shown because you have not called blur Province
        // comment this or add the line below to pass the test
        // fireEvent.blur(getByLabelText("Province"));
        const provinceAlert = await findByRole("alert", {
          name: "Province must be provided"
        });
        expect(provinceAlert).toBeInTheDocument();
      });
    });
    

    Edit react-ts-unit-test-example