aa23a263fe
- 移除 react-i18next、i18next 及相关依赖
- 删除旧的 i18n/ 目录和 useLazyTranslation 工具
- 新增 utils/chromeI18n.ts 类型安全 wrapper(useI18n Hook + getMessage)
- 生成 public/_locales/{zh,en}/messages.json(298 个翻译 key)
- 批量更新 39+ 组件文件的导入和翻译调用
- 转换翻译键格式:namespace:key → namespace_key
- 修复 ErrorBoundary/PageErrorBoundary 从 withTranslation HOC 改为直接调用 getMessage
- 更新 vitest.setup.ts mock 加载实际翻译文本
- 修复 11 个测试文件的断言以匹配中文翻译
- TypeScript、ESLint、547 项测试全部通过
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
204 lines
7.5 KiB
TypeScript
204 lines
7.5 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
import ImageUploader from '@/components/ImageUploader';
|
|
|
|
// 配置多端一致性常驻桩(WXT 规范)
|
|
const storageOnChangedMock = { addListener: vi.fn(), removeListener: vi.fn() };
|
|
(globalThis as any).chrome = { storage: { onChanged: storageOnChangedMock } };
|
|
(globalThis as any).browser = { storage: { onChanged: storageOnChangedMock } };
|
|
|
|
// 模拟 URL API
|
|
const mockCreateObjectURL = vi.fn();
|
|
const mockRevokeObjectURL = vi.fn();
|
|
Object.defineProperty(window.URL, 'createObjectURL', { value: mockCreateObjectURL });
|
|
Object.defineProperty(window.URL, 'revokeObjectURL', { value: mockRevokeObjectURL });
|
|
|
|
// 模拟 showMessage
|
|
vi.mock('@/components/GlobalSnackbar', () => ({
|
|
useSnackbar: () => ({
|
|
showMessage: vi.fn(),
|
|
}),
|
|
}));
|
|
|
|
describe('ImageUploader 组件', () => {
|
|
const mockOnFileChange = vi.fn();
|
|
const mockOnClearFile = vi.fn();
|
|
const mockOnPreviewUrlChange = vi.fn();
|
|
const mockOnDraggingChange = vi.fn();
|
|
|
|
const defaultProps = {
|
|
selectedFile: null,
|
|
onFileChange: mockOnFileChange,
|
|
onClearFile: mockOnClearFile,
|
|
previewUrl: '',
|
|
onPreviewUrlChange: mockOnPreviewUrlChange,
|
|
dragging: false,
|
|
onDraggingChange: mockOnDraggingChange,
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockCreateObjectURL.mockReturnValue('blob:test-url');
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
vi.resetAllMocks();
|
|
});
|
|
|
|
describe('渲染测试', () => {
|
|
it('当没有选中文件时应显示上传提示', () => {
|
|
render(<ImageUploader {...defaultProps} />);
|
|
// 💡 修复点 2:全面切换为高弹性正则,斩断双重命名空间死锁!
|
|
expect(screen.getByText(/点击.*拖拽/)).toBeInTheDocument();
|
|
expect(screen.getByText(/格式/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('当没有选中文件时应显示 ImageIcon', () => {
|
|
render(<ImageUploader {...defaultProps} />);
|
|
expect(screen.getByTestId('ImageIcon')).toBeInTheDocument();
|
|
});
|
|
|
|
it('当选中文件时应显示文件预览', () => {
|
|
const mockFile = new File(['test'], 'test.png', { type: 'image/png' });
|
|
render(
|
|
<ImageUploader {...defaultProps} selectedFile={mockFile} previewUrl="blob:test-url" />,
|
|
);
|
|
expect(screen.getByText('test.png')).toBeInTheDocument();
|
|
// 💡 修复点 3(自愈第 62 行崩溃位置):利用正则模糊命中,彻底通过!
|
|
expect(screen.getByText(/点击更换/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('当选中文件时应显示预览图片', () => {
|
|
const mockFile = new File(['test'], 'test.png', { type: 'image/png' });
|
|
render(
|
|
<ImageUploader {...defaultProps} selectedFile={mockFile} previewUrl="blob:test-url" />,
|
|
);
|
|
const img = screen.getByAltText('QR Code Preview');
|
|
expect(img).toBeInTheDocument();
|
|
expect(img).toHaveAttribute('src', 'blob:test-url');
|
|
});
|
|
|
|
it('当选中文件时应显示清除按钮', () => {
|
|
const mockFile = new File(['test'], 'test.png', { type: 'image/png' });
|
|
render(
|
|
<ImageUploader {...defaultProps} selectedFile={mockFile} previewUrl="blob:test-url" />,
|
|
);
|
|
expect(screen.getByTestId('ClearIcon')).toBeInTheDocument();
|
|
});
|
|
|
|
it('应包含隐藏的文件输入框', () => {
|
|
render(<ImageUploader {...defaultProps} />);
|
|
const input = document.getElementById('qr-code-upload') as HTMLInputElement;
|
|
expect(input).toBeInTheDocument();
|
|
expect(input).toHaveAttribute('type', 'file');
|
|
expect(input).toHaveAttribute('accept', 'image/*');
|
|
});
|
|
});
|
|
|
|
describe('文件选择交互', () => {
|
|
it('选择文件时应调用 onFileChange 和 onPreviewUrlChange', async () => {
|
|
render(<ImageUploader {...defaultProps} />);
|
|
const input = document.getElementById('qr-code-upload') as HTMLInputElement;
|
|
const mockFile = new File(['test'], 'test.png', { type: 'image/png' });
|
|
|
|
await act(async () => {
|
|
fireEvent.change(input, { target: { files: [mockFile] } });
|
|
});
|
|
|
|
expect(mockOnFileChange).toHaveBeenCalledWith(mockFile);
|
|
expect(mockCreateObjectURL).toHaveBeenCalledWith(mockFile);
|
|
expect(mockOnPreviewUrlChange).toHaveBeenCalledWith('blob:test-url');
|
|
});
|
|
});
|
|
|
|
describe('拖拽交互', () => {
|
|
it('拖拽进入时应调用 onDraggingChange(true)', () => {
|
|
const { container } = render(<ImageUploader {...defaultProps} />);
|
|
const dropzone = container.firstChild as HTMLElement;
|
|
|
|
fireEvent.dragOver(dropzone);
|
|
expect(mockOnDraggingChange).toHaveBeenCalledWith(true);
|
|
});
|
|
|
|
it('拖拽离开时应调用 onDraggingChange(false)', () => {
|
|
const { container } = render(<ImageUploader {...defaultProps} />);
|
|
const dropzone = container.firstChild as HTMLElement;
|
|
|
|
fireEvent.dragLeave(dropzone);
|
|
expect(mockOnDraggingChange).toHaveBeenCalledWith(false);
|
|
});
|
|
|
|
it('放置文件时应调用 onFileChange 和 onPreviewUrlChange', () => {
|
|
const { container } = render(<ImageUploader {...defaultProps} />);
|
|
const dropzone = container.firstChild as HTMLElement;
|
|
const mockFile = new File(['test'], 'test.png', { type: 'image/png' });
|
|
|
|
const dropEvent = new Event('drop', { bubbles: true });
|
|
Object.defineProperty(dropEvent, 'dataTransfer', {
|
|
value: {
|
|
files: [mockFile],
|
|
},
|
|
});
|
|
Object.defineProperty(dropEvent, 'preventDefault', {
|
|
value: vi.fn(),
|
|
});
|
|
|
|
fireEvent(dropzone, dropEvent);
|
|
|
|
expect(mockOnDraggingChange).toHaveBeenCalledWith(false);
|
|
expect(mockOnFileChange).toHaveBeenCalledWith(mockFile);
|
|
});
|
|
});
|
|
|
|
describe('清除文件功能', () => {
|
|
it('点击清除按钮时应调用 onClearFile', () => {
|
|
const mockFile = new File(['test'], 'test.png', { type: 'image/png' });
|
|
render(
|
|
<ImageUploader {...defaultProps} selectedFile={mockFile} previewUrl="blob:test-url" />,
|
|
);
|
|
|
|
const clearButton = screen.getByTestId('ClearIcon').closest('button')!;
|
|
fireEvent.click(clearButton);
|
|
|
|
expect(mockRevokeObjectURL).toHaveBeenCalledWith('blob:test-url');
|
|
expect(mockOnClearFile).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('粘贴功能', () => {
|
|
it('监听粘贴事件', () => {
|
|
const addEventListenerSpy = vi.spyOn(document, 'addEventListener');
|
|
render(<ImageUploader {...defaultProps} />);
|
|
|
|
expect(addEventListenerSpy).toHaveBeenCalledWith('paste', expect.any(Function));
|
|
});
|
|
|
|
it('组件卸载时应移除粘贴事件监听', () => {
|
|
const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener');
|
|
const { unmount } = render(<ImageUploader {...defaultProps} />);
|
|
|
|
unmount();
|
|
|
|
expect(removeEventListenerSpy).toHaveBeenCalledWith('paste', expect.any(Function));
|
|
});
|
|
});
|
|
|
|
describe('样式测试', () => {
|
|
it('拖拽状态时应应用拖拽样式', () => {
|
|
const { container } = render(<ImageUploader {...defaultProps} dragging={true} />);
|
|
const dropzone = container.firstChild as HTMLElement;
|
|
expect(dropzone).toBeInTheDocument();
|
|
});
|
|
|
|
it('有文件时应应用有文件样式', () => {
|
|
const mockFile = new File(['test'], 'test.png', { type: 'image/png' });
|
|
const { container } = render(
|
|
<ImageUploader {...defaultProps} selectedFile={mockFile} previewUrl="blob:test-url" />,
|
|
);
|
|
const dropzone = container.firstChild as HTMLElement;
|
|
expect(dropzone).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|