Build OrderPages, the default export: a small tea shop's order history, one page at a time. It fetches /api/orders?page=1 first. Every answer is JSON like { orders: [{ id: 11, item: "Sencha, 100 g" }], totalPages: 3 }.
Until the first page arrives, show only Loading… (with the single … character). Then show each order's item in a list, one item each, the text Page 1 of 3, and two buttons, Previous and Next.
Next asks for the next page and Previous for the one before, counting from the page you asked for last, even if it hasn't arrived yet. Previous is disabled on page 1, and Next on the last page, using totalPages from the latest answer.
While a page loads, keep the orders you have on screen and mark their list with aria-busy="true". Dim it too, any way you like: the tests only look at aria-busy. Page 1 of 3 keeps describing the orders on screen until the new ones arrive. Answers can arrive in any order: only the page you asked for last may show.
If a page fails to load (fetch rejects, or the response isn't ok), show Couldn't load page 2. (with that page's number) and a Retry button that asks for it again. Orders already on screen stay there.
// While page 2 loads:
<ul aria-busy="true" style="opacity: 0.5">
<li>Sencha, 100 g</li>
...
</ul>
<p>Page 1 of 3</p>
<button>Previous</button> // disabled on page 1
<button>Next</button>
const answer = deferred();
const fetch = mockFetch(() => answer.promise);
render(<App />);
expect(screen.getByText("Loading…")).toBeInTheDocument();
expect(fetch.calls.map((call) => call.url)).toEqual(["/api/orders?page=1"]);
answer.resolve({ orders: [{ id: 11, item: "Sencha, 100 g" }], totalPages: 3 });
expect(await screen.findByText("Sencha, 100 g")).toBeInTheDocument();
expect(screen.queryByText("Loading…")).not.toBeInTheDocument();
const pageOf = (n, totalPages = 3) => ({
orders: [
{ id: n * 10 + 1, item: "Order " + n + "a" },
{ id: n * 10 + 2, item: "Order " + n + "b" },
],
totalPages,
});
const pageIn = (url) => Number(url.split("page=")[1]);
mockFetch((url) => pageOf(pageIn(url)));
render(<App />);
const items = await screen.findAllByRole("listitem");
expect(items.map((item) => item.textContent)).toEqual(["Order 1a", "Order 1b"]);
expect(screen.getByText("Page 1 of 3")).toBeInTheDocument();
const pageOf = (n, totalPages = 3) => ({
orders: [
{ id: n * 10 + 1, item: "Order " + n + "a" },
{ id: n * 10 + 2, item: "Order " + n + "b" },
],
totalPages,
});
const pageIn = (url) => Number(url.split("page=")[1]);
mockFetch((url) => pageOf(pageIn(url)));
render(<App />);
await screen.findByText("Order 1a");
expect(screen.getByRole("button", { name: "Previous" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Next" })).toBeEnabled();
const pageOf = (n, totalPages = 3) => ({
orders: [
{ id: n * 10 + 1, item: "Order " + n + "a" },
{ id: n * 10 + 2, item: "Order " + n + "b" },
],
totalPages,
});
const pageIn = (url) => Number(url.split("page=")[1]);
const fetch = mockFetch((url) => pageOf(pageIn(url)));
render(<App />);
await screen.findByText("Order 1a");
await user.click(screen.getByRole("button", { name: "Next" }));
expect(await screen.findByText("Page 2 of 3")).toBeInTheDocument();
const items = screen.getAllByRole("listitem");
expect(items.map((item) => item.textContent)).toEqual(["Order 2a", "Order 2b"]);
expect(fetch.calls.map((call) => call.url)).toEqual(["/api/orders?page=1", "/api/orders?page=2"]);
const pageOf = (n, totalPages = 3) => ({
orders: [
{ id: n * 10 + 1, item: "Order " + n + "a" },
{ id: n * 10 + 2, item: "Order " + n + "b" },
],
totalPages,
});
const pageIn = (url) => Number(url.split("page=")[1]);
const second = deferred();
mockFetch((url) => (pageIn(url) === 2 ? second.promise : pageOf(1)));
render(<App />);
await screen.findByText("Order 1a");
await user.click(screen.getByRole("button", { name: "Next" }));
expect(screen.getByText("Order 1a")).toBeInTheDocument();
expect(screen.getByText("Page 1 of 3")).toBeInTheDocument();
expect(screen.getByRole("list")).toHaveAttribute("aria-busy", "true");
second.resolve(pageOf(2));
expect(await screen.findByText("Order 2a")).toBeInTheDocument();
expect(screen.queryByText("Order 1a")).not.toBeInTheDocument();
expect(screen.getByRole("list")).not.toHaveAttribute("aria-busy", "true");
const pageOf = (n, totalPages = 3) => ({
orders: [
{ id: n * 10 + 1, item: "Order " + n + "a" },
{ id: n * 10 + 2, item: "Order " + n + "b" },
],
totalPages,
});
const pageIn = (url) => Number(url.split("page=")[1]);
mockFetch((url) => pageOf(pageIn(url), 2));
render(<App />);
await screen.findByText("Order 1a");
await user.click(screen.getByRole("button", { name: "Next" }));
expect(await screen.findByText("Page 2 of 2")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Next" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Previous" })).toBeEnabled();
const pageOf = (n, totalPages = 3) => ({
orders: [
{ id: n * 10 + 1, item: "Order " + n + "a" },
{ id: n * 10 + 2, item: "Order " + n + "b" },
],
totalPages,
});
const pageIn = (url) => Number(url.split("page=")[1]);
const fetch = mockFetch((url) => pageOf(pageIn(url)));
render(<App />);
await screen.findByText("Order 1a");
await user.click(screen.getByRole("button", { name: "Next" }));
await screen.findByText("Order 2a");
await user.click(screen.getByRole("button", { name: "Previous" }));
expect(await screen.findByText("Page 1 of 3")).toBeInTheDocument();
expect(screen.getByText("Order 1a")).toBeInTheDocument();
expect(fetch.calls.map((call) => pageIn(call.url))).toEqual([1, 2, 1]);
const pageOf = (n, totalPages = 3) => ({
orders: [
{ id: n * 10 + 1, item: "Order " + n + "a" },
{ id: n * 10 + 2, item: "Order " + n + "b" },
],
totalPages,
});
const pageIn = (url) => Number(url.split("page=")[1]);
const second = deferred();
const fetch = mockFetch((url) => (pageIn(url) === 2 ? second.promise : pageOf(pageIn(url))));
render(<App />);
await screen.findByText("Order 1a");
await user.click(screen.getByRole("button", { name: "Next" }));
await user.click(screen.getByRole("button", { name: "Next" }));
expect(fetch.calls.map((call) => pageIn(call.url))).toEqual([1, 2, 3]);
expect(await screen.findByText("Page 3 of 3")).toBeInTheDocument();
const pageOf = (n, totalPages = 3) => ({
orders: [
{ id: n * 10 + 1, item: "Order " + n + "a" },
{ id: n * 10 + 2, item: "Order " + n + "b" },
],
totalPages,
});
const pageIn = (url) => Number(url.split("page=")[1]);
const second = deferred();
mockFetch((url) => (pageIn(url) === 2 ? second.promise : pageOf(pageIn(url))));
render(<App />);
await screen.findByText("Order 1a");
await user.click(screen.getByRole("button", { name: "Next" }));
await user.click(screen.getByRole("button", { name: "Next" }));
expect(await screen.findByText("Order 3a")).toBeInTheDocument();
second.resolve(pageOf(2));
await flush();
expect(screen.queryByText("Order 2a")).not.toBeInTheDocument();
expect(screen.getByText("Page 3 of 3")).toBeInTheDocument();
const pageOf = (n, totalPages = 3) => ({
orders: [
{ id: n * 10 + 1, item: "Order " + n + "a" },
{ id: n * 10 + 2, item: "Order " + n + "b" },
],
totalPages,
});
const pageIn = (url) => Number(url.split("page=")[1]);
let tries = 0;
const fetch = mockFetch((url) => (pageIn(url) === 2 && ++tries === 1 ? fail(500) : pageOf(pageIn(url))));
render(<App />);
await screen.findByText("Order 1a");
await user.click(screen.getByRole("button", { name: "Next" }));
expect(await screen.findByText("Couldn't load page 2.")).toBeInTheDocument();
expect(screen.getByText("Order 1a")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Retry" }));
expect(await screen.findByText("Order 2a")).toBeInTheDocument();
expect(screen.queryByText("Couldn't load page 2.")).not.toBeInTheDocument();
expect(fetch.calls.map((call) => pageIn(call.url))).toEqual([1, 2, 2]);
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 Paginated list. 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.