/**
* Modal 组件测试
* 测试覆盖: Modal, ConfirmModal, 副作用(ESC、overflow)
*/
import { render, screen, fireEvent, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { Modal, ConfirmModal } from './Modal';
describe('Modal', () => {
const mockOnClose = vi.fn();
beforeEach(() => {
mockOnClose.mockClear();
document.body.style.overflow = '';
});
afterEach(() => {
// 确保副作用被清理
document.body.style.overflow = '';
});
// ==================== 基础渲染测试 ====================
describe('基础渲染', () => {
it('isOpen=true 时渲染内容', () => {
render(
模态框内容
);
expect(screen.getByText('模态框内容')).toBeInTheDocument();
});
it('isOpen=false 时不渲染', () => {
render(
模态框内容
);
expect(screen.queryByText('模态框内容')).not.toBeInTheDocument();
});
it('渲染标题', () => {
render(
内容
);
expect(screen.getByText('弹窗标题')).toBeInTheDocument();
});
it('渲染页脚', () => {
render(
确定}>
内容
);
expect(screen.getByText('确定')).toBeInTheDocument();
});
});
// ==================== 关闭按钮测试 ====================
describe('关闭按钮', () => {
it('默认显示关闭按钮', () => {
render(
内容
);
// 使用 aria-label 精确选择关闭按钮
expect(screen.getByLabelText('关闭')).toBeInTheDocument();
});
it('点击关闭按钮触发 onClose', () => {
render(
内容
);
// 使用 aria-label 精确选择关闭按钮
const closeButton = screen.getByLabelText('关闭');
fireEvent.click(closeButton);
expect(mockOnClose).toHaveBeenCalledTimes(1);
});
it('showCloseButton=false 隐藏关闭按钮', () => {
render(
内容
);
// 关闭按钮不存在
expect(screen.queryByLabelText('关闭')).not.toBeInTheDocument();
});
});
// ==================== 遮罩点击测试 ====================
describe('遮罩点击', () => {
it('点击遮罩默认关闭', () => {
render(
内容
);
const overlay = document.querySelector('.bg-black\\/60');
fireEvent.click(overlay!);
expect(mockOnClose).toHaveBeenCalledTimes(1);
});
it('closeOnOverlay=false 禁用遮罩点击关闭', () => {
render(
内容
);
const overlay = document.querySelector('.bg-black\\/60');
fireEvent.click(overlay!);
expect(mockOnClose).not.toHaveBeenCalled();
});
});
// ==================== ESC 键测试 ====================
describe('ESC 键关闭', () => {
it('按 ESC 键默认关闭', async () => {
render(
内容
);
await act(async () => {
fireEvent.keyDown(document, { key: 'Escape' });
});
expect(mockOnClose).toHaveBeenCalledTimes(1);
});
it('closeOnEsc=false 禁用 ESC 关闭', async () => {
render(
内容
);
await act(async () => {
fireEvent.keyDown(document, { key: 'Escape' });
});
expect(mockOnClose).not.toHaveBeenCalled();
});
it('其他按键不触发关闭', async () => {
render(
内容
);
await act(async () => {
fireEvent.keyDown(document, { key: 'Enter' });
});
expect(mockOnClose).not.toHaveBeenCalled();
});
});
// ==================== Body Overflow 副作用测试 ====================
describe('Body overflow 副作用', () => {
it('打开时锁定 body 滚动', () => {
render(
内容
);
expect(document.body.style.overflow).toBe('hidden');
});
it('关闭时解锁 body 滚动', () => {
const { rerender } = render(
内容
);
expect(document.body.style.overflow).toBe('hidden');
rerender(
内容
);
expect(document.body.style.overflow).toBe('');
});
it('卸载时清理 overflow', () => {
const { unmount } = render(
内容
);
expect(document.body.style.overflow).toBe('hidden');
unmount();
expect(document.body.style.overflow).toBe('');
});
});
// ==================== Size 测试 ====================
describe('Size 样式', () => {
it('默认 md size', () => {
const { container } = render(
内容
);
expect(container.querySelector('.max-w-md')).toBeInTheDocument();
});
it('sm size', () => {
const { container } = render(
内容
);
expect(container.querySelector('.max-w-sm')).toBeInTheDocument();
});
it('lg size', () => {
const { container } = render(
内容
);
expect(container.querySelector('.max-w-lg')).toBeInTheDocument();
});
it('xl size', () => {
const { container } = render(
内容
);
expect(container.querySelector('.max-w-xl')).toBeInTheDocument();
});
});
// ==================== ClassName 测试 ====================
describe('ClassName', () => {
it('支持自定义 className', () => {
const { container } = render(
内容
);
expect(container.querySelector('.custom-modal')).toBeInTheDocument();
});
});
});
describe('ConfirmModal', () => {
const mockOnClose = vi.fn();
const mockOnConfirm = vi.fn();
beforeEach(() => {
mockOnClose.mockClear();
mockOnConfirm.mockClear();
});
// ==================== 基础渲染测试 ====================
describe('基础渲染', () => {
it('渲染标题和消息', () => {
render(
);
expect(screen.getByText('确认删除')).toBeInTheDocument();
expect(screen.getByText('确定要删除吗?')).toBeInTheDocument();
});
it('渲染确认和取消按钮', () => {
render(
);
// Modal 有关闭按钮(X),ConfirmModal 有确认和取消按钮,共 3 个
const buttons = screen.getAllByRole('button');
expect(buttons.length).toBeGreaterThanOrEqual(2);
expect(screen.getByText('确定')).toBeInTheDocument();
expect(screen.getByText('取消')).toBeInTheDocument();
});
});
// ==================== 按钮文本自定义测试 ====================
describe('按钮文本自定义', () => {
it('支持自定义确认按钮文本', () => {
render(
);
expect(screen.getByText('删除')).toBeInTheDocument();
});
it('支持自定义取消按钮文本', () => {
render(
);
expect(screen.getByText('返回')).toBeInTheDocument();
});
});
// ==================== 事件处理测试 ====================
describe('事件处理', () => {
it('点击确认按钮触发 onConfirm', () => {
render(
);
fireEvent.click(screen.getByText('确定'));
expect(mockOnConfirm).toHaveBeenCalledTimes(1);
});
it('点击取消按钮触发 onClose', () => {
render(
);
fireEvent.click(screen.getByText('取消'));
expect(mockOnClose).toHaveBeenCalledTimes(1);
});
});
// ==================== Variant 测试 ====================
describe('Variant 样式', () => {
it('danger variant 使用红色确认按钮', () => {
render(
);
const confirmButton = screen.getByText('确认').closest('button');
expect(confirmButton).toHaveClass('bg-accent-coral');
});
});
// ==================== Loading 测试 ====================
describe('Loading 状态', () => {
it('loading 时确认按钮显示加载状态', () => {
render(
);
const confirmButton = screen.getByText('确定').closest('button');
expect(confirmButton).toBeDisabled();
});
it('loading 时取消按钮也被禁用', () => {
render(
);
const cancelButton = screen.getByText('取消').closest('button');
expect(cancelButton).toBeDisabled();
});
});
});