Build Feed, the default export: the news feed of a small tea shop's blog. When it appears, it fetches /api/posts?page=1. Every answer is JSON like { posts: [{ id: 1, title: "New oolong in stock" }], hasMore: true }.
Show every post's title in one list, one item each, in the order they came. Under it, a Load more button fetches the next page (page=2, then page=3, and so on) and adds its posts to the end of the list.
While a page loads, show Loading… (with the single … character) and disable Load more. A double click must fetch the next page only once. When an answer says hasMore: false, remove the button: there's nothing left to load.
New posts can push older ones onto the next page, so a page may repeat a post you already have (the same id). Show each post only once.
If a page fails to load (fetch rejects, or the response isn't ok), show Couldn't load posts. and keep the posts you have. Load more then asks for the same page again.
<ul>
<li>New oolong in stock</li>
<li>How we roast our hojicha</li>
</ul>
<button>Load more</button> // gone once hasMore is false
const postsOf = (n, hasMore = n < 3) => ({
posts: [
{ id: n * 10 + 1, title: "Post " + n + "a" },
{ id: n * 10 + 2, title: "Post " + n + "b" },
],
hasMore,
});
const pageIn = (url) => Number(url.split("page=")[1]);
const answer = deferred();
const fetch = mockFetch(() => answer.promise);
render(<App />);
expect(screen.getByText("Loading…")).toBeInTheDocument();
expect(fetch.calls.map((call) => call.url)).toEqual(["/api/posts?page=1"]);
answer.resolve(postsOf(1));
const items = await screen.findAllByRole("listitem");
expect(items.map((item) => item.textContent)).toEqual(["Post 1a", "Post 1b"]);
expect(screen.queryByText("Loading…")).not.toBeInTheDocument();
const postsOf = (n, hasMore = n < 3) => ({
posts: [
{ id: n * 10 + 1, title: "Post " + n + "a" },
{ id: n * 10 + 2, title: "Post " + n + "b" },
],
hasMore,
});
const pageIn = (url) => Number(url.split("page=")[1]);
const fetch = mockFetch((url) => postsOf(pageIn(url)));
render(<App />);
await screen.findByText("Post 1a");
await user.click(screen.getByRole("button", { name: "Load more" }));
await screen.findByText("Post 2a");
const items = screen.getAllByRole("listitem");
expect(items.map((item) => item.textContent)).toEqual(["Post 1a", "Post 1b", "Post 2a", "Post 2b"]);
expect(fetch.calls.map((call) => call.url)).toEqual(["/api/posts?page=1", "/api/posts?page=2"]);
const postsOf = (n, hasMore = n < 3) => ({
posts: [
{ id: n * 10 + 1, title: "Post " + n + "a" },
{ id: n * 10 + 2, title: "Post " + n + "b" },
],
hasMore,
});
const pageIn = (url) => Number(url.split("page=")[1]);
const fetch = mockFetch((url) => postsOf(pageIn(url), true));
render(<App />);
await screen.findByText("Post 1a");
await user.click(screen.getByRole("button", { name: "Load more" }));
await screen.findByText("Post 2a");
await user.click(screen.getByRole("button", { name: "Load more" }));
await screen.findByText("Post 3a");
expect(screen.getAllByRole("listitem")).toHaveLength(6);
expect(fetch.calls.map((call) => pageIn(call.url))).toEqual([1, 2, 3]);
const postsOf = (n, hasMore = n < 3) => ({
posts: [
{ id: n * 10 + 1, title: "Post " + n + "a" },
{ id: n * 10 + 2, title: "Post " + n + "b" },
],
hasMore,
});
const pageIn = (url) => Number(url.split("page=")[1]);
const second = deferred();
mockFetch((url) => (pageIn(url) === 2 ? second.promise : postsOf(1)));
render(<App />);
await screen.findByText("Post 1a");
await user.click(screen.getByRole("button", { name: "Load more" }));
expect(screen.getByRole("button", { name: "Load more" })).toBeDisabled();
expect(screen.getByText("Loading…")).toBeInTheDocument();
expect(screen.getByText("Post 1a")).toBeInTheDocument();
second.resolve(postsOf(2));
await screen.findByText("Post 2a");
expect(screen.getByRole("button", { name: "Load more" })).toBeEnabled();
expect(screen.queryByText("Loading…")).not.toBeInTheDocument();
const postsOf = (n, hasMore = n < 3) => ({
posts: [
{ id: n * 10 + 1, title: "Post " + n + "a" },
{ id: n * 10 + 2, title: "Post " + n + "b" },
],
hasMore,
});
const pageIn = (url) => Number(url.split("page=")[1]);
const second = deferred();
const fetch = mockFetch((url) => (pageIn(url) === 2 ? second.promise : postsOf(pageIn(url))));
render(<App />);
await screen.findByText("Post 1a");
await user.dblClick(screen.getByRole("button", { name: "Load more" }));
second.resolve(postsOf(2));
await screen.findByText("Post 2a");
await flush();
expect(fetch.calls.map((call) => pageIn(call.url))).toEqual([1, 2]);
const items = screen.getAllByRole("listitem");
expect(items.map((item) => item.textContent)).toEqual(["Post 1a", "Post 1b", "Post 2a", "Post 2b"]);
const postsOf = (n, hasMore = n < 3) => ({
posts: [
{ id: n * 10 + 1, title: "Post " + n + "a" },
{ id: n * 10 + 2, title: "Post " + n + "b" },
],
hasMore,
});
const pageIn = (url) => Number(url.split("page=")[1]);
mockFetch((url) => postsOf(pageIn(url), pageIn(url) < 2));
render(<App />);
await screen.findByText("Post 1a");
await user.click(screen.getByRole("button", { name: "Load more" }));
await screen.findByText("Post 2a");
expect(screen.queryByRole("button", { name: "Load more" })).not.toBeInTheDocument();
const postsOf = (n, hasMore = n < 3) => ({
posts: [
{ id: n * 10 + 1, title: "Post " + n + "a" },
{ id: n * 10 + 2, title: "Post " + n + "b" },
],
hasMore,
});
const pageIn = (url) => Number(url.split("page=")[1]);
mockFetch((url) =>
pageIn(url) === 1
? postsOf(1)
: { posts: [{ id: 12, title: "Post 1b" }, { id: 21, title: "Post 2a" }], hasMore: true },
);
render(<App />);
await screen.findByText("Post 1a");
await user.click(screen.getByRole("button", { name: "Load more" }));
await screen.findByText("Post 2a");
const items = screen.getAllByRole("listitem");
expect(items.map((item) => item.textContent)).toEqual(["Post 1a", "Post 1b", "Post 2a"]);
const postsOf = (n, hasMore = n < 3) => ({
posts: [
{ id: n * 10 + 1, title: "Post " + n + "a" },
{ id: n * 10 + 2, title: "Post " + n + "b" },
],
hasMore,
});
const pageIn = (url) => Number(url.split("page=")[1]);
let tries = 0;
const fetch = mockFetch((url) => (pageIn(url) === 2 && ++tries === 1 ? fail(500) : postsOf(pageIn(url))));
render(<App />);
await screen.findByText("Post 1a");
await user.click(screen.getByRole("button", { name: "Load more" }));
expect(await screen.findByText("Couldn't load posts.")).toBeInTheDocument();
expect(screen.getAllByRole("listitem")).toHaveLength(2);
await user.click(screen.getByRole("button", { name: "Load more" }));
await screen.findByText("Post 2a");
expect(screen.queryByText("Couldn't load posts.")).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 Load more feed. 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.