Build TeaTabs, the default export: a small tea shop's menu with three tabs, Green, Black, and Herbal. Make them buttons with role="tab" inside an element with role="tablist", and give the open one aria-selected="true" (the others "false"). Green is open at first.
Each tab's list comes from /api/teas?type=green (or black, or herbal): a JSON array like [{ id: 1, name: "Sencha" }]. Show the open tab's teas in a list, one item each, or Loading… (with the single … character) while that tab has no list yet.
Fetch each list once and keep it. Opening a tab you've seen before shows its saved list straight away, with no request. Opening a tab whose list is still on its way doesn't ask again. An answer that arrives after you've left its tab is saved for that tab, and never shows on another one.
A Refresh button fetches the open tab's list again. While it waits, the saved list stays on screen and Refresh is disabled. Then the new list replaces the old one, on screen and in what you've saved.
If a request fails (fetch rejects, or the response isn't ok), show Couldn't load these teas. whenever that tab is open, and keep any saved list on screen. Refresh tries again; opening the tab again doesn't, and still shows the error.
<div role="tablist">
<button role="tab" aria-selected="true">Green</button>
<button role="tab" aria-selected="false">Black</button>
<button role="tab" aria-selected="false">Herbal</button>
</div>
<button>Refresh</button>
<ul><li>Sencha</li><li>Matcha</li></ul>
const menu = {
green: [{ id: 1, name: "Sencha" }, { id: 2, name: "Matcha" }],
black: [{ id: 3, name: "Assam" }],
herbal: [{ id: 4, name: "Rooibos" }],
};
const typeIn = (url) => url.split("type=")[1];
const fetch = mockFetch((url) => menu[typeIn(url)]);
render(<App />);
expect(screen.getByRole("tab", { name: "Green" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("tab", { name: "Black" })).toHaveAttribute("aria-selected", "false");
expect(screen.getByText("Loading…")).toBeInTheDocument();
const items = await screen.findAllByRole("listitem");
expect(items.map((item) => item.textContent)).toEqual(["Sencha", "Matcha"]);
expect(fetch.calls.map((call) => call.url)).toEqual(["/api/teas?type=green"]);
const menu = {
green: [{ id: 1, name: "Sencha" }, { id: 2, name: "Matcha" }],
black: [{ id: 3, name: "Assam" }],
herbal: [{ id: 4, name: "Rooibos" }],
};
const typeIn = (url) => url.split("type=")[1];
const fetch = mockFetch((url) => menu[typeIn(url)]);
render(<App />);
await screen.findByText("Sencha");
await user.click(screen.getByRole("tab", { name: "Black" }));
expect(screen.getByRole("tab", { name: "Black" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("tab", { name: "Green" })).toHaveAttribute("aria-selected", "false");
expect(await screen.findByText("Assam")).toBeInTheDocument();
expect(screen.queryByText("Sencha")).not.toBeInTheDocument();
expect(fetch.calls.map((call) => call.url)).toEqual(["/api/teas?type=green", "/api/teas?type=black"]);
const menu = {
green: [{ id: 1, name: "Sencha" }, { id: 2, name: "Matcha" }],
black: [{ id: 3, name: "Assam" }],
herbal: [{ id: 4, name: "Rooibos" }],
};
const typeIn = (url) => url.split("type=")[1];
const fetch = mockFetch((url) => menu[typeIn(url)]);
render(<App />);
await screen.findByText("Sencha");
await user.click(screen.getByRole("tab", { name: "Black" }));
await screen.findByText("Assam");
await user.click(screen.getByRole("tab", { name: "Green" }));
expect(screen.getByText("Sencha")).toBeInTheDocument();
expect(screen.queryByText("Loading…")).not.toBeInTheDocument();
await flush();
expect(fetch.calls).toHaveLength(2);
const menu = {
green: [{ id: 1, name: "Sencha" }, { id: 2, name: "Matcha" }],
black: [{ id: 3, name: "Assam" }],
herbal: [{ id: 4, name: "Rooibos" }],
};
const typeIn = (url) => url.split("type=")[1];
const green = deferred();
const fetch = mockFetch((url) => (typeIn(url) === "green" ? green.promise : menu[typeIn(url)]));
render(<App />);
await user.click(screen.getByRole("tab", { name: "Black" }));
await screen.findByText("Assam");
await user.click(screen.getByRole("tab", { name: "Green" }));
expect(screen.getByText("Loading…")).toBeInTheDocument();
green.resolve(menu.green);
expect(await screen.findByText("Sencha")).toBeInTheDocument();
expect(fetch.calls.map((call) => typeIn(call.url))).toEqual(["green", "black"]);
const menu = {
green: [{ id: 1, name: "Sencha" }, { id: 2, name: "Matcha" }],
black: [{ id: 3, name: "Assam" }],
herbal: [{ id: 4, name: "Rooibos" }],
};
const typeIn = (url) => url.split("type=")[1];
const green = deferred();
const fetch = mockFetch((url) => (typeIn(url) === "green" ? green.promise : menu[typeIn(url)]));
render(<App />);
await user.click(screen.getByRole("tab", { name: "Black" }));
await screen.findByText("Assam");
green.resolve(menu.green);
await flush();
expect(screen.queryByText("Sencha")).not.toBeInTheDocument();
expect(screen.getByText("Assam")).toBeInTheDocument();
await user.click(screen.getByRole("tab", { name: "Green" }));
expect(screen.getByText("Sencha")).toBeInTheDocument();
await flush();
expect(fetch.calls).toHaveLength(2);
const menu = {
green: [{ id: 1, name: "Sencha" }, { id: 2, name: "Matcha" }],
black: [{ id: 3, name: "Assam" }],
herbal: [{ id: 4, name: "Rooibos" }],
};
const typeIn = (url) => url.split("type=")[1];
let greens = 0;
const fetch = mockFetch((url) => (++greens === 1 ? menu.green : [{ id: 5, name: "Gyokuro" }]));
render(<App />);
await screen.findByText("Sencha");
await user.click(screen.getByRole("button", { name: "Refresh" }));
expect(await screen.findByText("Gyokuro")).toBeInTheDocument();
expect(screen.queryByText("Sencha")).not.toBeInTheDocument();
expect(fetch.calls.map((call) => call.url)).toEqual(["/api/teas?type=green", "/api/teas?type=green"]);
const menu = {
green: [{ id: 1, name: "Sencha" }, { id: 2, name: "Matcha" }],
black: [{ id: 3, name: "Assam" }],
herbal: [{ id: 4, name: "Rooibos" }],
};
const typeIn = (url) => url.split("type=")[1];
const again = deferred();
let greens = 0;
mockFetch(() => (++greens === 1 ? menu.green : again.promise));
render(<App />);
await screen.findByText("Sencha");
await user.click(screen.getByRole("button", { name: "Refresh" }));
expect(screen.getByText("Sencha")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Refresh" })).toBeDisabled();
again.resolve([{ id: 5, name: "Gyokuro" }]);
expect(await screen.findByText("Gyokuro")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Refresh" })).toBeEnabled();
const menu = {
green: [{ id: 1, name: "Sencha" }, { id: 2, name: "Matcha" }],
black: [{ id: 3, name: "Assam" }],
herbal: [{ id: 4, name: "Rooibos" }],
};
const typeIn = (url) => url.split("type=")[1];
let greens = 0;
const fetch = mockFetch((url) =>
typeIn(url) !== "green" ? menu[typeIn(url)] : ++greens === 1 ? menu.green : [{ id: 5, name: "Gyokuro" }],
);
render(<App />);
await screen.findByText("Sencha");
await user.click(screen.getByRole("button", { name: "Refresh" }));
await screen.findByText("Gyokuro");
await user.click(screen.getByRole("tab", { name: "Herbal" }));
await screen.findByText("Rooibos");
await user.click(screen.getByRole("tab", { name: "Green" }));
expect(screen.getByText("Gyokuro")).toBeInTheDocument();
expect(screen.queryByText("Sencha")).not.toBeInTheDocument();
await flush();
expect(fetch.calls).toHaveLength(3);
const menu = {
green: [{ id: 1, name: "Sencha" }, { id: 2, name: "Matcha" }],
black: [{ id: 3, name: "Assam" }],
herbal: [{ id: 4, name: "Rooibos" }],
};
const typeIn = (url) => url.split("type=")[1];
let greens = 0;
const fetch = mockFetch((url) => (typeIn(url) !== "green" ? menu[typeIn(url)] : ++greens === 1 ? fail(500) : menu.green));
render(<App />);
expect(await screen.findByText("Couldn't load these teas.")).toBeInTheDocument();
await user.click(screen.getByRole("tab", { name: "Black" }));
await screen.findByText("Assam");
await user.click(screen.getByRole("tab", { name: "Green" }));
expect(screen.getByText("Couldn't load these teas.")).toBeInTheDocument();
await flush();
expect(fetch.calls.map((call) => typeIn(call.url))).toEqual(["green", "black"]);
await user.click(screen.getByRole("button", { name: "Refresh" }));
expect(await screen.findByText("Sencha")).toBeInTheDocument();
expect(screen.queryByText("Couldn't load these teas.")).not.toBeInTheDocument();
const menu = {
green: [{ id: 1, name: "Sencha" }, { id: 2, name: "Matcha" }],
black: [{ id: 3, name: "Assam" }],
herbal: [{ id: 4, name: "Rooibos" }],
};
const typeIn = (url) => url.split("type=")[1];
let greens = 0;
mockFetch(() => (++greens === 1 ? menu.green : fail(500)));
render(<App />);
await screen.findByText("Sencha");
await user.click(screen.getByRole("button", { name: "Refresh" }));
expect(await screen.findByText("Couldn't load these teas.")).toBeInTheDocument();
expect(screen.getByText("Sencha")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Refresh" })).toBeEnabled();
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 Cached fetch. 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.