9097c48b1f
- 新增 dashboardFeatures.ts 和 useDashboard.ts 文件,封装仪表板功能的解析和状态管理逻辑。 - 更新 Dashboard 组件,使用 useDashboard 钩子简化可见工具和最近使用工具的处理。 - 添加仪表板功能的单元测试,确保功能正确性。 - 更新 README.md,移除不再使用的 ToolCard 组件说明。
338 lines
11 KiB
TypeScript
338 lines
11 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
||
import {
|
||
textToBase64,
|
||
base64ToText,
|
||
isValidBase64,
|
||
isFileSizeValid,
|
||
isSupportedImageType,
|
||
isSupportedImageExtension,
|
||
extractMimeTypeFromDataUri,
|
||
base64ToBytes,
|
||
sniffMimeFromBytes,
|
||
base64ToBlob,
|
||
MAX_FILE_SIZE,
|
||
} from '@/utils/base64Converter';
|
||
|
||
describe('textToBase64', () => {
|
||
it('应该编码 ASCII 文本', () => {
|
||
const result = textToBase64('Hello, World!');
|
||
expect(result.output).toBe('SGVsbG8sIFdvcmxkIQ==');
|
||
expect(result.originalBytes).toBe(13);
|
||
});
|
||
|
||
it('应该编码中文文本', () => {
|
||
const result = textToBase64('你好世界');
|
||
expect(result.output).toBeTruthy();
|
||
// 验证可以正确解码回来
|
||
const decoded = base64ToText(result.output);
|
||
expect(decoded).toBe('你好世界');
|
||
});
|
||
|
||
it('应该编码空字符串', () => {
|
||
const result = textToBase64('');
|
||
expect(result.output).toBe('');
|
||
expect(result.originalBytes).toBe(0);
|
||
});
|
||
|
||
it('应该编码包含特殊字符的文本', () => {
|
||
const text = 'line1\nline2\ttab';
|
||
const result = textToBase64(text);
|
||
const decoded = base64ToText(result.output);
|
||
expect(decoded).toBe(text);
|
||
});
|
||
|
||
it('应该编码 Unicode 表情符号', () => {
|
||
const text = '🎉🚀';
|
||
const result = textToBase64(text);
|
||
const decoded = base64ToText(result.output);
|
||
expect(decoded).toBe(text);
|
||
});
|
||
});
|
||
|
||
describe('base64ToText', () => {
|
||
it('应该解码标准 Base64 字符串', () => {
|
||
const result = base64ToText('SGVsbG8sIFdvcmxkIQ==');
|
||
expect(result).toBe('Hello, World!');
|
||
});
|
||
|
||
it('应该解码中文 Base64', () => {
|
||
const encoded = textToBase64('测试文本');
|
||
const decoded = base64ToText(encoded.output);
|
||
expect(decoded).toBe('测试文本');
|
||
});
|
||
|
||
it('应该对无效 Base64 抛出错误', () => {
|
||
expect(() => base64ToText('这不是base64!!!')).toThrow();
|
||
});
|
||
|
||
it('应该修剪输入字符串的空白', () => {
|
||
const result = base64ToText(' SGVsbG8= ');
|
||
expect(result).toBe('Hello');
|
||
});
|
||
|
||
it('应该自动剥离 data:<mime>;base64, 前缀后再解码', () => {
|
||
// "hello" -> base64 "aGVsbG8="
|
||
const result = base64ToText('data:text/plain;base64,aGVsbG8=');
|
||
expect(result).toBe('hello');
|
||
});
|
||
|
||
it('应该处理带参数(如 charset)的 data URI 前缀', () => {
|
||
const result = base64ToText('data:text/plain;charset=utf-8;base64,aGVsbG8=');
|
||
expect(result).toBe('hello');
|
||
});
|
||
|
||
it('应该剥离带空白的 data URI 前缀', () => {
|
||
const result = base64ToText(' data:text/plain;base64,aGVsbG8= ');
|
||
expect(result).toBe('hello');
|
||
});
|
||
|
||
it('应该对二进制(如 PNG)数据抛出更清晰的错误', () => {
|
||
// PNG 文件签名 89 50 4E 47 0D 0A 1A 0A 的 Base64 编码
|
||
const pngSignatureBase64 = 'iVBORw0KGgo=';
|
||
expect(() => base64ToText(pngSignatureBase64)).toThrow(/binary|二进制|image|图像/i);
|
||
});
|
||
|
||
it('应该对带 data:image/png 前缀的 PNG 数据抛出二进制错误', () => {
|
||
const pngDataUri = 'data:image/png;base64,iVBORw0KGgo=';
|
||
expect(() => base64ToText(pngDataUri)).toThrow(/binary|二进制|image|图像/i);
|
||
});
|
||
});
|
||
|
||
describe('isValidBase64', () => {
|
||
it('应该返回 true 对于合法的 Base64', () => {
|
||
expect(isValidBase64('SGVsbG8=')).toBe(true);
|
||
expect(isValidBase64('SGVsbG8sIFdvcmxkIQ==')).toBe(true);
|
||
});
|
||
|
||
it('应该返回 false 对于空字符串', () => {
|
||
expect(isValidBase64('')).toBe(false);
|
||
});
|
||
|
||
it('应该返回 false 对于非法字符', () => {
|
||
expect(isValidBase64('SGVsbG8!')).toBe(false);
|
||
});
|
||
|
||
it('应该返回 false 对于长度不是 4 的倍数', () => {
|
||
expect(isValidBase64('SGVsbG8')).toBe(false); // 7 chars, not divisible by 4
|
||
});
|
||
});
|
||
|
||
describe('isFileSizeValid', () => {
|
||
it('应该对正常大小的文件返回 true', () => {
|
||
expect(isFileSizeValid(1024)).toBe(true);
|
||
expect(isFileSizeValid(MAX_FILE_SIZE)).toBe(true);
|
||
});
|
||
|
||
it('应该对 0 字节返回 false', () => {
|
||
expect(isFileSizeValid(0)).toBe(false);
|
||
});
|
||
|
||
it('应该对超出限制的文件返回 false', () => {
|
||
expect(isFileSizeValid(MAX_FILE_SIZE + 1)).toBe(false);
|
||
});
|
||
|
||
it('应该对负数返回 false', () => {
|
||
expect(isFileSizeValid(-1)).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('isSupportedImageType', () => {
|
||
it('应该识别支持的图像类型', () => {
|
||
expect(isSupportedImageType('image/png')).toBe(true);
|
||
expect(isSupportedImageType('image/jpeg')).toBe(true);
|
||
expect(isSupportedImageType('image/webp')).toBe(true);
|
||
expect(isSupportedImageType('image/gif')).toBe(true);
|
||
expect(isSupportedImageType('image/svg+xml')).toBe(true);
|
||
});
|
||
|
||
it('应该拒绝不支持的 MIME 类型', () => {
|
||
expect(isSupportedImageType('application/pdf')).toBe(false);
|
||
expect(isSupportedImageType('text/plain')).toBe(false);
|
||
expect(isSupportedImageType('video/mp4')).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('isSupportedImageExtension', () => {
|
||
it('应该识别支持的图像扩展名', () => {
|
||
expect(isSupportedImageExtension('photo.png')).toBe(true);
|
||
expect(isSupportedImageExtension('photo.JPG')).toBe(true);
|
||
expect(isSupportedImageExtension('photo.webp')).toBe(true);
|
||
expect(isSupportedImageExtension('photo.gif')).toBe(true);
|
||
});
|
||
|
||
it('应该拒绝不支持的扩展名', () => {
|
||
expect(isSupportedImageExtension('document.pdf')).toBe(false);
|
||
expect(isSupportedImageExtension('video.mp4')).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('extractMimeTypeFromDataUri', () => {
|
||
it('应该从 data URI 中提取 MIME 类型', () => {
|
||
expect(extractMimeTypeFromDataUri('data:image/png;base64,iVBOR')).toBe('image/png');
|
||
expect(extractMimeTypeFromDataUri('data:text/plain;base64,SGVsbG8=')).toBe('text/plain');
|
||
});
|
||
|
||
it('应该从带参数的 data URI 中提取 MIME 类型', () => {
|
||
expect(extractMimeTypeFromDataUri('data:text/plain;charset=utf-8;base64,SGVsbG8=')).toBe(
|
||
'text/plain',
|
||
);
|
||
expect(extractMimeTypeFromDataUri('data:image/svg+xml;base64,PHN2Zw==')).toBe('image/svg+xml');
|
||
});
|
||
|
||
it('应该对无效的 data URI 返回默认类型', () => {
|
||
expect(extractMimeTypeFromDataUri('invalid')).toBe('application/octet-stream');
|
||
});
|
||
});
|
||
|
||
describe('base64ToBytes', () => {
|
||
it('应该解码标准 ASCII Base64 为字节序列', () => {
|
||
const bytes = base64ToBytes('aGVsbG8=');
|
||
expect(Array.from(bytes)).toEqual([0x68, 0x65, 0x6c, 0x6c, 0x6f]);
|
||
});
|
||
|
||
it('应该正确解码 PNG 文件签名', () => {
|
||
// PNG 签名:89 50 4E 47 0D 0A 1A 0A
|
||
const bytes = base64ToBytes('iVBORw0KGgo=');
|
||
expect(Array.from(bytes)).toEqual([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||
});
|
||
|
||
it('应该返回空 Uint8Array 对于空字符串输入', () => {
|
||
const bytes = base64ToBytes('');
|
||
expect(bytes.length).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('sniffMimeFromBytes', () => {
|
||
it('应该识别 PNG', () => {
|
||
const bytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||
expect(sniffMimeFromBytes(bytes)).toEqual({ mime: 'image/png', ext: '.png' });
|
||
});
|
||
|
||
it('应该识别 JPEG', () => {
|
||
const bytes = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]);
|
||
expect(sniffMimeFromBytes(bytes)).toEqual({ mime: 'image/jpeg', ext: '.jpg' });
|
||
});
|
||
|
||
it('应该识别 GIF', () => {
|
||
const bytes = new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x39, 0x61]);
|
||
expect(sniffMimeFromBytes(bytes)).toEqual({ mime: 'image/gif', ext: '.gif' });
|
||
});
|
||
|
||
it('应该识别 BMP', () => {
|
||
const bytes = new Uint8Array([0x42, 0x4d, 0x36, 0x00, 0x00, 0x00]);
|
||
expect(sniffMimeFromBytes(bytes)).toEqual({ mime: 'image/bmp', ext: '.bmp' });
|
||
});
|
||
|
||
it('应该识别 WebP(RIFF....WEBP 复合签名)', () => {
|
||
const bytes = new Uint8Array([
|
||
0x52,
|
||
0x49,
|
||
0x46,
|
||
0x46, // "RIFF"
|
||
0x24,
|
||
0x00,
|
||
0x00,
|
||
0x00, // 文件大小占位
|
||
0x57,
|
||
0x45,
|
||
0x42,
|
||
0x50, // "WEBP"
|
||
0x56,
|
||
0x50,
|
||
0x38,
|
||
0x20, // "VP8 " 子块
|
||
]);
|
||
expect(sniffMimeFromBytes(bytes)).toEqual({ mime: 'image/webp', ext: '.webp' });
|
||
});
|
||
|
||
it('不应将 WAV(RIFF 容器但非 WEBP)识别为 WebP', () => {
|
||
const bytes = new Uint8Array([
|
||
0x52,
|
||
0x49,
|
||
0x46,
|
||
0x46, // "RIFF"
|
||
0x24,
|
||
0x00,
|
||
0x00,
|
||
0x00,
|
||
0x57,
|
||
0x41,
|
||
0x56,
|
||
0x45, // "WAVE"
|
||
]);
|
||
expect(sniffMimeFromBytes(bytes)).toBeNull();
|
||
});
|
||
it('应该识别 PDF', () => {
|
||
const bytes = new Uint8Array([0x25, 0x50, 0x44, 0x46, 0x2d]);
|
||
expect(sniffMimeFromBytes(bytes)).toEqual({ mime: 'application/pdf', ext: '.pdf' });
|
||
});
|
||
|
||
it('应该识别 ZIP', () => {
|
||
const bytes = new Uint8Array([0x50, 0x4b, 0x03, 0x04]);
|
||
expect(sniffMimeFromBytes(bytes)).toEqual({ mime: 'application/zip', ext: '.zip' });
|
||
});
|
||
|
||
it('应该对未匹配的字节返回 null', () => {
|
||
const bytes = new Uint8Array([0x00, 0x01, 0x02, 0x03]);
|
||
expect(sniffMimeFromBytes(bytes)).toBeNull();
|
||
});
|
||
|
||
it('应该对过短的字节返回 null', () => {
|
||
const bytes = new Uint8Array([0x89]);
|
||
expect(sniffMimeFromBytes(bytes)).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('base64ToBlob', () => {
|
||
it('应该优先使用 data URI 中的 MIME 类型', () => {
|
||
const result = base64ToBlob('data:application/json;base64,eyJhIjoxfQ==');
|
||
expect(result.mimeType).toBe('application/json');
|
||
expect(result.blob).toBeInstanceOf(Blob);
|
||
expect(result.blob.size).toBe(7);
|
||
});
|
||
|
||
it('应该通过魔数识别 PNG', () => {
|
||
const result = base64ToBlob('iVBORw0KGgo=');
|
||
expect(result.mimeType).toBe('image/png');
|
||
expect(result.suggestedExtension).toBe('.png');
|
||
});
|
||
|
||
it('应该通过魔数识别 PDF', () => {
|
||
// "%PDF-" + 一些字节
|
||
const result = base64ToBlob('JVBERi0K');
|
||
expect(result.mimeType).toBe('application/pdf');
|
||
expect(result.suggestedExtension).toBe('.pdf');
|
||
});
|
||
|
||
it('应该对未匹配的纯 Base64 回退为 application/octet-stream + .bin', () => {
|
||
const result = base64ToBlob('AAECAwQF');
|
||
expect(result.mimeType).toBe('application/octet-stream');
|
||
expect(result.suggestedExtension).toBe('.bin');
|
||
});
|
||
|
||
it('应该 trim 前后空白', () => {
|
||
const result = base64ToBlob(' iVBORw0KGgo= ');
|
||
expect(result.mimeType).toBe('image/png');
|
||
});
|
||
|
||
it('应该对非法 Base64 抛出 Invalid Base64 string', () => {
|
||
expect(() => base64ToBlob('这不是 base64!')).toThrow('无效的 Base64 字符串');
|
||
});
|
||
|
||
it('应该返回原始 Base64(已去除 data URI 前缀)', () => {
|
||
const result = base64ToBlob('data:image/png;base64,iVBORw0KGgo=');
|
||
expect(result.rawBase64).toBe('iVBORw0KGgo=');
|
||
});
|
||
|
||
it('应该处理带参数的 data URI 前缀', () => {
|
||
const result = base64ToBlob('data:text/plain;charset=utf-8;base64,aGVsbG8=');
|
||
expect(result.rawBase64).toBe('aGVsbG8=');
|
||
expect(result.mimeType).toBe('text/plain');
|
||
});
|
||
|
||
it('blob 大小应该等于解码后的字节数', () => {
|
||
const result = base64ToBlob('aGVsbG8=');
|
||
expect(result.blob.size).toBe(5);
|
||
});
|
||
});
|