Build LikeButton, the default export, for a post on a small tea shop's blog. It takes postId (default 1), initialLiked (default false), and initialLikes (default 12). It shows a toggle button named Like with aria-pressed set to whether you like the post, and the count as 12 likes (always likes, to keep it simple).
A click flips the like at once: aria-pressed and the count change before the server answers, by one up or one down. The click also sends POST /api/posts/:postId/like with the JSON body { "liked": true } or { "liked": false }, the value the click asked for. The server answers with the saved state, like { liked: true, likes: 13 }, and once it has answered, show what it says.
If a save fails (fetch rejects, or the response isn't ok), that click is undone and Couldn't save your like. shows in an element with role="alert". The next click hides the message.
People click fast. The button stays enabled while saves are pending, and every click builds on what's on screen, so like, unlike, like sends true, false, true. While saves are pending, the screen shows the last saved state with every pending click on top. A failed click must never undo the clicks that came after it: when all saves have finished, the screen shows what the server last said.
<LikeButton postId={7} initialLiked={false} initialLikes={12} />
<button aria-pressed="true">Like</button>
<p>13 likes</p>
render(<App postId={7} initialLiked={true} initialLikes={30} />);
expect(screen.getByRole("button", { name: "Like" })).toHaveAttribute("aria-pressed", "true");
expect(screen.getByText("30 likes")).toBeInTheDocument();
const server = { liked: false, likes: 12 };
const answers = [];
const fetch = mockFetch((url, init) => {
const answer = deferred();
answers.push(answer);
return answer.promise.then((saved) => {
if (!saved) return fail(500);
const { liked } = JSON.parse(init.body);
if (liked !== server.liked) server.likes += liked ? 1 : -1;
server.liked = liked;
return { ...server };
});
});
render(<App postId={7} initialLiked={false} initialLikes={12} />);
await user.click(screen.getByRole("button", { name: "Like" }));
expect(answers).toHaveLength(1);
expect(screen.getByRole("button", { name: "Like" })).toHaveAttribute("aria-pressed", "true");
expect(screen.getByText("13 likes")).toBeInTheDocument();
const server = { liked: false, likes: 12 };
const answers = [];
const fetch = mockFetch((url, init) => {
const answer = deferred();
answers.push(answer);
return answer.promise.then((saved) => {
if (!saved) return fail(500);
const { liked } = JSON.parse(init.body);
if (liked !== server.liked) server.likes += liked ? 1 : -1;
server.liked = liked;
return { ...server };
});
});
render(<App postId={7} initialLiked={false} initialLikes={12} />);
const button = screen.getByRole("button", { name: "Like" });
await user.click(button);
expect(fetch.calls[0].url).toBe("/api/posts/7/like");
expect(fetch.calls[0].method).toBe("POST");
expect(JSON.parse(fetch.calls[0].body)).toEqual({ liked: true });
answers[0].resolve(true);
await flush();
await user.click(button);
expect(JSON.parse(fetch.calls[1].body)).toEqual({ liked: false });
mockFetch(() => ({ liked: true, likes: 40 }));
render(<App postId={7} initialLiked={false} initialLikes={12} />);
await user.click(screen.getByRole("button", { name: "Like" }));
expect(await screen.findByText("40 likes")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Like" })).toHaveAttribute("aria-pressed", "true");
const server = { liked: false, likes: 12 };
const answers = [];
const fetch = mockFetch((url, init) => {
const answer = deferred();
answers.push(answer);
return answer.promise.then((saved) => {
if (!saved) return fail(500);
const { liked } = JSON.parse(init.body);
if (liked !== server.liked) server.likes += liked ? 1 : -1;
server.liked = liked;
return { ...server };
});
});
render(<App postId={7} initialLiked={false} initialLikes={12} />);
await user.click(screen.getByRole("button", { name: "Like" }));
answers[0].resolve(false);
expect(await screen.findByRole("alert")).toHaveTextContent("Couldn't save your like.");
expect(screen.getByText("12 likes")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Like" })).toHaveAttribute("aria-pressed", "false");
mockFetch(() => {
throw new TypeError("Failed to fetch");
});
render(<App postId={7} initialLiked={true} initialLikes={20} />);
await user.click(screen.getByRole("button", { name: "Like" }));
expect(await screen.findByText("Couldn't save your like.")).toBeInTheDocument();
expect(screen.getByText("20 likes")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Like" })).toHaveAttribute("aria-pressed", "true");
const server = { liked: false, likes: 12 };
const answers = [];
const fetch = mockFetch((url, init) => {
const answer = deferred();
answers.push(answer);
return answer.promise.then((saved) => {
if (!saved) return fail(500);
const { liked } = JSON.parse(init.body);
if (liked !== server.liked) server.likes += liked ? 1 : -1;
server.liked = liked;
return { ...server };
});
});
render(<App postId={7} initialLiked={false} initialLikes={12} />);
const button = screen.getByRole("button", { name: "Like" });
await user.click(button);
answers[0].resolve(false);
await screen.findByText("Couldn't save your like.");
await user.click(button);
expect(screen.queryByText("Couldn't save your like.")).not.toBeInTheDocument();
answers[1].resolve(true);
await flush();
expect(screen.getByText("13 likes")).toBeInTheDocument();
const server = { liked: false, likes: 12 };
const answers = [];
const fetch = mockFetch((url, init) => {
const answer = deferred();
answers.push(answer);
return answer.promise.then((saved) => {
if (!saved) return fail(500);
const { liked } = JSON.parse(init.body);
if (liked !== server.liked) server.likes += liked ? 1 : -1;
server.liked = liked;
return { ...server };
});
});
render(<App postId={7} initialLiked={false} initialLikes={12} />);
const button = screen.getByRole("button", { name: "Like" });
await user.click(button);
await user.click(button);
expect(screen.getByText("12 likes")).toBeInTheDocument();
await user.click(button);
expect(button).toBeEnabled();
expect(button).toHaveAttribute("aria-pressed", "true");
expect(screen.getByText("13 likes")).toBeInTheDocument();
expect(fetch.calls.map((call) => JSON.parse(call.body).liked)).toEqual([true, false, true]);
const server = { liked: false, likes: 12 };
const answers = [];
const fetch = mockFetch((url, init) => {
const answer = deferred();
answers.push(answer);
return answer.promise.then((saved) => {
if (!saved) return fail(500);
const { liked } = JSON.parse(init.body);
if (liked !== server.liked) server.likes += liked ? 1 : -1;
server.liked = liked;
return { ...server };
});
});
render(<App postId={7} initialLiked={false} initialLikes={12} />);
const button = screen.getByRole("button", { name: "Like" });
await user.click(button);
await user.click(button);
await user.click(button);
answers[1].resolve(true);
await flush();
answers[2].resolve(true);
await flush();
answers[0].resolve(false);
await flush();
expect(screen.getByText("Couldn't save your like.")).toBeInTheDocument();
expect(server).toEqual({ liked: true, likes: 13 });
expect(button).toHaveAttribute("aria-pressed", "true");
expect(screen.getByText("13 likes")).toBeInTheDocument();
const server = { liked: false, likes: 12 };
const answers = [];
const fetch = mockFetch((url, init) => {
const answer = deferred();
answers.push(answer);
return answer.promise.then((saved) => {
if (!saved) return fail(500);
const { liked } = JSON.parse(init.body);
if (liked !== server.liked) server.likes += liked ? 1 : -1;
server.liked = liked;
return { ...server };
});
});
render(<App postId={7} initialLiked={false} initialLikes={12} />);
const button = screen.getByRole("button", { name: "Like" });
await user.click(button);
await user.click(button);
answers[0].resolve(true);
await flush();
expect(button).toHaveAttribute("aria-pressed", "false");
expect(screen.getByText("12 likes")).toBeInTheDocument();
answers[1].resolve(true);
await flush();
expect(button).toHaveAttribute("aria-pressed", "false");
expect(screen.getByText("12 likes")).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 Optimistic like with rollback. 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.