Add slider provider coverage

active-uix
dev 5 months ago
parent 9975ba8631
commit 41da8a2b26

@ -89,9 +89,11 @@ Actualizacion 2026-05-15:
- Cobertura nueva para Tabs: registro de trigger/content, activacion
automatic/manual, navegacion roving, fallback tab stop y partes
list/content/indicator.
- Cobertura nueva para Slider: contrato provider/range/thumb/tick,
snapping/clamping, drag pointer confirmado y teclado de thumb.
- Validado por fase:
- `npm run check` -> 0 errores, 0 warnings.
- `npm run test` -> 204 archivos, 2162 tests OK.
- `npm run test` -> 205 archivos, 2166 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.
@ -124,6 +126,7 @@ Actualizacion 2026-05-15:
- `toggle-provider.svelte.test.ts` -> 4 tests OK.
- `toggle-group-provider.svelte.test.ts` -> 4 tests OK.
- `tabs-provider.svelte.test.ts` -> 4 tests OK.
- `slider-provider.svelte.test.ts` -> 4 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.

@ -520,6 +520,8 @@ Los tests por componente estan creciendo desde las piezas de mayor riesgo. A
roving tabindex y navegación de items saltando disabled.
- `tabs/tabs-provider.svelte.test.ts` — registros trigger/content, activacion
automatic/manual, navegacion roving, fallback tab stop y partes auxiliares.
- `slider/slider-provider.svelte.test.ts` — contrato provider/range/thumb/tick,
snapping/clamping, drag pointer confirmado y teclado de thumb.
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,288 @@
// @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 {
SliderProvider,
SliderRangeProvider,
SliderThumbProvider,
SliderTickProvider
} from './slider-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(SliderProvider.ctx, 'set').mockImplementation((value) => value);
return { dom };
}
function createSliderRoot() {
const root = document.createElement('div');
root.getBoundingClientRect = vi.fn(
() =>
({
x: 0,
y: 0,
left: 0,
top: 0,
right: 100,
bottom: 20,
width: 100,
height: 20,
toJSON: () => ({})
}) as DOMRect
);
root.setPointerCapture = vi.fn();
root.releasePointerCapture = vi.fn();
document.body.appendChild(root);
return root;
}
function sliderOpts(root = createSliderRoot()) {
return {
id: state('slider-root'),
ref: state<HTMLElement | null>(root),
value: state([25, 75]),
type: state<'single' | 'multiple'>('multiple'),
min: state(0),
max: state(100),
step: state(5),
orientation: state<'horizontal' | 'vertical'>('horizontal'),
disabled: state(false),
dir: state<'ltr' | 'rtl'>('ltr'),
name: state('price'),
thumbLabels: state<string[] | undefined>(['Minimum', 'Maximum']),
onValueCommit: state<(value: number[]) => void>(() => undefined)
};
}
function pointerEvent(clientX: number, clientY: number, pointerId = 1): PointerEvent {
return { clientX, clientY, pointerId } as unknown as PointerEvent;
}
function keyEvent(key: string): KeyboardEvent {
return {
key,
preventDefault: vi.fn()
} as unknown as KeyboardEvent;
}
describe('SliderProvider', () => {
afterEach(() => {
vi.restoreAllMocks();
document.body.innerHTML = '';
});
it('exposes provider, range, thumb and tick contracts', async () => {
const { dom } = installSomaHarness();
const root = createSliderRoot();
const rangeEl = document.createElement('span');
const thumbEl = document.createElement('span');
const tickEl = document.createElement('span');
const opts = sliderOpts(root);
const { result, cleanup } = withEffectRoot(() => {
const provider = SliderProvider.create(opts);
vi.spyOn(SliderProvider, 'require').mockReturnValue(provider);
const range = SliderRangeProvider.create({
id: state('slider-range'),
ref: state<HTMLElement | null>(rangeEl)
});
const thumb = SliderThumbProvider.create({
id: state('slider-thumb-0'),
ref: state<HTMLElement | null>(thumbEl),
index: state(0)
});
const tick = SliderTickProvider.create({
id: state('slider-tick-50'),
ref: state<HTMLElement | null>(tickEl),
value: state(50)
});
return { provider, range, thumb, tick };
});
expect(result.provider.props).toMatchObject({
id: 'slider-root',
'data-slider': '',
dir: 'ltr'
});
expect(result.provider.props.style).toMatchObject({
position: 'relative',
'touch-action': 'pan-y'
});
expect(result.range.props).toMatchObject({
id: 'slider-range',
'data-slider-range': ''
});
expect(result.range.props.style).toMatchObject({
position: 'absolute',
left: '25%',
width: '50%'
});
expect(result.thumb.props).toMatchObject({
id: 'slider-thumb-0',
'data-slider-thumb': '',
role: 'slider',
tabindex: 0,
'aria-valuemin': 0,
'aria-valuemax': 100,
'aria-valuenow': 25,
'aria-label': 'Minimum',
'aria-orientation': 'horizontal'
});
expect(result.thumb.props.style).toMatchObject({
position: 'absolute',
left: '25%',
transform: 'translate(-50%, -50%)'
});
expect(result.thumb.inputProps).toMatchObject({
type: 'hidden',
name: 'price[0]',
value: 25,
disabled: undefined
});
expect(result.tick.props.style).toMatchObject({
position: 'absolute',
left: '50%',
transform: 'translateX(-50%)'
});
await tick();
expect(root.getAttribute('role')).toBe('group');
expect(root.getAttribute('data-orientation')).toBe('horizontal');
expect(tickEl.getAttribute('data-value')).toBe('50');
expect(tickEl.getAttribute('data-active')).toBe('');
cleanup();
dom.dispose();
});
it('snaps, clamps and prevents crossover for multiple thumbs', () => {
const { dom } = installSomaHarness();
const opts = sliderOpts();
const { result: provider, cleanup } = withEffectRoot(() => SliderProvider.create(opts));
provider.updateValue(0, 33);
expect(opts.value.current).toEqual([35, 75]);
provider.updateValue(0, 120);
expect(opts.value.current).toEqual([75, 75]);
opts.value.current = [25, 75];
provider.updateValue(1, 10);
expect(opts.value.current).toEqual([25, 25]);
opts.disabled.current = true;
provider.updateValue(0, 50);
expect(opts.value.current).toEqual([25, 25]);
cleanup();
dom.dispose();
});
it('updates and commits value during confirmed pointer drag', () => {
const { dom } = installSomaHarness();
const root = createSliderRoot();
const opts = sliderOpts(root);
opts.value.current = [0];
opts.type.current = 'single';
const onValueCommit = vi.fn();
opts.onValueCommit.current = onValueCommit;
const { result: provider, cleanup } = withEffectRoot(() => SliderProvider.create(opts));
provider.onpointerdown(pointerEvent(30, 10));
expect(provider.activeThumbIndex).toBe(0);
provider.onpointermove(pointerEvent(32, 10));
expect(opts.value.current).toEqual([0]);
expect(root.setPointerCapture).not.toHaveBeenCalled();
provider.onpointermove(pointerEvent(50, 10));
expect(root.setPointerCapture).toHaveBeenCalledWith(1);
expect(opts.value.current).toEqual([50]);
provider.onpointerup(pointerEvent(50, 10));
expect(root.releasePointerCapture).toHaveBeenCalledWith(1);
expect(provider.activeThumbIndex).toBeNull();
expect(onValueCommit).toHaveBeenCalledWith([50]);
cleanup();
dom.dispose();
});
it('commits thumb keyboard changes and respects disabled state', () => {
const { dom } = installSomaHarness();
const opts = sliderOpts();
opts.value.current = [50];
opts.type.current = 'single';
const onValueCommit = vi.fn();
opts.onValueCommit.current = onValueCommit;
const { result: thumb, cleanup } = withEffectRoot(() => {
const provider = SliderProvider.create(opts);
vi.spyOn(SliderProvider, 'require').mockReturnValue(provider);
return SliderThumbProvider.create({
id: state('slider-thumb'),
ref: state<HTMLElement | null>(document.createElement('span')),
index: state(0)
});
});
const right = keyEvent('ArrowRight');
thumb.onkeydown(right);
expect(right.preventDefault).toHaveBeenCalledOnce();
expect(opts.value.current).toEqual([55]);
expect(onValueCommit).toHaveBeenLastCalledWith([55]);
const pageDown = keyEvent('PageDown');
thumb.onkeydown(pageDown);
expect(opts.value.current).toEqual([5]);
const home = keyEvent('Home');
thumb.onkeydown(home);
expect(opts.value.current).toEqual([0]);
const end = keyEvent('End');
thumb.onkeydown(end);
expect(opts.value.current).toEqual([100]);
opts.disabled.current = true;
const left = keyEvent('ArrowLeft');
thumb.onkeydown(left);
expect(left.preventDefault).not.toHaveBeenCalled();
expect(opts.value.current).toEqual([100]);
expect(thumb.props.tabindex).toBeUndefined();
expect(thumb.inputProps.disabled).toBe(true);
cleanup();
dom.dispose();
});
});
Loading…
Cancel
Save

Powered by TurnKey Linux.