import assert from "node:assert/strict"; import { test } from "node:test"; import { renderToStaticMarkup } from "react-dom/server"; import { CursorEntityTable } from "./components/cursor-entity-table"; import { EntityTable } from "./components/entity-list"; import { groupColumnOptions, normalizeColumnSearch } from "./lib/entity-column-configuration"; import { readColumnVisibility, writeColumnVisibility, } from "./lib/entity-column-visibility-storage"; import { entityListLabels } from "./lib/entity-list"; function withStorage(run: (values: Map) => void) { const original = Object.getOwnPropertyDescriptor(globalThis, "window"); const values = new Map(); Object.defineProperty(globalThis, "window", { configurable: true, value: { localStorage: { getItem: (key: string) => values.get(key) ?? null, setItem: (key: string, value: string) => values.set(key, value), }, }, }); try { run(values); } finally { if (original) { Object.defineProperty(globalThis, "window", original); } else { Reflect.deleteProperty(globalThis, "window"); } } } test("las columnas elegidas se restauran al volver a crear la tabla, incluidas las que llegan después", () => { withStorage((storage) => { writeColumnVisibility("assets", { detail: true, later: true, optional: false, name: false, actions: false, unknownColumn: true, }); const render = (includeLater: boolean) => renderToStaticMarkup( "Equipo" }, { id: "detail", label: "Detalle", defaultVisible: false, render: () => "Dato elegido" }, { id: "optional", label: "Optativa", render: () => "Dato oculto" }, ...(includeLater ? [ { id: "later", label: "Campo posterior", defaultVisible: false, render: () => "Columna asíncrona", }, ] : []), { id: "actions", label: "Acciones", render: () => , }, ]} columnVisibilityStorageKey="assets" items={[{ id: "one" }]} title="Activos" /> ); const initial = render(false); const restored = render(true); assert.ok(initial.includes("Dato elegido")); assert.ok(restored.includes("Dato elegido")); assert.ok(restored.includes("Columna asíncrona")); assert.ok(restored.includes("Equipo")); assert.ok(restored.includes("Editar")); assert.equal(restored.includes("unknownColumn"), false); assert.equal(restored.includes("Dato oculto"), false); assert.equal(readColumnVisibility("assets").later, true); assert.equal(storage.has("entity-page-size"), false); }); }); test("las preferencias quedan aisladas por lista y no se aplican cuando no se solicita persistencia", () => { withStorage(() => { writeColumnVisibility("assets", { detail: true }); writeColumnVisibility("picker", { detail: false }); assert.deepEqual(readColumnVisibility("assets"), { detail: true }); assert.deepEqual(readColumnVisibility("picker"), { detail: false }); assert.deepEqual(readColumnVisibility(undefined), {}); const html = renderToStaticMarkup( "Preferencia ajena", }, ]} items={[{ id: "one" }]} title="Selector" /> ); assert.equal(html.includes("Preferencia ajena"), false); }); }); test("datos locales inválidos no rompen el listado y solo se aceptan valores booleanos", () => { withStorage((storage) => { for (const serialized of ["{invalid", "[]", "null", '"text"', "42"]) { storage.set("assets", serialized); assert.deepEqual(readColumnVisibility("assets"), {}); } storage.set("assets", '{"detail":true,"optional":false,"text":"true","object":{},"array":[]}'); assert.deepEqual(readColumnVisibility("assets"), { detail: true, optional: false }); }); }); test("almacenamiento bloqueado o sin cuota permite seguir utilizando la selección local", () => { const original = Object.getOwnPropertyDescriptor(globalThis, "window"); Object.defineProperty(globalThis, "window", { configurable: true, value: { localStorage: { getItem: () => { throw new Error("Storage blocked"); }, setItem: () => { throw new Error("Quota exceeded"); }, }, }, }); try { assert.deepEqual(readColumnVisibility("assets"), {}); assert.doesNotThrow(() => writeColumnVisibility("assets", { detail: true })); } finally { if (original) { Object.defineProperty(globalThis, "window", original); } else { Reflect.deleteProperty(globalThis, "window"); } } }); test("buscar columnas ignora tildes y mayúsculas, conserva ñ y no modifica visibilidad", () => { const options = [ { id: "audit", label: "Última modificación", keywords: ["Revisión técnica"], visible: false, hideable: true, }, { id: "year", label: "Año", visible: true, hideable: false }, { id: "anus", label: "Ano", visible: false, hideable: true }, ]; const initial = structuredClone(options); for (const query of ["Última modificación", "modific", " ULTIMA ", "revision", "TECNICA"]) { assert.deepEqual( groupColumnOptions(options, [], query).flatMap((group) => group.options.map((column) => column.id) ), ["audit"] ); } assert.equal(normalizeColumnSearch(" AÑÓ "), "año"); assert.equal(normalizeColumnSearch("AN\u0303O"), "año"); assert.deepEqual( groupColumnOptions(options, [], "año").flatMap((group) => group.options.map((column) => column.id) ), ["year"] ); assert.deepEqual( groupColumnOptions(options, [], "ano").flatMap((group) => group.options.map((column) => column.id) ), ["anus"] ); assert.deepEqual(groupColumnOptions(options, [], "nada"), []); assert.equal(groupColumnOptions(options, [], " ")[0]?.options.length, 3); assert.deepEqual(options, initial); }); test("los grupos conservan orden, omiten vacíos y deduplican IDs sin perder columnas sin grupo", () => { const options = [ { id: "one", label: "Uno", groupId: "second", visible: true, hideable: false }, { id: "two", label: "Dos", groupId: "first", visible: false, hideable: true }, { id: "one", label: "Duplicada", groupId: "first", visible: false, hideable: true }, { id: "loose", label: "Suelta", visible: true, hideable: true }, { id: "unknown", label: "Sin definición", groupId: "unknown", visible: true, hideable: true }, ]; const groups = [ { id: "first", label: "Primero" }, { id: "second", label: "Segundo" }, { id: "first", label: "Otra etiqueta" }, { id: "empty", label: "Vacío" }, ]; const result = groupColumnOptions(options, groups, ""); assert.deepEqual( result.map((entry) => entry.group?.label), ["Primero", "Segundo", undefined] ); assert.deepEqual( result.map((entry) => entry.options.map((option) => option.id)), [["two"], ["one"], ["loose", "unknown"]] ); assert.equal(groupColumnOptions(options, groups, "Dos").length, 1); assert.equal(groupColumnOptions(options, [], "").length, 1); assert.equal(groupColumnOptions(options, [{ id: "first", label: "Primero" }], "Dos").length, 1); assert.deepEqual(groupColumnOptions([], groups, ""), []); }); test("restaurar preferencias vacías recupera defaults actuales sin tocar otras tablas", () => { withStorage((storage) => { writeColumnVisibility("other", { optional: true }); writeColumnVisibility("assets", { name: false, detail: true, retired: true }); writeColumnVisibility("assets", {}); const html = renderToStaticMarkup( "Identidad" }, { id: "detail", label: "Detalle", defaultVisible: false, render: () => "Opcional oculta", }, { id: "new", label: "Nueva", defaultVisible: true, render: () => "Nueva visible" }, ]} columnVisibilityStorageKey="assets" items={[{ id: "one" }]} title="Activos" /> ); assert.ok(html.includes("Identidad")); assert.ok(html.includes("Nueva visible")); assert.equal(html.includes("Opcional oculta"), false); assert.equal(html.includes("retired"), false); assert.equal(storage.get("assets"), "{}"); assert.deepEqual(readColumnVisibility("other"), { optional: true }); }); }); test("los contadores distinguen visibilidad, total y coincidencias en es/en", () => { assert.equal(entityListLabels("es").visibleColumns(8, 75), "8 columnas visibles de 75"); assert.equal(entityListLabels("en").visibleColumns(1, 3), "1 visible column of 3"); assert.equal(entityListLabels("es").matchingColumns(1), "1 coincidencia"); assert.equal(entityListLabels("en").matchingColumns(0), "0 matches"); }); test("listado remoto: recupera columnas por ámbito y protege nombre y acciones", () => { withStorage(() => { writeColumnVisibility("applications-one", { provider: false, name: false, actions: false }); const render = (key: string) => renderToStaticMarkup( "Aplicación", render: () => "Aplicación", }, { id: "provider", label: "Proveedor", render: () => "Proveedor de prueba" }, { id: "actions", label: "Acciones", render: () => "Editar" }, ]} columnVisibilityStorageKey={key} emptyMessage="Sin aplicaciones" filters={null} filtersApplied={false} items={[{ id: "one" }]} labels={{ range: "Una aplicación", page: "Página 1", previous: "Anterior", next: "Siguiente", pageSize: "Tamaño", clear: "Limpiar", retry: "Reintentar", }} locale="es" onClearFilters={() => undefined} onRetry={async () => undefined} onSearchChange={() => undefined} onSortingChange={() => undefined} pagination={{ index: 0, size: 25, hasNext: false, onPrevious: () => undefined, onNext: () => undefined, onSizeChange: () => undefined, }} search="" searchLabel="Buscar" sorting={[{ id: "name", desc: false }]} state={{ status: "ready" }} title="Aplicaciones" /> ); const restored = render("applications-one"); assert.ok(restored.includes("Aplicación")); assert.ok(restored.includes("Editar")); assert.equal(restored.includes("Proveedor de prueba"), false); assert.ok(render("applications-two").includes("Proveedor de prueba")); }); });