主要更新: - 更新代理商端文档,明确项目由品牌方分配流程 - 新增Brief配置详情页(已配置)设计稿 - 完善工作台紧急待办中品牌新任务功能 - 整理Pencil设计文件中代理商端页面顺序 - 新增后端FastAPI框架及核心API - 新增前端Next.js页面和组件库 - 添加.gitignore排除构建和缓存文件 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
417 lines
11 KiB
TypeScript
417 lines
11 KiB
TypeScript
/**
|
||
* 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(
|
||
<Modal isOpen={true} onClose={mockOnClose}>
|
||
<p>模态框内容</p>
|
||
</Modal>
|
||
);
|
||
expect(screen.getByText('模态框内容')).toBeInTheDocument();
|
||
});
|
||
|
||
it('isOpen=false 时不渲染', () => {
|
||
render(
|
||
<Modal isOpen={false} onClose={mockOnClose}>
|
||
<p>模态框内容</p>
|
||
</Modal>
|
||
);
|
||
expect(screen.queryByText('模态框内容')).not.toBeInTheDocument();
|
||
});
|
||
|
||
it('渲染标题', () => {
|
||
render(
|
||
<Modal isOpen={true} onClose={mockOnClose} title="弹窗标题">
|
||
内容
|
||
</Modal>
|
||
);
|
||
expect(screen.getByText('弹窗标题')).toBeInTheDocument();
|
||
});
|
||
|
||
it('渲染页脚', () => {
|
||
render(
|
||
<Modal isOpen={true} onClose={mockOnClose} footer={<button>确定</button>}>
|
||
内容
|
||
</Modal>
|
||
);
|
||
expect(screen.getByText('确定')).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
// ==================== 关闭按钮测试 ====================
|
||
describe('关闭按钮', () => {
|
||
it('默认显示关闭按钮', () => {
|
||
render(
|
||
<Modal isOpen={true} onClose={mockOnClose} title="标题">
|
||
内容
|
||
</Modal>
|
||
);
|
||
// 使用 aria-label 精确选择关闭按钮
|
||
expect(screen.getByLabelText('关闭')).toBeInTheDocument();
|
||
});
|
||
|
||
it('点击关闭按钮触发 onClose', () => {
|
||
render(
|
||
<Modal isOpen={true} onClose={mockOnClose} title="标题">
|
||
内容
|
||
</Modal>
|
||
);
|
||
// 使用 aria-label 精确选择关闭按钮
|
||
const closeButton = screen.getByLabelText('关闭');
|
||
fireEvent.click(closeButton);
|
||
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
it('showCloseButton=false 隐藏关闭按钮', () => {
|
||
render(
|
||
<Modal isOpen={true} onClose={mockOnClose} title="标题" showCloseButton={false}>
|
||
内容
|
||
</Modal>
|
||
);
|
||
// 关闭按钮不存在
|
||
expect(screen.queryByLabelText('关闭')).not.toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
// ==================== 遮罩点击测试 ====================
|
||
describe('遮罩点击', () => {
|
||
it('点击遮罩默认关闭', () => {
|
||
render(
|
||
<Modal isOpen={true} onClose={mockOnClose}>
|
||
内容
|
||
</Modal>
|
||
);
|
||
const overlay = document.querySelector('.bg-black\\/60');
|
||
fireEvent.click(overlay!);
|
||
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
it('closeOnOverlay=false 禁用遮罩点击关闭', () => {
|
||
render(
|
||
<Modal isOpen={true} onClose={mockOnClose} closeOnOverlay={false}>
|
||
内容
|
||
</Modal>
|
||
);
|
||
const overlay = document.querySelector('.bg-black\\/60');
|
||
fireEvent.click(overlay!);
|
||
expect(mockOnClose).not.toHaveBeenCalled();
|
||
});
|
||
});
|
||
|
||
// ==================== ESC 键测试 ====================
|
||
describe('ESC 键关闭', () => {
|
||
it('按 ESC 键默认关闭', async () => {
|
||
render(
|
||
<Modal isOpen={true} onClose={mockOnClose}>
|
||
内容
|
||
</Modal>
|
||
);
|
||
|
||
await act(async () => {
|
||
fireEvent.keyDown(document, { key: 'Escape' });
|
||
});
|
||
|
||
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
it('closeOnEsc=false 禁用 ESC 关闭', async () => {
|
||
render(
|
||
<Modal isOpen={true} onClose={mockOnClose} closeOnEsc={false}>
|
||
内容
|
||
</Modal>
|
||
);
|
||
|
||
await act(async () => {
|
||
fireEvent.keyDown(document, { key: 'Escape' });
|
||
});
|
||
|
||
expect(mockOnClose).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('其他按键不触发关闭', async () => {
|
||
render(
|
||
<Modal isOpen={true} onClose={mockOnClose}>
|
||
内容
|
||
</Modal>
|
||
);
|
||
|
||
await act(async () => {
|
||
fireEvent.keyDown(document, { key: 'Enter' });
|
||
});
|
||
|
||
expect(mockOnClose).not.toHaveBeenCalled();
|
||
});
|
||
});
|
||
|
||
// ==================== Body Overflow 副作用测试 ====================
|
||
describe('Body overflow 副作用', () => {
|
||
it('打开时锁定 body 滚动', () => {
|
||
render(
|
||
<Modal isOpen={true} onClose={mockOnClose}>
|
||
内容
|
||
</Modal>
|
||
);
|
||
expect(document.body.style.overflow).toBe('hidden');
|
||
});
|
||
|
||
it('关闭时解锁 body 滚动', () => {
|
||
const { rerender } = render(
|
||
<Modal isOpen={true} onClose={mockOnClose}>
|
||
内容
|
||
</Modal>
|
||
);
|
||
expect(document.body.style.overflow).toBe('hidden');
|
||
|
||
rerender(
|
||
<Modal isOpen={false} onClose={mockOnClose}>
|
||
内容
|
||
</Modal>
|
||
);
|
||
expect(document.body.style.overflow).toBe('');
|
||
});
|
||
|
||
it('卸载时清理 overflow', () => {
|
||
const { unmount } = render(
|
||
<Modal isOpen={true} onClose={mockOnClose}>
|
||
内容
|
||
</Modal>
|
||
);
|
||
expect(document.body.style.overflow).toBe('hidden');
|
||
|
||
unmount();
|
||
expect(document.body.style.overflow).toBe('');
|
||
});
|
||
});
|
||
|
||
// ==================== Size 测试 ====================
|
||
describe('Size 样式', () => {
|
||
it('默认 md size', () => {
|
||
const { container } = render(
|
||
<Modal isOpen={true} onClose={mockOnClose}>
|
||
内容
|
||
</Modal>
|
||
);
|
||
expect(container.querySelector('.max-w-md')).toBeInTheDocument();
|
||
});
|
||
|
||
it('sm size', () => {
|
||
const { container } = render(
|
||
<Modal isOpen={true} onClose={mockOnClose} size="sm">
|
||
内容
|
||
</Modal>
|
||
);
|
||
expect(container.querySelector('.max-w-sm')).toBeInTheDocument();
|
||
});
|
||
|
||
it('lg size', () => {
|
||
const { container } = render(
|
||
<Modal isOpen={true} onClose={mockOnClose} size="lg">
|
||
内容
|
||
</Modal>
|
||
);
|
||
expect(container.querySelector('.max-w-lg')).toBeInTheDocument();
|
||
});
|
||
|
||
it('xl size', () => {
|
||
const { container } = render(
|
||
<Modal isOpen={true} onClose={mockOnClose} size="xl">
|
||
内容
|
||
</Modal>
|
||
);
|
||
expect(container.querySelector('.max-w-xl')).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
// ==================== ClassName 测试 ====================
|
||
describe('ClassName', () => {
|
||
it('支持自定义 className', () => {
|
||
const { container } = render(
|
||
<Modal isOpen={true} onClose={mockOnClose} className="custom-modal">
|
||
内容
|
||
</Modal>
|
||
);
|
||
expect(container.querySelector('.custom-modal')).toBeInTheDocument();
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('ConfirmModal', () => {
|
||
const mockOnClose = vi.fn();
|
||
const mockOnConfirm = vi.fn();
|
||
|
||
beforeEach(() => {
|
||
mockOnClose.mockClear();
|
||
mockOnConfirm.mockClear();
|
||
});
|
||
|
||
// ==================== 基础渲染测试 ====================
|
||
describe('基础渲染', () => {
|
||
it('渲染标题和消息', () => {
|
||
render(
|
||
<ConfirmModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
onConfirm={mockOnConfirm}
|
||
title="确认删除"
|
||
message="确定要删除吗?"
|
||
/>
|
||
);
|
||
expect(screen.getByText('确认删除')).toBeInTheDocument();
|
||
expect(screen.getByText('确定要删除吗?')).toBeInTheDocument();
|
||
});
|
||
|
||
it('渲染确认和取消按钮', () => {
|
||
render(
|
||
<ConfirmModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
onConfirm={mockOnConfirm}
|
||
title="操作确认"
|
||
message="消息"
|
||
confirmText="确定"
|
||
/>
|
||
);
|
||
// 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(
|
||
<ConfirmModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
onConfirm={mockOnConfirm}
|
||
title="确认"
|
||
message="消息"
|
||
confirmText="删除"
|
||
/>
|
||
);
|
||
expect(screen.getByText('删除')).toBeInTheDocument();
|
||
});
|
||
|
||
it('支持自定义取消按钮文本', () => {
|
||
render(
|
||
<ConfirmModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
onConfirm={mockOnConfirm}
|
||
title="确认"
|
||
message="消息"
|
||
cancelText="返回"
|
||
/>
|
||
);
|
||
expect(screen.getByText('返回')).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
// ==================== 事件处理测试 ====================
|
||
describe('事件处理', () => {
|
||
it('点击确认按钮触发 onConfirm', () => {
|
||
render(
|
||
<ConfirmModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
onConfirm={mockOnConfirm}
|
||
title="操作确认"
|
||
message="消息"
|
||
confirmText="确定"
|
||
/>
|
||
);
|
||
fireEvent.click(screen.getByText('确定'));
|
||
expect(mockOnConfirm).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
it('点击取消按钮触发 onClose', () => {
|
||
render(
|
||
<ConfirmModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
onConfirm={mockOnConfirm}
|
||
title="操作确认"
|
||
message="消息"
|
||
/>
|
||
);
|
||
fireEvent.click(screen.getByText('取消'));
|
||
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
||
});
|
||
});
|
||
|
||
// ==================== Variant 测试 ====================
|
||
describe('Variant 样式', () => {
|
||
it('danger variant 使用红色确认按钮', () => {
|
||
render(
|
||
<ConfirmModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
onConfirm={mockOnConfirm}
|
||
title="确认删除"
|
||
message="消息"
|
||
variant="danger"
|
||
/>
|
||
);
|
||
const confirmButton = screen.getByText('确认').closest('button');
|
||
expect(confirmButton).toHaveClass('bg-accent-coral');
|
||
});
|
||
});
|
||
|
||
// ==================== Loading 测试 ====================
|
||
describe('Loading 状态', () => {
|
||
it('loading 时确认按钮显示加载状态', () => {
|
||
render(
|
||
<ConfirmModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
onConfirm={mockOnConfirm}
|
||
title="操作确认"
|
||
message="消息"
|
||
confirmText="确定"
|
||
loading={true}
|
||
/>
|
||
);
|
||
const confirmButton = screen.getByText('确定').closest('button');
|
||
expect(confirmButton).toBeDisabled();
|
||
});
|
||
|
||
it('loading 时取消按钮也被禁用', () => {
|
||
render(
|
||
<ConfirmModal
|
||
isOpen={true}
|
||
onClose={mockOnClose}
|
||
onConfirm={mockOnConfirm}
|
||
title="操作确认"
|
||
message="消息"
|
||
loading={true}
|
||
/>
|
||
);
|
||
const cancelButton = screen.getByText('取消').closest('button');
|
||
expect(cancelButton).toBeDisabled();
|
||
});
|
||
});
|
||
});
|