From 5e558f022e7d6a0311e3bae0d778ebc91559fcd8 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 23 Aug 2024 00:09:54 +0300 Subject: [PATCH] test: added slider tests --- packages/core/src/useSlider/useSlider.spec.ts | 459 ++++++++++++++++++ packages/core/src/useSlider/useSlider.ts | 1 - .../core/src/useSlider/useSliderThumb.spec.ts | 8 + packages/core/src/useSlider/useSliderThumb.ts | 2 + 4 files changed, 469 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/useSlider/useSlider.spec.ts create mode 100644 packages/core/src/useSlider/useSliderThumb.spec.ts diff --git a/packages/core/src/useSlider/useSlider.spec.ts b/packages/core/src/useSlider/useSlider.spec.ts new file mode 100644 index 00000000..e9741dbe --- /dev/null +++ b/packages/core/src/useSlider/useSlider.spec.ts @@ -0,0 +1,459 @@ +import { type Component, onMounted } from 'vue'; +import { SliderThumbProps, useSliderThumb } from './useSliderThumb'; +import { SliderProps, useSlider } from './useSlider'; +import { fireEvent, render, screen } from '@testing-library/vue'; +import { flush } from '@test-utils/flush'; +import { axe } from 'vitest-axe'; +import { describe } from 'vitest'; + +function createThumbComponent(props: SliderThumbProps): Component { + return { + setup() { + const { thumbProps, currentValue } = useSliderThumb(props); + + return { + thumbProps, + currentValue, + ...props, + }; + }, + template: `
{{ currentValue }}
`, + }; +} + +function createSliderComponent(props: SliderProps): Component { + return { + setup() { + const { sliderValue, labelProps, trackProps, groupProps, outputProps, trackRef } = useSlider(props); + + onMounted(() => setUpRect(trackRef.value)); + + return { + sliderValue, + labelProps, + trackProps, + groupProps, + outputProps, + ...props, + }; + }, + template: `
+ {{ label }} +
+ +
+ + {{ sliderValue }} +
+ `, + }; +} + +function setUpRect(el: HTMLElement | undefined) { + if (!el) { + return; + } + + el.getBoundingClientRect = vi.fn(() => ({ + x: 0, + y: 0, + width: 100, + height: 1, + top: 0, + right: 100, + bottom: 1, + left: 0, + toJSON: () => {}, + })); +} + +describe('should not have a11y errors', () => { + const Thumb = createThumbComponent({}); + const Slider = createSliderComponent({ + label: 'Slider', + }); + + test('with single thumb set up', async () => { + await render({ + components: { Thumb, Slider }, + template: ` +
+ + + +
+ `, + }); + + await flush(); + vi.useRealTimers(); + expect(await axe(screen.getByTestId('fixture'))).toHaveNoViolations(); + vi.useFakeTimers(); + }); + + test('with multiple thumb set up', async () => { + await render({ + components: { Thumb, Slider }, + template: ` +
+ + + + +
+ `, + }); + + await flush(); + vi.useRealTimers(); + expect(await axe(screen.getByTestId('fixture'))).toHaveNoViolations(); + vi.useFakeTimers(); + }); +}); + +describe('thumb behavior', () => { + const Thumb = createThumbComponent({}); + const Slider = createSliderComponent({ + label: 'Slider', + }); + + test('can be dragged to set value', async () => { + await render({ + components: { Thumb, Slider }, + template: ` + + + + `, + }); + + await fireEvent.mouseDown(screen.getByRole('slider'), { clientX: 0, clientY: 0 }); + await fireEvent.mouseMove(screen.getByRole('slider'), { clientX: 83, clientY: 0 }); + await fireEvent.mouseUp(screen.getByRole('slider')); + + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '83'); + expect(screen.getByTestId('slider-value')).toHaveTextContent('83'); + }); + + test('can be dragged to set value in RTL', async () => { + const RtlSlider = createSliderComponent({ + label: 'Slider', + dir: 'rtl', + }); + + await render({ + components: { Thumb, RtlSlider }, + template: ` + + + + `, + }); + + await fireEvent.mouseDown(screen.getByRole('slider'), { clientX: 0, clientY: 0 }); + await fireEvent.mouseMove(screen.getByRole('slider'), { clientX: 17, clientY: 0 }); + await fireEvent.mouseUp(screen.getByRole('slider')); + + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '83'); + expect(screen.getByTestId('slider-value')).toHaveTextContent('83'); + }); + + test('does not respond to right clicks', async () => { + await render({ + components: { Thumb, Slider }, + template: ` + + + + `, + }); + + await fireEvent.mouseDown(screen.getByRole('slider'), { button: 1, clientX: 0, clientY: 0 }); + await fireEvent.mouseMove(screen.getByRole('slider'), { clientX: 50, clientY: 0 }); + await fireEvent.mouseUp(screen.getByRole('slider')); + + expect(screen.getByTestId('slider-value')).toHaveTextContent(''); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0'); + }); + + test('does not respond if slider is disabled', async () => { + const DisabledSlider = createSliderComponent({ + label: 'Slider', + disabled: true, + }); + + await render({ + components: { Thumb, DisabledSlider }, + template: ` + + + + `, + }); + + await fireEvent.mouseDown(screen.getByRole('slider'), { button: 1, clientX: 0, clientY: 0 }); + await fireEvent.mouseMove(screen.getByRole('slider'), { clientX: 50, clientY: 0 }); + await fireEvent.mouseUp(screen.getByRole('slider')); + + expect(screen.getByTestId('slider-value')).toHaveTextContent(''); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0'); + }); +}); + +describe('keyboard behavior', () => { + const Thumb = createThumbComponent({}); + const Slider = createSliderComponent({ + label: 'Slider', + step: 2, + }); + const RtlSlider = createSliderComponent({ + label: 'Slider', + dir: 'rtl', + step: 2, + }); + const VerticalSlider = createSliderComponent({ + label: 'Slider', + orientation: 'vertical', + step: 2, + }); + + describe('Left/Right Arrows', () => { + test('Decrases/Increases the value in LTR', async () => { + await render({ + components: { Thumb, Slider }, + template: ` + + + + `, + }); + + expect(screen.getByTestId('slider-value')).toHaveTextContent(''); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0'); + await fireEvent.keyDown(screen.getByRole('slider'), { code: 'ArrowRight' }); + expect(screen.getByTestId('slider-value')).toHaveTextContent('2'); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '2'); + await fireEvent.keyDown(screen.getByRole('slider'), { code: 'ArrowLeft' }); + expect(screen.getByTestId('slider-value')).toHaveTextContent('0'); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0'); + }); + + test('Increases/Decreases the value in RTL', async () => { + await render({ + components: { Thumb, RtlSlider }, + template: ` + + + + `, + }); + + expect(screen.getByTestId('slider-value')).toHaveTextContent(''); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0'); + await fireEvent.keyDown(screen.getByRole('slider'), { code: 'ArrowLeft' }); + expect(screen.getByTestId('slider-value')).toHaveTextContent('2'); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '2'); + await fireEvent.keyDown(screen.getByRole('slider'), { code: 'ArrowRight' }); + expect(screen.getByTestId('slider-value')).toHaveTextContent('0'); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0'); + }); + + test('does not respond if slider is disabled', async () => { + const DisabledSlider = createSliderComponent({ + label: 'Slider', + disabled: true, + }); + + await render({ + components: { Thumb, DisabledSlider }, + template: ` + + + + `, + }); + + expect(screen.getByTestId('slider-value')).toHaveTextContent(''); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0'); + await fireEvent.keyDown(screen.getByRole('slider'), { code: 'ArrowRight' }); + expect(screen.getByTestId('slider-value')).toHaveTextContent(''); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0'); + }); + }); + + describe('Up/Down Arrows', () => { + test('Decrases/Increases the value horizontally', async () => { + await render({ + components: { Thumb, Slider }, + template: ` + + + + `, + }); + + expect(screen.getByTestId('slider-value')).toHaveTextContent(''); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0'); + await fireEvent.keyDown(screen.getByRole('slider'), { code: 'ArrowUp' }); + expect(screen.getByTestId('slider-value')).toHaveTextContent('2'); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '2'); + await fireEvent.keyDown(screen.getByRole('slider'), { code: 'ArrowDown' }); + expect(screen.getByTestId('slider-value')).toHaveTextContent('0'); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0'); + }); + + test('Decreases/Increases the value vertically', async () => { + await render({ + components: { Thumb, VerticalSlider }, + template: ` + + + + `, + }); + + expect(screen.getByTestId('slider-value')).toHaveTextContent(''); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0'); + await fireEvent.keyDown(screen.getByRole('slider'), { code: 'ArrowUp' }); + expect(screen.getByTestId('slider-value')).toHaveTextContent('2'); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '2'); + await fireEvent.keyDown(screen.getByRole('slider'), { code: 'ArrowDown' }); + expect(screen.getByTestId('slider-value')).toHaveTextContent('0'); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0'); + }); + + test('does not respond if slider is disabled', async () => { + const DisabledSlider = createSliderComponent({ + label: 'Slider', + disabled: true, + }); + + await render({ + components: { Thumb, DisabledSlider }, + template: ` + + + + `, + }); + + expect(screen.getByTestId('slider-value')).toHaveTextContent(''); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0'); + await fireEvent.keyDown(screen.getByRole('slider'), { code: 'ArrowUp' }); + expect(screen.getByTestId('slider-value')).toHaveTextContent(''); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0'); + }); + }); + + describe('Page Up/Down Keys', () => { + test('Increases/Decreases the value', async () => { + await render({ + components: { Thumb, Slider }, + template: ` + + + + `, + }); + + expect(screen.getByTestId('slider-value')).toHaveTextContent(''); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0'); + await fireEvent.keyDown(screen.getByRole('slider'), { code: 'PageUp' }); + expect(screen.getByTestId('slider-value')).toHaveTextContent('100'); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '100'); + await fireEvent.keyDown(screen.getByRole('slider'), { code: 'PageDown' }); + expect(screen.getByTestId('slider-value')).toHaveTextContent('0'); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0'); + }); + }); + + describe('Home/End Keys', () => { + test('Increases/Decreases the value', async () => { + await render({ + components: { Thumb, Slider }, + template: ` + + + + `, + }); + + expect(screen.getByTestId('slider-value')).toHaveTextContent(''); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0'); + await fireEvent.keyDown(screen.getByRole('slider'), { code: 'Home' }); + expect(screen.getByTestId('slider-value')).toHaveTextContent('100'); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '100'); + await fireEvent.keyDown(screen.getByRole('slider'), { code: 'End' }); + expect(screen.getByTestId('slider-value')).toHaveTextContent('0'); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0'); + }); + }); +}); + +describe('track behavior', () => { + const Thumb = createThumbComponent({}); + const Slider = createSliderComponent({ + label: 'Slider', + }); + + test('clicking the track sets the thumb position and value', async () => { + await render({ + components: { Thumb, Slider }, + template: ` + + + + `, + }); + + await fireEvent.mouseDown(screen.getByTestId('track'), { clientX: 50, clientY: 1 }); + expect(screen.getByTestId('slider-value')).toHaveTextContent('50'); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '50'); + }); + + test('clicking the track sets the nearest thumb position for multi thumb sliders', async () => { + const MultiSlider = createSliderComponent({ + label: 'MultiSlider', + modelValue: [0, 50], + }); + + await render({ + components: { Thumb, MultiSlider }, + template: ` + + + + + `, + }); + + await fireEvent.mouseDown(screen.getByTestId('track'), { clientX: 80, clientY: 1 }); + expect(screen.getByTestId('slider-value')).toHaveTextContent('[ 0, 80 ]'); + const sliders = screen.getAllByRole('slider'); + expect(sliders[0]).toHaveAttribute('aria-valuenow', '0'); + expect(sliders[1]).toHaveAttribute('aria-valuenow', '80'); + await fireEvent.mouseDown(screen.getByTestId('track'), { clientX: 10, clientY: 1 }); + expect(sliders[0]).toHaveAttribute('aria-valuenow', '10'); + expect(sliders[1]).toHaveAttribute('aria-valuenow', '80'); + }); + + test('does not respond if slider is disabled', async () => { + const DisabledSlider = createSliderComponent({ + label: 'Slider', + disabled: true, + }); + + await render({ + components: { Thumb, DisabledSlider }, + template: ` + + + + `, + }); + + await fireEvent.mouseDown(screen.getByTestId('track'), { clientX: 50, clientY: 1 }); + expect(screen.getByTestId('slider-value')).toHaveTextContent(''); + expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0'); + }); +}); diff --git a/packages/core/src/useSlider/useSlider.ts b/packages/core/src/useSlider/useSlider.ts index 5ab251dd..3dce15a4 100644 --- a/packages/core/src/useSlider/useSlider.ts +++ b/packages/core/src/useSlider/useSlider.ts @@ -122,7 +122,6 @@ export function useSlider(_props: Reactivify) { id: inputId, role: 'group', dir: toValue(props.dir), - 'aria-orientation': toValue(props.orientation) || 'horizontal', })); function getThumbValue(idx: number) { diff --git a/packages/core/src/useSlider/useSliderThumb.spec.ts b/packages/core/src/useSlider/useSliderThumb.spec.ts new file mode 100644 index 00000000..c2983402 --- /dev/null +++ b/packages/core/src/useSlider/useSliderThumb.spec.ts @@ -0,0 +1,8 @@ +import { renderSetup } from '@test-utils/renderSetup'; +import { useSliderThumb } from './useSliderThumb'; + +test('warns if no slider exists in context', async () => { + const warn = vi.spyOn(console, 'warn'); + await renderSetup(() => useSliderThumb({})); + expect(warn).toHaveBeenCalledOnce(); +}); diff --git a/packages/core/src/useSlider/useSliderThumb.ts b/packages/core/src/useSlider/useSliderThumb.ts index dd644428..56d2b621 100644 --- a/packages/core/src/useSlider/useSliderThumb.ts +++ b/packages/core/src/useSlider/useSliderThumb.ts @@ -75,6 +75,8 @@ export function useSliderThumb(_props: Reactivify, elementRef? return withRefCapture( { tabindex: '0', + role: 'slider', + 'aria-orientation': slider.getOrientation(), 'aria-label': ownLabel ?? undefined, ...(ownLabel ? {} : slider.getSliderLabelProps()), ...spinButtonProps.value,