The tea shop's menu shows one tea's details at a time. Build TeaPicker, the default export, which loads the details with use() and lets Suspense show the waiting state, instead of an Effect and a loading flag.
It takes fetchTea(id), which returns a promise of { name, origin, notes }. The default in the starter is a pretend server that answers after a short wait. In this problem the promise never rejects.
Render three buttons, Sencha, Assam, and Rooibos (ids sencha, assam, rooibos), in a group labelled Teas. The chosen one has aria-pressed="true", the others aria-pressed="false". Sencha is chosen at first.
Below the buttons, show the chosen tea: its name in a heading and From followed by its origin in a paragraph, then its notes. While that tea is loading, show Loading tea… (with the single … character) in its place. The buttons stay on screen the whole time.
Call fetchTea once per tea, ever: keep each tea's promise in a cache that lives as long as the component, and read it with use(). Going back to a tea that's already loaded shows it at once, with no Loading tea…. A tea that answers after you moved on to another must never be shown.
<TeaPicker fetchTea={fetchTea} />
// [Sencha] [Assam] [Rooibos]
// Loading tea…
// ...then:
// Sencha
// From Japan
// Grassy and bright.
// settle(work): render or click inside an awaited act(), so a component that
// suspends on use() can show its content once the promise resolves.
const loads = {};
const fetchTea = spy((id) => (loads[id] = deferred()).promise);
await settle(() => render(<App fetchTea={fetchTea} />));
expect(fetchTea).toHaveBeenCalledWith("sencha");
expect(screen.getByText("Loading tea…")).toBeInTheDocument();
loads.sencha.resolve({ name: "Sencha", origin: "Japan", notes: "Grassy and bright." });
expect(await screen.findByRole("heading", { name: "Sencha" })).toBeInTheDocument();
expect(screen.getByText("From Japan")).toBeInTheDocument();
expect(screen.getByText("Grassy and bright.")).toBeInTheDocument();
expect(screen.queryByText("Loading tea…")).not.toBeInTheDocument();
// settle(work): render or click inside an awaited act(), so a component that
// suspends on use() can show its content once the promise resolves.
const fetchTea = spy(async () => ({ name: "Sencha", origin: "Japan", notes: "Grassy and bright." }));
await settle(() => render(<App fetchTea={fetchTea} />));
expect(await screen.findByRole("heading", { name: "Sencha" })).toBeInTheDocument();
await flush();
expect(fetchTea).toHaveBeenCalledTimes(1);
// settle(work): render or click inside an awaited act(), so a component that
// suspends on use() can show its content once the promise resolves.
await settle(() => render(<App fetchTea={() => new Promise(() => {})} />));
const group = screen.getByRole("group", { name: "Teas" });
expect(within(group).getAllByRole("button").map((button) => button.textContent)).toEqual(["Sencha", "Assam", "Rooibos"]);
expect(screen.getByText("Loading tea…")).toBeInTheDocument();
// settle(work): render or click inside an awaited act(), so a component that
// suspends on use() can show its content once the promise resolves.
await settle(() => render(<App fetchTea={() => new Promise(() => {})} />));
expect(screen.getByRole("button", { name: "Sencha" })).toHaveAttribute("aria-pressed", "true");
expect(screen.getByRole("button", { name: "Assam" })).toHaveAttribute("aria-pressed", "false");
await settle(() => fireEvent.click(screen.getByRole("button", { name: "Assam" })));
expect(screen.getByRole("button", { name: "Assam" })).toHaveAttribute("aria-pressed", "true");
expect(screen.getByRole("button", { name: "Sencha" })).toHaveAttribute("aria-pressed", "false");
// settle(work): render or click inside an awaited act(), so a component that
// suspends on use() can show its content once the promise resolves.
const loads = {};
const fetchTea = spy((id) => (loads[id] ??= deferred()).promise);
await settle(() => render(<App fetchTea={fetchTea} />));
loads.sencha.resolve({ name: "Sencha", origin: "Japan", notes: "Grassy and bright." });
expect(await screen.findByRole("heading", { name: "Sencha" })).toBeInTheDocument();
await settle(() => fireEvent.click(screen.getByRole("button", { name: "Assam" })));
expect(fetchTea).toHaveBeenCalledWith("assam");
expect(screen.getByText("Loading tea…")).toBeInTheDocument();
loads.assam.resolve({ name: "Assam", origin: "India", notes: "Malty and strong." });
expect(await screen.findByRole("heading", { name: "Assam" })).toBeInTheDocument();
expect(screen.getByText("From India")).toBeInTheDocument();
// settle(work): render or click inside an awaited act(), so a component that
// suspends on use() can show its content once the promise resolves.
const loads = {};
const fetchTea = spy((id) => (loads[id] ??= deferred()).promise);
await settle(() => render(<App fetchTea={fetchTea} />));
loads.sencha.resolve({ name: "Sencha", origin: "Japan", notes: "Grassy and bright." });
expect(await screen.findByRole("heading", { name: "Sencha" })).toBeInTheDocument();
await settle(() => fireEvent.click(screen.getByRole("button", { name: "Assam" })));
loads.assam.resolve({ name: "Assam", origin: "India", notes: "Malty and strong." });
expect(await screen.findByRole("heading", { name: "Assam" })).toBeInTheDocument();
await settle(() => fireEvent.click(screen.getByRole("button", { name: "Sencha" })));
expect(screen.getByRole("heading", { name: "Sencha" })).toBeInTheDocument();
expect(screen.queryByText("Loading tea…")).not.toBeInTheDocument();
expect(fetchTea.calls.map((call) => call.args[0])).toEqual(["sencha", "assam"]);
// settle(work): render or click inside an awaited act(), so a component that
// suspends on use() can show its content once the promise resolves.
const loads = {};
await settle(() => render(<App fetchTea={(id) => (loads[id] ??= deferred()).promise} />));
await settle(() => fireEvent.click(screen.getByRole("button", { name: "Assam" })));
await settle(() => fireEvent.click(screen.getByRole("button", { name: "Rooibos" })));
loads.rooibos.resolve({ name: "Rooibos", origin: "South Africa", notes: "Sweet and caffeine free." });
expect(await screen.findByRole("heading", { name: "Rooibos" })).toBeInTheDocument();
loads.assam.resolve({ name: "Assam", origin: "India", notes: "Malty and strong." });
loads.sencha.resolve({ name: "Sencha", origin: "Japan", notes: "Grassy and bright." });
await flush();
expect(screen.getByRole("heading", { name: "Rooibos" })).toBeInTheDocument();
expect(screen.queryByRole("heading", { name: "Assam" })).not.toBeInTheDocument();
expect(screen.queryByRole("heading", { name: "Sencha" })).not.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 Tea details with use and Suspense. 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.