Build App({ onSubmit }): a tea shop's checkout in three steps, Details, Address, and Review. The starter gives onSubmit a default so the Preview works; the tests pass their own.
At the top, an ordered list labelled Progress (<ol aria-label="Progress">) has three list items: Details, Address, and Review. The current step's item has aria-current="step", and the others don't.
Details has text boxes labelled Name and Email. Address has Street, City, and Postcode. Only the current step's fields are on the page. Review shows three lines built from the trimmed values: Name: Asha, Email: asha@tea.shop, and Address: 12 Leaf Lane, Darjeeling, 734101.
Details and Address have a Next button. Address and Review have a Back button; Details has none. Pressing Enter in a field does the same as Next.
Next checks the current step's fields, trimmed: every field must be filled in, and the email must be some text, an @, and more text, with no spaces. If anything is wrong, it stays on the step and shows each problem next to its field: Enter your name., Enter a valid email., Enter your street., Enter your city., or Enter your postcode. After that, the messages follow what's typed, so fixing a field removes its message. Messages appear only once Next has been pressed on the step, and start hidden again whenever you move to another step.
Back goes to the step before without checking anything. Every value typed is kept, on every step, whichever way you move.
Review has a Place order button. It calls onSubmit(data) once with { name, email, street, city, postcode }, all trimmed, and then replaces the form with Thanks, Asha!, using the name.
<form>
<ol aria-label="Progress">
<li>Details</li>
<li aria-current="step">Address</li>
<li>Review</li>
</ol>
<label for="street">Street</label> <input id="street" />
<!-- City and Postcode the same way -->
<button type="button">Back</button>
<button>Next</button>
</form>
render(<App onSubmit={spy()} />);
const steps = within(screen.getByRole("list", { name: "Progress" })).getAllByRole("listitem");
expect(steps.map((step) => step.textContent)).toEqual(["Details", "Address", "Review"]);
expect(steps[0]).toHaveAttribute("aria-current", "step");
expect(steps[1]).not.toHaveAttribute("aria-current", "step");
expect(steps[2]).not.toHaveAttribute("aria-current", "step");
expect(screen.getByLabelText("Name")).toBeInTheDocument();
expect(screen.getByLabelText("Email")).toBeInTheDocument();
expect(screen.queryByLabelText("Street")).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Back" })).not.toBeInTheDocument();
render(<App onSubmit={spy()} />);
expect(screen.queryByText("Enter your name.")).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Next" }));
expect(screen.getByText("Enter your name.")).toBeInTheDocument();
expect(screen.getByText("Enter a valid email.")).toBeInTheDocument();
expect(screen.getByLabelText("Name")).toBeInTheDocument();
expect(screen.queryByLabelText("Street")).not.toBeInTheDocument();
render(<App onSubmit={spy()} />);
await user.type(screen.getByLabelText("Email"), "asha");
await user.click(screen.getByRole("button", { name: "Next" }));
expect(screen.getByText("Enter a valid email.")).toBeInTheDocument();
await user.type(screen.getByLabelText("Name"), "Asha");
expect(screen.queryByText("Enter your name.")).not.toBeInTheDocument();
expect(screen.getByText("Enter a valid email.")).toBeInTheDocument();
await user.type(screen.getByLabelText("Email"), "@tea.shop");
expect(screen.queryByText("Enter a valid email.")).not.toBeInTheDocument();
render(<App onSubmit={spy()} />);
await user.type(screen.getByLabelText("Name"), "Asha");
await user.type(screen.getByLabelText("Email"), "asha@tea.shop");
await user.click(screen.getByRole("button", { name: "Next" }));
const steps = within(screen.getByRole("list", { name: "Progress" })).getAllByRole("listitem");
expect(steps[1]).toHaveAttribute("aria-current", "step");
expect(steps[0]).not.toHaveAttribute("aria-current", "step");
expect(screen.getByLabelText("Street")).toBeInTheDocument();
expect(screen.getByLabelText("City")).toBeInTheDocument();
expect(screen.getByLabelText("Postcode")).toBeInTheDocument();
expect(screen.queryByLabelText("Name")).not.toBeInTheDocument();
expect(screen.queryByText("Enter your street.")).not.toBeInTheDocument();
render(<App onSubmit={spy()} />);
await user.type(screen.getByLabelText("Name"), "Asha");
await user.type(screen.getByLabelText("Email"), "asha@tea.shop{Enter}");
expect(screen.getByLabelText("Street")).toBeInTheDocument();
render(<App onSubmit={spy()} />);
await user.type(screen.getByLabelText("Name"), "Asha");
await user.type(screen.getByLabelText("Email"), "asha@tea.shop");
await user.click(screen.getByRole("button", { name: "Next" }));
await user.type(screen.getByLabelText("City"), " ");
await user.click(screen.getByRole("button", { name: "Next" }));
expect(screen.getByText("Enter your street.")).toBeInTheDocument();
expect(screen.getByText("Enter your city.")).toBeInTheDocument();
expect(screen.getByText("Enter your postcode.")).toBeInTheDocument();
expect(screen.getByLabelText("Street")).toBeInTheDocument();
render(<App onSubmit={spy()} />);
await user.type(screen.getByLabelText("Name"), "Asha");
await user.type(screen.getByLabelText("Email"), "asha@tea.shop");
await user.click(screen.getByRole("button", { name: "Next" }));
await user.type(screen.getByLabelText("Street"), "12 Leaf Lane");
await user.click(screen.getByRole("button", { name: "Back" }));
const steps = within(screen.getByRole("list", { name: "Progress" })).getAllByRole("listitem");
expect(steps[0]).toHaveAttribute("aria-current", "step");
expect(screen.getByLabelText("Name")).toHaveValue("Asha");
expect(screen.getByLabelText("Email")).toHaveValue("asha@tea.shop");
await user.click(screen.getByRole("button", { name: "Next" }));
expect(screen.getByLabelText("Street")).toHaveValue("12 Leaf Lane");
expect(screen.queryByText("Enter your city.")).not.toBeInTheDocument();
render(<App onSubmit={spy()} />);
await user.type(screen.getByLabelText("Name"), " Asha ");
await user.type(screen.getByLabelText("Email"), "asha@tea.shop ");
await user.click(screen.getByRole("button", { name: "Next" }));
await user.type(screen.getByLabelText("Street"), "12 Leaf Lane");
await user.type(screen.getByLabelText("City"), " Darjeeling");
await user.type(screen.getByLabelText("Postcode"), "734101");
await user.click(screen.getByRole("button", { name: "Next" }));
const steps = within(screen.getByRole("list", { name: "Progress" })).getAllByRole("listitem");
expect(steps[2]).toHaveAttribute("aria-current", "step");
expect(screen.getByText("Name: Asha")).toBeInTheDocument();
expect(screen.getByText("Email: asha@tea.shop")).toBeInTheDocument();
expect(screen.getByText("Address: 12 Leaf Lane, Darjeeling, 734101")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Back" }));
expect(screen.getByLabelText("City")).toHaveValue(" Darjeeling");
const onSubmit = spy();
render(<App onSubmit={onSubmit} />);
await user.type(screen.getByLabelText("Name"), "Ravi ");
await user.type(screen.getByLabelText("Email"), "ravi@chai.house");
await user.click(screen.getByRole("button", { name: "Next" }));
await user.type(screen.getByLabelText("Street"), "4 Spice Road");
await user.type(screen.getByLabelText("City"), "Munnar");
await user.type(screen.getByLabelText("Postcode"), " 685612 ");
await user.click(screen.getByRole("button", { name: "Next" }));
expect(onSubmit).not.toHaveBeenCalled();
await user.click(screen.getByRole("button", { name: "Place order" }));
expect(onSubmit).toHaveBeenCalledTimes(1);
expect(onSubmit).toHaveBeenCalledWith({ name: "Ravi", email: "ravi@chai.house", street: "4 Spice Road", city: "Munnar", postcode: "685612" });
expect(screen.getByText("Thanks, Ravi!")).toBeInTheDocument();
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).
I'm here to help with Multi-step checkout form. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.