The tea shop's stock page lists its teas in a table. Build App: it takes teas, a list of { id, name, origin, price } (price is a number), and shows them in a <table> with three columns, Name, Origin, and Price. When teas isn't given, as in the Preview, show a few of your own.
Each header cell is a <th scope="col"> holding a <button> with the column's name. Prices show as dollars with two decimals, like $8.50. At first the rows are in the order they were given.
Clicking a column's button sorts the rows by that column, ascending, and gives its header aria-sort="ascending". Clicking the same button again sorts descending (aria-sort="descending"), and again flips it back. Clicking another column sorts by that one, ascending, and the old header loses its aria-sort. Only the sorted column's header has aria-sort.
Names and origins sort alphabetically, prices as numbers, so $8.50 comes before $12.00. Rows that tie keep the order they were given, in both directions.
Above the table, a text box labelled Filter teas keeps only the rows whose name or origin contains its text, ignoring case and the spaces around it, still in the current sort order. When no row matches, the table body shows one row with one cell: No teas match. Don't change the teas array you're given.
<App teas={[
{ id: 1, name: "Sencha", origin: "Japan", price: 12 },
{ id: 2, name: "Assam", origin: "India", price: 8.5 },
]} />
// after clicking Price:
<th scope="col" aria-sort="ascending"><button>Price</button></th>
// rows: Assam, India, $8.50 | Sencha, Japan, $12.00
const teas = [
{ id: 1, name: "Sencha", origin: "Japan", price: 12 },
{ id: 2, name: "Assam", origin: "India", price: 8.5 },
{ id: 3, name: "Gyokuro", origin: "Japan", price: 30 },
{ id: 4, name: "Darjeeling", origin: "India", price: 14 },
{ id: 5, name: "Rooibos", origin: "South Africa", price: 6 },
];
const column = (n) => screen.getAllByRole("row").slice(1).map((row) => within(row).getAllByRole("cell")[n].textContent);
render(<App teas={teas} />);
expect(column(0)).toEqual(["Sencha", "Assam", "Gyokuro", "Darjeeling", "Rooibos"]);
expect(column(2)).toEqual(["$12.00", "$8.50", "$30.00", "$14.00", "$6.00"]);
for (const header of screen.getAllByRole("columnheader")) {
expect(header).toHaveAttribute("scope", "col");
expect(header).not.toHaveAttribute("aria-sort");
}
const teas = [
{ id: 1, name: "Sencha", origin: "Japan", price: 12 },
{ id: 2, name: "Assam", origin: "India", price: 8.5 },
{ id: 3, name: "Gyokuro", origin: "Japan", price: 30 },
{ id: 4, name: "Darjeeling", origin: "India", price: 14 },
{ id: 5, name: "Rooibos", origin: "South Africa", price: 6 },
];
const column = (n) => screen.getAllByRole("row").slice(1).map((row) => within(row).getAllByRole("cell")[n].textContent);
render(<App teas={teas} />);
await user.click(screen.getByRole("button", { name: "Name" }));
expect(column(0)).toEqual(["Assam", "Darjeeling", "Gyokuro", "Rooibos", "Sencha"]);
expect(screen.getByRole("columnheader", { name: "Name" })).toHaveAttribute("aria-sort", "ascending");
const teas = [
{ id: 1, name: "Sencha", origin: "Japan", price: 12 },
{ id: 2, name: "Assam", origin: "India", price: 8.5 },
{ id: 3, name: "Gyokuro", origin: "Japan", price: 30 },
{ id: 4, name: "Darjeeling", origin: "India", price: 14 },
{ id: 5, name: "Rooibos", origin: "South Africa", price: 6 },
];
const column = (n) => screen.getAllByRole("row").slice(1).map((row) => within(row).getAllByRole("cell")[n].textContent);
render(<App teas={teas} />);
const name = screen.getByRole("button", { name: "Name" });
await user.click(name);
await user.click(name);
expect(column(0)).toEqual(["Sencha", "Rooibos", "Gyokuro", "Darjeeling", "Assam"]);
expect(screen.getByRole("columnheader", { name: "Name" })).toHaveAttribute("aria-sort", "descending");
await user.click(name);
expect(column(0)).toEqual(["Assam", "Darjeeling", "Gyokuro", "Rooibos", "Sencha"]);
expect(screen.getByRole("columnheader", { name: "Name" })).toHaveAttribute("aria-sort", "ascending");
const teas = [
{ id: 1, name: "Sencha", origin: "Japan", price: 12 },
{ id: 2, name: "Assam", origin: "India", price: 8.5 },
{ id: 3, name: "Gyokuro", origin: "Japan", price: 30 },
{ id: 4, name: "Darjeeling", origin: "India", price: 14 },
{ id: 5, name: "Rooibos", origin: "South Africa", price: 6 },
];
const column = (n) => screen.getAllByRole("row").slice(1).map((row) => within(row).getAllByRole("cell")[n].textContent);
render(<App teas={teas} />);
await user.click(screen.getByRole("button", { name: "Price" }));
expect(column(2)).toEqual(["$6.00", "$8.50", "$12.00", "$14.00", "$30.00"]);
await user.click(screen.getByRole("button", { name: "Price" }));
expect(column(2)).toEqual(["$30.00", "$14.00", "$12.00", "$8.50", "$6.00"]);
const teas = [
{ id: 1, name: "Sencha", origin: "Japan", price: 12 },
{ id: 2, name: "Assam", origin: "India", price: 8.5 },
{ id: 3, name: "Gyokuro", origin: "Japan", price: 30 },
{ id: 4, name: "Darjeeling", origin: "India", price: 14 },
{ id: 5, name: "Rooibos", origin: "South Africa", price: 6 },
];
const column = (n) => screen.getAllByRole("row").slice(1).map((row) => within(row).getAllByRole("cell")[n].textContent);
render(<App teas={teas} />);
const origin = screen.getByRole("button", { name: "Origin" });
await user.click(origin);
expect(column(0)).toEqual(["Assam", "Darjeeling", "Sencha", "Gyokuro", "Rooibos"]);
await user.click(origin);
expect(column(0)).toEqual(["Rooibos", "Sencha", "Gyokuro", "Assam", "Darjeeling"]);
const teas = [
{ id: 1, name: "Sencha", origin: "Japan", price: 12 },
{ id: 2, name: "Assam", origin: "India", price: 8.5 },
{ id: 3, name: "Gyokuro", origin: "Japan", price: 30 },
{ id: 4, name: "Darjeeling", origin: "India", price: 14 },
{ id: 5, name: "Rooibos", origin: "South Africa", price: 6 },
];
const column = (n) => screen.getAllByRole("row").slice(1).map((row) => within(row).getAllByRole("cell")[n].textContent);
render(<App teas={teas} />);
await user.click(screen.getByRole("button", { name: "Name" }));
await user.click(screen.getByRole("button", { name: "Price" }));
expect(column(0)).toEqual(["Rooibos", "Assam", "Sencha", "Darjeeling", "Gyokuro"]);
expect(screen.getByRole("columnheader", { name: "Price" })).toHaveAttribute("aria-sort", "ascending");
expect(screen.getByRole("columnheader", { name: "Name" })).not.toHaveAttribute("aria-sort");
const teas = [
{ id: 1, name: "Sencha", origin: "Japan", price: 12 },
{ id: 2, name: "Assam", origin: "India", price: 8.5 },
{ id: 3, name: "Gyokuro", origin: "Japan", price: 30 },
{ id: 4, name: "Darjeeling", origin: "India", price: 14 },
{ id: 5, name: "Rooibos", origin: "South Africa", price: 6 },
];
const column = (n) => screen.getAllByRole("row").slice(1).map((row) => within(row).getAllByRole("cell")[n].textContent);
render(<App teas={teas} />);
await user.click(screen.getByRole("button", { name: "Price" }));
await user.click(screen.getByRole("button", { name: "Price" }));
const filter = screen.getByLabelText("Filter teas");
await user.type(filter, " JA ");
expect(column(0)).toEqual(["Gyokuro", "Sencha"]);
await user.clear(filter);
await user.type(filter, "dar");
expect(column(0)).toEqual(["Darjeeling"]);
const teas = [
{ id: 1, name: "Sencha", origin: "Japan", price: 12 },
{ id: 2, name: "Assam", origin: "India", price: 8.5 },
{ id: 3, name: "Gyokuro", origin: "Japan", price: 30 },
{ id: 4, name: "Darjeeling", origin: "India", price: 14 },
{ id: 5, name: "Rooibos", origin: "South Africa", price: 6 },
];
const column = (n) => screen.getAllByRole("row").slice(1).map((row) => within(row).getAllByRole("cell")[n].textContent);
render(<App teas={teas} />);
await user.type(screen.getByLabelText("Filter teas"), "matcha");
const rows = screen.getAllByRole("row").slice(1);
expect(rows).toHaveLength(1);
expect(within(rows[0]).getAllByRole("cell").map((cell) => cell.textContent)).toEqual(["No teas match"]);
const teas = [
{ id: 1, name: "Sencha", origin: "Japan", price: 12 },
{ id: 2, name: "Assam", origin: "India", price: 8.5 },
{ id: 3, name: "Gyokuro", origin: "Japan", price: 30 },
{ id: 4, name: "Darjeeling", origin: "India", price: 14 },
{ id: 5, name: "Rooibos", origin: "South Africa", price: 6 },
];
const column = (n) => screen.getAllByRole("row").slice(1).map((row) => within(row).getAllByRole("cell")[n].textContent);
const frozen = Object.freeze(teas.map((tea) => Object.freeze({ ...tea })));
render(<App teas={frozen} />);
await user.click(screen.getByRole("button", { name: "Price" }));
await user.click(screen.getByRole("button", { name: "Price" }));
expect(column(0)).toEqual(["Gyokuro", "Darjeeling", "Sencha", "Assam", "Rooibos"]);
expect(frozen.map((tea) => tea.name)).toEqual(["Sencha", "Assam", "Gyokuro", "Darjeeling", "Rooibos"]);
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 Sortable, filterable table. 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.