Build App({ onSubmit }): the ingredient list on a tea shop's recipe editor, say for a masala chai. It's a form with one row per ingredient, and it starts with one empty row.
Each row is a <fieldset> whose <legend> is Ingredient 1, Ingredient 2, and so on, numbered by the row's place in the list right now. Inside are two text boxes labelled Name and Amount, and a Remove button.
An Add ingredient button adds an empty row at the end. Remove deletes its own row, and is disabled while only one row is left. Neither button submits the form.
Rows keep what was typed in them. Removing a row in the middle leaves every other row's name and amount as they were, in the same order, only renumbered. A row added later starts empty.
A Save recipe button submits the form. That calls onSubmit(list) once, where list is an array of { name, amount }, one per row in order, with both values trimmed. Rows whose name is blank are left out.
// After typing two rows and pressing Save recipe:
onSubmit([
{ name: "Assam", amount: "2 tsp" },
{ name: "Milk", amount: "1 cup" },
]);
render(<App onSubmit={spy()} />);
expect(screen.getAllByRole("group")).toHaveLength(1);
const row = screen.getByRole("group", { name: "Ingredient 1" });
expect(within(row).getByLabelText("Name")).toHaveValue("");
expect(within(row).getByLabelText("Amount")).toHaveValue("");
const onSubmit = spy();
render(<App onSubmit={onSubmit} />);
await user.type(within(screen.getByRole("group", { name: "Ingredient 1" })).getByLabelText("Name"), "Assam");
await user.click(screen.getByRole("button", { name: "Add ingredient" }));
expect(screen.getAllByRole("group")).toHaveLength(2);
expect(within(screen.getByRole("group", { name: "Ingredient 1" })).getByLabelText("Name")).toHaveValue("Assam");
expect(within(screen.getByRole("group", { name: "Ingredient 2" })).getByLabelText("Name")).toHaveValue("");
expect(onSubmit).not.toHaveBeenCalled();
render(<App onSubmit={spy()} />);
expect(screen.getByRole("button", { name: "Remove" })).toBeDisabled();
await user.click(screen.getByRole("button", { name: "Add ingredient" }));
for (const button of screen.getAllByRole("button", { name: "Remove" })) expect(button).toBeEnabled();
const row = (n) => screen.getByRole("group", { name: "Ingredient " + n });
render(<App onSubmit={spy()} />);
const add = screen.getByRole("button", { name: "Add ingredient" });
await user.click(add);
await user.click(add);
await user.type(within(row(1)).getByLabelText("Name"), "Assam");
await user.type(within(row(1)).getByLabelText("Amount"), "2 tsp");
await user.type(within(row(2)).getByLabelText("Name"), "Milk");
await user.type(within(row(2)).getByLabelText("Amount"), "1 cup");
await user.type(within(row(3)).getByLabelText("Name"), "Cardamom");
await user.type(within(row(3)).getByLabelText("Amount"), "3 pods");
await user.click(within(row(2)).getByRole("button", { name: "Remove" }));
expect(screen.getAllByRole("group")).toHaveLength(2);
expect(within(row(1)).getByLabelText("Name")).toHaveValue("Assam");
expect(within(row(1)).getByLabelText("Amount")).toHaveValue("2 tsp");
expect(within(row(2)).getByLabelText("Name")).toHaveValue("Cardamom");
expect(within(row(2)).getByLabelText("Amount")).toHaveValue("3 pods");
const row = (n) => screen.getByRole("group", { name: "Ingredient " + n });
render(<App onSubmit={spy()} />);
await user.click(screen.getByRole("button", { name: "Add ingredient" }));
await user.type(within(row(1)).getByLabelText("Name"), "Assam");
await user.type(within(row(2)).getByLabelText("Name"), "Ginger");
await user.click(within(row(1)).getByRole("button", { name: "Remove" }));
expect(screen.getAllByRole("group")).toHaveLength(1);
expect(within(row(1)).getByLabelText("Name")).toHaveValue("Ginger");
const row = (n) => screen.getByRole("group", { name: "Ingredient " + n });
render(<App onSubmit={spy()} />);
await user.click(screen.getByRole("button", { name: "Add ingredient" }));
await user.type(within(row(1)).getByLabelText("Name"), "Assam");
await user.type(within(row(2)).getByLabelText("Name"), "Milk");
await user.click(within(row(1)).getByRole("button", { name: "Remove" }));
await user.click(screen.getByRole("button", { name: "Add ingredient" }));
expect(within(row(1)).getByLabelText("Name")).toHaveValue("Milk");
expect(within(row(2)).getByLabelText("Name")).toHaveValue("");
expect(within(row(2)).getByLabelText("Amount")).toHaveValue("");
const row = (n) => screen.getByRole("group", { name: "Ingredient " + n });
const onSubmit = spy();
render(<App onSubmit={onSubmit} />);
await user.click(screen.getByRole("button", { name: "Add ingredient" }));
await user.type(within(row(1)).getByLabelText("Name"), " Assam ");
await user.type(within(row(1)).getByLabelText("Amount"), "2 tsp ");
await user.type(within(row(2)).getByLabelText("Name"), "Milk");
await user.type(within(row(2)).getByLabelText("Amount"), " 1 cup");
await user.click(screen.getByRole("button", { name: "Save recipe" }));
expect(onSubmit).toHaveBeenCalledTimes(1);
expect(onSubmit).toHaveBeenCalledWith([
{ name: "Assam", amount: "2 tsp" },
{ name: "Milk", amount: "1 cup" },
]);
const row = (n) => screen.getByRole("group", { name: "Ingredient " + n });
const onSubmit = spy();
render(<App onSubmit={onSubmit} />);
const add = screen.getByRole("button", { name: "Add ingredient" });
await user.click(add);
await user.click(add);
await user.type(within(row(1)).getByLabelText("Name"), "Ginger");
await user.type(within(row(2)).getByLabelText("Name"), " ");
await user.type(within(row(2)).getByLabelText("Amount"), "a pinch");
await user.click(screen.getByRole("button", { name: "Save recipe" }));
expect(onSubmit).toHaveBeenCalledWith([{ name: "Ginger", amount: "" }]);
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 Dynamic ingredient fields. 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.