Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 12s
233 lines
9.2 KiB
TypeScript
233 lines
9.2 KiB
TypeScript
import React from 'react';
|
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
import { describe, it, expect, vi } from 'vitest';
|
|
import { Button } from './Button';
|
|
|
|
describe('Button', () => {
|
|
describe('variants', () => {
|
|
it('renders primary variant correctly', () => {
|
|
render(<Button variant="primary">Primary Button</Button>);
|
|
const button = screen.getByRole('button', { name: /primary button/i });
|
|
expect(button).toBeInTheDocument();
|
|
expect(button.className).toContain('bg-brand-secondary');
|
|
expect(button.className).toContain('hover:bg-brand-dark');
|
|
expect(button.className).toContain('text-white');
|
|
});
|
|
|
|
it('renders secondary variant correctly', () => {
|
|
render(<Button variant="secondary">Secondary Button</Button>);
|
|
const button = screen.getByRole('button', { name: /secondary button/i });
|
|
expect(button).toBeInTheDocument();
|
|
expect(button.className).toContain('bg-gray-200');
|
|
expect(button.className).toContain('hover:bg-gray-300');
|
|
});
|
|
|
|
it('renders danger variant correctly', () => {
|
|
render(<Button variant="danger">Delete</Button>);
|
|
const button = screen.getByRole('button', { name: /delete/i });
|
|
expect(button).toBeInTheDocument();
|
|
expect(button.className).toContain('bg-red-100');
|
|
expect(button.className).toContain('hover:bg-red-200');
|
|
expect(button.className).toContain('text-red-700');
|
|
});
|
|
|
|
it('renders ghost variant correctly', () => {
|
|
render(<Button variant="ghost">Ghost Button</Button>);
|
|
const button = screen.getByRole('button', { name: /ghost button/i });
|
|
expect(button).toBeInTheDocument();
|
|
expect(button.className).toContain('bg-transparent');
|
|
expect(button.className).toContain('hover:bg-gray-100');
|
|
});
|
|
|
|
it('defaults to primary variant when not specified', () => {
|
|
render(<Button>Default Button</Button>);
|
|
const button = screen.getByRole('button', { name: /default button/i });
|
|
expect(button.className).toContain('bg-brand-secondary');
|
|
});
|
|
});
|
|
|
|
describe('sizes', () => {
|
|
it('renders small size correctly', () => {
|
|
render(<Button size="sm">Small</Button>);
|
|
const button = screen.getByRole('button', { name: /small/i });
|
|
expect(button.className).toContain('px-3');
|
|
expect(button.className).toContain('py-1.5');
|
|
expect(button.className).toContain('text-sm');
|
|
});
|
|
|
|
it('renders medium size correctly (default)', () => {
|
|
render(<Button size="md">Medium</Button>);
|
|
const button = screen.getByRole('button', { name: /medium/i });
|
|
expect(button.className).toContain('px-4');
|
|
expect(button.className).toContain('py-2');
|
|
expect(button.className).toContain('text-base');
|
|
});
|
|
|
|
it('renders large size correctly', () => {
|
|
render(<Button size="lg">Large</Button>);
|
|
const button = screen.getByRole('button', { name: /large/i });
|
|
expect(button.className).toContain('px-6');
|
|
expect(button.className).toContain('py-3');
|
|
expect(button.className).toContain('text-lg');
|
|
});
|
|
|
|
it('defaults to medium size when not specified', () => {
|
|
render(<Button>Default Size</Button>);
|
|
const button = screen.getByRole('button', { name: /default size/i });
|
|
expect(button.className).toContain('px-4');
|
|
expect(button.className).toContain('py-2');
|
|
});
|
|
});
|
|
|
|
describe('loading state', () => {
|
|
it('shows loading spinner when isLoading is true', () => {
|
|
render(<Button isLoading>Loading Button</Button>);
|
|
const button = screen.getByRole('button', { name: /loading button/i });
|
|
expect(button).toBeDisabled();
|
|
expect(button.textContent).toContain('Loading Button');
|
|
});
|
|
|
|
it('disables button when loading', () => {
|
|
render(<Button isLoading>Loading</Button>);
|
|
const button = screen.getByRole('button', { name: /loading/i });
|
|
expect(button).toBeDisabled();
|
|
});
|
|
|
|
it('does not show loading spinner when isLoading is false', () => {
|
|
render(<Button isLoading={false}>Not Loading</Button>);
|
|
const button = screen.getByRole('button', { name: /not loading/i });
|
|
expect(button).not.toBeDisabled();
|
|
});
|
|
});
|
|
|
|
describe('disabled state', () => {
|
|
it('disables button when disabled prop is true', () => {
|
|
render(<Button disabled>Disabled Button</Button>);
|
|
const button = screen.getByRole('button', { name: /disabled button/i });
|
|
expect(button).toBeDisabled();
|
|
expect(button.className).toContain('disabled:cursor-not-allowed');
|
|
});
|
|
|
|
it('does not trigger onClick when disabled', () => {
|
|
const handleClick = vi.fn();
|
|
render(
|
|
<Button disabled onClick={handleClick}>
|
|
Disabled
|
|
</Button>,
|
|
);
|
|
const button = screen.getByRole('button', { name: /disabled/i });
|
|
fireEvent.click(button);
|
|
expect(handleClick).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('triggers onClick when not disabled', () => {
|
|
const handleClick = vi.fn();
|
|
render(<Button onClick={handleClick}>Click Me</Button>);
|
|
const button = screen.getByRole('button', { name: /click me/i });
|
|
fireEvent.click(button);
|
|
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('icons', () => {
|
|
it('renders left icon correctly', () => {
|
|
const leftIcon = <span data-testid="left-icon">←</span>;
|
|
render(<Button leftIcon={leftIcon}>With Left Icon</Button>);
|
|
expect(screen.getByTestId('left-icon')).toBeInTheDocument();
|
|
const button = screen.getByRole('button', { name: /with left icon/i });
|
|
expect(button.textContent).toBe('←With Left Icon');
|
|
});
|
|
|
|
it('renders right icon correctly', () => {
|
|
const rightIcon = <span data-testid="right-icon">→</span>;
|
|
render(<Button rightIcon={rightIcon}>With Right Icon</Button>);
|
|
expect(screen.getByTestId('right-icon')).toBeInTheDocument();
|
|
const button = screen.getByRole('button', { name: /with right icon/i });
|
|
expect(button.textContent).toBe('With Right Icon→');
|
|
});
|
|
|
|
it('renders both left and right icons', () => {
|
|
const leftIcon = <span data-testid="left-icon">←</span>;
|
|
const rightIcon = <span data-testid="right-icon">→</span>;
|
|
render(
|
|
<Button leftIcon={leftIcon} rightIcon={rightIcon}>
|
|
With Both Icons
|
|
</Button>,
|
|
);
|
|
expect(screen.getByTestId('left-icon')).toBeInTheDocument();
|
|
expect(screen.getByTestId('right-icon')).toBeInTheDocument();
|
|
});
|
|
|
|
it('hides icons when loading', () => {
|
|
const leftIcon = <span data-testid="left-icon">←</span>;
|
|
const rightIcon = <span data-testid="right-icon">→</span>;
|
|
render(
|
|
<Button isLoading leftIcon={leftIcon} rightIcon={rightIcon}>
|
|
Loading
|
|
</Button>,
|
|
);
|
|
expect(screen.queryByTestId('left-icon')).not.toBeInTheDocument();
|
|
expect(screen.queryByTestId('right-icon')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('fullWidth', () => {
|
|
it('applies full width class when fullWidth is true', () => {
|
|
render(<Button fullWidth>Full Width</Button>);
|
|
const button = screen.getByRole('button', { name: /full width/i });
|
|
expect(button.className).toContain('w-full');
|
|
});
|
|
|
|
it('does not apply full width class when fullWidth is false', () => {
|
|
render(<Button fullWidth={false}>Not Full Width</Button>);
|
|
const button = screen.getByRole('button', { name: /not full width/i });
|
|
expect(button.className).not.toContain('w-full');
|
|
});
|
|
});
|
|
|
|
describe('custom className', () => {
|
|
it('merges custom className with default classes', () => {
|
|
render(<Button className="custom-class">Custom</Button>);
|
|
const button = screen.getByRole('button', { name: /custom/i });
|
|
expect(button.className).toContain('custom-class');
|
|
expect(button.className).toContain('bg-brand-secondary');
|
|
});
|
|
});
|
|
|
|
describe('HTML button attributes', () => {
|
|
it('passes through type attribute', () => {
|
|
render(<Button type="submit">Submit</Button>);
|
|
const button = screen.getByRole('button', { name: /submit/i });
|
|
expect(button).toHaveAttribute('type', 'submit');
|
|
});
|
|
|
|
it('passes through aria attributes', () => {
|
|
render(<Button aria-label="Custom label">Button</Button>);
|
|
const button = screen.getByRole('button', { name: /custom label/i });
|
|
expect(button).toHaveAttribute('aria-label', 'Custom label');
|
|
});
|
|
|
|
it('passes through data attributes', () => {
|
|
render(<Button data-testid="custom-button">Button</Button>);
|
|
const button = screen.getByTestId('custom-button');
|
|
expect(button).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('focus management', () => {
|
|
it('applies focus ring classes', () => {
|
|
render(<Button>Focus Me</Button>);
|
|
const button = screen.getByRole('button', { name: /focus me/i });
|
|
expect(button.className).toContain('focus:outline-none');
|
|
expect(button.className).toContain('focus:ring-2');
|
|
expect(button.className).toContain('focus:ring-offset-2');
|
|
});
|
|
|
|
it('has focus ring for primary variant', () => {
|
|
render(<Button variant="primary">Primary</Button>);
|
|
const button = screen.getByRole('button', { name: /primary/i });
|
|
expect(button.className).toContain('focus:ring-brand-primary');
|
|
});
|
|
});
|
|
});
|