parent
419ec80890
commit
c108d02512
@ -0,0 +1,274 @@
|
||||
// @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 { FieldProvider } from '$soma/components/field/field-provider.svelte';
|
||||
|
||||
import { RatingGroupItemProvider, RatingGroupProvider } from './rating-group-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 soma = {
|
||||
dom,
|
||||
langs: {
|
||||
ts: vi.fn(() => 'Rating')
|
||||
},
|
||||
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(RatingGroupProvider.ctx, 'set').mockImplementation((value) => value);
|
||||
|
||||
return { dom };
|
||||
}
|
||||
|
||||
function ratingOpts(root = document.createElement('div')) {
|
||||
return {
|
||||
id: state('rating-root'),
|
||||
ref: state<HTMLElement | null>(root),
|
||||
value: state(2.5),
|
||||
min: state(0),
|
||||
max: state(5),
|
||||
allowHalf: state(true),
|
||||
hoverPreview: state(true),
|
||||
clearable: state(true),
|
||||
disabled: state(false),
|
||||
readonly: state(false),
|
||||
required: state(false),
|
||||
invalid: state(false),
|
||||
orientation: state<'horizontal' | 'vertical'>('horizontal'),
|
||||
dir: state<'ltr' | 'rtl'>('ltr'),
|
||||
name: state<string | undefined>('rating'),
|
||||
ariaLabel: state<string | undefined>(undefined),
|
||||
onValueChange: state<((value: number) => void) | undefined>(undefined)
|
||||
};
|
||||
}
|
||||
|
||||
function itemOpts(index: number, el: HTMLElement) {
|
||||
return {
|
||||
id: state(`rating-item-${index}`),
|
||||
ref: state<HTMLElement | null>(el),
|
||||
index: state(index),
|
||||
disabled: state(false)
|
||||
};
|
||||
}
|
||||
|
||||
function pointerEvent(
|
||||
el: HTMLElement,
|
||||
overrides: Partial<PointerEvent> & {
|
||||
clientX?: number;
|
||||
clientY?: number;
|
||||
pointerType?: string;
|
||||
} = {}
|
||||
) {
|
||||
return {
|
||||
currentTarget: el,
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
pointerType: 'mouse',
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
function keyEvent(key: string) {
|
||||
return {
|
||||
key,
|
||||
preventDefault: vi.fn()
|
||||
};
|
||||
}
|
||||
|
||||
function fakeField() {
|
||||
return {
|
||||
get isDisabled() {
|
||||
return false;
|
||||
},
|
||||
get isReadonly() {
|
||||
return false;
|
||||
},
|
||||
get isRequired() {
|
||||
return true;
|
||||
},
|
||||
get isInvalid() {
|
||||
return true;
|
||||
}
|
||||
} as FieldProvider;
|
||||
}
|
||||
|
||||
describe('RatingGroupProvider', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('projects slider attrs, Field flags and item states', async () => {
|
||||
const { dom } = installSomaHarness(fakeField());
|
||||
const root = document.createElement('div');
|
||||
const activeEl = document.createElement('span');
|
||||
const partialEl = document.createElement('span');
|
||||
document.body.append(root, activeEl, partialEl);
|
||||
const opts = ratingOpts(root);
|
||||
|
||||
const { result, cleanup } = withEffectRoot(() => {
|
||||
const provider = RatingGroupProvider.create(opts);
|
||||
vi.spyOn(RatingGroupProvider, 'require').mockReturnValue(provider);
|
||||
const active = RatingGroupItemProvider.create(itemOpts(1, activeEl));
|
||||
const partial = RatingGroupItemProvider.create(itemOpts(2, partialEl));
|
||||
return { provider, active, partial };
|
||||
});
|
||||
|
||||
expect(result.provider.items).toEqual([
|
||||
{ index: 0, state: 'active' },
|
||||
{ index: 1, state: 'active' },
|
||||
{ index: 2, state: 'partial' },
|
||||
{ index: 3, state: 'inactive' },
|
||||
{ index: 4, state: 'inactive' }
|
||||
]);
|
||||
expect(result.provider.snippetProps).toMatchObject({
|
||||
value: 2.5,
|
||||
min: 0,
|
||||
max: 5,
|
||||
isFocused: false,
|
||||
isHovering: false
|
||||
});
|
||||
expect(result.provider.props.tabindex).toBe(0);
|
||||
expect(result.active.snippetProps).toEqual({ index: 1, state: 'active' });
|
||||
expect(result.partial.snippetProps).toEqual({ index: 2, state: 'partial' });
|
||||
|
||||
result.provider.onfocus();
|
||||
await tick();
|
||||
expect(root.getAttribute('role')).toBe('slider');
|
||||
expect(root.getAttribute('aria-label')).toBe('Rating');
|
||||
expect(root.getAttribute('aria-valuenow')).toBe('2.5');
|
||||
expect(root.getAttribute('aria-valuemin')).toBe('0');
|
||||
expect(root.getAttribute('aria-valuemax')).toBe('5');
|
||||
expect(root.getAttribute('data-required')).toBe('');
|
||||
expect(root.getAttribute('data-invalid')).toBe('');
|
||||
expect(root.getAttribute('data-focused')).toBe('');
|
||||
expect(activeEl.getAttribute('role')).toBe('presentation');
|
||||
expect(activeEl.getAttribute('data-state')).toBe('active');
|
||||
expect(activeEl.getAttribute('data-value')).toBe('2');
|
||||
expect(partialEl.getAttribute('data-state')).toBe('partial');
|
||||
expect(partialEl.getAttribute('data-value')).toBe('3');
|
||||
|
||||
cleanup();
|
||||
dom.dispose();
|
||||
});
|
||||
|
||||
it('previews half values, commits clicks and clears the current value', () => {
|
||||
const { dom } = installSomaHarness();
|
||||
const focusSpy = vi.spyOn(dom, 'focus');
|
||||
const root = document.createElement('div');
|
||||
const itemEl = document.createElement('span');
|
||||
itemEl.getBoundingClientRect = () => ({ left: 0, top: 0, width: 100, height: 20 }) as DOMRect;
|
||||
document.body.append(root, itemEl);
|
||||
const opts = ratingOpts(root);
|
||||
const onValueChange = vi.fn();
|
||||
opts.onValueChange.current = onValueChange;
|
||||
|
||||
const { result, cleanup } = withEffectRoot(() => {
|
||||
const provider = RatingGroupProvider.create(opts);
|
||||
vi.spyOn(RatingGroupProvider, 'require').mockReturnValue(provider);
|
||||
const item = RatingGroupItemProvider.create(itemOpts(2, itemEl));
|
||||
return { provider, item };
|
||||
});
|
||||
|
||||
result.item.onpointermove(pointerEvent(itemEl, { clientX: 25 }) as never);
|
||||
expect(result.provider.hoverValue).toBe(2.5);
|
||||
expect(result.provider.items[2].state).toBe('partial');
|
||||
|
||||
result.item.onpointermove(pointerEvent(itemEl, { clientX: 75, pointerType: 'touch' }) as never);
|
||||
expect(result.provider.hoverValue).toBe(2.5);
|
||||
|
||||
result.provider.onpointerleave();
|
||||
expect(result.provider.hoverValue).toBeNull();
|
||||
|
||||
result.item.onclick(pointerEvent(itemEl, { clientX: 75 }) as never);
|
||||
expect(opts.value.current).toBe(3);
|
||||
expect(onValueChange).toHaveBeenLastCalledWith(3);
|
||||
expect(focusSpy).toHaveBeenCalledWith(root);
|
||||
|
||||
result.item.onclick(pointerEvent(itemEl, { clientX: 75 }) as never);
|
||||
expect(opts.value.current).toBe(0);
|
||||
expect(onValueChange).toHaveBeenLastCalledWith(0);
|
||||
|
||||
cleanup();
|
||||
dom.dispose();
|
||||
});
|
||||
|
||||
it('resizes with keyboard and guards disabled or readonly state', () => {
|
||||
const { dom } = installSomaHarness();
|
||||
const root = document.createElement('div');
|
||||
document.body.appendChild(root);
|
||||
const opts = ratingOpts(root);
|
||||
opts.value.current = 2;
|
||||
const onValueChange = vi.fn();
|
||||
opts.onValueChange.current = onValueChange;
|
||||
|
||||
const { result, cleanup } = withEffectRoot(() => RatingGroupProvider.create(opts));
|
||||
|
||||
const next = keyEvent('ArrowRight');
|
||||
result.onkeydown(next as never);
|
||||
expect(next.preventDefault).toHaveBeenCalledOnce();
|
||||
expect(opts.value.current).toBe(2.5);
|
||||
|
||||
const prev = keyEvent('ArrowLeft');
|
||||
result.onkeydown(prev as never);
|
||||
expect(prev.preventDefault).toHaveBeenCalledOnce();
|
||||
expect(opts.value.current).toBe(2);
|
||||
|
||||
const home = keyEvent('Home');
|
||||
result.onkeydown(home as never);
|
||||
expect(home.preventDefault).toHaveBeenCalledOnce();
|
||||
expect(opts.value.current).toBe(0);
|
||||
|
||||
const end = keyEvent('End');
|
||||
result.onkeydown(end as never);
|
||||
expect(end.preventDefault).toHaveBeenCalledOnce();
|
||||
expect(opts.value.current).toBe(5);
|
||||
|
||||
const numeric = keyEvent('4');
|
||||
result.onkeydown(numeric as never);
|
||||
expect(numeric.preventDefault).toHaveBeenCalledOnce();
|
||||
expect(opts.value.current).toBe(4);
|
||||
|
||||
opts.dir.current = 'rtl';
|
||||
const rtlLeft = keyEvent('ArrowLeft');
|
||||
result.onkeydown(rtlLeft as never);
|
||||
expect(rtlLeft.preventDefault).toHaveBeenCalledOnce();
|
||||
expect(opts.value.current).toBe(4.5);
|
||||
|
||||
opts.readonly.current = true;
|
||||
result.onkeydown(keyEvent('ArrowLeft') as never);
|
||||
expect(opts.value.current).toBe(4.5);
|
||||
|
||||
opts.readonly.current = false;
|
||||
opts.disabled.current = true;
|
||||
expect(result.props.tabindex).toBe(-1);
|
||||
result.onkeydown(keyEvent('End') as never);
|
||||
expect(opts.value.current).toBe(4.5);
|
||||
expect(onValueChange).toHaveBeenLastCalledWith(4.5);
|
||||
|
||||
cleanup();
|
||||
dom.dispose();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in new issue