The tea shop's cart can add a tea, remove a line, change a line's quantity, and empty itself. Written as separate event handlers, that logic gets scattered. Gather it into one reducer function and run the cart with useReducer.
The cart is an array of lines, { id, name, price, quantity }, in the order they were first added. It starts empty. Export cartReducer(cart, action) by name. It returns the next cart for these actions, which are plain objects:
{ type: "added", tea: { id, name, price } } // a new line with quantity 1, or one more of a line that's there
{ type: "removed", id } // drop that line
{ type: "quantityChanged", id, quantity } // set that line's quantity; 0 or less drops the line
{ type: "cleared" } // an empty cart
The reducer is pure: it never changes the cart or the lines it's given, and returns new arrays and objects instead. Any other type throws an Error whose message is Unknown action: followed by the type, like Unknown action: bought.
The default export, App, is the cart screen. It takes teas, a list of { id, name, price } with prices in dollars; when it isn't given, as in the Preview, show a few teas of your own. For each tea there's a button Add Sencha (with the tea's name).
Under the heading Your cart, show Your cart is empty. while it's empty. Otherwise show a list with one item per line. Each item has the text Sencha x 2 (name, x, quantity) and three buttons: - labelled One less Sencha, + labelled One more Sencha, and Remove Sencha. Going below 1 with - takes the line out. Below the cart, show Total: $11.00, the sum of price times quantity with two decimals, and an Empty cart button.
<li>
Sencha x 2
<button aria-label="One less Sencha">-</button>
<button aria-label="One more Sencha">+</button>
<button>Remove Sencha</button>
</li>
const cart = cartReducer([], { type: "added", tea: { id: "sencha", name: "Sencha", price: 4 } });
expect(cart).toEqual([{ id: "sencha", name: "Sencha", price: 4, quantity: 1 }]);
const next = cartReducer(cart, { type: "added", tea: { id: "assam", name: "Assam", price: 3 } });
expect(next).toEqual([
{ id: "sencha", name: "Sencha", price: 4, quantity: 1 },
{ id: "assam", name: "Assam", price: 3, quantity: 1 },
]);
const sencha = { id: "sencha", name: "Sencha", price: 4 };
let cart = cartReducer([], { type: "added", tea: sencha });
cart = cartReducer(cart, { type: "added", tea: { id: "assam", name: "Assam", price: 3 } });
cart = cartReducer(cart, { type: "added", tea: sencha });
expect(cart).toEqual([
{ id: "sencha", name: "Sencha", price: 4, quantity: 2 },
{ id: "assam", name: "Assam", price: 3, quantity: 1 },
]);
const cart = [
{ id: "sencha", name: "Sencha", price: 4, quantity: 2 },
{ id: "assam", name: "Assam", price: 3, quantity: 1 },
];
expect(cartReducer(cart, { type: "removed", id: "sencha" })).toEqual([{ id: "assam", name: "Assam", price: 3, quantity: 1 }]);
expect(cartReducer(cart, { type: "quantityChanged", id: "assam", quantity: 5 })).toEqual([
{ id: "sencha", name: "Sencha", price: 4, quantity: 2 },
{ id: "assam", name: "Assam", price: 3, quantity: 5 },
]);
expect(cartReducer(cart, { type: "cleared" })).toEqual([]);
const cart = [
{ id: "sencha", name: "Sencha", price: 4, quantity: 2 },
{ id: "assam", name: "Assam", price: 3, quantity: 1 },
];
expect(cartReducer(cart, { type: "quantityChanged", id: "sencha", quantity: 0 })).toEqual([{ id: "assam", name: "Assam", price: 3, quantity: 1 }]);
expect(cartReducer(cart, { type: "quantityChanged", id: "assam", quantity: -1 })).toEqual([{ id: "sencha", name: "Sencha", price: 4, quantity: 2 }]);
const line = Object.freeze({ id: "sencha", name: "Sencha", price: 4, quantity: 1 });
const cart = Object.freeze([line]);
const actions = [
{ type: "added", tea: { id: "sencha", name: "Sencha", price: 4 } },
{ type: "added", tea: { id: "assam", name: "Assam", price: 3 } },
{ type: "quantityChanged", id: "sencha", quantity: 3 },
{ type: "removed", id: "sencha" },
{ type: "cleared" },
];
for (const action of actions) {
const next = cartReducer(cart, action);
expect(next).not.toBe(cart);
}
expect(cart).toEqual([{ id: "sencha", name: "Sencha", price: 4, quantity: 1 }]);
expect(cartReducer(cart, actions[0])[0]).not.toBe(line);
expect(() => cartReducer([], { type: "bought" })).toThrow("Unknown action: bought");
const teas = [
{ id: "sencha", name: "Sencha", price: 4 },
{ id: "assam", name: "Assam", price: 3 },
];
render(<App teas={teas} />);
expect(screen.getByText("Your cart is empty.")).toBeInTheDocument();
expect(screen.getByText("Total: $0.00")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Add Sencha" }));
await user.click(screen.getByRole("button", { name: "Add Assam" }));
await user.click(screen.getByRole("button", { name: "Add Sencha" }));
expect(screen.queryByText("Your cart is empty.")).not.toBeInTheDocument();
expect(screen.getByText("Sencha x 2")).toBeInTheDocument();
expect(screen.getByText("Assam x 1")).toBeInTheDocument();
expect(screen.getByText("Total: $11.00")).toBeInTheDocument();
const teas = [
{ id: "sencha", name: "Sencha", price: 4 },
{ id: "oolong", name: "Oolong", price: 5.5 },
];
render(<App teas={teas} />);
await user.click(screen.getByRole("button", { name: "Add Oolong" }));
await user.click(screen.getByRole("button", { name: "One more Oolong" }));
await user.click(screen.getByRole("button", { name: "One more Oolong" }));
expect(screen.getByText("Oolong x 3")).toBeInTheDocument();
expect(screen.getByText("Total: $16.50")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "One less Oolong" }));
expect(screen.getByText("Oolong x 2")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "One less Oolong" }));
await user.click(screen.getByRole("button", { name: "One less Oolong" }));
expect(screen.queryByText(/Oolong x/)).not.toBeInTheDocument();
expect(screen.getByText("Your cart is empty.")).toBeInTheDocument();
const teas = [
{ id: "sencha", name: "Sencha", price: 4 },
{ id: "assam", name: "Assam", price: 3 },
];
render(<App teas={teas} />);
await user.click(screen.getByRole("button", { name: "Add Sencha" }));
await user.click(screen.getByRole("button", { name: "Add Assam" }));
await user.click(screen.getByRole("button", { name: "Remove Sencha" }));
expect(screen.queryByText("Sencha x 1")).not.toBeInTheDocument();
expect(screen.getByText("Assam x 1")).toBeInTheDocument();
expect(screen.getByText("Total: $3.00")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Empty cart" }));
expect(screen.getByText("Your cart is empty.")).toBeInTheDocument();
expect(screen.getByText("Total: $0.00")).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 Cart reducer. 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.