Add grid list provider coverage

active-uix
dev 5 months ago
parent 9f60aa62a2
commit ef157feccc

@ -133,9 +133,11 @@ Actualizacion 2026-05-15:
traducidos/override y cierre delegado al Dialog base.
- Cobertura nueva para Carousel: detección de slides, navegación por
trigger/teclado, props de partes e intervalos autoplay via `uix.timers`.
- Cobertura nueva para GridList: merge con Field, selección/rango básico,
navegación/typeahead, celdas focusables y checkbox de fila.
- Validado por fase:
- `npm run check` -> 0 errores, 0 warnings.
- `npm run test` -> 227 archivos, 2229 tests OK.
- `npm run test` -> 228 archivos, 2232 tests OK.
- tests focales de Select/Popover/Toast/RangeCalendar -> 11 tests OK.
- tests focales Dialog/Drawer/Popover/Select -> 10 tests OK.
- `command-provider.svelte.test.ts` -> 2 tests OK.
@ -191,6 +193,7 @@ Actualizacion 2026-05-15:
- `editable-provider.svelte.test.ts` -> 3 tests OK.
- `alert-dialog-provider.svelte.test.ts` -> 2 tests OK.
- `carousel-provider.svelte.test.ts` -> 3 tests OK.
- `grid-list-provider.svelte.test.ts` -> 3 tests OK.
- guardia `rg "from './.*provider\\.svelte'" src/uix/soma/components -g 'exports.ts'`
-> solo queda `select/components/select-provider.svelte`, que es wrapper
publico, no runtime provider.

@ -565,6 +565,8 @@ Los tests por componente estan creciendo desde las piezas de mayor riesgo. A
action/cancel, labels traducidos/override y cierre delegado a Dialog.
- `carousel/carousel-provider.svelte.test.ts` — detección de slides,
navegación trigger/teclado, props de partes y autoplay via `uix.timers`.
- `grid-list/grid-list-provider.svelte.test.ts` — merge con Field,
selección, navegación/typeahead, celdas focusables y checkbox de fila.
Pendiente: seguir ampliando cobertura por el resto del catalogo Soma, ya por
componentes de riesgo medio y familias menos centrales. `table-core`,

