import { cleanup, fireEvent, render, screen } from '@testing-library/react' import Button from '../index' afterEach(cleanup) describe('Button', () => { describe('rendering', () => { it('renders children text', () => { render() expect(screen.getByRole('button')).toHaveTextContent('Click me') }) it('renders as a native button element by default', () => { render() expect(screen.getByRole('button').tagName).toBe('BUTTON') }) it('defaults to type="button"', () => { render() expect(screen.getByRole('button')).toHaveAttribute('type', 'button') }) it('allows type override to submit', () => { render() expect(screen.getByRole('button')).toHaveAttribute('type', 'submit') }) it('renders custom element via render prop', () => { render() const link = screen.getByRole('link') expect(link).toHaveTextContent('Link') expect(link).toHaveAttribute('href', '/test') }) }) describe('variants', () => { it('applies default secondary variant', () => { render() expect(screen.getByRole('button').className).toContain('btn-secondary') }) it.each([ 'primary', 'warning', 'secondary', 'secondary-accent', 'ghost', 'ghost-accent', 'tertiary', ] as const)('applies %s variant', (variant) => { render() expect(screen.getByRole('button').className).toContain(`btn-${variant}`) }) it('applies destructive modifier', () => { render() expect(screen.getByRole('button').className).toContain('btn-destructive') }) }) describe('sizes', () => { it('applies default medium size', () => { render() expect(screen.getByRole('button').className).toContain('btn-medium') }) it.each(['small', 'medium', 'large'] as const)('applies %s size', (size) => { render() expect(screen.getByRole('button').className).toContain(`btn-${size}`) }) }) describe('loading', () => { it('shows spinner when loading', () => { render() expect(screen.getByRole('button').querySelector('.animate-spin')).toBeInTheDocument() }) it('hides spinner when not loading', () => { render() expect(screen.getByRole('button').querySelector('.animate-spin')).not.toBeInTheDocument() }) it('auto-disables when loading', () => { render() expect(screen.getByRole('button')).toBeDisabled() }) it('sets aria-busy when loading', () => { render() expect(screen.getByRole('button')).toHaveAttribute('aria-busy', 'true') }) it('does not set aria-busy when not loading', () => { render() expect(screen.getByRole('button')).not.toHaveAttribute('aria-busy') }) it('applies custom spinnerClassName', () => { const animClassName = 'anim-breath' render() expect(screen.getByRole('button').querySelector('.animate-spin')?.className).toContain(animClassName) }) }) describe('disabled', () => { it('disables button when disabled prop is set', () => { render() expect(screen.getByRole('button')).toBeDisabled() }) it('keeps focusable when loading with focusableWhenDisabled', () => { render() const button = screen.getByRole('button') expect(button).toHaveAttribute('aria-disabled', 'true') }) }) describe('events', () => { it('fires onClick when clicked', () => { const onClick = vi.fn() render() fireEvent.click(screen.getByRole('button')) expect(onClick).toHaveBeenCalledTimes(1) }) it('does not fire onClick when disabled', () => { const onClick = vi.fn() render() fireEvent.click(screen.getByRole('button')) expect(onClick).not.toHaveBeenCalled() }) it('does not fire onClick when loading', () => { const onClick = vi.fn() render() fireEvent.click(screen.getByRole('button')) expect(onClick).not.toHaveBeenCalled() }) }) describe('ref forwarding', () => { it('forwards ref to the button element', () => { let buttonRef: HTMLButtonElement | null = null render( , ) expect(buttonRef).toBeInstanceOf(HTMLButtonElement) }) }) })