diff --git a/src/shared/ui/CheckboxFilter/CheckboxFilter.svelte.test.ts b/src/shared/ui/CheckboxFilter/CheckboxFilter.svelte.test.ts new file mode 100644 index 0000000..615d23a --- /dev/null +++ b/src/shared/ui/CheckboxFilter/CheckboxFilter.svelte.test.ts @@ -0,0 +1,85 @@ +import type { Property } from '$shared/store/createFilterStore'; +import { + fireEvent, + render, + screen, +} from '@testing-library/svelte'; +import { + beforeEach, + describe, + expect, + it, + vi, +} from 'vitest'; +import CheckboxFilter from './CheckboxFilter.svelte'; + +describe('CheckboxFilter', () => { + const mockProperties: Property[] = [ + { id: '1', name: 'Sans-serif', selected: false }, + { id: '2', name: 'Serif', selected: true }, + { id: '3', name: 'Display', selected: false }, + ]; + + const mockOnPropertyToggle = vi.fn(); + + beforeEach(() => { + mockOnPropertyToggle.mockClear(); + }); + + it('renders with correct label', () => { + render(CheckboxFilter, { + displayedLabel: 'Font Categories', + properties: mockProperties, + onPropertyToggle: mockOnPropertyToggle, + }); + + expect(screen.getByText('Font Categories')).toBeInTheDocument(); + }); + + it('displays all properties as checkboxes', () => { + render(CheckboxFilter, { + displayedLabel: 'Categories', + properties: mockProperties, + onPropertyToggle: mockOnPropertyToggle, + }); + + expect(screen.getByLabelText('Sans-serif')).toBeInTheDocument(); + expect(screen.getByLabelText('Serif')).toBeInTheDocument(); + expect(screen.getByLabelText('Display')).toBeInTheDocument(); + }); + + it('shows selected count badge when items selected', () => { + render(CheckboxFilter, { + displayedLabel: 'Categories', + properties: mockProperties, + onPropertyToggle: mockOnPropertyToggle, + }); + + expect(screen.getByText('1')).toBeInTheDocument(); + }); + + it('does not show badge when no items selected', () => { + const allUnselected = mockProperties.map(p => ({ ...p, selected: false })); + + render(CheckboxFilter, { + displayedLabel: 'Categories', + properties: allUnselected, + onPropertyToggle: mockOnPropertyToggle, + }); + + expect(screen.queryByText('0')).not.toBeInTheDocument(); + }); + + it('calls onPropertyToggle when checkbox clicked', async () => { + render(CheckboxFilter, { + displayedLabel: 'Categories', + properties: mockProperties, + onPropertyToggle: mockOnPropertyToggle, + }); + + const checkbox = screen.getByLabelText('Sans-serif'); + await checkbox.click(); + + expect(mockOnPropertyToggle).toHaveBeenCalledWith('1'); + }); +}); diff --git a/src/shared/ui/ComboControl/ComboControl.svelte.test.ts b/src/shared/ui/ComboControl/ComboControl.svelte.test.ts new file mode 100644 index 0000000..762db05 --- /dev/null +++ b/src/shared/ui/ComboControl/ComboControl.svelte.test.ts @@ -0,0 +1,308 @@ +import { + fireEvent, + render, +} from '@testing-library/svelte'; +import { + beforeEach, + describe, + expect, + it, + vi, +} from 'vitest'; +import ComboControl from './ComboControl.svelte'; + +describe('ComboControl', () => { + const onChangeMock = vi.fn() as (value: number) => void; + const onIncreaseMock = vi.fn() as () => void; + const onDecreaseMock = vi.fn() as () => void; + + it('renders with default values', () => { + const { container } = render(ComboControl, { + value: 50, + onChange: onChangeMock, + onIncrease: onIncreaseMock, + onDecrease: onDecreaseMock, + }); + + // Check that the control button displays the value + const controlButton = container.querySelector( + 'button[variant="outline"][size="icon"]:nth-child(2)', + ); + expect(controlButton?.textContent).toBe('50'); + }); + + it('renders with custom min/max/step', () => { + const { container } = render(ComboControl, { + value: 5, + minValue: 0, + maxValue: 10, + step: 0.5, + onChange: onChangeMock, + onIncrease: onIncreaseMock, + onDecrease: onDecreaseMock, + }); + + const controlButton = container.querySelector( + 'button[variant="outline"][size="icon"]:nth-child(2)', + ); + expect(controlButton?.textContent).toBe('5'); + }); + + it('calls onIncrease when increase button is clicked', async () => { + const { getByLabelText } = render(ComboControl, { + value: 5, + onChange: onChangeMock, + onIncrease: onIncreaseMock, + onDecrease: onDecreaseMock, + increaseLabel: 'Increase value', + }); + + const increaseButton = getByLabelText('Increase value'); + await fireEvent.click(increaseButton); + + expect(onIncreaseMock).toHaveBeenCalledTimes(1); + }); + + it('calls onDecrease when decrease button is clicked', async () => { + const { getByLabelText } = render(ComboControl, { + value: 5, + onChange: onChangeMock, + onIncrease: onIncreaseMock, + onDecrease: onDecreaseMock, + decreaseLabel: 'Decrease value', + }); + + const decreaseButton = getByLabelText('Decrease value'); + await fireEvent.click(decreaseButton); + + expect(onDecreaseMock).toHaveBeenCalledTimes(1); + }); + + it('disables increase button when increaseDisabled is true', () => { + const { getByLabelText } = render(ComboControl, { + value: 100, + minValue: 0, + maxValue: 100, + onChange: onChangeMock, + onIncrease: onIncreaseMock, + onDecrease: onDecreaseMock, + increaseDisabled: true, + increaseLabel: 'Increase value', + }); + + const increaseButton = getByLabelText('Increase value'); + expect(increaseButton).toBeDisabled(); + }); + + it('disables decrease button when decreaseDisabled is true', () => { + const { getByLabelText } = render(ComboControl, { + value: 0, + minValue: 0, + maxValue: 100, + onChange: onChangeMock, + onIncrease: onIncreaseMock, + onDecrease: onDecreaseMock, + decreaseDisabled: true, + decreaseLabel: 'Decrease value', + }); + + const decreaseButton = getByLabelText('Decrease value'); + expect(decreaseButton).toBeDisabled(); + }); + + it('opens popover when control button is clicked', async () => { + const { getByLabelText, queryByRole } = render(ComboControl, { + value: 5, + onChange: onChangeMock, + onIncrease: onIncreaseMock, + onDecrease: onDecreaseMock, + controlLabel: 'Control value', + }); + + // Initially, popover content should not be visible + expect(queryByRole('dialog')).not.toBeInTheDocument(); + + const controlButton = getByLabelText('Control value'); + await fireEvent.click(controlButton); + + // After clicking, popover content should be visible + expect(queryByRole('dialog')).toBeInTheDocument(); + }); + + it('updates value when slider changes', async () => { + const { getByLabelText, container } = render(ComboControl, { + value: 5, + minValue: 0, + maxValue: 10, + step: 1, + onChange: onChangeMock, + onIncrease: onIncreaseMock, + onDecrease: onDecreaseMock, + controlLabel: 'Control value', + }); + + // Open popover + const controlButton = getByLabelText('Control value'); + await fireEvent.click(controlButton); + + // Find slider - the Slider component should render an input with role slider + const slider = container.querySelector('[role="slider"]'); + expect(slider).toBeInTheDocument(); + + // Simulate slider change + await fireEvent.input(slider!, { target: { value: '7' } }); + + expect(onChangeMock).toHaveBeenCalledWith(7); + }); + + it('updates value when number input changes', async () => { + const { getByLabelText, container } = render(ComboControl, { + value: 5, + minValue: 0, + maxValue: 10, + onChange: onChangeMock, + onIncrease: onIncreaseMock, + onDecrease: onDecreaseMock, + controlLabel: 'Control value', + }); + + // Open popover + const controlButton = getByLabelText('Control value'); + await fireEvent.click(controlButton); + + // Find number input + const input = container.querySelector('input[type="text"], input[type="number"]'); + expect(input).toBeInTheDocument(); + + // Simulate input change + await fireEvent.change(input!, { target: { value: '8' } }); + + expect(onChangeMock).toHaveBeenCalledWith(8); + }); + + it('respects min and max values on input', () => { + const { getByLabelText, container } = render(ComboControl, { + value: 5, + minValue: 0, + maxValue: 10, + onChange: onChangeMock, + onIncrease: onIncreaseMock, + onDecrease: onDecreaseMock, + controlLabel: 'Control value', + }); + + // Open popover + const controlButton = getByLabelText('Control value'); + fireEvent.click(controlButton); + + // Find input + const input = container.querySelector('input[type="text"], input[type="number"]'); + + // Check min and max attributes + expect(input).toHaveAttribute('min', '0'); + expect(input).toHaveAttribute('max', '10'); + }); + + it('uses custom aria-labels', () => { + const { getByLabelText } = render(ComboControl, { + value: 5, + onChange: onChangeMock, + onIncrease: onIncreaseMock, + onDecrease: onDecreaseMock, + increaseLabel: 'Increase by step', + decreaseLabel: 'Decrease by step', + controlLabel: 'Change value', + }); + + expect(getByLabelText('Increase by step')).toBeInTheDocument(); + expect(getByLabelText('Decrease by step')).toBeInTheDocument(); + expect(getByLabelText('Change value')).toBeInTheDocument(); + }); + + it('uses default min/max/step values when not provided', () => { + const { getByLabelText, container } = render(ComboControl, { + value: 50, + onChange: onChangeMock, + onIncrease: onIncreaseMock, + onDecrease: onDecreaseMock, + controlLabel: 'Control value', + }); + + // Open popover + const controlButton = getByLabelText('Control value'); + fireEvent.click(controlButton); + + // Find input + const input = container.querySelector('input[type="text"], input[type="number"]'); + + // Check default values (0, 100, 1) + expect(input).toHaveAttribute('min', '0'); + expect(input).toHaveAttribute('max', '100'); + }); + + it('does not call onChange when input value is invalid', async () => { + const { getByLabelText, container } = render(ComboControl, { + value: 5, + onChange: onChangeMock, + onIncrease: onIncreaseMock, + onDecrease: onDecreaseMock, + controlLabel: 'Control value', + }); + + // Open popover + const controlButton = getByLabelText('Control value'); + await fireEvent.click(controlButton); + + // Find input + const input = container.querySelector('input[type="text"], input[type="number"]'); + + // Simulate invalid input + await fireEvent.change(input!, { target: { value: 'invalid' } }); + + expect(onChangeMock).not.toHaveBeenCalled(); + }); + + it('displays current value in input field', async () => { + const { getByLabelText, container } = render(ComboControl, { + value: 42, + onChange: onChangeMock, + onIncrease: onIncreaseMock, + onDecrease: onDecreaseMock, + controlLabel: 'Control value', + }); + + // Open popover + const controlButton = getByLabelText('Control value'); + await fireEvent.click(controlButton); + + // Find input + const input = container.querySelector('input[type="text"], input[type="number"]'); + + expect(input).toHaveValue('42'); + }); + + it('handles step value for slider precision', async () => { + const { getByLabelText, container } = render(ComboControl, { + value: 5, + minValue: 0, + maxValue: 10, + step: 0.25, + onChange: onChangeMock, + onIncrease: onIncreaseMock, + onDecrease: onDecreaseMock, + controlLabel: 'Control value', + }); + + // Open popover + const controlButton = getByLabelText('Control value'); + await fireEvent.click(controlButton); + + // Find slider + const slider = container.querySelector('[role="slider"]'); + + // Simulate slider change + await fireEvent.input(slider!, { target: { value: '5.5' } }); + + expect(onChangeMock).toHaveBeenCalledWith(5.5); + }); +});