The shop's back office shows stock, orders, and suppliers in tables that all look alike. Build Table, a named export of App.jsx, that draws any of them from a description of its columns. The default export, App, is only for the Preview.
Table takes caption (a string), columns (an array of { key, header, render }, where render is optional), and rows (an array of objects, each with a unique id).
Draw a <table> with the caption in a <caption>. The head has one row with a <th scope="col"> per column, showing its header, in the order of columns.
The body has one <tr> per row, in order. Each row has one <td> per column, in the order of columns: if the column has a render function, the cell shows render(row) (it gets the whole row, and may return text or JSX), and otherwise it shows row[key].
When rows is empty, the body has a single row with a single cell that reads No rows to show. and spans every column (colSpan set to the number of columns). Use keys so React logs no key warning.
<Table
caption="Teas in stock"
columns={[
{ key: "name", header: "Tea" },
{ key: "price", header: "Price", render: (row) => "$" + row.price.toFixed(2) },
]}
rows={[{ id: 1, name: "Sencha", price: 4 }]}
/>
// draws:
<table>
<caption>Teas in stock</caption>
<thead><tr><th scope="col">Tea</th><th scope="col">Price</th></tr></thead>
<tbody><tr><td>Sencha</td><td>$4.00</td></tr></tbody>
</table>
render(<Table caption="Teas in stock" columns={[{ key: "name", header: "Tea" }]} rows={[{ id: 1, name: "Sencha" }]} />);
expect(screen.getByRole("table", { name: "Teas in stock" })).toBeInTheDocument();
const columns = [
{ key: "name", header: "Tea" },
{ key: "price", header: "Price" },
{ key: "stock", header: "Stock" },
];
render(<Table caption="Teas" columns={columns} rows={[{ id: 1, name: "Sencha", price: 4, stock: 12 }]} />);
const headers = screen.getAllByRole("columnheader");
expect(headers.map((header) => header.textContent)).toEqual(["Tea", "Price", "Stock"]);
for (const header of headers) {
expect(header.tagName).toBe("TH");
expect(header).toHaveAttribute("scope", "col");
}
const columns = [
{ key: "name", header: "Tea" },
{ key: "origin", header: "From" },
];
const rows = [
{ id: 1, name: "Sencha", origin: "Japan" },
{ id: 2, name: "Oolong", origin: "Taiwan" },
];
render(<Table caption="Teas" columns={columns} rows={rows} />);
const bodyRows = screen.getAllByRole("row").slice(1);
expect(bodyRows).toHaveLength(2);
expect(within(bodyRows[0]).getAllByRole("cell").map((cell) => cell.textContent)).toEqual(["Sencha", "Japan"]);
expect(within(bodyRows[1]).getAllByRole("cell").map((cell) => cell.textContent)).toEqual(["Oolong", "Taiwan"]);
const columns = [
{ key: "name", header: "Tea" },
{ key: "price", header: "Price", render: (row) => "$" + row.price.toFixed(2) },
{ key: "stock", header: "Stock", render: (row) => (row.stock < 5 ? <strong>Low</strong> : row.stock) },
];
const rows = [
{ id: 1, name: "Sencha", price: 4, stock: 12 },
{ id: 2, name: "Oolong", price: 6.5, stock: 3 },
];
render(<Table caption="Teas" columns={columns} rows={rows} />);
const bodyRows = screen.getAllByRole("row").slice(1);
expect(within(bodyRows[0]).getAllByRole("cell").map((cell) => cell.textContent)).toEqual(["Sencha", "$4.00", "12"]);
const lowCell = within(bodyRows[1]).getAllByRole("cell")[2];
expect(within(lowCell).getByText("Low").tagName).toBe("STRONG");
const columns = [{ key: "label", header: "Tea", render: (row) => row.name + " (" + row.origin + ")" }];
render(<Table caption="Teas" columns={columns} rows={[{ id: 7, name: "Sencha", origin: "Japan" }]} />);
expect(screen.getByRole("cell", { name: "Sencha (Japan)" })).toBeInTheDocument();
const rows = [{ id: 1, name: "Sencha", origin: "Japan" }];
const { rerender } = render(<Table caption="Teas" columns={[{ key: "name", header: "Tea" }, { key: "origin", header: "From" }]} rows={rows} />);
rerender(<Table caption="Teas" columns={[{ key: "origin", header: "From" }, { key: "name", header: "Tea" }]} rows={rows} />);
expect(screen.getAllByRole("columnheader").map((header) => header.textContent)).toEqual(["From", "Tea"]);
expect(screen.getAllByRole("cell").map((cell) => cell.textContent)).toEqual(["Japan", "Sencha"]);
const columns = [
{ key: "name", header: "Tea" },
{ key: "price", header: "Price" },
{ key: "stock", header: "Stock" },
];
render(<Table caption="Teas" columns={columns} rows={[]} />);
expect(screen.getAllByRole("row")).toHaveLength(2);
const cells = screen.getAllByRole("cell");
expect(cells).toHaveLength(1);
expect(cells[0]).toHaveTextContent("No rows to show.");
expect(cells[0]).toHaveAttribute("colspan", "3");
const columns = [
{ key: "name", header: "Tea" },
{ key: "stock", header: "Stock" },
];
render(<Table caption="Teas" columns={columns} rows={[{ id: 1, name: "Matcha", stock: 0 }]} />);
expect(screen.getAllByRole("cell").map((cell) => cell.textContent)).toEqual(["Matcha", "0"]);
const columns = [
{ key: "name", header: "Tea" },
{ key: "price", header: "Price", render: (row) => "$" + row.price.toFixed(2) },
];
const rows = [
{ id: 1, name: "Sencha", price: 4 },
{ id: 2, name: "Oolong", price: 6.5 },
];
render(<Table caption="Teas" columns={columns} rows={rows} />);
expect(logs.join("\n")).not.toMatch(/unique "key"/);
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 Table from a columns config. 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.