import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import Switch from '../index'
import { SwitchSkeleton } from '../skeleton'
const getThumb = (switchElement: HTMLElement) => switchElement.querySelector('span')
describe('Switch', () => {
it('should render in unchecked state when value is false', () => {
render()
const switchElement = screen.getByRole('switch')
expect(switchElement).toBeInTheDocument()
expect(switchElement).toHaveAttribute('aria-checked', 'false')
expect(switchElement).not.toHaveAttribute('data-checked')
})
it('should render in checked state when value is true', () => {
render()
const switchElement = screen.getByRole('switch')
expect(switchElement).toHaveAttribute('aria-checked', 'true')
expect(switchElement).toHaveAttribute('data-checked', '')
})
it('should call onChange with next value when clicked', async () => {
const onChange = vi.fn()
const user = userEvent.setup()
render()
const switchElement = screen.getByRole('switch')
await user.click(switchElement)
expect(onChange).toHaveBeenCalledWith(true)
expect(onChange).toHaveBeenCalledTimes(1)
expect(switchElement).toHaveAttribute('aria-checked', 'false')
})
it('should work in controlled mode with value prop', async () => {
const onChange = vi.fn()
const user = userEvent.setup()
const { rerender } = render()
const switchElement = screen.getByRole('switch')
expect(switchElement).toHaveAttribute('aria-checked', 'false')
await user.click(switchElement)
expect(onChange).toHaveBeenCalledWith(true)
expect(switchElement).toHaveAttribute('aria-checked', 'false')
rerender()
expect(switchElement).toHaveAttribute('aria-checked', 'true')
})
it('should not call onChange when disabled', async () => {
const onChange = vi.fn()
const user = userEvent.setup()
render()
const switchElement = screen.getByRole('switch')
expect(switchElement).toHaveClass('data-[disabled]:cursor-not-allowed')
expect(switchElement).toHaveAttribute('data-disabled', '')
await user.click(switchElement)
expect(onChange).not.toHaveBeenCalled()
})
it('should apply correct size classes', () => {
const { rerender } = render()
const switchElement = screen.getByRole('switch')
expect(switchElement).toHaveClass('h-2.5', 'w-3.5', 'rounded-[2px]')
rerender()
expect(switchElement).toHaveClass('h-3', 'w-5')
rerender()
expect(switchElement).toHaveClass('h-4', 'w-7')
rerender()
expect(switchElement).toHaveClass('h-5', 'w-9')
})
it('should apply custom className', () => {
render()
expect(screen.getByRole('switch')).toHaveClass('custom-test-class')
})
it('should expose checked state styling hooks on the root and thumb', () => {
const { rerender } = render()
const switchElement = screen.getByRole('switch')
const thumb = getThumb(switchElement)
expect(switchElement).toHaveClass('bg-components-toggle-bg-unchecked', 'data-[checked]:bg-components-toggle-bg')
expect(thumb).toHaveClass('data-[checked]:translate-x-[14px]')
expect(thumb).not.toHaveAttribute('data-checked')
rerender()
expect(switchElement).toHaveAttribute('data-checked', '')
expect(thumb).toHaveAttribute('data-checked', '')
})
it('should expose disabled state styling hooks instead of relying on opacity', () => {
const { rerender } = render()
const switchElement = screen.getByRole('switch')
expect(switchElement).toHaveClass(
'data-[disabled]:bg-components-toggle-bg-unchecked-disabled',
'data-[disabled]:data-[checked]:bg-components-toggle-bg-disabled',
)
expect(switchElement).toHaveAttribute('data-disabled', '')
rerender()
expect(switchElement).toHaveAttribute('data-disabled', '')
expect(switchElement).toHaveAttribute('data-checked', '')
})
it('should have focus-visible ring styles', () => {
render()
const switchElement = screen.getByRole('switch')
expect(switchElement).toHaveClass('focus-visible:ring-2')
})
it('should respect prefers-reduced-motion', () => {
render()
const switchElement = screen.getByRole('switch')
expect(switchElement).toHaveClass('motion-reduce:transition-none')
})
describe('loading state', () => {
it('should render as disabled when loading', async () => {
const onChange = vi.fn()
const user = userEvent.setup()
render()
const switchElement = screen.getByRole('switch')
expect(switchElement).toHaveClass('data-[disabled]:cursor-not-allowed')
expect(switchElement).toHaveAttribute('aria-busy', 'true')
expect(switchElement).toHaveAttribute('data-disabled', '')
await user.click(switchElement)
expect(onChange).not.toHaveBeenCalled()
})
it('should show spinner icon for md and lg sizes', () => {
const { rerender, container } = render()
expect(container.querySelector('span[aria-hidden="true"] i')).toBeInTheDocument()
rerender()
expect(container.querySelector('span[aria-hidden="true"] i')).toBeInTheDocument()
})
it('should not show spinner for xs and sm sizes', () => {
const { rerender, container } = render()
expect(container.querySelector('span[aria-hidden="true"] i')).not.toBeInTheDocument()
rerender()
expect(container.querySelector('span[aria-hidden="true"] i')).not.toBeInTheDocument()
})
it('should apply disabled data-state hooks when loading', () => {
const { rerender } = render()
const switchElement = screen.getByRole('switch')
expect(switchElement).toHaveAttribute('data-disabled', '')
rerender()
expect(switchElement).toHaveAttribute('data-disabled', '')
expect(switchElement).toHaveAttribute('data-checked', '')
})
})
})
describe('SwitchSkeleton', () => {
it('should render a plain div without switch role', () => {
render()
expect(screen.queryByRole('switch')).not.toBeInTheDocument()
expect(screen.getByTestId('skeleton-switch')).toBeInTheDocument()
})
it('should apply skeleton styles', () => {
render()
const el = screen.getByTestId('skeleton-switch')
expect(el).toHaveClass('bg-text-quaternary', 'opacity-20')
})
it('should apply correct skeleton size classes', () => {
const { rerender } = render()
const el = screen.getByTestId('s')
expect(el).toHaveClass('h-2.5', 'w-3.5', 'rounded-[2px]')
rerender()
expect(el).toHaveClass('h-5', 'w-9', 'rounded-[6px]')
})
it('should apply custom className to skeleton', () => {
render()
expect(screen.getByTestId('s')).toHaveClass('custom-class')
})
})