Build App: the questions on the tea shop's help page, where only one answer is open at a time. It takes items, a list of { id, title, content } (ids are short strings like "delivery"), and shows each title as a button inside an <h3>, with its answer in a panel under it. When items isn't given, as in the Preview, show a few questions of your own.
Every answer starts closed. Clicking a title opens its answer and closes the one that was open. Clicking the title of the open answer closes it, so none is open.
Each title button has aria-expanded, "true" when its answer is open and "false" otherwise, and aria-controls set to the id of its panel. Each panel is a <div role="region"> with aria-labelledby set to its button's id. Panels are always in the page: a closed one has the hidden attribute.
A page can hold more than one of these lists, so every id must be unique on the whole page, not only inside one list. Each list's answers open and close on their own.
<App items={[{ id: "delivery", title: "How long does delivery take?", content: "Two to three working days." }]} />
// renders, with the answer open
// ("r1" stands for a prefix that's unique to this list):
<h3>
<button id="r1-delivery-title" aria-expanded="true" aria-controls="r1-delivery-panel">
How long does delivery take?
</button>
</h3>
<div role="region" id="r1-delivery-panel" aria-labelledby="r1-delivery-title">
Two to three working days.
</div>
const items = [
{ id: "delivery", title: "How long does delivery take?", content: "Two to three working days." },
{ id: "returns", title: "Can I return an open tin?", content: "Yes, within 30 days." },
{ id: "storage", title: "How should I store loose tea?", content: "In a sealed tin, away from light." },
];
render(<App items={items} />);
const headings = screen.getAllByRole("heading", { level: 3 });
expect(headings).toHaveLength(3);
items.forEach((item, index) => {
expect(within(headings[index]).getByRole("button", { name: item.title })).toBeInTheDocument();
});
const items = [
{ id: "delivery", title: "How long does delivery take?", content: "Two to three working days." },
{ id: "returns", title: "Can I return an open tin?", content: "Yes, within 30 days." },
{ id: "storage", title: "How should I store loose tea?", content: "In a sealed tin, away from light." },
];
render(<App items={items} />);
for (const item of items) {
expect(screen.getByRole("button", { name: item.title })).toHaveAttribute("aria-expanded", "false");
expect(screen.getByText(item.content)).not.toBeVisible();
}
const items = [
{ id: "delivery", title: "How long does delivery take?", content: "Two to three working days." },
{ id: "returns", title: "Can I return an open tin?", content: "Yes, within 30 days." },
{ id: "storage", title: "How should I store loose tea?", content: "In a sealed tin, away from light." },
];
render(<App items={items} />);
const button = screen.getByRole("button", { name: "Can I return an open tin?" });
await user.click(button);
expect(button).toHaveAttribute("aria-expanded", "true");
expect(screen.getByText("Yes, within 30 days.")).toBeVisible();
expect(screen.getByRole("region", { name: "Can I return an open tin?" })).toHaveTextContent("Yes, within 30 days.");
const items = [
{ id: "delivery", title: "How long does delivery take?", content: "Two to three working days." },
{ id: "returns", title: "Can I return an open tin?", content: "Yes, within 30 days." },
{ id: "storage", title: "How should I store loose tea?", content: "In a sealed tin, away from light." },
];
render(<App items={items} />);
const delivery = screen.getByRole("button", { name: "How long does delivery take?" });
const storage = screen.getByRole("button", { name: "How should I store loose tea?" });
await user.click(delivery);
await user.click(storage);
expect(delivery).toHaveAttribute("aria-expanded", "false");
expect(screen.getByText("Two to three working days.")).not.toBeVisible();
expect(storage).toHaveAttribute("aria-expanded", "true");
expect(screen.getByText("In a sealed tin, away from light.")).toBeVisible();
const items = [
{ id: "delivery", title: "How long does delivery take?", content: "Two to three working days." },
{ id: "returns", title: "Can I return an open tin?", content: "Yes, within 30 days." },
];
render(<App items={items} />);
const delivery = screen.getByRole("button", { name: "How long does delivery take?" });
await user.click(delivery);
await user.click(delivery);
expect(delivery).toHaveAttribute("aria-expanded", "false");
expect(screen.getByText("Two to three working days.")).not.toBeVisible();
expect(screen.getByRole("button", { name: "Can I return an open tin?" })).toHaveAttribute("aria-expanded", "false");
const items = [
{ id: "delivery", title: "How long does delivery take?", content: "Two to three working days." },
{ id: "returns", title: "Can I return an open tin?", content: "Yes, within 30 days." },
{ id: "storage", title: "How should I store loose tea?", content: "In a sealed tin, away from light." },
];
render(<App items={items} />);
for (const item of items) {
const button = screen.getByRole("button", { name: item.title });
const panel = document.getElementById(button.getAttribute("aria-controls"));
expect(panel).toBeInTheDocument();
expect(panel).toHaveAttribute("role", "region");
expect(panel).toHaveTextContent(item.content);
expect(button.id).toBeTruthy();
expect(panel).toHaveAttribute("aria-labelledby", button.id);
}
const items = [
{ id: "delivery", title: "How long does delivery take?", content: "Two to three working days." },
{ id: "returns", title: "Can I return an open tin?", content: "Yes, within 30 days." },
];
render(
<>
<App items={items} />
<App items={items} />
</>
);
const ids = screen.getAllByRole("button").flatMap((button) => [button.id, button.getAttribute("aria-controls")]);
expect(ids.every(Boolean)).toBe(true);
expect(new Set(ids).size).toBe(8);
const [first, second] = screen.getAllByRole("button", { name: "How long does delivery take?" });
await user.click(first);
expect(first).toHaveAttribute("aria-expanded", "true");
expect(second).toHaveAttribute("aria-expanded", "false");
expect(document.getElementById(second.getAttribute("aria-controls"))).not.toBeVisible();
const items = [
{ id: "delivery", title: "How long does delivery take?", content: "Two to three working days." },
{ id: "returns", title: "Can I return an open tin?", content: "Yes, within 30 days." },
];
render(<App items={items} />);
await user.tab();
await user.tab();
expect(screen.getByRole("button", { name: "Can I return an open tin?" })).toHaveFocus();
await user.keyboard("{Enter}");
expect(screen.getByRole("button", { name: "Can I return an open tin?" })).toHaveAttribute("aria-expanded", "true");
expect(screen.getByText("Yes, within 30 days.")).toBeVisible();
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 Accordion with one panel open. 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.