@ -0,0 +1,340 @@
// @vitest-environment jsdom
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createActiveDom } from '$adom';
import { state } from '$libs/reactive';
import type { Morfo } from '$uix/morfo';
import { Soma } from '$soma/core/soma.svelte';
import { createSomaRuntime, type SomaRuntimeSources } from '$soma/runtime.svelte';
import { FieldProvider } from '$soma/components/field/field-provider.svelte';
import {
GridListCellProvider,
GridListProvider,
GridListRowProvider,
GridListSelectionCheckboxProvider
} from './grid-list-provider.svelte';
function withEffectRoot<T>(fn: () => T): { result: T; cleanup: () => void } {
let result!: T;
const cleanup = $effect.root(() => {
result = fn();
});
return { result, cleanup };
}
function installSomaHarness(field: FieldProvider | undefined = undefined) {
const dom = createActiveDom();
const timers = {
schedule: vi.fn((key: string, delay: number, callback: () => void) => ({
key,
delay,
callback,
cancel: vi.fn()
}))
};
const soma = {
dom,
prefs: { getDir: () => 'ltr' },
langs: {
ts: vi.fn((key: string) => (key.includes('select-row') ? 'Select row' : 'Grid list'))
},
uix: { timers },
runtime: (morfo: Morfo, sources: Omit<SomaRuntimeSources, 'dom' | 'eventEngine'>) =>
createSomaRuntime(morfo, {
dom,
translate: (key) => key,
...sources
})
} as unknown as Soma;
vi.spyOn(Soma, 'require').mockReturnValue(soma);
vi.spyOn(FieldProvider, 'get').mockReturnValue(field);
vi.spyOn(GridListProvider.ctx, 'set').mockImplementation((value) => value);
return { dom, timers };
}
function createRoot() {
const root = document.createElement('div');
root.setAttribute('data-grid-list', '');
document.body.appendChild(root);
return root;
}
function createRowEl(value: string, text: string, disabled = false) {
const row = document.createElement('div');
row.setAttribute('data-grid-list-row', '');
row.setAttribute('data-value', value);
row.setAttribute('data-text-value', text);
row.textContent = text;
if (disabled) row.setAttribute('data-disabled', '');
return row;
}
function gridListOpts(root = createRoot()) {
return {
id: state('grid-list-root'),
ref: state<HTMLElement | null>(root),
value: state<string[]>([]),
selectionMode: state<'none' | 'single' | 'multiple'>('multiple'),
loop: state(false),
typeahead: state(true),
typeaheadTimeout: state(500),
disabled: state(false),
readonly: state(false),
required: state(false),
invalid: state(false),
name: state<string | undefined>(undefined),
ariaLabel: state<string | undefined>(undefined),
ariaLabelledby: state<string | undefined>(undefined),
onValueChange: state<((value: string[]) => void) | undefined>(undefined)
};
}
function rowOpts(id: string, value: string, el: HTMLElement, disabled = false) {
return {
id: state(id),
ref: state<HTMLElement | null>(el),
value: state(value),
textValue: state<string | undefined>(el.getAttribute('data-text-value') ?? undefined),
disabled: state(disabled)
};
}
function keyEvent(
key: string,
currentTarget: HTMLElement,
init: Partial<KeyboardEvent> & { target?: HTMLElement } = {}
) {
return {
key,
currentTarget,
target: init.target ?? currentTarget,
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
...init
};
}
function clickEvent(currentTarget: HTMLElement, init: Partial<MouseEvent> = {}) {
return {
currentTarget,
ctrlKey: false,
metaKey: false,
shiftKey: false,
...init
};
}
function fakeField() {
return {
get isDisabled() {
return false;
},
get isReadonly() {
return false;
},
get isRequired() {
return true;
},
get isInvalid() {
return true;
}
} as FieldProvider;
}
describe('GridListProvider', () => {
afterEach(() => {
vi.restoreAllMocks();
document.body.innerHTML = '';
});
it('merges Field flags, exposes row props and selects rows by click helpers', () => {
const { dom } = installSomaHarness(fakeField());
const focusSpy = vi.spyOn(dom, 'focus');
const root = createRoot();
const alphaEl = createRowEl('alpha', 'Alpha');
const betaEl = createRowEl('beta', 'Beta');
const gammaEl = createRowEl('gamma', 'Gamma', true);
root.append(alphaEl, betaEl, gammaEl);
const opts = gridListOpts(root);
opts.value.current = ['beta'];
const onValueChange = vi.fn();
opts.onValueChange.current = onValueChange;
const { result, cleanup } = withEffectRoot(() => {
const provider = GridListProvider.create(opts);
vi.spyOn(GridListProvider, 'require').mockReturnValue(provider);
const alpha = GridListRowProvider.create(rowOpts('row-alpha', 'alpha', alphaEl));
const beta = GridListRowProvider.create(rowOpts('row-beta', 'beta', betaEl));
const gamma = GridListRowProvider.create(rowOpts('row-gamma', 'gamma', gammaEl, true));
return { provider, alpha, beta, gamma };
});
expect(result.provider.props).toMatchObject({
role: 'grid',
'aria-label': 'Grid list',
'aria-multiselectable': true,
'aria-required': true,
'aria-invalid': true,
'data-selection-mode': 'multiple',
'data-required': '',
'data-invalid': ''
});
expect(result.alpha.props).toMatchObject({
role: 'row',
tabindex: -1,
'aria-selected': false,
'data-state': 'unselected'
});
expect(result.beta.props).toMatchObject({
tabindex: 0,
'aria-selected': true,
'data-state': 'selected',
'data-highlighted': ''
});
expect(result.gamma.props).toMatchObject({
tabindex: -1,
'aria-disabled': true,
'data-disabled': ''
});
result.alpha.onclick(clickEvent(alphaEl) as never);
expect(opts.value.current).toEqual(['alpha']);
expect(onValueChange).toHaveBeenLastCalledWith(['alpha']);
expect(focusSpy).toHaveBeenLastCalledWith(alphaEl);
result.beta.onclick(clickEvent(betaEl, { ctrlKey: true }) as never);
expect(opts.value.current).toEqual(['alpha', 'beta']);
result.provider.selectAll();
expect(opts.value.current).toEqual(['alpha', 'beta']);
result.provider.clear();
expect(opts.value.current).toEqual([]);
cleanup();
dom.dispose();
});
it('navigates rows, handles select-all/clear and typeahead', () => {
const { dom, timers } = installSomaHarness();
const focusSpy = vi.spyOn(dom, 'focus');
const root = createRoot();
const alphaEl = createRowEl('alpha', 'Alpha');
const betaEl = createRowEl('beta', 'Beta');
const gammaEl = createRowEl('gamma', 'Gamma');
root.append(alphaEl, betaEl, gammaEl);
const opts = gridListOpts(root);
const { result: provider, cleanup } = withEffectRoot(() => GridListProvider.create(opts));
const down = keyEvent('ArrowDown', alphaEl);
provider.handleRowKeydown(down as never);
expect(down.preventDefault).toHaveBeenCalledOnce();
expect(focusSpy).toHaveBeenLastCalledWith(betaEl);
const end = keyEvent('End', alphaEl);
provider.handleRowKeydown(end as never);
expect(end.preventDefault).toHaveBeenCalledOnce();
expect(focusSpy).toHaveBeenLastCalledWith(gammaEl);
const space = keyEvent(' ', betaEl);
provider.handleRowKeydown(space as never);
expect(space.preventDefault).toHaveBeenCalledOnce();
expect(opts.value.current).toEqual(['beta']);
const selectAll = keyEvent('a', betaEl, { ctrlKey: true });
provider.handleRowKeydown(selectAll as never);
expect(selectAll.preventDefault).toHaveBeenCalledOnce();
expect(opts.value.current).toEqual(['alpha', 'beta', 'gamma']);
const escape = keyEvent('Escape', betaEl);
provider.handleRowKeydown(escape as never);
expect(escape.preventDefault).toHaveBeenCalledOnce();
expect(opts.value.current).toEqual([]);
provider.handleRowKeydown(keyEvent('g', alphaEl) as never);
expect(focusSpy).toHaveBeenLastCalledWith(gammaEl);
expect(timers.schedule).toHaveBeenCalledWith(
'soma:grid-list:grid-list-root:typeahead',
500,
expect.any(Function),
expect.objectContaining({
replace: true,
meta: { component: 'grid-list', action: 'typeahead-reset' }
})
);
cleanup();
dom.dispose();
});
it('navigates focusable cells and toggles row selection through the checkbox part', () => {
const { dom } = installSomaHarness();
const focusSpy = vi.spyOn(dom, 'focus');
const root = createRoot();
const rowEl = createRowEl('alpha', 'Alpha');
const cellEl = document.createElement('div');
const firstButton = document.createElement('button');
const secondButton = document.createElement('button');
const checkboxEl = document.createElement('button');
cellEl.append(firstButton, secondButton, checkboxEl);
rowEl.appendChild(cellEl);
root.appendChild(rowEl);
const opts = gridListOpts(root);
const { result, cleanup } = withEffectRoot(() => {
const provider = GridListProvider.create(opts);
vi.spyOn(GridListProvider, 'require').mockReturnValue(provider);
const cell = GridListCellProvider.create({
id: state('grid-cell-alpha'),
ref: state<HTMLElement | null>(cellEl)
});
const checkbox = GridListSelectionCheckboxProvider.create({
id: state('grid-checkbox-alpha'),
ref: state<HTMLButtonElement | null>(checkboxEl),
ariaLabel: state<string | undefined>(undefined)
});
return { provider, cell, checkbox };
});
expect(result.cell.props.role).toBe('gridcell');
expect(result.checkbox.props).toMatchObject({
type: 'button',
role: 'checkbox',
'aria-checked': false,
'aria-label': 'Select row',
'data-state': 'unchecked'
});
const right = keyEvent('ArrowRight', cellEl, { target: firstButton });
result.cell.onkeydown(right as never);
expect(right.preventDefault).toHaveBeenCalledOnce();
expect(right.stopPropagation).toHaveBeenCalledOnce();
expect(focusSpy).toHaveBeenLastCalledWith(secondButton);
const left = keyEvent('ArrowLeft', cellEl, { target: firstButton });
result.cell.onkeydown(left as never);
expect(left.preventDefault).toHaveBeenCalledOnce();
expect(left.stopPropagation).toHaveBeenCalledOnce();
expect(focusSpy).toHaveBeenLastCalledWith(rowEl);
const checkboxClick = { currentTarget: checkboxEl, stopPropagation: vi.fn() };
result.checkbox.onclick(checkboxClick as never);
expect(checkboxClick.stopPropagation).toHaveBeenCalledOnce();
expect(opts.value.current).toEqual(['alpha']);
expect(result.checkbox.props).toMatchObject({
'aria-checked': true,
'data-state': 'checked'
});
cleanup();
dom.dispose();
});
});
Loading…
Cancel
Save

Powered by TurnKey Linux.