Skip to the code
cf.

Sign-up form with validation

MediumForms

Build App: the sign-up form for a small tea shop's online club. It has three fields, labelled Email, Password, and Confirm password, and a Sign up button.

The rules. The email has some text, an @, more text, a dot, and more text, with no spaces: asha@tea.shop passes, while asha, asha@tea, and @tea.shop don't. The password has at least 8 characters. Confirm password is exactly the same as Password.

When a field breaks its rule, its message is Enter a valid email., Password must be at least 8 characters., or Passwords do not match.

Don't nag people while they're still typing: a field's message shows only after they have left that field once (it lost focus). From then on the message follows what's in the fields, so it goes away the moment the field is fixed, and Confirm password's message also follows changes to Password. While a field's message shows, the field has aria-invalid="true".

Sign up is disabled until all three fields are valid. Submitting the form, by clicking Sign up or pressing Enter in a field, replaces the form with Signed up as asha@tea.shop., using the email that was typed.

<form>
  <label for="email">Email</label>
  <input id="email" type="email" aria-invalid="true" />
  <p>Enter a valid email.</p>
  <!-- Password and Confirm password the same way -->
  <button disabled>Sign up</button>
</form>

What the tests check

  1. Sign up stays disabled until all three fields are valid
    render(<App />);
    const button = screen.getByRole("button", { name: "Sign up" });
    expect(button).toBeDisabled();
    await user.type(screen.getByLabelText("Email"), "asha@tea.shop");
    await user.type(screen.getByLabelText("Password"), "oolong12");
    expect(button).toBeDisabled();
    await user.type(screen.getByLabelText("Confirm password"), "oolong12");
    expect(button).toBeEnabled();
    
  2. shows no message while you're still typing in a field
    render(<App />);
    await user.type(screen.getByLabelText("Email"), "asha");
    expect(screen.queryByText("Enter a valid email.")).not.toBeInTheDocument();
    expect(screen.getByLabelText("Email")).not.toHaveAttribute("aria-invalid", "true");
    
  3. shows a field's message once you leave it, and marks the field aria-invalid
    render(<App />);
    const email = screen.getByLabelText("Email");
    await user.type(email, "asha@tea");
    await user.tab();
    expect(screen.getByText("Enter a valid email.")).toBeInTheDocument();
    expect(email).toHaveAttribute("aria-invalid", "true");
    
  4. leaving an empty field shows only that field's message
    render(<App />);
    await user.click(screen.getByLabelText("Password"));
    await user.tab();
    expect(screen.getByText("Password must be at least 8 characters.")).toBeInTheDocument();
    expect(screen.queryByText("Enter a valid email.")).not.toBeInTheDocument();
    
  5. an email needs text before the @
    render(<App />);
    await user.type(screen.getByLabelText("Email"), "@tea.shop");
    await user.tab();
    expect(screen.getByText("Enter a valid email.")).toBeInTheDocument();
    
  6. a message goes away as soon as the field is fixed
    render(<App />);
    const email = screen.getByLabelText("Email");
    await user.type(email, "asha@tea");
    await user.tab();
    expect(screen.getByText("Enter a valid email.")).toBeInTheDocument();
    await user.type(email, ".shop");
    expect(screen.queryByText("Enter a valid email.")).not.toBeInTheDocument();
    expect(email).not.toHaveAttribute("aria-invalid", "true");
    
  7. Confirm password is checked again when Password changes
    render(<App />);
    const password = screen.getByLabelText("Password");
    await user.type(password, "oolong12");
    await user.type(screen.getByLabelText("Confirm password"), "oolong12");
    await user.tab();
    expect(screen.queryByText("Passwords do not match.")).not.toBeInTheDocument();
    await user.type(password, "3");
    expect(screen.getByText("Passwords do not match.")).toBeInTheDocument();
    
  8. signing up replaces the form with the email
    render(<App />);
    await user.type(screen.getByLabelText("Email"), "asha@tea.shop");
    await user.type(screen.getByLabelText("Password"), "oolong12");
    await user.type(screen.getByLabelText("Confirm password"), "oolong12");
    await user.click(screen.getByRole("button", { name: "Sign up" }));
    expect(screen.getByText("Signed up as asha@tea.shop.")).toBeInTheDocument();
    expect(screen.queryByLabelText("Email")).not.toBeInTheDocument();
    
  9. pressing Enter in a field signs up a valid form
    render(<App />);
    await user.type(screen.getByLabelText("Email"), "ravi@chai.house");
    await user.type(screen.getByLabelText("Password"), "masala-99");
    await user.type(screen.getByLabelText("Confirm password"), "masala-99{Enter}");
    expect(screen.getByText("Signed up as ravi@chai.house.")).toBeInTheDocument();
    

Learn more

App.jsxSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).