import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { createRef } from 'react'; import { describe, expect, it, vi } from 'vitest'; import { Switch } from './Switch'; import React from 'react'; describe('Switch', () => { describe('Rendering', () => { it('should render a switch button', () => { render(); const switchEl = screen.getByRole('switch'); expect(switchEl).toBeInTheDocument(); expect(switchEl).toHaveAttribute('type', 'button'); }); it('should render with a label when provided', () => { render(); expect(screen.getByText('Enable notifications')).toBeInTheDocument(); }); it('should render without a label by default', () => { const { container } = render(); expect(container.querySelector('label')).not.toBeInTheDocument(); }); it('should use provided ID', () => { render(); expect(screen.getByRole('switch')).toHaveAttribute('id', 'my-switch'); }); it('should generate unique ID when not provided', () => { const { container } = render( <> ); const switches = container.querySelectorAll('button[role="switch"]'); expect(switches).toHaveLength(2); expect(switches[0]?.id).not.toBe(switches[1]?.id); }); }); describe('States', () => { it('should apply default state classes', () => { render(); const switchEl = screen.getByRole('switch'); expect(switchEl).toHaveClass('bg-border'); }); it('should apply error state classes', () => { render(); const switchEl = screen.getByRole('switch'); expect(switchEl).toHaveClass('data-[state=checked]:bg-error'); }); it('should apply success state classes', () => { render(); const switchEl = screen.getByRole('switch'); expect(switchEl).toHaveClass('data-[state=checked]:bg-success'); }); }); describe('Sizes', () => { it('should apply small size classes', () => { render(); expect(screen.getByRole('switch')).toHaveClass('h-5', 'w-9'); }); it('should apply medium size classes by default', () => { render(); expect(screen.getByRole('switch')).toHaveClass('h-6', 'w-11'); }); it('should apply large size classes', () => { render(); expect(screen.getByRole('switch')).toHaveClass('h-7', 'w-13'); }); it('should apply size-specific label classes', () => { const { rerender } = render(); expect(screen.getByText('Text')).toHaveClass('text-xs'); rerender(); expect(screen.getByText('Text')).toHaveClass('text-sm'); rerender(); expect(screen.getByText('Text')).toHaveClass('text-base'); }); }); describe('Label Position', () => { it('should position label on the right by default', () => { render(); const label = screen.getByText('Right label'); expect(label).toHaveClass('ml-2'); expect(label).not.toHaveClass('mr-2'); }); it('should position label on the left when specified', () => { render(); const label = screen.getByText('Left label'); expect(label).toHaveClass('mr-2'); expect(label).not.toHaveClass('ml-2'); }); }); describe('Checked State', () => { it('should be unchecked by default', () => { render(); expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'false'); }); it('should be checked when defaultChecked is true', () => { render(); expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true'); }); it('should be checked when checked is true (controlled)', () => { render( {}} />); expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true'); }); it('should toggle checked state when clicked (uncontrolled)', async () => { const user = userEvent.setup(); render(); const switchEl = screen.getByRole('switch'); expect(switchEl).toHaveAttribute('aria-checked', 'false'); await user.click(switchEl); expect(switchEl).toHaveAttribute('aria-checked', 'true'); await user.click(switchEl); expect(switchEl).toHaveAttribute('aria-checked', 'false'); }); it('should update data-state attribute based on checked state', async () => { const user = userEvent.setup(); render(); const switchEl = screen.getByRole('switch'); expect(switchEl).toHaveAttribute('data-state', 'unchecked'); await user.click(switchEl); expect(switchEl).toHaveAttribute('data-state', 'checked'); }); }); describe('Loading State', () => { it('should show loading spinner when loading', () => { const { container } = render(); const spinner = container.querySelector('svg.animate-spin'); expect(spinner).toBeInTheDocument(); }); it('should not show spinner by default', () => { const { container } = render(); const spinner = container.querySelector('svg.animate-spin'); expect(spinner).not.toBeInTheDocument(); }); it('should apply loading classes when loading', () => { render(); expect(screen.getByRole('switch')).toHaveClass('cursor-wait', 'pointer-events-none'); }); it('should disable interaction when loading', async () => { const user = userEvent.setup(); const handleChange = vi.fn(); render(); const switchEl = screen.getByRole('switch'); await user.click(switchEl); expect(handleChange).not.toHaveBeenCalled(); }); it('should apply correct spinner size based on switch size', () => { const { container, rerender } = render(); let spinner = container.querySelector('svg.animate-spin'); expect(spinner).toHaveAttribute('width', '10'); expect(spinner).toHaveAttribute('height', '10'); rerender(); spinner = container.querySelector('svg.animate-spin'); expect(spinner).toHaveAttribute('width', '12'); expect(spinner).toHaveAttribute('height', '12'); rerender(); spinner = container.querySelector('svg.animate-spin'); expect(spinner).toHaveAttribute('width', '14'); expect(spinner).toHaveAttribute('height', '14'); }); }); describe('Disabled State', () => { it('should be disabled when disabled prop is true', () => { render(); expect(screen.getByRole('switch')).toBeDisabled(); }); it('should not be disabled by default', () => { render(); expect(screen.getByRole('switch')).not.toBeDisabled(); }); it('should not toggle when disabled', async () => { const user = userEvent.setup(); const handleChange = vi.fn(); render(); const switchEl = screen.getByRole('switch'); await user.click(switchEl); expect(handleChange).not.toHaveBeenCalled(); expect(switchEl).toHaveAttribute('aria-checked', 'false'); }); it('should apply disabled cursor to wrapper', () => { const { container } = render(); const wrapper = container.querySelector('div'); expect(wrapper).toHaveClass('cursor-not-allowed'); }); it('should apply disabled opacity to label', () => { render(); const label = screen.getByText('Disabled'); expect(label).toHaveClass('opacity-50'); }); }); describe('Form Integration', () => { it('should include hidden input when name is provided', () => { const { container } = render(); const hiddenInput = container.querySelector('input[name="notifications"]'); expect(hiddenInput).toBeInTheDocument(); expect(hiddenInput).toHaveAttribute('type', 'checkbox'); expect(hiddenInput).toHaveAttribute('tabindex', '-1'); expect(hiddenInput).toHaveAttribute('aria-hidden', 'true'); }); it('should not include hidden input when name is not provided', () => { const { container } = render(); const hiddenInput = container.querySelector('input[type="checkbox"]'); expect(hiddenInput).not.toBeInTheDocument(); }); it('should sync hidden input checked state', async () => { const user = userEvent.setup(); const { container } = render(); const hiddenInput = container.querySelector( 'input[name="test"]' ) as HTMLInputElement; const switchEl = screen.getByRole('switch'); expect(hiddenInput?.checked).toBe(false); await user.click(switchEl); expect(hiddenInput?.checked).toBe(true); }); it('should include value in hidden input', () => { const { container } = render(); const hiddenInput = container.querySelector('input[name="test"]'); expect(hiddenInput).toHaveAttribute('value', 'yes'); }); }); describe('ARIA Attributes', () => { it('should have role="switch"', () => { render(); expect(screen.getByRole('switch')).toBeInTheDocument(); }); it('should set aria-checked correctly', async () => { const user = userEvent.setup(); render(); const switchEl = screen.getByRole('switch'); expect(switchEl).toHaveAttribute('aria-checked', 'false'); await user.click(switchEl); expect(switchEl).toHaveAttribute('aria-checked', 'true'); }); it('should support aria-label when no label is provided', () => { render(); expect(screen.getByLabelText('Toggle feature')).toBeInTheDocument(); }); it('should use aria-labelledby when label is provided', () => { render(); const switchEl = screen.getByRole('switch'); expect(switchEl).toHaveAttribute('aria-labelledby', 'my-switch-label'); }); it('should support custom aria-labelledby', () => { render(); expect(screen.getByRole('switch')).toHaveAttribute( 'aria-labelledby', 'custom-label' ); }); it('should set aria-hidden on thumb', () => { const { container } = render(); const thumb = container.querySelector('span[aria-hidden="true"]'); expect(thumb).toBeInTheDocument(); }); }); describe('Event Handlers', () => { it('should call onChange when clicked', async () => { const user = userEvent.setup(); const handleChange = vi.fn(); render(); await user.click(screen.getByRole('switch')); expect(handleChange).toHaveBeenCalledTimes(1); expect(handleChange).toHaveBeenCalledWith(true); }); it('should call onChange with correct value', async () => { const user = userEvent.setup(); const handleChange = vi.fn(); render(); const switchEl = screen.getByRole('switch'); await user.click(switchEl); expect(handleChange).toHaveBeenCalledWith(true); await user.click(switchEl); expect(handleChange).toHaveBeenCalledWith(false); }); it('should toggle with Space key', async () => { const user = userEvent.setup(); render(); const switchEl = screen.getByRole('switch'); switchEl.focus(); expect(switchEl).toHaveAttribute('aria-checked', 'false'); await user.keyboard(' '); expect(switchEl).toHaveAttribute('aria-checked', 'true'); await user.keyboard(' '); expect(switchEl).toHaveAttribute('aria-checked', 'false'); }); it('should toggle with Enter key', async () => { const user = userEvent.setup(); render(); const switchEl = screen.getByRole('switch'); switchEl.focus(); expect(switchEl).toHaveAttribute('aria-checked', 'false'); await user.keyboard('{Enter}'); expect(switchEl).toHaveAttribute('aria-checked', 'true'); }); }); describe('Custom className', () => { it('should merge custom className with default classes', () => { render(); const switchEl = screen.getByRole('switch'); expect(switchEl).toHaveClass('custom-class'); expect(switchEl).toHaveClass('rounded-full'); }); }); describe('Ref Forwarding', () => { it('should forward ref to button element', () => { const ref = createRef(); render(); expect(ref.current).toBeInstanceOf(HTMLButtonElement); expect(ref.current?.tagName).toBe('BUTTON'); }); it('should allow programmatic focus via ref', () => { const ref = createRef(); render(); ref.current?.focus(); expect(ref.current).toHaveFocus(); }); it('should allow programmatic click via ref', async () => { const ref = createRef(); const handleChange = vi.fn(); render(); ref.current?.click(); expect(handleChange).toHaveBeenCalledWith(true); }); }); describe('Base Classes', () => { it('should include layout classes', () => { render(); expect(screen.getByRole('switch')).toHaveClass('inline-flex', 'items-center'); }); it('should include transition classes', () => { render(); expect(screen.getByRole('switch')).toHaveClass('transition-all', 'duration-200'); }); it('should include rounded-full for circular shape', () => { render(); expect(screen.getByRole('switch')).toHaveClass('rounded-full'); }); it('should render animated thumb', () => { const { container } = render(); const thumb = container.querySelector('span[data-state]'); expect(thumb).toBeInTheDocument(); expect(thumb).toHaveClass('transition-transform'); }); it('should update thumb position based on checked state', async () => { const user = userEvent.setup(); const { container } = render(); const thumb = container.querySelector('span[data-state]'); expect(thumb).toHaveAttribute('data-state', 'unchecked'); await user.click(screen.getByRole('switch')); expect(thumb).toHaveAttribute('data-state', 'checked'); }); }); describe('Label Integration', () => { it('should toggle switch when label is clicked', async () => { const user = userEvent.setup(); render(); const switchEl = screen.getByRole('switch'); expect(switchEl).toHaveAttribute('aria-checked', 'false'); await user.click(screen.getByText('Click me')); expect(switchEl).toHaveAttribute('aria-checked', 'true'); }); it('should associate label with switch', () => { render(); const label = screen.getByText('My label'); expect(label).toHaveAttribute('id', 'my-id-label'); }); it('should not toggle when label clicked and disabled', async () => { const user = userEvent.setup(); const handleChange = vi.fn(); render(); await user.click(screen.getByText('Disabled')); expect(handleChange).not.toHaveBeenCalled(); }); it('should not toggle when label clicked and loading', async () => { const user = userEvent.setup(); const handleChange = vi.fn(); render(); await user.click(screen.getByText('Loading')); expect(handleChange).not.toHaveBeenCalled(); }); }); describe('Controlled vs Uncontrolled', () => { it('should work as uncontrolled with defaultChecked', async () => { const user = userEvent.setup(); render(); const switchEl = screen.getByRole('switch'); expect(switchEl).toHaveAttribute('aria-checked', 'true'); await user.click(switchEl); expect(switchEl).toHaveAttribute('aria-checked', 'false'); }); it('should work as controlled with checked and onChange', async () => { const user = userEvent.setup(); const ControlledSwitch = () => { const [checked, setChecked] = React.useState(false); return ; }; render(); const switchEl = screen.getByRole('switch'); expect(switchEl).toHaveAttribute('aria-checked', 'false'); await user.click(switchEl); expect(switchEl).toHaveAttribute('aria-checked', 'true'); await user.click(switchEl); expect(switchEl).toHaveAttribute('aria-checked', 'false'); }); it('should not change when controlled without onChange updating value', async () => { const user = userEvent.setup(); render( {}} />); const switchEl = screen.getByRole('switch'); expect(switchEl).toHaveAttribute('aria-checked', 'false'); await user.click(switchEl); expect(switchEl).toHaveAttribute('aria-checked', 'false'); }); }); describe('Compound Scenarios', () => { it('should work with error state and small size', () => { render(); const switchEl = screen.getByRole('switch'); expect(switchEl).toHaveClass('h-5', 'w-9'); expect(switchEl).toHaveClass('data-[state=checked]:bg-error'); }); it('should work with success state and large size', () => { render(); const switchEl = screen.getByRole('switch'); expect(switchEl).toHaveClass('h-7', 'w-13'); expect(switchEl).toHaveClass('data-[state=checked]:bg-success'); }); it('should work with loading and checked states', () => { const { container } = render( {}} />); const switchEl = screen.getByRole('switch'); expect(switchEl).toHaveAttribute('aria-checked', 'true'); expect(switchEl).toHaveAttribute('data-state', 'checked'); expect(container.querySelector('svg.animate-spin')).toBeInTheDocument(); }); it('should work with all props combined', () => { const handleChange = vi.fn(); render( ); const switchEl = screen.getByRole('switch'); expect(switchEl).toHaveAttribute('aria-checked', 'true'); expect(screen.getByText('Enable feature')).toBeInTheDocument(); }); }); });