import assert from "node:assert/strict"; import { test } from "node:test"; import { renderToStaticMarkup } from "react-dom/server"; import { EntityAction, EntityRowActions, EntityTable } from "./components/entity-list"; import { entityListLabels, entityListPages, entityListRange } from "./lib/entity-list"; 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("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: siempre última, compacta y anclada a la derecha, aunque se declare primero", () => { const html = renderToStaticMarkup( "Acción", render: () => ( undefined}>Editar ), }, ...columns, ]} items={items.slice(0, 1)} title="Personas" /> ); const header = html.match(//)?.[0] ?? ""; const row = html.match(//)?.[0] ?? ""; assert.ok(header.indexOf(">Nombre") < header.indexOf(">Acciones")); assert.ok(row.indexOf("Persona 01") < row.indexOf('aria-label="Acciones"')); assert.equal(html.split('data-pinned="right"').length - 1, 2); assert.equal(html.split("sticky right-0").length - 1, 2); assert.equal(header.includes('aria-label="Acciones"'), false); assert.ok(header.includes("text-right")); assert.ok(row.includes("text-right")); assert.ok(row.includes("bg-card")); }); test("los textos comunes mantienen claves, idiomas y parámetros del rango", () => { const spanish = entityListLabels("es"); const english = entityListLabels("en-GB"); assert.deepEqual(Object.keys(spanish).sort(), Object.keys(english).sort()); assert.equal(spanish.range(1, 10, 25, "marcas"), "Mostrando 1-10 de 25 marcas"); assert.equal(english.range(0, 0, 0, "brands"), "Showing 0-0 of 0 brands"); assert.equal(entityListLabels("fr").actions, spanish.actions); assert.equal(english.range(1, 1, 1, "$&"), "Showing 1-1 of 1 $&"); });