diff --git a/src/shared/ui/Slider/Slider.svelte.test.ts b/src/shared/ui/Slider/Slider.svelte.test.ts index bb0aff9..6901c2d 100644 --- a/src/shared/ui/Slider/Slider.svelte.test.ts +++ b/src/shared/ui/Slider/Slider.svelte.test.ts @@ -1,9 +1,31 @@ import { + fireEvent, render, screen, } from '@testing-library/svelte'; +import { + beforeAll, + vi, +} from 'vitest'; import Slider from './Slider.svelte'; +// jsdom lacks PointerEvent; back it with MouseEvent so clientX/clientY survive. +beforeAll(() => { + if (typeof PointerEvent === 'undefined') { + class PointerEventPolyfill extends MouseEvent { + pointerId: number; + constructor(type: string, params: PointerEventInit = {}) { + super(type, params); + this.pointerId = params.pointerId ?? 1; + } + } + // @ts-expect-error assigning polyfill to global + global.PointerEvent = PointerEventPolyfill; + } + HTMLElement.prototype.setPointerCapture = vi.fn(); + HTMLElement.prototype.releasePointerCapture = vi.fn(); +}); + describe('Slider', () => { describe('Rendering', () => { it('renders a slider element', () => { @@ -60,3 +82,91 @@ describe('Slider', () => { }); }); }); + +describe('Keyboard', () => { + it('increments by step on ArrowRight / ArrowUp', async () => { + const onValueChange = vi.fn(); + render(Slider, { value: 50, step: 5, onValueChange }); + const thumb = screen.getByRole('slider'); + await fireEvent.keyDown(thumb, { key: 'ArrowRight' }); + expect(thumb).toHaveAttribute('aria-valuenow', '55'); + expect(onValueChange).toHaveBeenCalledWith(55); + }); + + it('decrements by step on ArrowLeft / ArrowDown', async () => { + render(Slider, { value: 50, step: 5 }); + const thumb = screen.getByRole('slider'); + await fireEvent.keyDown(thumb, { key: 'ArrowDown' }); + expect(thumb).toHaveAttribute('aria-valuenow', '45'); + }); + + it('jumps to min on Home and max on End', async () => { + render(Slider, { value: 50, min: 10, max: 90 }); + const thumb = screen.getByRole('slider'); + await fireEvent.keyDown(thumb, { key: 'Home' }); + expect(thumb).toHaveAttribute('aria-valuenow', '10'); + await fireEvent.keyDown(thumb, { key: 'End' }); + expect(thumb).toHaveAttribute('aria-valuenow', '90'); + }); + + it('moves by step*10 on PageUp / PageDown', async () => { + render(Slider, { value: 50, step: 2 }); + const thumb = screen.getByRole('slider'); + await fireEvent.keyDown(thumb, { key: 'PageUp' }); + expect(thumb).toHaveAttribute('aria-valuenow', '70'); + await fireEvent.keyDown(thumb, { key: 'PageDown' }); + expect(thumb).toHaveAttribute('aria-valuenow', '50'); + }); + + it('clamps at the bounds', async () => { + render(Slider, { value: 98, max: 100, step: 5 }); + const thumb = screen.getByRole('slider'); + await fireEvent.keyDown(thumb, { key: 'End' }); + expect(thumb).toHaveAttribute('aria-valuenow', '100'); + }); + + it('does nothing when disabled', async () => { + const onValueChange = vi.fn(); + render(Slider, { value: 50, disabled: true, onValueChange }); + const thumb = screen.getByRole('slider'); + await fireEvent.keyDown(thumb, { key: 'ArrowRight' }); + expect(thumb).toHaveAttribute('aria-valuenow', '50'); + expect(onValueChange).not.toHaveBeenCalled(); + }); +}); + +describe('Pointer', () => { + /** + * Force a deterministic track rect since jsdom has no layout. + */ + function mockTrackRect(container: HTMLElement) { + const track = container.querySelector('[role="presentation"]') as HTMLElement; + track.getBoundingClientRect = () => + ({ left: 0, right: 200, top: 0, bottom: 20, width: 200, height: 20 }) as DOMRect; + return track; + } + + it('seeks to the clicked position (click-to-seek)', async () => { + const onValueChange = vi.fn(); + const { container } = render(Slider, { value: 0, min: 0, max: 100, onValueChange }); + const track = mockTrackRect(container); + await fireEvent.pointerDown(track, { clientX: 100, clientY: 10, pointerId: 1 }); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '50'); + expect(onValueChange).toHaveBeenCalledWith(50); + }); + + it('updates while dragging after pointerdown', async () => { + const { container } = render(Slider, { value: 0, min: 0, max: 100 }); + const track = mockTrackRect(container); + await fireEvent.pointerDown(track, { clientX: 50, clientY: 10, pointerId: 1 }); + await fireEvent.pointerMove(track, { clientX: 150, clientY: 10, pointerId: 1 }); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '75'); + }); + + it('ignores pointer when disabled', async () => { + const { container } = render(Slider, { value: 0, disabled: true }); + const track = mockTrackRect(container); + await fireEvent.pointerDown(track, { clientX: 100, clientY: 10, pointerId: 1 }); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0'); + }); +});