The tea shop's search shows results that are slow to render. Typing feels sticky, because each letter has to wait for the whole list to render before it appears in the box. Make the box urgent and the list a step behind: React should show each letter at once, then render the results for it when it can, keeping the old results on screen, marked as stale, in the meantime.
TeaSearch takes teas, a list of { id, name }, slowMs, how long each result takes to render (0 by default; the Preview's App sets it so you can feel the lag), and an optional onResultsRender. It has a text box labelled Search teas. Below it, a <section aria-label="Results"> holds SlowResults, which lists the teas whose name contains the search text, ignoring case and spaces at either end, one <li> per tea, or shows No teas found.
SlowResults takes teas, query, slowMs, and onRender, and calls onRender(query) every time it renders: that's how the tests see which search text it rendered with. Keep that line.
What to build: each keystroke renders SlowResults exactly once, with the new search text, and never again with the text it already shows. While the results are behind the box, the section has aria-busy="true" and opacity 0.5. When they've caught up, it has aria-busy="false" and opacity 1. Rendering TeaSearch again with the same props doesn't render SlowResults.
In the tests, React finishes all its work after each keystroke, so the results always catch up in the end. To check the stale marker, the tests look at the section's aria-busy while SlowResults renders with new text: with a deferred value, the box has already been updated and the section already says true. The tests render without Strict Mode. Export TeaSearch and SlowResults by name.
<TeaSearch teas={teas} onResultsRender={(query) => console.log("results for", query)} />
// while the results are behind:
<label>Search teas <input value="gre" /></label>
<section aria-label="Results" aria-busy="true" style="opacity: 0.5;">
<ul><li>Green</li><li>Earl Grey</li></ul>
</section>
const teas = [{ id: "sencha", name: "Sencha" }, { id: "assam", name: "Assam" }];
render(<TeaSearch teas={teas} />);
const results = screen.getByRole("region", { name: "Results" });
expect(within(results).getAllByRole("listitem")).toHaveLength(2);
expect(results).toHaveAttribute("aria-busy", "false");
expect(results.style.opacity).toBe("1");
const teas = [
{ id: "sencha", name: "Sencha" },
{ id: "jasmine", name: "Jasmine Green" },
{ id: "gunpowder", name: "Gunpowder Green" },
];
render(<TeaSearch teas={teas} />);
await user.type(screen.getByLabelText("Search teas"), " GREEN ");
expect(screen.getByLabelText("Search teas")).toHaveValue(" GREEN ");
const items = within(screen.getByRole("region", { name: "Results" })).getAllByRole("listitem");
expect(items.map((item) => item.textContent)).toEqual(["Jasmine Green", "Gunpowder Green"]);
render(<TeaSearch teas={[{ id: "sencha", name: "Sencha" }]} />);
await user.type(screen.getByLabelText("Search teas"), "mint");
expect(screen.getByText("No teas found")).toBeInTheDocument();
const teas = [{ id: "sencha", name: "Sencha" }, { id: "green", name: "Green" }];
const seen = [];
render(<TeaSearch teas={teas} onResultsRender={(query) => seen.push(query)} />);
await user.type(screen.getByLabelText("Search teas"), "gre");
expect(seen).toEqual(["", "g", "gr", "gre"]);
const teas = [{ id: "sencha", name: "Sencha" }, { id: "green", name: "Green" }];
const marks = [];
render(
<TeaSearch
teas={teas}
onResultsRender={(query) => {
const section = document.querySelector("section");
if (section) marks.push([query, section.getAttribute("aria-busy"), section.style.opacity]);
}}
/>,
);
await user.type(screen.getByLabelText("Search teas"), "gr");
expect(marks).toEqual([
["g", "true", "0.5"],
["gr", "true", "0.5"],
]);
const teas = [{ id: "sencha", name: "Sencha" }, { id: "green", name: "Green" }];
render(<TeaSearch teas={teas} />);
await user.type(screen.getByLabelText("Search teas"), "sen");
const results = screen.getByRole("region", { name: "Results" });
expect(results).toHaveAttribute("aria-busy", "false");
expect(results.style.opacity).toBe("1");
expect(within(results).getAllByRole("listitem").map((item) => item.textContent)).toEqual(["Sencha"]);
const teas = [{ id: "sencha", name: "Sencha" }];
let renders = 0;
const onResultsRender = () => renders++;
const { rerender } = render(<TeaSearch teas={teas} onResultsRender={onResultsRender} />);
rerender(<TeaSearch teas={teas} onResultsRender={onResultsRender} />);
expect(renders).toBe(1);
const { rerender } = render(<TeaSearch teas={[{ id: "sencha", name: "Sencha" }]} />);
await user.type(screen.getByLabelText("Search teas"), "o");
expect(screen.getByText("No teas found")).toBeInTheDocument();
rerender(<TeaSearch teas={[{ id: "oolong", name: "Oolong" }, { id: "assam", name: "Assam" }]} />);
expect(screen.getByText("Oolong")).toBeInTheDocument();
expect(screen.queryByText("Assam")).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 Deferred search results. 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.