import assert from "node:assert/strict"; import { test } from "node:test"; import type { ReactNode } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { CursorPagination } from "./components/cursor-pagination"; import { EntityAction, EntityRowActions, EntityStatus, EntityTable, } from "./components/entity-list"; import { entityListLabels, entityListPages, entityListRange } from "./lib/entity-list"; import { PencilIcon } from "./lib/icons"; const items = Array.from({ length: 21 }, (_, index) => ({ id: String(index), name: `Persona ${String(index + 1).padStart(2, "0")}`, })); const columns = [ { id: "name", label: "Nombre", value: (item: (typeof items)[number]) => item.name, render: (item: (typeof items)[number]) => item.name, hideable: false, }, ]; test("los rangos vacíos y la última página nunca muestran índices fuera del conjunto", () => { assert.deepEqual(entityListRange(0, 5, 10), { page: 0, pages: 1, from: 0, to: 0 }); assert.deepEqual(entityListRange(21, 2, 10), { page: 2, pages: 3, from: 21, to: 21 }); assert.deepEqual(entityListRange(9, 2, 10), { page: 0, pages: 1, from: 1, to: 9 }); assert.deepEqual(entityListPages(0, 12), [0, 1, 2, 3, 4]); assert.deepEqual(entityListPages(11, 12), [7, 8, 9, 10, 11]); assert.deepEqual(entityListPages(0, 1), [0]); }); test("el listado pagina el conjunto y repite su rango conservando controles accesibles", () => { const html = renderToStaticMarkup( ); assert.equal(html.split("Mostrando 1-10 de 21 personas").length - 1, 2); assert.ok(html.includes("Persona 10")); assert.equal(html.includes("Persona 11"), false); assert.ok(html.includes('aria-label="Página siguiente"')); assert.ok(html.includes('aria-current="page"')); assert.ok(html.includes('scope="col"')); assert.ok(html.includes("Columnas")); assert.ok(html.includes("Ordenar por")); }); test("la búsqueda local reduce el conjunto completo antes de paginar y permite limpiarla", () => { const html = renderToStaticMarkup( ); assert.ok(html.includes("Mostrando 1-1 de 1 personas")); assert.ok(html.includes("Persona 21")); assert.ok(html.includes("Limpiar filtros")); assert.equal(html.includes("Persona 01"), false); }); test("el orden inicial ascendente se indica en la cabecera y se aplica antes de paginar", () => { const html = renderToStaticMarkup( ); assert.match(html, /]*aria-sort="ascending"[^>]*>[\s\S]*?Nombre/); assert.ok(html.indexOf("Persona 01") < html.indexOf("Persona 02")); assert.ok(html.includes("Persona 10")); assert.equal(html.includes("Persona 11"), false); assert.equal(html.includes("Persona 21"), false); }); test("una columna explícita puede establecer un orden inicial descendente", () => { const html = renderToStaticMarkup( item.updated, render: (item) => item.updated, }, ]} items={[ { id: "old", name: "Ana", updated: "2026-01-01T12:00:00Z" }, { id: "new", name: "Zoe", updated: "2026-02-01T12:00:00Z" }, ]} title="Actividad" /> ); assert.match(html, /]*aria-sort="descending"[^>]*>[\s\S]*?Actualización/); assert.equal(html.includes('aria-sort="ascending"'), false); assert.ok(html.indexOf(">Zoe<") < html.indexOf(">Ana<")); }); test("el orden inicial ignora acciones y columnas sin valor comparable", () => { const html = renderToStaticMarkup( "Imagen" }, { id: "actions", label: "Acciones", defaultSort: "desc", value: (item) => item.name, render: () => "Editar", }, ...columns, ]} items={items.slice(0, 2).reverse()} title="Personas" /> ); assert.match(html, /]*aria-sort="ascending"[^>]*>[\s\S]*?Nombre/); assert.equal(html.includes('aria-sort="descending"'), false); assert.ok(html.indexOf("Persona 01") < html.indexOf("Persona 02")); }); test("las tablas sin columnas ordenables conservan el orden recibido", () => { const html = renderToStaticMarkup( item.name }]} items={[...items].reverse()} title="Personas" /> ); assert.equal(html.includes("aria-sort="), false); assert.ok(html.indexOf("Persona 21") < html.indexOf("Persona 20")); }); test("el orden inicial mantiene la estabilidad en empates y se indica con el listado vacío", () => { const tied = renderToStaticMarkup( item.name, render: (item) => item.id, }, ]} items={[ { id: "second", name: "Igual" }, { id: "first", name: "Igual" }, ]} title="Personas" /> ); const empty = renderToStaticMarkup(); assert.ok(tied.indexOf(">second<") < tied.indexOf(">first<")); assert.ok(empty.includes('aria-sort="ascending"')); }); test("una búsqueda controlada conserva los resultados filtrados por la pantalla", () => { const html = renderToStaticMarkup( undefined} search="criterio externo" title="Personas" /> ); assert.ok(html.includes("Mostrando 1-1 de 1 personas")); assert.ok(html.includes("Persona 01")); }); test("carga, error y búsqueda vacía se distinguen de un listado sin registros", () => { const render = (props: { loading?: boolean; unavailable?: boolean; search?: string }) => renderToStaticMarkup( ); assert.ok(render({}).includes("No hay personas.")); assert.ok(render({ search: "Ana" }).includes("No hay resultados para estos criterios.")); assert.ok(render({ loading: true }).includes('aria-busy="true"')); assert.equal(render({ loading: true }).includes("No hay personas."), false); assert.ok(render({ unavailable: true }).includes("No se ha podido cargar el listado.")); assert.equal(render({ unavailable: true }).includes("Mostrando 0-0"), false); }); test("el rango y la configuración también se traducen en inglés", () => { const html = renderToStaticMarkup( ); assert.ok(html.includes("Showing 0-0 of 0 people")); assert.ok(html.includes("Columns")); assert.ok(html.includes("Items per page")); }); test("acciones: disparador de solo icono con etiqueta accesible y menú contextual", () => { const html = renderToStaticMarkup( undefined}>Editar ); assert.ok(html.includes('aria-label="Acciones de Ana"')); assert.ok(html.includes('title="Acciones de Ana"')); assert.ok(html.includes('aria-haspopup="menu"')); assert.ok(html.includes("Acciones de Ana"), false); }); test("acciones: operación principal visible con nombre accesible y secundarias en el menú", () => { for (const label of ["Editar", "Edit"]) { const html = renderToStaticMarkup( ); assert.ok(html.includes(`aria-label="${label}"`)); assert.ok(html.includes('data-slot="tooltip-trigger"')); assert.ok(html.includes('aria-haspopup="menu"')); assert.ok(html.indexOf(`aria-label="${label}"`) < html.indexOf('aria-label="Acciones"')); assert.equal(html.includes(`>${label}`), false); } }); test("acciones: conserva la restricción de la principal y evita menús sin secundarias", () => { const primaryAction = { label: "Editar", icon: