import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { Button } from './Button';
describe('Button', () => {
describe('Rendering', () => {
it('renders with default variant and size', () => {
render();
const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
expect(button).toHaveTextContent('Click me');
});
it('renders as a button element', () => {
render();
expect(screen.getByRole('button').tagName).toBe('BUTTON');
});
});
describe('Variants', () => {
it('applies default variant classes', () => {
render(
);
const button = screen.getByTestId('button');
expect(button.className).toContain('bg-primary');
expect(button.className).toContain('text-primary-foreground');
});
it('applies destructive variant classes', () => {
render(
);
const button = screen.getByTestId('button');
expect(button.className).toContain('bg-error');
expect(button.className).toContain('text-error-foreground');
});
it('applies outline variant classes', () => {
render(
);
const button = screen.getByTestId('button');
expect(button.className).toContain('border');
expect(button.className).toContain('bg-transparent');
});
it('applies secondary variant classes', () => {
render(
);
const button = screen.getByTestId('button');
expect(button.className).toContain('bg-secondary');
expect(button.className).toContain('text-secondary-foreground');
});
it('applies ghost variant classes', () => {
render(
);
const button = screen.getByTestId('button');
expect(button.className).toContain('hover:bg-accent');
});
it('applies link variant classes', () => {
render(
);
const button = screen.getByTestId('button');
expect(button.className).toContain('text-primary');
expect(button.className).toContain('underline-offset-4');
});
});
describe('Sizes', () => {
it('applies small size classes', () => {
render(
);
const button = screen.getByTestId('button');
expect(button.className).toContain('h-9');
expect(button.className).toContain('px-3');
});
it('applies medium size classes (default)', () => {
render(
);
const button = screen.getByTestId('button');
expect(button.className).toContain('h-10');
expect(button.className).toContain('px-4');
});
it('applies large size classes', () => {
render(
);
const button = screen.getByTestId('button');
expect(button.className).toContain('h-11');
expect(button.className).toContain('px-8');
});
it('applies icon size classes', () => {
render();
const button = screen.getByTestId('button');
expect(button.className).toContain('h-10');
expect(button.className).toContain('w-10');
});
});
describe('Compound Variants', () => {
it('applies compound variant for outline + small', () => {
render(
);
const button = screen.getByTestId('button');
expect(button.className).toContain('border');
expect(button.className).toContain('h-9');
});
it('applies compound variant for link variants (removes padding)', () => {
render(
);
const button = screen.getByTestId('button');
expect(button.className).toContain('px-0');
});
});
describe('Loading State', () => {
it('shows loading spinner when loading is true', () => {
render();
const button = screen.getByRole('button');
// Should have loading indicator
const spinner = button.querySelector('[aria-hidden="true"]');
expect(spinner).toBeInTheDocument();
expect(spinner?.className).toContain('animate-spin');
});
it('disables button when loading', () => {
render();
const button = screen.getByRole('button');
expect(button).toBeDisabled();
expect(button).toHaveAttribute('aria-busy', 'true');
});
it('applies cursor-wait class when loading', () => {
render(
);
const button = screen.getByTestId('button');
expect(button.className).toContain('cursor-wait');
});
it('still shows children text when loading', () => {
render();
expect(screen.getByText('Processing')).toBeInTheDocument();
});
});
describe('Disabled State', () => {
it('disables button when disabled prop is true', () => {
render();
const button = screen.getByRole('button');
expect(button).toBeDisabled();
expect(button).toHaveAttribute('aria-disabled', 'true');
});
it('applies disabled opacity class', () => {
render(
);
const button = screen.getByTestId('button');
expect(button.className).toContain('disabled:opacity-50');
expect(button.className).toContain('disabled:pointer-events-none');
});
it('is disabled when both disabled and loading are true', () => {
render(
);
const button = screen.getByRole('button');
expect(button).toBeDisabled();
});
});
describe('Custom className', () => {
it('merges custom className with variant classes', () => {
render(
);
const button = screen.getByTestId('button');
expect(button.className).toContain('custom-class');
expect(button.className).toContain('bg-primary');
});
});
describe('Ref Forwarding', () => {
it('forwards ref to button element', () => {
let ref: HTMLButtonElement | null = null;
render(
);
expect(ref).toBeInstanceOf(HTMLButtonElement);
expect(ref!.tagName).toBe('BUTTON');
});
});
describe('HTML Attributes', () => {
it('passes through HTML button attributes', () => {
render(
);
const button = screen.getByTestId('button');
expect(button).toHaveAttribute('type', 'submit');
expect(button).toHaveAttribute('name', 'test-button');
expect(button).toHaveAttribute('value', 'test-value');
});
it('supports onClick handler', () => {
let clicked = false;
render(
);
const button = screen.getByRole('button');
button.click();
expect(clicked).toBe(true);
});
});
describe('Accessibility', () => {
it('has proper ARIA attributes for loading state', () => {
render();
const button = screen.getByRole('button');
expect(button).toHaveAttribute('aria-busy', 'true');
expect(button).toHaveAttribute('aria-disabled', 'true');
});
it('has proper ARIA attributes for disabled state', () => {
render();
const button = screen.getByRole('button');
expect(button).toHaveAttribute('aria-disabled', 'true');
});
it('hides loading spinner from screen readers', () => {
render();
const button = screen.getByRole('button');
const spinner = button.querySelector('[aria-hidden="true"]');
expect(spinner).toHaveAttribute('aria-hidden', 'true');
});
it('supports aria-label for icon buttons', () => {
render(
);
const button = screen.getByRole('button');
expect(button).toHaveAccessibleName('Close dialog');
});
});
describe('Base Classes', () => {
it('always includes base classes', () => {
render();
const button = screen.getByTestId('button');
expect(button.className).toContain('inline-flex');
expect(button.className).toContain('items-center');
expect(button.className).toContain('justify-center');
expect(button.className).toContain('font-medium');
expect(button.className).toContain('rounded-md');
});
it('includes focus ring classes', () => {
render();
const button = screen.getByTestId('button');
expect(button.className).toContain('focus-ring');
});
it('includes transition classes', () => {
render();
const button = screen.getByTestId('button');
expect(button.className).toContain('transition-base');
});
});
});