Add splitter provider coverage

active-uix
dev 5 months ago
parent 445c5387f0
commit 4b5d9078ed

@ -113,9 +113,11 @@ Actualizacion 2026-05-15:
selección lineal/no lineal, teclado y prev/next/completed. selección lineal/no lineal, teclado y prev/next/completed.
- Cobertura nueva para ScrollArea: medición de overflow, viewport, - Cobertura nueva para ScrollArea: medición de overflow, viewport,
visibilidad/timers de scrollbar, track click, thumb y corner. visibilidad/timers de scrollbar, track click, thumb y corner.
- Cobertura nueva para Splitter: registro de paneles, clamp,
collapse/expand y resize por pointer/teclado con attrs ARIA/data.
- Validado por fase: - Validado por fase:
- `npm run check` -> 0 errores, 0 warnings. - `npm run check` -> 0 errores, 0 warnings.
- `npm run test` -> 217 archivos, 2200 tests OK. - `npm run test` -> 218 archivos, 2203 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.
@ -161,6 +163,7 @@ Actualizacion 2026-05-15:
- `toolbar-provider.svelte.test.ts` -> 3 tests OK. - `toolbar-provider.svelte.test.ts` -> 3 tests OK.
- `stepper-provider.svelte.test.ts` -> 3 tests OK. - `stepper-provider.svelte.test.ts` -> 3 tests OK.
- `scroll-area-provider.svelte.test.ts` -> 3 tests OK. - `scroll-area-provider.svelte.test.ts` -> 3 tests OK.
- `splitter-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.

@ -545,6 +545,8 @@ Los tests por componente estan creciendo desde las piezas de mayor riesgo. A
attrs runtime, selección lineal/no lineal, teclado y prev/next/completed. attrs runtime, selección lineal/no lineal, teclado y prev/next/completed.
- `scroll-area/scroll-area-provider.svelte.test.ts` — medición de overflow, - `scroll-area/scroll-area-provider.svelte.test.ts` — medición de overflow,
viewport, visibilidad/timers de scrollbar, track click, thumb y corner. viewport, visibilidad/timers de scrollbar, track click, thumb y corner.
- `splitter/splitter-provider.svelte.test.ts` — registro de paneles, clamp,
collapse/expand y resize por pointer/teclado con attrs ARIA/data.
Pendiente: seguir ampliando cobertura por el resto del catalogo Soma, ya por Pendiente: seguir ampliando cobertura por el resto del catalogo Soma, ya por
componentes de riesgo medio y familias menos centrales. `table-core`, componentes de riesgo medio y familias menos centrales. `table-core`,

@ -0,0 +1,257 @@
// @vitest-environment jsdom
import { tick } from 'svelte';
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 {
SplitterPanelProvider,
SplitterProvider,
SplitterResizeTriggerProvider
} from './splitter-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.includes('splitter.resize') ? 'Resize panels' : key),
...sources
})
} as unknown as Soma;
vi.spyOn(Soma, 'require').mockReturnValue(soma);
vi.spyOn(SplitterProvider.ctx, 'set').mockImplementation((value) => value);
return { dom };
}
function splitterOpts(root = document.createElement('div')) {
return {
id: state('splitter-root'),
ref: state<HTMLElement | null>(root),
orientation: state<'horizontal' | 'vertical'>('horizontal'),
disabled: state(false),
dir: state<'ltr' | 'rtl'>('ltr'),
keyboardStep: state(5),
onResize: state(vi.fn()),
onResizeEnd: state(vi.fn())
};
}
function panelOpts(panelId: string, el: HTMLElement, defaultSize: number) {
return {
id: state(`splitter-panel-${panelId}`),
ref: state<HTMLElement | null>(el),
panelId: state(panelId),
defaultSize: state(defaultSize),
minSize: state(10),
maxSize: state(90),
collapsible: state(false),
collapsedSize: state(0)
};
}
function pointerEvent(
overrides: Partial<PointerEvent> & {
currentTarget?: EventTarget;
clientX?: number;
clientY?: number;
button?: number;
pointerId?: number;
} = {}
) {
return {
button: 0,
clientX: 0,
clientY: 0,
pointerId: 1,
currentTarget: document.createElement('div'),
preventDefault: vi.fn(),
...overrides
};
}
function keyEvent(key: string) {
return {
key,
preventDefault: vi.fn()
};
}
describe('SplitterProvider', () => {
afterEach(() => {
vi.restoreAllMocks();
document.body.innerHTML = '';
});
it('registers panels, clamps resize and projects root/panel attrs', async () => {
const { dom } = installSomaHarness();
const root = document.createElement('div');
const panelAEl = document.createElement('div');
const panelBEl = document.createElement('div');
document.body.append(root, panelAEl, panelBEl);
const opts = splitterOpts(root);
const { result, cleanup } = withEffectRoot(() => {
const provider = SplitterProvider.create(opts);
vi.spyOn(SplitterProvider, 'require').mockReturnValue(provider);
const panelA = SplitterPanelProvider.create(panelOpts('a', panelAEl, 40));
const panelB = SplitterPanelProvider.create(panelOpts('b', panelBEl, 60));
return { provider, panelA, panelB };
});
expect(result.provider.getSizes()).toEqual([40, 60]);
expect(result.panelA.props.style).toMatchObject({
'flex-basis': '0px',
'flex-grow': 40,
'flex-shrink': 0,
'min-width': '0px'
});
expect(result.panelB.isCollapsed).toBe(false);
result.provider.resizePanels('a', 'b', 70);
expect(result.provider.getSizes()).toEqual([90, 10]);
expect(opts.onResize.current).toHaveBeenLastCalledWith([90, 10]);
opts.disabled.current = true;
result.provider.resizePanels('a', 'b', -50);
expect(result.provider.getSizes()).toEqual([90, 10]);
await tick();
expect(root.getAttribute('data-orientation')).toBe('horizontal');
expect(root.getAttribute('data-disabled')).toBe('');
expect(panelAEl.getAttribute('data-state')).toBe('expanded');
expect(panelAEl.getAttribute('data-orientation')).toBe('horizontal');
expect(panelAEl.getAttribute('data-splitter-panel-id')).toBe('a');
expect(panelBEl.getAttribute('data-splitter-panel-id')).toBe('b');
cleanup();
dom.dispose();
});
it('collapses and expands collapsible panels while respecting adjacent constraints', () => {
const { dom } = installSomaHarness();
const root = document.createElement('div');
const panelAEl = document.createElement('div');
const panelBEl = document.createElement('div');
document.body.append(root, panelAEl, panelBEl);
const opts = splitterOpts(root);
const firstOpts = panelOpts('a', panelAEl, 30);
firstOpts.collapsible.current = true;
firstOpts.collapsedSize.current = 0;
const secondOpts = panelOpts('b', panelBEl, 70);
secondOpts.maxSize.current = 100;
const { result, cleanup } = withEffectRoot(() => {
const provider = SplitterProvider.create(opts);
vi.spyOn(SplitterProvider, 'require').mockReturnValue(provider);
const panelA = SplitterPanelProvider.create(firstOpts);
const panelB = SplitterPanelProvider.create(secondOpts);
return { provider, panelA, panelB };
});
result.provider.collapsePanel('a');
expect(result.provider.getSizes()).toEqual([0, 100]);
expect(result.panelA.isCollapsed).toBe(true);
expect(result.panelA.props.style['flex-grow']).toBe(0);
expect(opts.onResize.current).toHaveBeenLastCalledWith([0, 100]);
result.provider.expandPanel('a');
expect(result.provider.getSizes()).toEqual([30, 70]);
expect(result.panelA.isCollapsed).toBe(false);
expect(result.panelB.props.style['flex-grow']).toBe(70);
cleanup();
dom.dispose();
});
it('resizes with pointer and keyboard through the resize trigger', async () => {
const { dom } = installSomaHarness();
const root = document.createElement('div');
const panelAEl = document.createElement('div');
const panelBEl = document.createElement('div');
const triggerEl = document.createElement('div');
const setPointerCapture = vi.fn();
triggerEl.setPointerCapture = setPointerCapture;
root.getBoundingClientRect = () => ({ width: 400, height: 100 }) as DOMRect;
document.body.append(root, panelAEl, panelBEl, triggerEl);
const opts = splitterOpts(root);
const { result, cleanup } = withEffectRoot(() => {
const provider = SplitterProvider.create(opts);
vi.spyOn(SplitterProvider, 'require').mockReturnValue(provider);
SplitterPanelProvider.create(panelOpts('a', panelAEl, 50));
SplitterPanelProvider.create(panelOpts('b', panelBEl, 50));
const trigger = SplitterResizeTriggerProvider.create({
id: state('splitter-trigger'),
ref: state<HTMLElement | null>(triggerEl),
panelBefore: state('a'),
panelAfter: state('b'),
disabled: state(false)
});
return { provider, trigger };
});
const down = pointerEvent({ currentTarget: triggerEl, clientX: 100, pointerId: 2 });
result.trigger.onpointerdown(down as never);
expect(down.preventDefault).toHaveBeenCalledOnce();
expect(setPointerCapture).toHaveBeenCalledWith(2);
expect(result.trigger.dragging).toBe(true);
expect(result.provider.resizing).toBe(true);
const move = pointerEvent({ clientX: 140 });
result.trigger.onpointermove(move as never);
expect(move.preventDefault).toHaveBeenCalledOnce();
expect(result.provider.getSizes()).toEqual([60, 40]);
expect(opts.onResize.current).toHaveBeenLastCalledWith([60, 40]);
result.trigger.onpointerup(pointerEvent() as never);
expect(result.trigger.dragging).toBe(false);
expect(result.provider.resizing).toBe(false);
expect(opts.onResizeEnd.current).toHaveBeenLastCalledWith([60, 40]);
const next = keyEvent('ArrowRight');
result.trigger.onkeydown(next as never);
expect(next.preventDefault).toHaveBeenCalledOnce();
expect(result.provider.getSizes()).toEqual([65, 35]);
expect(opts.onResizeEnd.current).toHaveBeenLastCalledWith([65, 35]);
const home = keyEvent('Home');
result.trigger.onkeydown(home as never);
expect(home.preventDefault).toHaveBeenCalledOnce();
expect(result.provider.getSizes()).toEqual([10, 90]);
const end = keyEvent('End');
result.trigger.onkeydown(end as never);
expect(end.preventDefault).toHaveBeenCalledOnce();
expect(result.provider.getSizes()).toEqual([90, 10]);
await tick();
expect(root.getAttribute('data-resizing')).toBeNull();
expect(triggerEl.getAttribute('role')).toBe('separator');
expect(triggerEl.getAttribute('aria-label')).toBe('Resize panels');
expect(triggerEl.getAttribute('aria-valuenow')).toBe('90');
expect(triggerEl.getAttribute('aria-valuemin')).toBe('10');
expect(triggerEl.getAttribute('aria-valuemax')).toBe('90');
expect(triggerEl.getAttribute('data-state')).toBe('inactive');
cleanup();
dom.dispose();
});
});
Loading…
Cancel
Save

Powered by TurnKey Linux.