331 lines
11 KiB
TypeScript
331 lines
11 KiB
TypeScript
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"));
|
|
});
|
|
});
|