fix(pages): 修复 TypeScript 错误、硬编码字符串,移除死代码
- 修复 LiveClock.tsx CopyButton size 属性类型错误 (small → sm) - 修复 ToolCard.tsx 紫色 RGB 值拼写错误 (147,51,2 purple → 147,51,232) - 替换 4 处硬编码中文为 i18n 调用 (StorageCleaner, JsonTools) - MarkdownToHtml 预览链接色改为 CSS 变量以支持暗黑模式 - 移除 Base64Converter 中已被 Base64ConverterSection 替代的死代码 (FileMode.tsx, ImageMode.tsx 及其测试文件) - 更新 README.md 移除对已删除文件的引用 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,235 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import FileMode from '../FileMode';
|
||||
|
||||
// Mock CopyButton
|
||||
vi.mock('@/components/CopyButton', () => ({
|
||||
default: ({ text, tooltip }: { text: string; tooltip?: string }) => (
|
||||
<button data-testid="copy-button" data-tooltip={tooltip}>
|
||||
{text.slice(0, 20)}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
// useStorageState's async loadState may overwrite user toggle if we click before the
|
||||
// initial chrome.storage read settles. Flush pending microtasks first.
|
||||
const waitForStorageReady = () => act(() => Promise.resolve());
|
||||
|
||||
describe('FileMode', () => {
|
||||
it('应该渲染文件上传区域', async () => {
|
||||
render(<FileMode />);
|
||||
await waitForStorageReady();
|
||||
expect(screen.getByText('base64Converter:clickOrDropToFile')).toBeInTheDocument();
|
||||
expect(screen.getByText('base64Converter:maxFileSize')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('应该处理有效的文件选择', async () => {
|
||||
render(<FileMode />);
|
||||
await waitForStorageReady();
|
||||
|
||||
const file = new File(['test content'], 'test.txt', { type: 'text/plain' });
|
||||
|
||||
// 文件输入是隐藏的,直接触发 change 事件
|
||||
const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
fireEvent.change(hiddenInput, { target: { files: [file] } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('test.txt')).toBeInTheDocument();
|
||||
expect(screen.getByText('base64Converter:base64Output')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('应该拒绝超出大小限制的文件', async () => {
|
||||
render(<FileMode />);
|
||||
await waitForStorageReady();
|
||||
|
||||
// 创建一个超过 10MB 的文件
|
||||
const largeContent = new Uint8Array(11 * 1024 * 1024);
|
||||
const file = new File([largeContent], 'large.bin', { type: 'application/octet-stream' });
|
||||
|
||||
const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
fireEvent.change(hiddenInput, { target: { files: [file] } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('alert')).toHaveTextContent('base64Converter:fileSizeExceeded');
|
||||
});
|
||||
});
|
||||
|
||||
it('点击清除按钮应该清空文件状态', async () => {
|
||||
render(<FileMode />);
|
||||
await waitForStorageReady();
|
||||
|
||||
const file = new File(['test'], 'test.txt', { type: 'text/plain' });
|
||||
const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
fireEvent.change(hiddenInput, { target: { files: [file] } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('test.txt')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('base64Converter:clear'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('test.txt')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('base64Converter:clickOrDropToFile')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('应该显示文件大小和类型信息', async () => {
|
||||
render(<FileMode />);
|
||||
await waitForStorageReady();
|
||||
|
||||
const file = new File(['test content'], 'test.txt', { type: 'text/plain' });
|
||||
const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
fireEvent.change(hiddenInput, { target: { files: [file] } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('test.txt')).toBeInTheDocument();
|
||||
});
|
||||
// 文件类型显示在 caption 中,格式为 "size · type"
|
||||
expect(screen.getByText(/test\.txt/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('应该显示原始大小和编码大小', async () => {
|
||||
render(<FileMode />);
|
||||
await waitForStorageReady();
|
||||
|
||||
const file = new File(['test content'], 'test.txt', { type: 'text/plain' });
|
||||
const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
fireEvent.change(hiddenInput, { target: { files: [file] } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/base64Converter:originalSize/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/base64Converter:encodedSize/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('应该提供复制按钮', async () => {
|
||||
render(<FileMode />);
|
||||
await waitForStorageReady();
|
||||
|
||||
const file = new File(['test'], 'test.txt', { type: 'text/plain' });
|
||||
const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
fireEvent.change(hiddenInput, { target: { files: [file] } });
|
||||
|
||||
await waitFor(() => {
|
||||
const copyButtons = screen.getAllByTestId('copy-button');
|
||||
expect(copyButtons.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('应该渲染 encode/decode 切换按钮', () => {
|
||||
render(<FileMode />);
|
||||
expect(screen.getByText('base64Converter:encode')).toBeInTheDocument();
|
||||
expect(screen.getByText('base64Converter:decode')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('切到 decode 应该显示 Base64 输入框', async () => {
|
||||
render(<FileMode />);
|
||||
await waitForStorageReady();
|
||||
fireEvent.click(screen.getByText('base64Converter:decode'));
|
||||
expect(
|
||||
await screen.findByPlaceholderText('base64Converter:decodeBase64Placeholder'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('解码 PDF Base64 后应该显示 application/pdf 与默认文件名 decoded.pdf', async () => {
|
||||
render(<FileMode />);
|
||||
await waitForStorageReady();
|
||||
fireEvent.click(screen.getByText('base64Converter:decode'));
|
||||
|
||||
const input = await screen.findByPlaceholderText('base64Converter:decodeBase64Placeholder');
|
||||
fireEvent.change(input, { target: { value: 'JVBERi0K' } });
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(250);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('base64Converter:decodedFileOutput')).toBeInTheDocument();
|
||||
expect(screen.getByText(/application\/pdf/)).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('decoded.pdf')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('解码后的文件名应该可编辑', async () => {
|
||||
render(<FileMode />);
|
||||
await waitForStorageReady();
|
||||
fireEvent.click(screen.getByText('base64Converter:decode'));
|
||||
|
||||
const input = await screen.findByPlaceholderText('base64Converter:decodeBase64Placeholder');
|
||||
fireEvent.change(input, { target: { value: 'JVBERi0K' } });
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(250);
|
||||
});
|
||||
|
||||
const filenameInput = (await screen.findByDisplayValue('decoded.pdf')) as HTMLInputElement;
|
||||
fireEvent.change(filenameInput, { target: { value: 'my-report.pdf' } });
|
||||
expect(filenameInput.value).toBe('my-report.pdf');
|
||||
});
|
||||
|
||||
it('解码后应该显示下载按钮', async () => {
|
||||
render(<FileMode />);
|
||||
await waitForStorageReady();
|
||||
fireEvent.click(screen.getByText('base64Converter:decode'));
|
||||
|
||||
const input = await screen.findByPlaceholderText('base64Converter:decodeBase64Placeholder');
|
||||
fireEvent.change(input, { target: { value: 'JVBERi0K' } });
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(250);
|
||||
});
|
||||
|
||||
expect(await screen.findByText('base64Converter:download')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('解码非法 Base64 应该显示 invalidBase64 错误', async () => {
|
||||
render(<FileMode />);
|
||||
await waitForStorageReady();
|
||||
fireEvent.click(screen.getByText('base64Converter:decode'));
|
||||
|
||||
const input = await screen.findByPlaceholderText('base64Converter:decodeBase64Placeholder');
|
||||
fireEvent.change(input, { target: { value: '!!!not base64' } });
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(250);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('base64Converter:invalidBase64')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('切换方向时应该清空解码状态', async () => {
|
||||
render(<FileMode />);
|
||||
await waitForStorageReady();
|
||||
fireEvent.click(screen.getByText('base64Converter:decode'));
|
||||
|
||||
const input = await screen.findByPlaceholderText('base64Converter:decodeBase64Placeholder');
|
||||
fireEvent.change(input, { target: { value: 'JVBERi0K' } });
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(250);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('base64Converter:decodedFileOutput')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('base64Converter:encode'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('decodedFileOutput')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,182 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import ImageMode from '../ImageMode';
|
||||
|
||||
// Mock CopyButton
|
||||
vi.mock('@/components/CopyButton', () => ({
|
||||
default: ({ text, tooltip }: { text: string; tooltip?: string }) => (
|
||||
<button data-testid="copy-button" data-tooltip={tooltip}>
|
||||
{text.slice(0, 20)}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const waitForStorageReady = () => act(() => Promise.resolve());
|
||||
|
||||
describe('ImageMode', () => {
|
||||
it('应该渲染图像上传区域', async () => {
|
||||
render(<ImageMode />);
|
||||
await waitForStorageReady();
|
||||
expect(screen.getByText('base64Converter:clickOrDropToImage')).toBeInTheDocument();
|
||||
expect(screen.getByText('base64Converter:supportedFormats')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('应该接受有效的图像文件', async () => {
|
||||
render(<ImageMode />);
|
||||
await waitForStorageReady();
|
||||
|
||||
const file = new File(['fake-image-data'], 'test.png', { type: 'image/png' });
|
||||
const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
fireEvent.change(hiddenInput, { target: { files: [file] } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('test.png')).toBeInTheDocument();
|
||||
expect(screen.getByText('base64Converter:base64Output')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('应该拒绝非图像文件', async () => {
|
||||
render(<ImageMode />);
|
||||
await waitForStorageReady();
|
||||
|
||||
const file = new File(['not an image'], 'test.txt', { type: 'text/plain' });
|
||||
const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
fireEvent.change(hiddenInput, { target: { files: [file] } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('alert')).toHaveTextContent('base64Converter:unsupportedImageType');
|
||||
});
|
||||
});
|
||||
|
||||
it('应该拒绝超出大小限制的图像', async () => {
|
||||
render(<ImageMode />);
|
||||
await waitForStorageReady();
|
||||
|
||||
const largeContent = new Uint8Array(11 * 1024 * 1024);
|
||||
const file = new File([largeContent], 'large.png', { type: 'image/png' });
|
||||
|
||||
const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
fireEvent.change(hiddenInput, { target: { files: [file] } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('alert')).toHaveTextContent('base64Converter:fileSizeExceeded');
|
||||
});
|
||||
});
|
||||
|
||||
it('应该通过扩展名识别图像', async () => {
|
||||
render(<ImageMode />);
|
||||
await waitForStorageReady();
|
||||
|
||||
// 没有 MIME 类型但有正确扩展名
|
||||
const file = new File(['fake'], 'test.jpg');
|
||||
const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
fireEvent.change(hiddenInput, { target: { files: [file] } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('test.jpg')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('点击清除按钮应该清空图像状态', async () => {
|
||||
render(<ImageMode />);
|
||||
await waitForStorageReady();
|
||||
|
||||
const file = new File(['fake'], 'test.png', { type: 'image/png' });
|
||||
const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
fireEvent.change(hiddenInput, { target: { files: [file] } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('test.png')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('base64Converter:clear'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('test.png')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('base64Converter:clickOrDropToImage')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('应该显示图像预览', async () => {
|
||||
render(<ImageMode />);
|
||||
await waitForStorageReady();
|
||||
|
||||
const file = new File(['fake-image'], 'test.png', { type: 'image/png' });
|
||||
const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
fireEvent.change(hiddenInput, { target: { files: [file] } });
|
||||
|
||||
await waitFor(() => {
|
||||
const img = screen.getByAltText('preview');
|
||||
expect(img).toBeInTheDocument();
|
||||
expect(img.tagName.toLowerCase()).toBe('img');
|
||||
});
|
||||
});
|
||||
|
||||
it('应该渲染 encode/decode 切换按钮', async () => {
|
||||
render(<ImageMode />);
|
||||
await waitForStorageReady();
|
||||
expect(screen.getByText('base64Converter:encode')).toBeInTheDocument();
|
||||
expect(screen.getByText('base64Converter:decode')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('解码 PNG Base64 后应该显示图像预览', async () => {
|
||||
render(<ImageMode />);
|
||||
await waitForStorageReady();
|
||||
fireEvent.click(screen.getByText('base64Converter:decode'));
|
||||
|
||||
const input = await screen.findByPlaceholderText('base64Converter:decodeBase64Placeholder');
|
||||
fireEvent.change(input, { target: { value: 'iVBORw0KGgo=' } });
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(250);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('base64Converter:decodedImageOutput')).toBeInTheDocument();
|
||||
const img = screen.getByAltText('decoded preview');
|
||||
expect(img).toBeInTheDocument();
|
||||
expect(img.tagName.toLowerCase()).toBe('img');
|
||||
});
|
||||
});
|
||||
|
||||
it('解码后默认文件名应该为 decoded.png', async () => {
|
||||
render(<ImageMode />);
|
||||
await waitForStorageReady();
|
||||
fireEvent.click(screen.getByText('base64Converter:decode'));
|
||||
|
||||
const input = await screen.findByPlaceholderText('base64Converter:decodeBase64Placeholder');
|
||||
fireEvent.change(input, { target: { value: 'iVBORw0KGgo=' } });
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(250);
|
||||
});
|
||||
|
||||
expect(await screen.findByDisplayValue('decoded.png')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('解码非法 Base64 应该显示 invalidBase64 错误', async () => {
|
||||
render(<ImageMode />);
|
||||
await waitForStorageReady();
|
||||
fireEvent.click(screen.getByText('base64Converter:decode'));
|
||||
|
||||
const input = await screen.findByPlaceholderText('base64Converter:decodeBase64Placeholder');
|
||||
fireEvent.change(input, { target: { value: '!!!not base64' } });
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(250);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('base64Converter:invalidBase64')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user