Add virtual list provider coverage

active-uix
dev 5 months ago
parent 5878e780bc
commit 859f873eb2

@ -42,9 +42,14 @@ Actualizacion 2026-05-15:
date/time y commit solo cuando los segmentos renderizados estan completos. date/time y commit solo cuando los segmentos renderizados estan completos.
- Correccion DateField: el announcer se inicializa con `$effect` en vez de - Correccion DateField: el announcer se inicializa con `$effect` en vez de
`onMount()` dentro de la clase. `onMount()` dentro de la clase.
- Cobertura nueva para VirtualList: cálculo de ventana, `scrollToIndex` y
compensación anti-jump.
- Correccion VirtualList: la compensación de mediciones dinámicas sobre
viewports internos pasa por `ActiveDom.scrollTo(...)` y usa
`scrollOffset + delta` como fuente reactiva.
- Validado por fase: - Validado por fase:
- `npm run check` -> 0 errores, 0 warnings. - `npm run check` -> 0 errores, 0 warnings.
- `npm run test` -> 184 archivos, 2080 tests OK. - `npm run test` -> 185 archivos, 2083 tests OK.
- tests focales de Select/Popover/Toast/RangeCalendar -> 11 tests OK. - tests focales de Select/Popover/Toast/RangeCalendar -> 11 tests OK.
- tests focales Dialog/Drawer/Popover/Select -> 10 tests OK. - tests focales Dialog/Drawer/Popover/Select -> 10 tests OK.
- `command-provider.svelte.test.ts` -> 2 tests OK. - `command-provider.svelte.test.ts` -> 2 tests OK.
@ -57,6 +62,7 @@ Actualizacion 2026-05-15:
- `time-field-provider.svelte.test.ts` -> 3 tests OK. - `time-field-provider.svelte.test.ts` -> 3 tests OK.
- `date-range-field-provider.svelte.test.ts` -> 3 tests OK. - `date-range-field-provider.svelte.test.ts` -> 3 tests OK.
- `date-field-provider.svelte.test.ts` -> 7 tests OK. - `date-field-provider.svelte.test.ts` -> 7 tests OK.
- `virtual-list-provider.svelte.test.ts` -> 3 tests OK.
- guardia `rg "from './.*provider\\.svelte'" src/uix/soma/components -g 'exports.ts'` - guardia `rg "from './.*provider\\.svelte'" src/uix/soma/components -g 'exports.ts'`
-> solo queda `select/components/select-provider.svelte`, que es wrapper -> solo queda `select/components/select-provider.svelte`, que es wrapper
publico, no runtime provider. publico, no runtime provider.

@ -478,11 +478,13 @@ Los tests por componente estan creciendo desde las piezas de mayor riesgo. A
completos. completos.
- `date-range-field/date-range-field-provider.svelte.test.ts` — validación de - `date-range-field/date-range-field-provider.svelte.test.ts` — validación de
orden/min/max, validación custom y foco de label via `ActiveDom`. orden/min/max, validación custom y foco de label via `ActiveDom`.
- `virtual-list/virtual-list-provider.svelte.test.ts` — cálculo de ventana,
`scrollToIndex` y compensación anti-jump via `ActiveDom`.
Pendiente: seguir ampliando cobertura por familias de alto riesgo fuera del Pendiente: seguir ampliando cobertura por familias de alto riesgo fuera del
bloque fecha/hora. `table-core`, `form-core` y el scorer de Command ya no viven bloque fecha/hora/virtualización. `table-core`, `form-core` y el scorer de
dentro de Soma: se consumen desde `$libs/datagrid`, `$libs/forms` y Command ya no viven dentro de Soma: se consumen desde `$libs/datagrid`,
`$libs/strings`. `$libs/forms` y `$libs/strings`.
### Convencion ### Convencion

@ -0,0 +1,121 @@
// @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 type { Orientation } from '../../types';
import { VirtualListProvider } from './virtual-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() {
const dom = createActiveDom();
const soma = {
dom,
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(VirtualListProvider.ctx, 'set').mockImplementation((value) => value);
return { dom };
}
function createVirtualListOpts() {
return {
id: state('virtual-list-root'),
ref: state<HTMLElement | null>(null),
count: state(10),
itemSize: state<number | undefined>(20),
estimateSize: state<(index: number) => number>(() => 20),
overscan: state(1),
orientation: state<Orientation>('vertical'),
getItemKey: state<(index: number) => string | number>((index) => `row-${index}`)
};
}
describe('VirtualListProvider', () => {
afterEach(() => {
vi.restoreAllMocks();
document.body.innerHTML = '';
});
it('computes fixed-size virtual items from viewport and overscan', () => {
const { dom } = installSomaHarness();
const opts = createVirtualListOpts();
const { result: provider, cleanup } = withEffectRoot(() => VirtualListProvider.create(opts));
provider.viewportSize = 100;
provider.scrollOffset = 50;
expect(provider.totalSize).toBe(200);
expect(provider.virtualItems.map((item) => item.index)).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
expect(provider.virtualItems[0]).toEqual({
index: 1,
start: 20,
size: 20,
key: 'row-1'
});
cleanup();
dom.dispose();
});
it('routes scrollToIndex through ActiveDom for internal viewports', () => {
const { dom } = installSomaHarness();
const opts = createVirtualListOpts();
const viewport = document.createElement('div');
const scrollTo = vi.spyOn(dom, 'scrollTo');
const { result: provider, cleanup } = withEffectRoot(() => VirtualListProvider.create(opts));
provider.viewportRef = viewport;
provider.viewportSize = 100;
provider.scrollToIndex(5, { align: 'center' });
expect(scrollTo).toHaveBeenCalledWith(viewport, {
top: 60,
behavior: 'auto'
});
cleanup();
dom.dispose();
});
it('compensates dynamic measurements above the viewport through ActiveDom', () => {
const { dom } = installSomaHarness();
const opts = createVirtualListOpts();
opts.itemSize.current = undefined;
const viewport = document.createElement('div');
viewport.scrollTop = 50;
const scrollTo = vi.spyOn(dom, 'scrollTo');
const { result: provider, cleanup } = withEffectRoot(() => VirtualListProvider.create(opts));
provider.viewportRef = viewport;
provider.viewportSize = 100;
provider.scrollOffset = 50;
provider.measureItem(0, 30);
expect(scrollTo).toHaveBeenCalledWith(viewport, { top: 60 });
cleanup();
dom.dispose();
});
});

@ -218,8 +218,10 @@ export class VirtualListProvider {
const vp = this.viewportRef; const vp = this.viewportRef;
if (vp) { if (vp) {
if (this.isHorizontal) vp.scrollLeft += delta; this.soma.dom.scrollTo(
else vp.scrollTop += delta; vp,
this.isHorizontal ? { left: this.scrollOffset + delta } : { top: this.scrollOffset + delta }
);
return; return;
} }
// Window-scroll mode: shift document scroll by the delta. // Window-scroll mode: shift document scroll by the delta.

Loading…
Cancel
Save

Powered by TurnKey Linux.