Gabriel Jiménez | Hace 5 días
En este artículo, vamos a aprender que elementos de una tabla debemos de enfocarnos en probar y lo pondremos en práctica usando React Testing Library. Al final del artículo serás capaz de detectar los escenarios críticos en una tabla.
Las tablas son elementos muy utilizados en la web para listar registros. Además, cada registro puede acompañarse de acciones dependiendo su estado o alguna lógica propia del negocio.
Los datos mostrados en cada una de las columnas son importantes, porque es la primera impresión que se llevan sobre el registro listado. Debemos asegurarnos que los datos de las columnas más importantes se muestren correctamente al usuario, de esta manera, le damos la seguridad y confianza al usuario de que todo anda bien.
La gran mayoría de registros de cierta forma mantiene un estado. Estos estados permiten que el usuario pueda realizar acciones sobre los registros. Por ejemplo, una compra guardada en un carrito, tendría el estado de “en espera del pago” y el usuario podría tener una acción de reanudar compra.
Cuando realizamos una acción sobre un registro, es importante mantener informado al usuario sobre si la acción se realizo correctamente o no.
Al realizar una acción no basta con mostrarle un mensaje de confirmación al usuario, hay que refrescar la tabla para que los registros muestran su información actualizada y el usuario pueda continuar operando.
React Testing Library es una librería que facilita el desarrollo de las pruebas en React. Con esta librería podemos validar la mayoría de los casos posibles de una tabla.
Si quieres saber más sobre React Testing Library, te dejo el siguiente artículo: Qué es React Testing Library y cómo funciona con Jest
// SimpleTable.jsx
export function SimpleTable() {
return (
<table>
<tbody>
<tr data-testid="row-1">
<td>Gabriel</td>
<td>[email protected]</td>
</tr>
<tr data-testid="row-2">
<td>Ana</td>
<td>[email protected]</td>
</tr>
</tbody>
</table>
);
}
// SimpleTable.test.jsx
import { render, screen, fireEvent, within } from "@testing-library/react";
import { SimpleTable } from "./SimpleTable";
describe("SimpleTable", () => {
it("selecciona una fila y valida sus campos", () => {
// Renderizamos el componente
render(<SimpleTable />);
// Seleccionamos solo la fila 1
const row = screen.getByTestId("row-1");
// Limitamos la búsqueda dentro de la fila 1
const utils = within(row);
expect(utils.getByText("Gabriel")).toBeInTheDocument();
expect(utils.getByText("[email protected]")).toBeInTheDocument();
});
});
import { useState } from "react";
export function SimpleTable() {
const [actionMessage, setActionMessage] = useState("");
const handleAction = (id, type) => {
setActionMessage(`Acción ${type} ejecutada en ID ${id}`);
};
return (
<div>
<table>
<tbody>
<tr data-testid="row-1">
<td>Gabriel</td>
<td>[email protected]</td>
<td>
<button onClick={() => handleAction(1, "A")}>Acción A</button>
<button onClick={() => handleAction(1, "B")}>Acción B</button>
</td>
</tr>
<tr data-testid="row-2">
<td>Ana</td>
<td>[email protected]</td>
<td>
<button onClick={() => handleAction(2, "A")}>Acción A</button>
<button onClick={() => handleAction(2, "B")}>Acción B</button>
</td>
</tr>
</tbody>
</table>
{actionMessage && <p data-testid="action-msg">{actionMessage}</p>}
</div>
);
}
import { render, screen, within, fireEvent } from "@testing-library/react";
import { SimpleTable } from "./SimpleTable";
describe("SimpleTable", () => {
it("ejecuta una acción en la fila y muestra el mensaje correspondiente", () => {
render(<SimpleTable />);
// Buscamos la fila 2
const row = screen.getByTestId("row-2");
const rowUtils = within(row);
// Ejecutamos la acción B
const actionBtn = rowUtils.getByText("Acción B");
fireEvent.click(actionBtn);
// Validamos el mensaje
expect(screen.getByTestId("action-msg"))
.toHaveTextContent("Acción B ejecutada en ID 2");
});
});
import { useState } from "react";
export function SimpleTable() {
const [actionMessage, setActionMessage] = useState("");
const handleAction = (id, type) => {
setActionMessage(`Acción ${type} ejecutada en ID ${id}`);
};
const goToDetail = (id) => {
document.location.href = `/detalles/${id}`;
};
return (
<div>
<table>
<tbody>
<tr data-testid="row-1">
<td>Gabriel</td>
<td>[email protected]</td>
<td>
<button onClick={() => handleAction(1, "A")}>Acción A</button>
<button onClick={() => handleAction(1, "B")}>Acción B</button>
<button onClick={() => goToDetail(1)}>Ir a detalle</button>
</td>
</tr>
<tr data-testid="row-2">
<td>Ana</td>
<td>[email protected]</td>
<td>
<button onClick={() => handleAction(2, "A")}>Acción A</button>
<button onClick={() => handleAction(2, "B")}>Acción B</button>
<button onClick={() => goToDetail(2)}>Ir a detalle</button>
</td>
</tr>
</tbody>
</table>
{actionMessage && <p data-testid="action-msg">{actionMessage}</p>}
</div>
);
}
import { render, screen, within, fireEvent } from "@testing-library/react";
import { SimpleTable } from "./SimpleTable";
describe("SimpleTable", () => {
it("redirecciona usando document.location.href", () => {
render(<SimpleTable />);
const row = screen.getByTestId("row-1");
const rowUtils = within(row);
// Hacemos clic en el botón "Ir a detalle"
fireEvent.click(rowUtils.getByText("Ir a detalle"));
// Validamos la nueva URL
expect(document.location.href).toContain("/detalles/1");
});
});
import { useState } from "react";
export function SimpleTable() {
const [actionMessage, setActionMessage] = useState("");
const handleAction = (id, type) => {
setActionMessage(`Acción ${type} ejecutada en ID ${id}`);
};
const goToDetail = (id) => {
document.location.href = `/detalles/${id}`;
};
const deleteRecord = async (id) => {
await fetch(`/api/records/${id}`, { method: "DELETE" });
setActionMessage(`Registro ${id} eliminado`);
};
return (
<div>
<table>
<tbody>
<tr data-testid="row-1">
<td>Gabriel</td>
<td>[email protected]</td>
<td>
<button onClick={() => handleAction(1, "A")}>Acción A</button>
<button onClick={() => handleAction(1, "B")}>Acción B</button>
<button onClick={() => goToDetail(1)}>Ir a detalle</button>
<button onClick={() => deleteRecord(1)}>Eliminar</button>
</td>
</tr>
<tr data-testid="row-2">
<td>Ana</td>
<td>[email protected]</td>
<td>
<button onClick={() => handleAction(2, "A")}>Acción A</button>
<button onClick={() => handleAction(2, "B")}>Acción B</button>
<button onClick={() => goToDetail(2)}>Ir a detalle</button>
<button onClick={() => deleteRecord(2)}>Eliminar</button>
</td>
</tr>
</tbody>
</table>
{actionMessage && <p data-testid="action-msg">{actionMessage}</p>}
</div>
);
}
import { render, screen, within, fireEvent } from "@testing-library/react";
import { SimpleTable } from "./SimpleTable";
describe("SimpleTable", () => {
it("ejecuta la acción de eliminar y muestra el mensaje", async () => {
// Mock global fetch
global.fetch = jest.fn(() =>
Promise.resolve({ ok: true })
);
render(<SimpleTable />);
const row = screen.getByTestId("row-2");
const rowUtils = within(row);
// Hacemos clic en "Eliminar"
fireEvent.click(rowUtils.getByText("Eliminar"));
// Validamos que fetch fue llamado correctamente
expect(global.fetch).toHaveBeenCalledWith("/api/records/2", {
method: "DELETE",
});
// Validamos el mensaje final
expect(await screen.findByTestId("action-msg"))
.toHaveTextContent("Registro 2 eliminado");
});
});
Otras funcionalidades muy utilizadas en las tablas es la paginación y los filtros. La paginación se encarga de reducir los resultados que se muestra al usuario, minimizando la velocidad con la que se muestran los registros. Por otro lado, los filtros permiten buscar en registro dependiendo ciertas características. Por ejemplo, id, nombre, estado.
// PaginatedTable.jsx
import { useState } from "react";
export function PaginatedTable() {
const [currentPage, setCurrentPage] = useState(1);
const pages = {
1: [
{ id: 1, name: "Producto A" },
{ id: 2, name: "Producto B" }
],
2: [
{ id: 3, name: "Producto C" },
{ id: 4, name: "Producto D" }
]
};
const rows = pages[currentPage];
return (
<div>
<table>
<tbody>
{rows.map((row) => (
<tr data-testid={`row-${row.id}`} key={row.id}>
<td>{row.name}</td>
</tr>
))}
</tbody>
</table>
<section>
<button
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
>
Página anterior
</button>
<button
onClick={() => setCurrentPage(2)}
disabled={currentPage === 2}
>
Página siguiente
</button>
</section>
</div>
);
}
// PaginatedTable.test.jsx
import { render, screen, fireEvent } from "@testing-library/react";
import { PaginatedTable } from "./PaginatedTable";
describe("PaginatedTable", () => {
it("cambia a la segunda página al hacer clic en 'Página siguiente'", () => {
render(<PaginatedTable />);
const nextButton = screen.getByText("Página siguiente");
fireEvent.click(nextButton);
// Debe aparecer contenido de página 2
expect(screen.getByText("Producto C")).toBeInTheDocument();
expect(screen.getByText("Producto D")).toBeInTheDocument();
// Y dejar de aparecer el contenido de página 1
expect(screen.queryByText("Producto A")).not.toBeInTheDocument();
expect(screen.queryByText("Producto B")).not.toBeInTheDocument();
});
});
// FilterableTable.jsx
import { useState } from "react";
export function FilterableTable() {
const [search, setSearch] = useState("");
const data = [
{ id: 1, name: "Gabriel" },
{ id: 2, name: "Ana" },
{ id: 3, name: "Mario" }
];
const filtered = data.filter((item) =>
item.name.toLowerCase().includes(search.toLowerCase())
);
return (
<div>
<input
placeholder="Buscar por nombre"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<table>
<tbody>
{filtered.map((row) => (
<tr key={row.id} data-testid={`row-${row.id}`}>
<td>{row.name}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
// FilterableTable.test.jsx
import { render, screen, fireEvent } from "@testing-library/react";
import { FilterableTable } from "./FilterableTable";
describe("FilterableTable", () => {
it("filtra por nombre correctamente", () => {
render(<FilterableTable />);
// Al inicio deben aparecer todos
expect(screen.getByText("Gabriel")).toBeInTheDocument();
expect(screen.getByText("Ana")).toBeInTheDocument();
expect(screen.getByText("Mario")).toBeInTheDocument();
// Escribimos en el input
const input = screen.getByPlaceholderText("Buscar por nombre");
fireEvent.change(input, { target: { value: "ga" } });
// Debe aparecer solo Gabriel
expect(screen.getByText("Gabriel")).toBeInTheDocument();
// Ya NO deben aparecer los demás
expect(screen.queryByText("Ana")).not.toBeInTheDocument();
expect(screen.queryByText("Mario")).not.toBeInTheDocument();
});
});
// FilterResetTable.jsx
import { useState } from "react";
export function FilterResetTable() {
const [search, setSearch] = useState("");
const data = [
{ id: 1, name: "Gabriel" },
{ id: 2, name: "Ana" },
{ id: 3, name: "Mario" }
];
const filtered = data.filter((item) =>
item.name.toLowerCase().includes(search.toLowerCase())
);
return (
<div>
<input
placeholder="Buscar"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<button onClick={() => setSearch("")}>
Limpiar filtros
</button>
<table>
<tbody>
{filtered.map((row) => (
<tr key={row.id} data-testid={`row-${row.id}`}>
<td>{row.name}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
// FilterResetTable.test.jsx
import { render, screen, fireEvent } from "@testing-library/react";
import { FilterResetTable } from "./FilterResetTable";
describe("FilterResetTable", () => {
it("resetea los filtros correctamente", () => {
render(<FilterResetTable />);
const input = screen.getByPlaceholderText("Buscar");
const resetBtn = screen.getByText("Limpiar filtros");
// Al inicio: todos visibles
expect(screen.getByText("Gabriel")).toBeInTheDocument();
expect(screen.getByText("Ana")).toBeInTheDocument();
expect(screen.getByText("Mario")).toBeInTheDocument();
// Aplicamos un filtro
fireEvent.change(input, { target: { value: "ga" } });
// Solo debe quedar Gabriel
expect(screen.getByText("Gabriel")).toBeInTheDocument();
expect(screen.queryByText("Ana")).not.toBeInTheDocument();
expect(screen.queryByText("Mario")).not.toBeInTheDocument();
// Reseteamos
fireEvent.click(resetBtn);
// Todos vuelven a aparecer
expect(screen.getByText("Gabriel")).toBeInTheDocument();
expect(screen.getByText("Ana")).toBeInTheDocument();
expect(screen.getByText("Mario")).toBeInTheDocument();
// Input vacío
expect(input).toHaveValue("");
});
});
Probar las tablas asegura que el usuario pueda continuar operando con normalidad. Si los datos de cada registro no se muestran acorde a lo que deben de expresar, el usuario puede sentir que la aplicación no está haciendo lo que debe.
Si quieres aprender a probar tus tablas como un profesional, tengo un libro enfocado únicamente en testing en React o un HUB donde hablo sobre testing en React.
HUB: Testing en React
Libro: Testing en React: Guía práctica con Jest y React Testing Library