Under each tea, customers leave comments, and reply to each other's. Build CommentThread, a named export of App.jsx, which takes comments: an array of { id, author, text, replies }, where replies is an array of comments of the same shape, as deep as people like. replies may be missing, which means none. The default export, App, is only for the Preview.
Draw the comments in a <ul>, one <li> per comment, in order. Each <li> shows the author in a <strong> and the text in a <p>. A comment's replies go in another <ul> inside its own <li>, so every level of replies sits one list deeper.
A comment with replies also has a button that hides and shows them. It counts every reply under the comment, at every depth, and reads Hide 3 replies while they're shown, or Show 3 replies while they're hidden (1 reply for one). Its aria-expanded is true while they're shown. Replies start shown, and hidden replies aren't on the page. A comment with no replies has no button.
Each comment's button works on its own: hiding one thread leaves the others as they are. When comments is empty, show the paragraph No comments yet. instead of the list.
<CommentThread
comments={[
{ id: 1, author: "Asha", text: "Love this sencha.", replies: [
{ id: 2, author: "Ben", text: "Try it iced.", replies: [] },
] },
]}
/>
// draws:
<ul>
<li>
<strong>Asha</strong>
<p>Love this sencha.</p>
<button aria-expanded="true">Hide 1 reply</button>
<ul>
<li><strong>Ben</strong><p>Try it iced.</p></li>
</ul>
</li>
</ul>
const comments = [
{ id: 1, author: "Asha", text: "Love this sencha.", replies: [
{ id: 2, author: "Ben", text: "Try it iced.", replies: [{ id: 3, author: "Chen", text: "Will do.", replies: [] }] },
] },
{ id: 7, author: "Gus", text: "Great shop." },
];
render(<CommentThread comments={comments} />);
for (const text of ["Love this sencha.", "Try it iced.", "Will do.", "Great shop."]) {
expect(screen.getByText(text)).toBeInTheDocument();
}
for (const author of ["Asha", "Ben", "Chen", "Gus"]) {
expect(screen.getByText(author).tagName).toBe("STRONG");
}
const comments = [
{ id: 1, author: "Asha", text: "Love this sencha.", replies: [
{ id: 2, author: "Ben", text: "Try it iced.", replies: [{ id: 3, author: "Chen", text: "Will do.", replies: [] }] },
] },
];
render(<CommentThread comments={comments} />);
const item = (text) => screen.getByText(text).closest("li");
expect(item("Try it iced.").parentElement.tagName).toBe("UL");
expect(item("Try it iced.").parentElement.closest("li")).toBe(item("Love this sencha."));
expect(item("Will do.").parentElement.closest("li")).toBe(item("Try it iced."));
expect(item("Love this sencha.").parentElement.closest("li")).toBeNull();
const comments = [
{ id: 1, author: "Asha", text: "Love this sencha.", replies: [
{ id: 2, author: "Ben", text: "Try it iced.", replies: [{ id: 3, author: "Chen", text: "Will do.", replies: [] }] },
{ id: 4, author: "Dara", text: "Same here.", replies: [] },
] },
{ id: 5, author: "Eli", text: "Is the oolong back?", replies: [{ id: 6, author: "Fen", text: "Next week.", replies: [] }] },
];
render(<CommentThread comments={comments} />);
const item = (text) => screen.getByText(text).closest("li");
expect(within(item("Love this sencha.")).getByRole("button", { name: "Hide 3 replies" })).toBeInTheDocument();
expect(within(item("Try it iced.")).getByRole("button", { name: "Hide 1 reply" })).toBeInTheDocument();
expect(within(item("Is the oolong back?")).getByRole("button", { name: "Hide 1 reply" })).toBeInTheDocument();
const comments = [
{ id: 1, author: "Asha", text: "Love this sencha.", replies: [{ id: 4, author: "Dara", text: "Same here.", replies: [] }] },
{ id: 7, author: "Gus", text: "Great shop." },
];
render(<CommentThread comments={comments} />);
const item = (text) => screen.getByText(text).closest("li");
expect(within(item("Same here.")).queryByRole("button")).not.toBeInTheDocument();
expect(within(item("Great shop.")).queryByRole("button")).not.toBeInTheDocument();
const comments = [
{ id: 1, author: "Asha", text: "Love this sencha.", replies: [
{ id: 2, author: "Ben", text: "Try it iced.", replies: [{ id: 3, author: "Chen", text: "Will do.", replies: [] }] },
{ id: 4, author: "Dara", text: "Same here.", replies: [] },
] },
];
render(<CommentThread comments={comments} />);
const button = screen.getByRole("button", { name: "Hide 3 replies" });
expect(button).toHaveAttribute("aria-expanded", "true");
await user.click(button);
expect(screen.queryByText("Try it iced.")).not.toBeInTheDocument();
expect(screen.queryByText("Will do.")).not.toBeInTheDocument();
expect(screen.queryByText("Same here.")).not.toBeInTheDocument();
expect(screen.getByText("Love this sencha.")).toBeInTheDocument();
const shown = screen.getByRole("button", { name: "Show 3 replies" });
expect(shown).toHaveAttribute("aria-expanded", "false");
const comments = [
{ id: 5, author: "Eli", text: "Is the oolong back?", replies: [{ id: 6, author: "Fen", text: "Next week.", replies: [] }] },
];
render(<CommentThread comments={comments} />);
await user.click(screen.getByRole("button", { name: "Hide 1 reply" }));
await user.click(screen.getByRole("button", { name: "Show 1 reply" }));
expect(screen.getByText("Next week.")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Hide 1 reply" })).toHaveAttribute("aria-expanded", "true");
const comments = [
{ id: 1, author: "Asha", text: "Love this sencha.", replies: [
{ id: 2, author: "Ben", text: "Try it iced.", replies: [{ id: 3, author: "Chen", text: "Will do.", replies: [] }] },
{ id: 4, author: "Dara", text: "Same here.", replies: [] },
] },
{ id: 5, author: "Eli", text: "Is the oolong back?", replies: [{ id: 6, author: "Fen", text: "Next week.", replies: [] }] },
];
render(<CommentThread comments={comments} />);
const item = (text) => screen.getByText(text).closest("li");
await user.click(within(item("Is the oolong back?")).getByRole("button", { name: "Hide 1 reply" }));
expect(screen.queryByText("Next week.")).not.toBeInTheDocument();
expect(screen.getByText("Try it iced.")).toBeInTheDocument();
await user.click(within(item("Try it iced.")).getByRole("button", { name: "Hide 1 reply" }));
expect(screen.queryByText("Will do.")).not.toBeInTheDocument();
expect(screen.getByText("Same here.")).toBeInTheDocument();
expect(within(item("Love this sencha.")).getByRole("button", { name: "Hide 3 replies" })).toBeInTheDocument();
render(<CommentThread comments={[]} />);
expect(screen.getByText("No comments yet.")).toBeInTheDocument();
expect(screen.queryByRole("list")).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 Nested comment thread. 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.