// @vitest-environment jsdom import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, fireEvent, cleanup } from '@testing-library/react'; import { Button, Input, SearchBar } from '../../components/ui'; describe('Button', () => { afterEach(() => cleanup()); it('renders children and triggers onClick', () => { const onClick = vi.fn(); render(); const btn = screen.getByRole('button', { name: /save/i }); fireEvent.click(btn); expect(onClick).toHaveBeenCalledTimes(1); }); it('renders loading state with aria-busy and a spinner', () => { render(); const btn = screen.getByRole('button', { name: /save/i }); expect(btn.getAttribute('aria-busy')).toBe('true'); expect(btn.disabled).toBe(true); }); it('disables on disabled prop and suppresses onClick', () => { const onClick = vi.fn(); render( ); fireEvent.click(screen.getByRole('button', { name: /save/i })); expect(onClick).not.toHaveBeenCalled(); }); it('renders all four variants without crashing', () => { const variants = ['primary', 'secondary', 'danger', 'ghost']; variants.forEach((variant) => { const { unmount } = render(); expect(screen.getByRole('button')).toBeTruthy(); unmount(); }); }); }); describe('Input', () => { afterEach(() => cleanup()); it('renders label associated with input via htmlFor/id', () => { render( {}} />); const input = screen.getByLabelText('Email'); expect(input.tagName).toBe('INPUT'); expect(input.id).toBe('email'); }); it('exposes aria-invalid + error message when error is set', () => { render( {}} error="Required" /> ); const input = screen.getByLabelText('Password'); expect(input.getAttribute('aria-invalid')).toBe('true'); expect(screen.getByText('Required')).toBeTruthy(); expect(input.getAttribute('aria-describedby')).toContain('x-error'); }); it('omits error and shows helperText when no error', () => { render( {}} helperText="3+ chars" /> ); expect(screen.getByText('3+ chars')).toBeTruthy(); const input = screen.getByLabelText('Username'); expect(input.getAttribute('aria-invalid')).toBeNull(); expect(input.getAttribute('aria-describedby')).toBe('y-helper'); }); }); describe('SearchBar', () => { afterEach(() => cleanup()); it('renders an input with placeholder and search icon', () => { render( {}} placeholder="Find a card" /> ); const input = screen.getByPlaceholderText('Find a card'); expect(input.tagName).toBe('INPUT'); expect(input.getAttribute('type')).toBe('search'); }); it('renders a clear button only when value is non-empty AND onClear is provided', () => { const onClear = vi.fn(); const { rerender } = render( {}} onClear={onClear} /> ); expect(screen.queryByRole('button', { name: /clear/i })).toBeNull(); rerender( {}} onClear={onClear} /> ); const clearBtn = screen.getByRole('button', { name: /clear/i }); fireEvent.click(clearBtn); expect(onClear).toHaveBeenCalledTimes(1); }); it('does NOT render a clear button when onClear is missing', () => { render( {}} />); expect(screen.queryByRole('button', { name: /clear/i })).toBeNull(); }); });