On the tea shop's page, the cart badge lives in the header, the Add buttons live in the product list, and the cart summary lives in a sidebar. They sit in different branches of the tree, but they all show one cart. Hold the cart in a reducer inside a provider, and let each part read it through context.
Export CartProvider by name. It takes children and keeps one cart for everything inside it. The cart starts empty. Its lines are { id, name, price, quantity }, in the order teas were first added.
Export useCart() by name. Inside a CartProvider it returns { lines, count, total, add, remove, clear }: lines is the cart, count is the number of items (the sum of the quantities), total is the sum of price times quantity in dollars, add(tea) adds one of a tea ({ id, name, price }), remove(id) takes out that tea's line, and clear() empties the cart. Outside any provider, useCart() throws an Error with the message useCart must be used inside a CartProvider. Two providers keep two separate carts.
Export three components by name, each reading the cart with useCart() and taking no cart props:
<CartBadge /> // <p>Cart: 3</p>, the count
<ProductList teas={teas} /> // a list with a button "Add Sencha" for each tea
<CartSummary /> // the lines and the total, described below
CartSummary shows a heading Your cart. When the cart is empty it shows Your cart is empty.; otherwise a list with an item per line, each with the text Sencha x 2 and a button Remove Sencha. Under that it always shows Total: $11.00 (two decimals) and an Empty cart button. ProductList uses a few teas of your own when teas isn't given.
The default export, App, puts them together for the Preview: a header with the badge, the product list, and the summary in a sidebar, all inside one CartProvider.
render(
<CartProvider>
<header>
<CartBadge />
</header>
<aside>
<CartSummary />
</aside>
</CartProvider>,
);
expect(screen.getByText("Cart: 0")).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Your cart" })).toBeInTheDocument();
expect(screen.getByText("Your cart is empty.")).toBeInTheDocument();
expect(screen.getByText("Total: $0.00")).toBeInTheDocument();
const teas = [{ id: "sencha", name: "Sencha", price: 4 }];
render(
<CartProvider>
<header>
<nav>
<CartBadge />
</nav>
</header>
<main>
<section>
<ProductList teas={teas} />
</section>
</main>
</CartProvider>,
);
await user.click(screen.getByRole("button", { name: "Add Sencha" }));
expect(screen.getByText("Cart: 1")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Add Sencha" }));
expect(screen.getByText("Cart: 2")).toBeInTheDocument();
const teas = [
{ id: "sencha", name: "Sencha", price: 4 },
{ id: "assam", name: "Assam", price: 3 },
];
render(
<CartProvider>
<CartBadge />
<ProductList teas={teas} />
<CartSummary />
</CartProvider>,
);
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.getByText("Cart: 3")).toBeInTheDocument();
expect(screen.getByText("Sencha x 2")).toBeInTheDocument();
expect(screen.getByText("Assam x 1")).toBeInTheDocument();
expect(screen.getByText("Total: $11.00")).toBeInTheDocument();
expect(screen.queryByText("Your cart is empty.")).not.toBeInTheDocument();
const teas = [
{ id: "sencha", name: "Sencha", price: 4 },
{ id: "assam", name: "Assam", price: 3 },
];
render(
<CartProvider>
<header>
<CartBadge />
</header>
<main>
<ProductList teas={teas} />
</main>
<aside>
<CartSummary />
</aside>
</CartProvider>,
);
await user.click(screen.getByRole("button", { name: "Add Sencha" }));
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.getByText("Cart: 1")).toBeInTheDocument();
expect(screen.queryByText("Sencha x 2")).not.toBeInTheDocument();
expect(screen.getByText("Total: $3.00")).toBeInTheDocument();
const teas = [{ id: "oolong", name: "Oolong", price: 5.5 }];
render(
<CartProvider>
<header>
<CartBadge />
</header>
<main>
<ProductList teas={teas} />
</main>
<aside>
<CartSummary />
</aside>
</CartProvider>,
);
await user.click(screen.getByRole("button", { name: "Add Oolong" }));
await user.click(screen.getByRole("button", { name: "Add Oolong" }));
await user.click(screen.getByRole("button", { name: "Empty cart" }));
expect(screen.getByText("Cart: 0")).toBeInTheDocument();
expect(screen.getByText("Your cart is empty.")).toBeInTheDocument();
expect(screen.getByText("Total: $0.00")).toBeInTheDocument();
function Probe() {
const { lines, count, total, add, remove, clear } = useCart();
return (
<div>
<p>{lines.map((line) => line.name + ":" + line.quantity).join(",") || "none"}</p>
<p>{count} items, {total} dollars</p>
<button onClick={() => add({ id: "chai", name: "Chai", price: 2.5 })}>Add chai</button>
<button onClick={() => add({ id: "mint", name: "Mint", price: 1 })}>Add mint</button>
<button onClick={() => remove("chai")}>Remove chai</button>
<button onClick={clear}>Clear</button>
</div>
);
}
render(
<CartProvider>
<Probe />
</CartProvider>,
);
await user.click(screen.getByRole("button", { name: "Add chai" }));
await user.click(screen.getByRole("button", { name: "Add mint" }));
await user.click(screen.getByRole("button", { name: "Add chai" }));
expect(screen.getByText("Chai:2,Mint:1")).toBeInTheDocument();
expect(screen.getByText("3 items, 6 dollars")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Remove chai" }));
expect(screen.getByText("Mint:1")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Clear" }));
expect(screen.getByText("none")).toBeInTheDocument();
expect(screen.getByText("0 items, 0 dollars")).toBeInTheDocument();
const teas = [{ id: "sencha", name: "Sencha", price: 4 }];
render(
<div>
<section aria-label="Shop">
<CartProvider>
<CartBadge />
<ProductList teas={teas} />
</CartProvider>
</section>
<section aria-label="Gift shop">
<CartProvider>
<CartBadge />
<ProductList teas={teas} />
</CartProvider>
</section>
</div>,
);
const shop = within(screen.getByRole("region", { name: "Shop" }));
const gifts = within(screen.getByRole("region", { name: "Gift shop" }));
await user.click(shop.getByRole("button", { name: "Add Sencha" }));
expect(shop.getByText("Cart: 1")).toBeInTheDocument();
expect(gifts.getByText("Cart: 0")).toBeInTheDocument();
await user.click(gifts.getByRole("button", { name: "Add Sencha" }));
expect(gifts.getByText("Cart: 1")).toBeInTheDocument();
expect(shop.getByText("Cart: 1")).toBeInTheDocument();
expect(() => render(<CartBadge />)).toThrow("useCart must be used inside a CartProvider");
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 with a reducer and context. 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.