/** * Button 组件测试 * 测试覆盖: variants, sizes, icons, loading, disabled, fullWidth */ import { render, screen, fireEvent } from '@testing-library/react'; import { describe, it, expect, vi } from 'vitest'; import { Search, ArrowRight } from 'lucide-react'; import { Button } from './Button'; describe('Button', () => { // ==================== 基础渲染测试 ==================== describe('基础渲染', () => { it('渲染按钮文本', () => { render(); expect(screen.getByRole('button', { name: '点击我' })).toBeInTheDocument(); }); it('默认使用 primary variant 和 md size', () => { render(); const button = screen.getByRole('button'); expect(button).toHaveClass('bg-accent-indigo'); expect(button).toHaveClass('px-4', 'py-2.5'); }); }); // ==================== Variant 测试 ==================== describe('Variant 样式', () => { it('primary variant 应用正确样式', () => { render(); expect(screen.getByRole('button')).toHaveClass('bg-accent-indigo', 'text-white'); }); it('secondary variant 应用正确样式', () => { render(); expect(screen.getByRole('button')).toHaveClass('bg-bg-elevated', 'text-text-secondary'); }); it('danger variant 应用正确样式', () => { render(); expect(screen.getByRole('button')).toHaveClass('bg-accent-coral', 'text-white'); }); it('success variant 应用正确样式', () => { render(); expect(screen.getByRole('button')).toHaveClass('bg-accent-green', 'text-white'); }); it('ghost variant 应用正确样式', () => { render(); expect(screen.getByRole('button')).toHaveClass('bg-transparent', 'text-text-secondary'); }); }); // ==================== Size 测试 ==================== describe('Size 样式', () => { it('sm size 应用正确样式', () => { render(); expect(screen.getByRole('button')).toHaveClass('px-3', 'py-1.5', 'text-small'); }); it('md size 应用正确样式', () => { render(); expect(screen.getByRole('button')).toHaveClass('px-4', 'py-2.5', 'text-body'); }); it('lg size 应用正确样式', () => { render(); expect(screen.getByRole('button')).toHaveClass('px-6', 'py-3', 'text-section-title'); }); }); // ==================== Icon 测试 ==================== describe('Icon 渲染', () => { // 使用 innerHTML 正则匹配验证图标和文本的相对位置 // 这种方式对 DOM 结构变化(如添加 wrapper)更健壮 it('左侧图标正确渲染(图标在文本之前)', () => { render(); const button = screen.getByRole('button'); expect(button.querySelector('svg')).toBeInTheDocument(); // 验证 SVG 在 "搜索" 文本之前 const html = button.innerHTML; const svgPos = html.indexOf(' { render(); const button = screen.getByRole('button'); expect(button.querySelector('svg')).toBeInTheDocument(); // 验证 SVG 在 "下一步" 文本之后 const html = button.innerHTML; const svgPos = html.indexOf(' { render(); const button = screen.getByRole('button'); expect(button.querySelector('svg')).toBeInTheDocument(); // 验证默认情况下 SVG 在 "搜索" 文本之前 const html = button.innerHTML; const svgPos = html.indexOf(' { it('loading 状态显示加载动画', () => { render(); const button = screen.getByRole('button'); const spinner = button.querySelector('svg.animate-spin'); expect(spinner).toBeInTheDocument(); }); it('loading 状态禁用按钮', () => { render(); expect(screen.getByRole('button')).toBeDisabled(); }); it('loading 状态应用 opacity-50 样式', () => { render(); expect(screen.getByRole('button')).toHaveClass('opacity-50'); }); it('loading 状态隐藏原有图标', () => { render(); const button = screen.getByRole('button'); // 应该只有 spinner,没有 Search 图标 const svgs = button.querySelectorAll('svg'); expect(svgs).toHaveLength(1); expect(svgs[0]).toHaveClass('animate-spin'); }); }); // ==================== Disabled 状态测试 ==================== describe('Disabled 状态', () => { it('disabled 属性禁用按钮', () => { render(); expect(screen.getByRole('button')).toBeDisabled(); }); it('disabled 状态应用正确样式', () => { render(); expect(screen.getByRole('button')).toHaveClass('opacity-50', 'cursor-not-allowed'); }); it('disabled 状态不触发点击事件', () => { const handleClick = vi.fn(); render(); fireEvent.click(screen.getByRole('button')); expect(handleClick).not.toHaveBeenCalled(); }); }); // ==================== FullWidth 测试 ==================== describe('FullWidth 属性', () => { it('fullWidth 应用 w-full 样式', () => { render(); expect(screen.getByRole('button')).toHaveClass('w-full'); }); it('非 fullWidth 不应用 w-full 样式', () => { render(); expect(screen.getByRole('button')).not.toHaveClass('w-full'); }); }); // ==================== 事件处理测试 ==================== describe('事件处理', () => { it('点击触发 onClick 事件', () => { const handleClick = vi.fn(); render(); fireEvent.click(screen.getByRole('button')); expect(handleClick).toHaveBeenCalledTimes(1); }); it('多次点击触发多次事件', () => { const handleClick = vi.fn(); render(); fireEvent.click(screen.getByRole('button')); fireEvent.click(screen.getByRole('button')); fireEvent.click(screen.getByRole('button')); expect(handleClick).toHaveBeenCalledTimes(3); }); }); // ==================== 自定义属性测试 ==================== describe('自定义属性', () => { it('支持自定义 className', () => { render(); expect(screen.getByRole('button')).toHaveClass('custom-class'); }); it('支持原生 button 属性', () => { render(); const button = screen.getByTestId('submit-btn'); expect(button).toHaveAttribute('type', 'submit'); }); }); });