Build App: a search box labelled Search teas. Every time its text changes, fetch /api/teas?q= followed by the text, encoded with encodeURIComponent. The response is JSON: an array of teas like { id: 1, name: "Green" }.
While the latest search is waiting for its answer, show Loading… (with the single … character). Then list the tea names in a list, one item each, or show No teas match. when the array is empty.
If the request fails (fetch rejects, or the response isn't ok), show Something went wrong. and a Retry button that runs the same search again. When the box is empty, show nothing at all and don't fetch.
Answers can arrive in any order. Only the latest search counts: an older one that answers late, with teas or with an error, must never change the page. Abort the older request, ignore its answer, or both.
// Typing "g", then "r":
// GET /api/teas?q=g answers last, with [Ginger]: never shown
// GET /api/teas?q=gr answers first, with [Green]: shown
const fetch = mockFetch(() => []);
render(<App />);
await user.type(screen.getByLabelText("Search teas"), "gr");
expect(fetch.calls.map((call) => call.url)).toEqual(["/api/teas?q=g", "/api/teas?q=gr"]);
const fetch = mockFetch(() => []);
render(<App />);
await user.type(screen.getByLabelText("Search teas"), "earl grey");
expect(fetch.calls[fetch.calls.length - 1].url).toBe("/api/teas?q=earl%20grey");
const answer = deferred();
mockFetch(() => answer.promise);
render(<App />);
await user.type(screen.getByLabelText("Search teas"), "g");
expect(screen.getByText("Loading…")).toBeInTheDocument();
answer.resolve([{ id: 1, name: "Green" }]);
expect(await screen.findByText("Green")).toBeInTheDocument();
expect(screen.queryByText("Loading…")).not.toBeInTheDocument();
mockFetch(() => [{ id: 1, name: "Green" }, { id: 2, name: "Genmaicha" }]);
render(<App />);
await user.type(screen.getByLabelText("Search teas"), "g");
const items = await screen.findAllByRole("listitem");
expect(items.map((item) => item.textContent)).toEqual(["Green", "Genmaicha"]);
mockFetch(() => []);
render(<App />);
await user.type(screen.getByLabelText("Search teas"), "zz");
expect(await screen.findByText("No teas match.")).toBeInTheDocument();
const fetch = mockFetch(() => [{ id: 1, name: "Green" }]);
render(<App />);
const box = screen.getByLabelText("Search teas");
await user.type(box, "g");
expect(await screen.findByText("Green")).toBeInTheDocument();
await user.clear(box);
await flush();
expect(screen.queryByText("Green")).not.toBeInTheDocument();
expect(screen.queryByText("No teas match.")).not.toBeInTheDocument();
expect(screen.queryByText("Loading…")).not.toBeInTheDocument();
expect(fetch.calls).toHaveLength(1);
const first = deferred();
const second = deferred();
mockFetch((url) => (url === "/api/teas?q=g" ? first.promise : second.promise));
render(<App />);
await user.type(screen.getByLabelText("Search teas"), "gr");
second.resolve([{ id: 2, name: "Green" }]);
expect(await screen.findByText("Green")).toBeInTheDocument();
first.resolve([{ id: 1, name: "Ginger" }]);
await flush();
expect(screen.queryByText("Ginger")).not.toBeInTheDocument();
expect(screen.getByText("Green")).toBeInTheDocument();
const first = deferred();
const second = deferred();
mockFetch((url) => (url === "/api/teas?q=g" ? first.promise : second.promise));
render(<App />);
await user.type(screen.getByLabelText("Search teas"), "gr");
first.resolve([{ id: 1, name: "Ginger" }]);
await flush();
expect(screen.queryByText("Ginger")).not.toBeInTheDocument();
expect(screen.getByText("Loading…")).toBeInTheDocument();
second.resolve([{ id: 2, name: "Green" }]);
expect(await screen.findByText("Green")).toBeInTheDocument();
const first = deferred();
mockFetch((url) => (url === "/api/teas?q=g" ? first.promise : [{ id: 2, name: "Green" }]));
render(<App />);
await user.type(screen.getByLabelText("Search teas"), "gr");
expect(await screen.findByText("Green")).toBeInTheDocument();
first.resolve(fail(500));
await flush();
expect(screen.queryByText("Something went wrong.")).not.toBeInTheDocument();
expect(screen.getByText("Green")).toBeInTheDocument();
let attempts = 0;
const fetch = mockFetch(() => (++attempts === 1 ? fail(500) : [{ id: 1, name: "Green" }]));
render(<App />);
await user.type(screen.getByLabelText("Search teas"), "g");
expect(await screen.findByText("Something went wrong.")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Retry" }));
expect(await screen.findByText("Green")).toBeInTheDocument();
expect(screen.queryByText("Something went wrong.")).not.toBeInTheDocument();
expect(fetch.calls.map((call) => call.url)).toEqual(["/api/teas?q=g", "/api/teas?q=g"]);
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 Search without races. 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.