metis-desk/packages/rdx-ui/src/entity-column-visibility.test.tsx

331 lines
11 KiB
TypeScript
Raw Normal View History

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<string, string>) => void) {
const original = Object.getOwnPropertyDescriptor(globalThis, "window");
const values = new Map<string, string>();
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(
<EntityTable
columns={[
{ id: "name", label: "Nombre", hideable: false, render: () => "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: () => <button type="button">Editar</button>,
},
]}
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(
<EntityTable
columns={[
{
id: "detail",
label: "Detalle",
defaultVisible: false,
render: () => "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(
<EntityTable
columns={[
{ id: "name", label: "Nombre", hideable: false, render: () => "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(
<CursorEntityTable
columns={[
{
id: "name",
label: "Nombre",
hideable: false,
value: () => "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"));
});
});