Develop (#22)
* docs: 添加组件文档注释和类型导入 refactor: 统一使用 SnackbarOptions 类型 style: 优化导入语句顺序和格式 * refactor: 简化假数据生成器中的faker导入和使用 Co-authored-by: Copilot <copilot@github.com> * feat(form-recognizer): 增强表单识别功能并优化UI交互 - 新增字段类型偏好设置功能,支持按域名保存字段类型 - 重构FieldList组件,改进字段选择和类型修改体验 - 添加字段定位闪烁功能,便于在页面上快速找到对应字段 - 优化表单填充逻辑,支持单个字段覆盖默认填充模式 - 移除独立的侧边栏页面,统一使用主页面组件 - 改进useStorageState钩子,增加加载状态管理和防抖处理 * refactor: 移除未使用的组件文件 * refactor(页面头部): 提取通用 PageHeader 组件并替换各页面头部实现 重构各页面头部为统一的 PageHeader 组件,提高代码复用性和维护性 * style(组件): 调整自动刷新开关和存储选项网格的样式 优化自动刷新开关的文本内边距,重构存储选项网格的布局结构,调整间距和边框样式 * style(ui): 调整时间戳页面和结果视图的样式 - 为时区选择器添加圆角 - 优化结果视图的布局和对齐方式 - 调整结果项的内边距和文本样式 * ci(workflow): 移除Firefox测试以简化CI流程 仅保留Chrome浏览器的构建步骤,减少CI运行时间和资源消耗 * refactor(qrcode): 重构二维码解析功能并提取为独立模块 * style(页面样式): 统一页面头部图标颜色并优化表单样式 更新各页面头部组件的图标颜色配置,移除冗余的表单标签属性,优化输入框样式和按钮悬停效果 * feat(formMapping): 添加表单映射功能 - 创建 FormMappingPage 页面组件 - 实现表单扫描器 scanner.ts - 实现高亮器 highlighter.ts - 更新路由配置 routes.ts - 更新内容脚本 content.ts - 更新类型定义 storage.d.ts - 添加 formMappingPageStyles 样式配置 * feat(表单映射): 添加配置导出功能及状态提示 添加配置导出为JSON文件的功能,包含导出按钮和错误处理。新增Snackbar组件用于显示导出成功和错误状态。优化页面布局结构,将状态提示移至全局容器外。 * feat(表单填充): 新增智能表单填充功能 添加智能表单填充功能,包括: 1. 新增表单填充页面和路由配置 2. 实现模糊匹配引擎和智能注入引擎 3. 添加Mock数据生成器和视觉反馈渲染器 4. 扩展消息接口支持填充操作 * ci: 精简触发CI的工作流分支 移除对develop分支及其变体的触发,仅保留main分支的触发 * test: 更新路由测试以匹配新增的路由数量 * test: 将测试文件中的描述和断言翻译为中文 * test(PageHeader): 添加组件测试用例验证渲染逻辑和样式 * refactor(theme): 重构主题样式并优化仪表盘卡片组件 将页面样式配置集中管理,移除各页面中硬编码的背景色 新增 DashboardCard 组件封装通用卡片逻辑 添加 dashboardCards 配置文件统一管理卡片数据 * refactor(Button): 统一按钮样式并移除重复样式定义 将按钮样式统一封装到 Button 组件中,移除各页面重复的样式定义 更新组件文档说明,提供更清晰的用法示例 * docs: 更新 README 文件中的目录结构说明 添加新组件和配置文件的说明,保持文档与代码同步 * refactor: 优化代码类型声明和UI布局 修复类型声明从any改为never以提高类型安全性 调整TimestampPage页面布局间距 移除DashboardPage中不必要的实时时钟状态 更新ESLint配置以使用推荐配置 添加选项页面打开的错误处理 * refactor(消息通信): 使用 @webext-core/messaging 重构消息处理逻辑 将原有的 chrome.runtime.onMessage 和 chrome.tabs.sendMessage 替换为类型安全的 @webext-core/messaging 实现 添加 ProtocolMap 类型定义确保消息类型安全 更新相关页面和后台脚本使用新的消息通信方式 * fix: 修复依赖项缺失导致的潜在问题 修复 useStorageState 中缺少 defaultValue 依赖的问题 重构 RouterProvider 的初始化逻辑,使用 useCallback 优化性能 * refactor: 重构表单识别和消息处理逻辑 将表单映射UI逻辑提取到独立文件 将消息处理器提取到独立文件 将表单识别页面逻辑提取到自定义hook 优化代码结构和可维护性 * feat(侧边栏): 添加侧边栏状态变化通知功能 - 在消息类型中新增侧边栏状态变化枚举和字段 - 侧边栏打开和关闭时发送状态通知 - 替换轮询检查方式为消息监听机制 * refactor: 优化高亮组件渲染逻辑并添加防抖处理 重构高亮组件渲染逻辑,使用 requestAnimationFrame 进行节流优化 在 useStorageCleaner 中添加防抖处理以避免频繁加载 * refactor: 统一组件导出方式为默认导出 * refactor: 重构样式系统并迁移至 MUI 主题 删除冗余的 CSS 文件,统一使用 MUI 主题管理样式 新增 useActiveTabDomain 和 useSidePanelState 自定义 Hook 优化各入口点的主题集成和布局处理 * docs: 更新项目文档以反映新增功能和技术栈细节 更新 AGENTS.md 文档,详细描述新增的功能模块(存储管理、URL 管理、二维码生成、表单工具套件等)和更新的技术栈信息 * feat: 添加错误边界组件以捕获子组件错误 在 popup、sidepanel 和 options 入口点添加 ErrorBoundary 组件,用于捕获并处理子组件中的 JavaScript 错误。当错误发生时,显示友好的错误界面并提供刷新功能。 * refactor(ui): 使用全局 snackbar 替换本地通知组件 重构表单页面中的通知系统,移除本地 Snackbar 和 Alert 组件,改用全局 GlobalSnackbar 组件统一管理通知 标准化主题配置中的 CSS 属性命名,移除带前缀的样式属性 * refactor(snackbar): 重构全局消息提示为 SnackbarProvider 组件 将原本分散在各页面的 GlobalSnackbar 组件重构为集中管理的 SnackbarProvider,通过 Context 提供统一的消息提示功能。主要变更包括: 1. 创建新的 SnackbarProvider 组件作为全局消息提示容器 2. 提供 useSnackbar hook 供子组件调用 3. 移除各页面中独立的 GlobalSnackbar 实例 4. 在 App 根组件中统一集成 SnackbarProvider 5. 优化消息提示样式和交互行为 * refactor(Snackbar): 重构消息提示组件并优化样式 重构 Snackbar 组件,将状态管理逻辑提取到 GlobalSnackbar 中复用 优化消息提示样式,调整阴影效果和最小尺寸 统一各页面使用 Snackbar 的方式,移除冗余配置 * refactor: 重构路由和功能配置系统 - 将路由配置和仪表盘卡片配置合并为统一的 features 配置 - 重构 RouterProvider 增加数据校验和本地快照功能 - 优化表单字段识别和填充逻辑,提取公共方法 - 更新相关组件和文档以适配新的配置系统 - 删除废弃的 dashboardCards 和 routes 配置文件 * refactor(GlobalSnackbar): 合并SnackbarProvider到GlobalSnackbar组件 重构Snackbar相关代码,将SnackbarProvider合并到GlobalSnackbar组件中,简化代码结构 更新相关组件引用路径,从SnackbarProvider改为GlobalSnackbar 修复StorageCleanerPage禁用状态逻辑问题 * feat(表单映射): 增强高亮器功能并优化元素定位 - 新增高亮器闪烁功能,支持自定义持续时间 - 改进元素定位算法,支持 Shadow DOM 穿透 - 优化拾取模式,支持连续拾取和悬停预览 - 增强错误处理和信息反馈 - 添加测试用例确保核心功能稳定性 * refactor: 移除表单映射相关功能 删除表单映射相关的UI、逻辑和测试代码,包括表单识别、智能填充和高亮功能 简化消息处理逻辑和配置文件,保留核心功能 * feat: 实现页面组件懒加载并优化加载状态显示 refactor: 移除未使用的数据生成和验证工具类 chore: 更新应用图标文件 * refactor(qrcode): 替换二维码生成和解析库以减小体积 使用 qr-scanner 替代 jsqr 进行二维码解析,使用 qrious 替代 qrcode 进行二维码生成 新的库体积更小且性能更好,同时移除了不再使用的 @faker-js/faker 依赖 * feat: 添加在标签页打开功能并优化错误处理 修复测试中的加载状态检查 将控制台日志改为错误捕获 添加全局消息提示组件 * feat: 支持独立标签页模式并重构路由配置 重构路由系统以支持独立标签页模式,替换原有的独立窗口模式 - 新增标签页路由、可见页面列表和页面顺序的存储键 - 修改 RouterProvider 以支持不同窗口类型的独立配置 - 更新选项页面以支持为不同窗口类型单独配置 - 添加相关单元测试 * feat(ui): 优化响应式布局和样式设计 重构选项页面布局,增强移动端适配性 更新主题配置和组件样式,提升用户体验 * refactor: 移除URL工具相关代码 移除不再需要的URL工具功能,包括组件、页面、样式和类型定义 * refactor: 简化页面标题和存储选项网格的响应式布局 * refactor: 优化样式和错误处理 - 简化 chrome.runtime.openOptionsPage 的错误处理 - 拆分 html, body, #root 的样式定义,使用 minHeight 替代 height * refactor: 重构页面组件结构 - 将页面组件从 entrypoints/popup/pages 移动到 pages 目录 - 更新文档中的路径引用 * style(DashboardPage): 优化仪表盘布局为响应式网格 refactor(ToolCard): 重构工具卡片组件并添加详细注释 * feat: 新增文本统计功能 添加文本统计工具页面及相关功能,包括: - 文本统计工具页面组件 - 文本统计计算工具函数 - 相关类型定义和样式配置 - 单元测试覆盖核心功能 * feat(jwt): 添加 JWT 解析工具功能 新增 JWT 解析工具页面,包含以下内容: - 添加 JWT 类型定义和页面样式配置 - 实现 JWT 解析核心功能及测试用例 - 创建 JWT 解析页面组件 - 在功能配置中注册 JWT 工具 * docs: 更新 README 以更清晰地描述项目功能和结构 - 重构功能描述部分,使用更简洁的列表形式展示 - 更新技术栈和项目结构说明 - 优化开发流程部分的描述 - 移除冗余的权限和依赖说明 * docs: 更新AGENTS.md文档以反映项目最新状态 - 重写项目概述和核心命令部分,使其更简洁准确 - 重构目录结构描述,采用更清晰的树状格式 - 移除过时的功能模块描述,聚焦当前核心功能 - 更新AI代理开发准则和权限管理说明 - 优化文档整体结构和可读性 * test: 更新特征计数测试以匹配新增 textStatistics 和 jwt 工具 * style: 统一导入语句顺序 * refactor(pages): 重构工具页面组件结构并优化功能实现 - 将各工具页面组件迁移至独立目录并重构为模块化结构 - 新增存储清理工具相关组件及状态管理逻辑 - 实现时间戳转换工具的自定义hook和UI组件 - 优化JWT解析工具的代码结构和样式 - 更新文本统计工具的性能计算逻辑 - 调整配置文件中的页面引用路径 - 删除无用文件和过时文档 * feat(顶部栏): 添加搜索功能和历史记录 实现顶部栏搜索框功能,支持模糊搜索工具并显示搜索结果列表 添加搜索历史记录功能,自动保存最近搜索并显示 支持键盘导航和选择搜索结果 * feat: 添加多语言支持(i18n) 实现应用的多语言功能,支持中英文切换 添加i18next和相关依赖 创建语言资源文件 更新组件以使用翻译功能 重构文本统计工具页面 完善测试和构建配置 * feat(顶部栏): 添加语言切换功能并优化国际化支持 feat(i18n): 增加语言归一化和同步功能 fix(剪贴板): 使用国际化文本替换剪贴板消息 test(顶部栏): 更新测试以匹配国际化标题 * refactor: 重构组件目录结构,将组件移动到对应功能页面目录 将多个组件从通用组件目录移动到各自功能页面目录下,包括 ToolCard、StorageCleanerConfirm、UrlToQrCodeSection 和 QrCodeToUrlSection。同时新增 Dashboard 页面相关组件 DashboardCard 和 ToolCard,并优化了组件导入路径。 重构旨在提高代码组织性,使组件与其所属功能页面更紧密关联,便于维护和理解代码结构。所有组件功能保持不变,仅调整文件位置和导入路径。 * refactor(Dashboard): 优化卡片组件结构并移除无用属性 重构 DashboardCard 组件接口,直接传递属性而非配置对象 移除 ToolCard 中无用的 hasAI 属性 使用 useMemo 优化可见性判断性能 统一使用 alpha 函数处理颜色透明度 * refactor(timestamp): 合并输入处理函数并优化样式结构 重构时间戳转换器的输入处理逻辑,将 setTsInput 和 setDtInput 合并为统一的 setInput 函数 将组件样式提取到 pageTheme 配置中统一管理 使用 ToggleButtonGroup 替换自定义模式切换器实现 移除不必要的 showMessage 属性传递,改用 useSnackbar 钩子 * refactor(dashboard): 重构仪表盘卡片组件并优化布局 - 删除冗余的 DashboardCard 组件,直接使用 ToolCard - 将网格布局样式移至主题配置 - 使用 CardActionArea 增强可访问性 - 简化点击事件处理逻辑 - 更新相关测试用例 * refactor(storageCleaner): 重构样式代码并优化国际化处理 将分散的样式代码集中到pageTheme.ts中统一管理 优化组件中的国际化文本引用 修复useStorageCleaner中的状态同步问题 更新测试用例以匹配国际化文本 * refactor(StorageCleaner): 优化条件判断和清理未使用的代码 简化禁用按钮的条件判断逻辑 移除未使用的 resultTimeoutRef 及相关清理代码 * refactor(QrCode): 重构二维码页面样式和组件结构 将内联样式提取到主题配置文件中 移除未使用的全局消息提示依赖 优化组件结构和代码复用 删除未使用的QrCodeUploader组件 * refactor(ErrorBoundary): 移除冗余状态更新并调整错误背景色 移除 componentDidCatch 中的冗余 setState 调用 将错误背景色从 'error.shortest' 改为 rgba 格式 * feat(TopBar): 优化顶部导航栏功能并添加样式配置 - 新增 openExtensionPage 工具函数用于打开扩展页面 - 将顶部导航栏样式配置提取到 pageTheme 中统一管理 - 改进搜索历史功能,增加错误处理和配置化限制 - 优化键盘导航和搜索结果的交互逻辑 * chore: 在.gitignore中添加docs目录 避免将文档生成目录提交到版本控制 * feat(JsonDiff): add type definitions for diff engine * feat: 新增 JSON 差异比较工具功能 添加 JSON 差异比较工具页面,支持并排和统一视图模式,包含以下功能: - 左右两侧 JSON 输入框及校验 - 差异高亮显示及导航 - 支持对象和数组的深度比较 - 国际化支持 - 单元测试覆盖核心差异算法 * feat(主题): 添加暗色模式支持并重构主题系统 - 新增 ThemeModeProvider 提供亮色/暗色/系统模式切换功能 - 重构所有组件使用 MUI 主题变量替代硬编码颜色 - 更新测试用例以适配新的主题系统 - 添加 i18n 翻译支持主题切换相关文本 - 统一入口点使用 AppRoot 组件封装主题和路由提供者 * feat(jsonDiff): 新增 JSON 格式化功能模块 - 新增 jsonFormat 语言资源,支持中英文文本 - 新增 JsonFormatSection 组件,提供 JSON 格式化输入与结果展示 - 在 JsonDiff 页面加入模式切换,支持差异比较与格式化双模式 - 实现 formatJson 和 validateJson 工具方法,支持缩进大小和键名排序选项 - 增加格式化结果显示大小统计和一键复制功能 - 写入大量单元测试,覆盖格式化和校验功能边界 - 优化页面布局与交互,提升用户体验和功能集成度 * feat(jsonDiff): 新增 JSON 转 YAML/TOML 及压缩功能 - 在语言文件(en/zh)中添加 YAML、TOML 和 JSON 压缩模式相关文案 - 扩展 JsonDiff 页面,新增 yaml、toml、minify 三种页面模式 - 新增 JsonConvertSection 组件,提供输入校验、转换操作及结果展示 - 实现 jsonToYaml 工具函数,将 JSON 转换为符合规范的 YAML 格式 - 实现 jsonToToml 工具函数,将 JSON 转换为符合规范且支持嵌套的 TOML 格式 - 实现 minifyJson 工具函数,实现 JSON 字符串压缩为单行格式 - 为所有新功能增加单元测试,覆盖正常及异常场景,保证转换正确性和稳定性 - 在页面中添加对应的切换按钮和图标,提升用户体验和操作便捷性 * refactor(jsonTools): 重命名 JSON 差异比较为 JSON 工具 - 将 JsonDiff 页面组件重命名为 JsonTools 并更新相关引用 - 修改功能配置中对应组件的名称以匹配新命名 - 更新英文和中文语言包中该功能的标题及描述 - 调整测试文件中导入路径以使用新的 JsonTools 文件夹名 * feat(storage): 优化本地存储状态管理和 JSON 工具页模式持久化 - 添加 useStorageState 钩子,支持同步快照和异步加载存储状态 - 实现快照机制,防止页面首屏闪烁现象 - 增加状态值验证器功能,保证恢复数据合法性 - JSON 工具页面模式改用受控存储状态持久化 - 新增 JsonToolsPageMode 类型定义,统一页面模式类型 - 为 useStorageState 编写完整单元测试,覆盖快照、校验及存储同步场景 * feat(base64Converter): 新增 Base64 转换器功能模块 - 添加 Base64 转换器页面及相关样式配置 - 支持文本、文件及图像三种转换模式,含编码和解码功能 - 实现拖拽、文件选择及图像预览功能 - 增加中文与英文多语言资源文件支持 - 补充相关工具函数,支持编码解码及格式校验 - 扩展路由提供者逻辑,合并已保存列表与默认功能集合 - 更新测试用例,覆盖合并新功能的场景验证 - 在特性配置中加入 Base64 转换器信息,支持图标和默认显示配置 * feat(markdown): 新增 Markdown 转 HTML 功能模块 - 添加 marked 依赖及类型定义支持 Markdown 解析 - 新增 MarkdownToHtml 页面,支持实时编辑、预览及 HTML 输出三种模式切换 - 实现 markdownToHtml 工具函数进行 Markdown 转 HTML 操作,含错误处理 - 包装完整 HTML 文档功能,支持打印和下载导出 - 配置全局主题样式,优化 Markdown 渲染视觉效果 - 完善 i18n 国际化资源,支持多语言页面展示 - 统一类型定义,新增 MarkdownToHtml 相关存储状态类型 - 添加相关单元测试,覆盖转换逻辑及功能组件 - 在功能配置中注册 Markdown 转 HTML 工具,支持界面调用和路由访问 * feat(htmlToMarkdown): 添加 HTML 转 Markdown 功能页面及转换工具 - 新增 HtmlToMarkdownPage 组件,支持实时转换 HTML 为 Markdown - 增加 htmlToMarkdown 工具函数,实现基于 DOM 解析的转换逻辑 - 新增下载 Markdown 文件功能,支持一键导出转换结果 - 配置文件增加 htmlToMarkdown 相关功能项和页面样式 - 国际化资源添加对应中英文文案 - 更新类型定义,新增 HTML 转 Markdown 相关类型和存储键 - 补充对应的单元测试,覆盖转换逻辑和导出功能 - 修正路由提供器测试,确保新增页面被正确识别和排序 --------- Co-authored-by: Copilot <copilot@github.com> Co-authored-by: yll <mtmtchb@outlook.com>
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
textToBase64,
|
||||
base64ToText,
|
||||
isValidBase64,
|
||||
isFileSizeValid,
|
||||
isSupportedImageType,
|
||||
isSupportedImageExtension,
|
||||
extractMimeTypeFromDataUri,
|
||||
formatFileSize,
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
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 返回默认类型', () => {
|
||||
expect(extractMimeTypeFromDataUri('invalid')).toBe('application/octet-stream');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatFileSize', () => {
|
||||
it('应该格式化字节', () => {
|
||||
expect(formatFileSize(0)).toBe('0 B');
|
||||
expect(formatFileSize(512)).toBe('512 B');
|
||||
});
|
||||
|
||||
it('应该格式化 KB', () => {
|
||||
expect(formatFileSize(1024)).toBe('1.0 KB');
|
||||
expect(formatFileSize(1536)).toBe('1.5 KB');
|
||||
});
|
||||
|
||||
it('应该格式化 MB', () => {
|
||||
expect(formatFileSize(1048576)).toBe('1.00 MB');
|
||||
});
|
||||
|
||||
it('应该格式化 GB', () => {
|
||||
expect(formatFileSize(1073741824)).toBe('1.00 GB');
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { diffJson } from '../../pages/JsonDiff/diffEngine';
|
||||
import { diffJson } from '../../pages/JsonTools/diffEngine';
|
||||
|
||||
describe('diffJson', () => {
|
||||
it('should detect added property', () => {
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { htmlToMarkdown, downloadMarkdownFile, SAMPLE_HTML } from '../htmlToMarkdown';
|
||||
|
||||
describe('htmlToMarkdown', () => {
|
||||
it('returns empty result for empty string', () => {
|
||||
const result = htmlToMarkdown('');
|
||||
expect(result.markdown).toBe('');
|
||||
expect(result.originalLength).toBe(0);
|
||||
expect(result.markdownLength).toBe(0);
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('returns empty result for whitespace-only string', () => {
|
||||
const result = htmlToMarkdown(' \n ');
|
||||
expect(result.markdown).toBe('');
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('converts h1 heading', () => {
|
||||
const result = htmlToMarkdown('<h1>Hello World</h1>');
|
||||
expect(result.markdown).toContain('# Hello World');
|
||||
});
|
||||
|
||||
it('converts h2-h6 headings', () => {
|
||||
const result = htmlToMarkdown(
|
||||
'<h2>Two</h2><h3>Three</h3><h4>Four</h4><h5>Five</h5><h6>Six</h6>',
|
||||
);
|
||||
expect(result.markdown).toContain('## Two');
|
||||
expect(result.markdown).toContain('### Three');
|
||||
expect(result.markdown).toContain('#### Four');
|
||||
expect(result.markdown).toContain('##### Five');
|
||||
expect(result.markdown).toContain('###### Six');
|
||||
});
|
||||
|
||||
it('converts paragraph', () => {
|
||||
const result = htmlToMarkdown('<p>This is a paragraph.</p>');
|
||||
expect(result.markdown).toContain('This is a paragraph.');
|
||||
});
|
||||
|
||||
it('converts strong and b tags', () => {
|
||||
const result = htmlToMarkdown('<strong>bold</strong> and <b>also bold</b>');
|
||||
expect(result.markdown).toContain('**bold**');
|
||||
expect(result.markdown).toContain('**also bold**');
|
||||
});
|
||||
|
||||
it('converts em and i tags', () => {
|
||||
const result = htmlToMarkdown('<em>italic</em> and <i>also italic</i>');
|
||||
expect(result.markdown).toContain('*italic*');
|
||||
expect(result.markdown).toContain('*also italic*');
|
||||
});
|
||||
|
||||
it('converts del and s tags', () => {
|
||||
const result = htmlToMarkdown('<del>deleted</del> and <s>strikethrough</s>');
|
||||
expect(result.markdown).toContain('~~deleted~~');
|
||||
expect(result.markdown).toContain('~~strikethrough~~');
|
||||
});
|
||||
|
||||
it('converts inline code', () => {
|
||||
const result = htmlToMarkdown('<code>const x = 1;</code>');
|
||||
expect(result.markdown).toContain('`const x = 1;`');
|
||||
});
|
||||
|
||||
it('converts pre code block', () => {
|
||||
const result = htmlToMarkdown('<pre><code>line1\nline2</code></pre>');
|
||||
expect(result.markdown).toContain('```');
|
||||
expect(result.markdown).toContain('line1');
|
||||
expect(result.markdown).toContain('line2');
|
||||
});
|
||||
|
||||
it('converts pre code block with language', () => {
|
||||
const result = htmlToMarkdown(
|
||||
'<pre><code class="language-javascript">const x = 1;</code></pre>',
|
||||
);
|
||||
expect(result.markdown).toContain('```javascript');
|
||||
expect(result.markdown).toContain('const x = 1;');
|
||||
});
|
||||
|
||||
it('converts anchor links', () => {
|
||||
const result = htmlToMarkdown('<a href="https://example.com">Link text</a>');
|
||||
expect(result.markdown).toContain('[Link text](https://example.com)');
|
||||
});
|
||||
|
||||
it('converts anchor links with title', () => {
|
||||
const result = htmlToMarkdown('<a href="https://example.com" title="Title">Link</a>');
|
||||
expect(result.markdown).toContain('[Link](https://example.com "Title")');
|
||||
});
|
||||
|
||||
it('converts images', () => {
|
||||
const result = htmlToMarkdown('<img src="image.png" alt="desc" />');
|
||||
expect(result.markdown).toContain('');
|
||||
});
|
||||
|
||||
it('converts images with title', () => {
|
||||
const result = htmlToMarkdown('<img src="image.png" alt="desc" title="Title" />');
|
||||
expect(result.markdown).toContain('');
|
||||
});
|
||||
|
||||
it('converts unordered list', () => {
|
||||
const result = htmlToMarkdown('<ul><li>Item 1</li><li>Item 2</li></ul>');
|
||||
expect(result.markdown).toContain('- Item 1');
|
||||
expect(result.markdown).toContain('- Item 2');
|
||||
});
|
||||
|
||||
it('converts ordered list', () => {
|
||||
const result = htmlToMarkdown('<ol><li>First</li><li>Second</li></ol>');
|
||||
expect(result.markdown).toContain('1. First');
|
||||
expect(result.markdown).toContain('2. Second');
|
||||
});
|
||||
|
||||
it('converts ordered list with start attribute', () => {
|
||||
const result = htmlToMarkdown('<ol start="5"><li>Item</li></ol>');
|
||||
expect(result.markdown).toContain('5. Item');
|
||||
});
|
||||
|
||||
it('converts blockquote', () => {
|
||||
const result = htmlToMarkdown('<blockquote><p>Quote text</p></blockquote>');
|
||||
expect(result.markdown).toContain('> Quote text');
|
||||
});
|
||||
|
||||
it('converts horizontal rule', () => {
|
||||
const result = htmlToMarkdown('<hr />');
|
||||
expect(result.markdown).toContain('---');
|
||||
});
|
||||
|
||||
it('converts line break', () => {
|
||||
const result = htmlToMarkdown('Line 1<br />Line 2');
|
||||
expect(result.markdown).toContain('\n');
|
||||
});
|
||||
|
||||
it('converts table', () => {
|
||||
const html =
|
||||
'<table><tr><th>Name</th><th>Type</th></tr><tr><td>John</td><td>User</td></tr></table>';
|
||||
const result = htmlToMarkdown(html);
|
||||
expect(result.markdown).toContain('| Name | Type |');
|
||||
expect(result.markdown).toContain('| --- | --- |');
|
||||
expect(result.markdown).toContain('| John | User |');
|
||||
});
|
||||
|
||||
it('ignores script and style tags', () => {
|
||||
const result = htmlToMarkdown('<script>alert(1)</script><style>.x{}</style><p>text</p>');
|
||||
expect(result.markdown).not.toContain('alert');
|
||||
expect(result.markdown).not.toContain('.x{}');
|
||||
expect(result.markdown).toContain('text');
|
||||
});
|
||||
|
||||
it('handles nested elements', () => {
|
||||
const result = htmlToMarkdown('<p><strong>bold</strong> and <em>italic</em></p>');
|
||||
expect(result.markdown).toContain('**bold**');
|
||||
expect(result.markdown).toContain('*italic*');
|
||||
});
|
||||
|
||||
it('returns hasError false for valid HTML', () => {
|
||||
const result = htmlToMarkdown('<p>Valid</p>');
|
||||
expect(result.hasError).toBe(false);
|
||||
expect(result.error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns correct length stats', () => {
|
||||
const html = '<p>Hello</p>';
|
||||
const result = htmlToMarkdown(html);
|
||||
expect(result.originalLength).toBe(html.length);
|
||||
expect(result.markdownLength).toBe(result.markdown.length);
|
||||
});
|
||||
|
||||
it('handles full HTML document', () => {
|
||||
const result = htmlToMarkdown(SAMPLE_HTML);
|
||||
expect(result.hasError).toBe(false);
|
||||
expect(result.markdown).toContain('# 欢迎使用 HTML 转 Markdown');
|
||||
expect(result.markdown).toContain('**HTML**');
|
||||
expect(result.markdown).toContain('*Markdown*');
|
||||
expect(result.markdown).toContain('```javascript');
|
||||
expect(result.markdown).toContain('| 名称 | 类型 |');
|
||||
expect(result.markdown).toContain('> 这是一段引用文本');
|
||||
});
|
||||
|
||||
it('handles plain text without tags', () => {
|
||||
const result = htmlToMarkdown('Just plain text');
|
||||
expect(result.markdown).toContain('Just plain text');
|
||||
expect(result.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('handles task list items', () => {
|
||||
const result = htmlToMarkdown(
|
||||
'<ul><li><input type="checkbox" checked /> Done</li><li><input type="checkbox" /> Todo</li></ul>',
|
||||
);
|
||||
expect(result.markdown).toContain('- [x] Done');
|
||||
expect(result.markdown).toContain('- [ ] Todo');
|
||||
});
|
||||
|
||||
it('handles div and span wrappers', () => {
|
||||
const result = htmlToMarkdown('<div><span><p>Content</p></span></div>');
|
||||
expect(result.markdown).toContain('Content');
|
||||
});
|
||||
|
||||
it('handles empty anchor with no text', () => {
|
||||
const result = htmlToMarkdown('<a href="https://example.com"></a>');
|
||||
expect(result.markdown).not.toContain('[');
|
||||
});
|
||||
|
||||
it('handles unknown tags gracefully', () => {
|
||||
const result = htmlToMarkdown('<custom-tag>Content</custom-tag>');
|
||||
expect(result.markdown).toContain('Content');
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadMarkdownFile', () => {
|
||||
const originalURL = globalThis.URL;
|
||||
let createObjectURLSpy: ReturnType<typeof vi.fn>;
|
||||
let revokeObjectURLSpy: ReturnType<typeof vi.fn>;
|
||||
let clickSpy: ReturnType<typeof vi.fn>;
|
||||
let appendChildSpy: ReturnType<typeof vi.fn>;
|
||||
let removeChildSpy: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
createObjectURLSpy = vi.fn().mockReturnValue('blob:test-url');
|
||||
revokeObjectURLSpy = vi.fn();
|
||||
(globalThis as any).URL = {
|
||||
createObjectURL: createObjectURLSpy,
|
||||
revokeObjectURL: revokeObjectURLSpy,
|
||||
};
|
||||
|
||||
clickSpy = vi.fn();
|
||||
appendChildSpy = vi.fn();
|
||||
removeChildSpy = vi.fn();
|
||||
|
||||
const mockLink = {
|
||||
href: '',
|
||||
download: '',
|
||||
click: clickSpy,
|
||||
};
|
||||
|
||||
vi.spyOn(document, 'createElement').mockReturnValue(mockLink as any);
|
||||
vi.spyOn(document.body, 'appendChild').mockImplementation(appendChildSpy);
|
||||
vi.spyOn(document.body, 'removeChild').mockImplementation(removeChildSpy);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
(globalThis as any).URL = originalURL;
|
||||
});
|
||||
|
||||
it('should create a blob and trigger download', () => {
|
||||
downloadMarkdownFile('# Hello', 'test.md');
|
||||
|
||||
expect(createObjectURLSpy).toHaveBeenCalledOnce();
|
||||
expect(clickSpy).toHaveBeenCalledOnce();
|
||||
expect(appendChildSpy).toHaveBeenCalledOnce();
|
||||
expect(removeChildSpy).toHaveBeenCalledOnce();
|
||||
expect(revokeObjectURLSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should use default filename when not provided', () => {
|
||||
downloadMarkdownFile('content');
|
||||
|
||||
const mockLink = (document.createElement as any).mock.results[0].value;
|
||||
expect(mockLink.download).toBe('export.md');
|
||||
});
|
||||
|
||||
it('should set correct MIME type', () => {
|
||||
downloadMarkdownFile('content');
|
||||
|
||||
const blobArg = createObjectURLSpy.mock.calls[0][0] as Blob;
|
||||
expect(blobArg.type).toBe('text/markdown;charset=utf-8');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { formatJson, validateJson, type JsonFormatOptions } from '../jsonFormatter';
|
||||
|
||||
describe('formatJson', () => {
|
||||
const defaultOptions: JsonFormatOptions = { indentSize: 2, sortKeys: false };
|
||||
|
||||
it('should format minified JSON', () => {
|
||||
const input = '{"name":"test","value":123}';
|
||||
const result = formatJson(input, defaultOptions);
|
||||
expect(result.formatted).toBe('{\n "name": "test",\n "value": 123\n}');
|
||||
expect(result.originalBytes).toBeGreaterThan(0);
|
||||
expect(result.formattedBytes).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should return empty result for empty input', () => {
|
||||
const result = formatJson('', defaultOptions);
|
||||
expect(result.formatted).toBe('');
|
||||
expect(result.originalBytes).toBe(0);
|
||||
expect(result.formattedBytes).toBe(0);
|
||||
});
|
||||
|
||||
it('should return empty result for whitespace-only input', () => {
|
||||
const result = formatJson(' ', defaultOptions);
|
||||
expect(result.formatted).toBe('');
|
||||
});
|
||||
|
||||
it('should format with custom indent size', () => {
|
||||
const input = '{"a":1}';
|
||||
const result4 = formatJson(input, { indentSize: 4, sortKeys: false });
|
||||
expect(result4.formatted).toBe('{\n "a": 1\n}');
|
||||
|
||||
const result8 = formatJson(input, { indentSize: 8, sortKeys: false });
|
||||
expect(result8.formatted).toBe('{\n "a": 1\n}');
|
||||
});
|
||||
|
||||
it('should sort keys alphabetically when sortKeys is true', () => {
|
||||
const input = '{"c":3,"a":1,"b":2}';
|
||||
const result = formatJson(input, { indentSize: 2, sortKeys: true });
|
||||
expect(result.formatted).toBe('{\n "a": 1,\n "b": 2,\n "c": 3\n}');
|
||||
});
|
||||
|
||||
it('should sort nested object keys recursively', () => {
|
||||
const input = '{"z":1,"a":{"d":4,"b":2,"c":3}}';
|
||||
const result = formatJson(input, { indentSize: 2, sortKeys: true });
|
||||
expect(result.formatted).toBe(
|
||||
'{\n "a": {\n "b": 2,\n "c": 3,\n "d": 4\n },\n "z": 1\n}',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not sort keys by default', () => {
|
||||
const input = '{"c":3,"a":1,"b":2}';
|
||||
const result = formatJson(input, defaultOptions);
|
||||
expect(result.formatted).toBe('{\n "c": 3,\n "a": 1,\n "b": 2\n}');
|
||||
});
|
||||
|
||||
it('should handle arrays correctly', () => {
|
||||
const input = '[1,2,3]';
|
||||
const result = formatJson(input, defaultOptions);
|
||||
expect(result.formatted).toBe('[\n 1,\n 2,\n 3\n]');
|
||||
});
|
||||
|
||||
it('should handle nested arrays and objects', () => {
|
||||
const input = '{"users":[{"name":"Alice"},{"name":"Bob"}]}';
|
||||
const result = formatJson(input, defaultOptions);
|
||||
expect(result.formatted).toContain('"users"');
|
||||
expect(result.formatted).toContain('"Alice"');
|
||||
expect(result.formatted).toContain('"Bob"');
|
||||
});
|
||||
|
||||
it('should handle primitive values', () => {
|
||||
expect(formatJson('null', defaultOptions).formatted).toBe('null');
|
||||
expect(formatJson('true', defaultOptions).formatted).toBe('true');
|
||||
expect(formatJson('42', defaultOptions).formatted).toBe('42');
|
||||
expect(formatJson('"hello"', defaultOptions).formatted).toBe('"hello"');
|
||||
});
|
||||
|
||||
it('should throw SyntaxError for invalid JSON', () => {
|
||||
expect(() => formatJson('{invalid}', defaultOptions)).toThrow(SyntaxError);
|
||||
});
|
||||
|
||||
it('should calculate byte sizes correctly', () => {
|
||||
const input = '{"a":1}';
|
||||
const result = formatJson(input, defaultOptions);
|
||||
// ASCII characters: each character = 1 byte
|
||||
expect(result.originalBytes).toBe(input.trim().length);
|
||||
expect(result.formattedBytes).toBe(result.formatted.length);
|
||||
});
|
||||
|
||||
it('should handle JSON with Unicode characters', () => {
|
||||
const input = '{"name":"测试"}';
|
||||
const result = formatJson(input, defaultOptions);
|
||||
expect(result.formatted).toContain('"测试"');
|
||||
// UTF-8: each Chinese character is 3 bytes
|
||||
expect(result.originalBytes).toBeGreaterThan(input.trim().length);
|
||||
});
|
||||
|
||||
it('should sort keys in arrays containing objects', () => {
|
||||
const input = '[{"z":1,"a":2}]';
|
||||
const result = formatJson(input, { indentSize: 2, sortKeys: true });
|
||||
expect(result.formatted).toContain('"a": 2');
|
||||
expect(result.formatted).toContain('"z": 1');
|
||||
// Ensure sorted order
|
||||
const aIndex = result.formatted.indexOf('"a": 2');
|
||||
const zIndex = result.formatted.indexOf('"z": 1');
|
||||
expect(aIndex).toBeLessThan(zIndex);
|
||||
});
|
||||
|
||||
it('should handle already formatted JSON', () => {
|
||||
const input = '{\n "name": "test",\n "value": 123\n}';
|
||||
const result = formatJson(input, defaultOptions);
|
||||
expect(result.formatted).toBe('{\n "name": "test",\n "value": 123\n}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateJson', () => {
|
||||
it('should return null for valid JSON object', () => {
|
||||
expect(validateJson('{"a":1}')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for valid JSON array', () => {
|
||||
expect(validateJson('[1,2,3]')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for valid JSON primitive', () => {
|
||||
expect(validateJson('null')).toBeNull();
|
||||
expect(validateJson('true')).toBeNull();
|
||||
expect(validateJson('42')).toBeNull();
|
||||
expect(validateJson('"hello"')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for empty input', () => {
|
||||
expect(validateJson('')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for whitespace-only input', () => {
|
||||
expect(validateJson(' ')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return error message for invalid JSON', () => {
|
||||
const error = validateJson('{invalid}');
|
||||
expect(error).not.toBeNull();
|
||||
expect(typeof error).toBe('string');
|
||||
});
|
||||
|
||||
it('should return error for unclosed bracket', () => {
|
||||
expect(validateJson('{"a":1')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should return error for trailing comma', () => {
|
||||
expect(validateJson('{"a":1,}')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should handle JSON with leading/trailing whitespace', () => {
|
||||
expect(validateJson(' {"a":1} ')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { minifyJson } from '../jsonFormatter';
|
||||
|
||||
describe('minifyJson', () => {
|
||||
it('should minify formatted JSON', () => {
|
||||
const input = '{\n "name": "test",\n "value": 123\n}';
|
||||
const result = minifyJson(input);
|
||||
expect(result.minified).toBe('{"name":"test","value":123}');
|
||||
});
|
||||
|
||||
it('should return empty result for empty input', () => {
|
||||
const result = minifyJson('');
|
||||
expect(result.minified).toBe('');
|
||||
expect(result.originalBytes).toBe(0);
|
||||
expect(result.minifiedBytes).toBe(0);
|
||||
});
|
||||
|
||||
it('should return empty result for whitespace-only input', () => {
|
||||
const result = minifyJson(' ');
|
||||
expect(result.minified).toBe('');
|
||||
});
|
||||
|
||||
it('should handle already minified JSON', () => {
|
||||
const input = '{"a":1}';
|
||||
const result = minifyJson(input);
|
||||
expect(result.minified).toBe('{"a":1}');
|
||||
});
|
||||
|
||||
it('should remove all whitespace', () => {
|
||||
const input = '{\n "key" : "value" ,\n "num" : 42\n}';
|
||||
const result = minifyJson(input);
|
||||
expect(result.minified).toBe('{"key":"value","num":42}');
|
||||
});
|
||||
|
||||
it('should preserve string content with spaces', () => {
|
||||
const input = '{"message":"hello world"}';
|
||||
const result = minifyJson(input);
|
||||
expect(result.minified).toBe('{"message":"hello world"}');
|
||||
});
|
||||
|
||||
it('should handle arrays', () => {
|
||||
const input = '[\n 1,\n 2,\n 3\n]';
|
||||
const result = minifyJson(input);
|
||||
expect(result.minified).toBe('[1,2,3]');
|
||||
});
|
||||
|
||||
it('should handle nested objects', () => {
|
||||
const input = '{\n "a": {\n "b": 1\n }\n}';
|
||||
const result = minifyJson(input);
|
||||
expect(result.minified).toBe('{"a":{"b":1}}');
|
||||
});
|
||||
|
||||
it('should handle primitive values', () => {
|
||||
expect(minifyJson('null').minified).toBe('null');
|
||||
expect(minifyJson('true').minified).toBe('true');
|
||||
expect(minifyJson('42').minified).toBe('42');
|
||||
expect(minifyJson('"hello"').minified).toBe('"hello"');
|
||||
});
|
||||
|
||||
it('should throw SyntaxError for invalid JSON', () => {
|
||||
expect(() => minifyJson('{invalid}')).toThrow(SyntaxError);
|
||||
});
|
||||
|
||||
it('should calculate byte sizes correctly', () => {
|
||||
const input = '{\n "a": 1\n}';
|
||||
const result = minifyJson(input);
|
||||
expect(result.originalBytes).toBeGreaterThan(0);
|
||||
expect(result.minifiedBytes).toBeGreaterThan(0);
|
||||
// Minified should be smaller than original for formatted input
|
||||
expect(result.minifiedBytes).toBeLessThan(result.originalBytes);
|
||||
});
|
||||
|
||||
it('should handle Unicode content', () => {
|
||||
const input = '{\n "名前": "テスト"\n}';
|
||||
const result = minifyJson(input);
|
||||
expect(result.minified).toBe('{"名前":"テスト"}');
|
||||
});
|
||||
|
||||
it('should handle empty object', () => {
|
||||
const result = minifyJson('{}');
|
||||
expect(result.minified).toBe('{}');
|
||||
});
|
||||
|
||||
it('should handle empty array', () => {
|
||||
const result = minifyJson('[]');
|
||||
expect(result.minified).toBe('[]');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { jsonToToml } from '../jsonToToml';
|
||||
|
||||
describe('jsonToToml', () => {
|
||||
it('should convert a simple object', () => {
|
||||
const result = jsonToToml('{"name":"test","value":123}');
|
||||
expect(result.output).toContain('name = "test"');
|
||||
expect(result.output).toContain('value = 123');
|
||||
});
|
||||
|
||||
it('should convert a nested object with table headers', () => {
|
||||
const result = jsonToToml('{"database":{"host":"localhost","port":5432}}');
|
||||
expect(result.output).toContain('[database]');
|
||||
expect(result.output).toContain('host = "localhost"');
|
||||
expect(result.output).toContain('port = 5432');
|
||||
});
|
||||
|
||||
it('should handle boolean values', () => {
|
||||
const result = jsonToToml('{"active":true,"deleted":false}');
|
||||
expect(result.output).toContain('active = true');
|
||||
expect(result.output).toContain('deleted = false');
|
||||
});
|
||||
|
||||
it('should handle null values as empty string', () => {
|
||||
const result = jsonToToml('{"key":null}');
|
||||
expect(result.output).toContain('key = ""');
|
||||
});
|
||||
|
||||
it('should handle number values', () => {
|
||||
const result = jsonToToml('{"count":42,"price":3.14}');
|
||||
expect(result.output).toContain('count = 42');
|
||||
expect(result.output).toContain('price = 3.14');
|
||||
});
|
||||
|
||||
it('should handle arrays of primitives', () => {
|
||||
const result = jsonToToml('{"ports":[80,443,8080]}');
|
||||
expect(result.output).toContain('ports = [80, 443, 8080]');
|
||||
});
|
||||
|
||||
it('should handle arrays of strings', () => {
|
||||
const result = jsonToToml('{"tags":["web","api"]}');
|
||||
expect(result.output).toContain('tags = ["web", "api"]');
|
||||
});
|
||||
|
||||
it('should handle object arrays with table array syntax', () => {
|
||||
const result = jsonToToml('{"users":[{"name":"Alice"},{"name":"Bob"}]}');
|
||||
expect(result.output).toContain('[[users]]');
|
||||
expect(result.output).toContain('name = "Alice"');
|
||||
expect(result.output).toContain('name = "Bob"');
|
||||
});
|
||||
|
||||
it('should handle deeply nested objects', () => {
|
||||
const result = jsonToToml('{"a":{"b":{"c":1}}}');
|
||||
expect(result.output).toContain('[a.b]');
|
||||
expect(result.output).toContain('c = 1');
|
||||
});
|
||||
|
||||
it('should handle empty object', () => {
|
||||
const result = jsonToToml('{}');
|
||||
expect(result.output).toBe('');
|
||||
});
|
||||
|
||||
it('should handle empty string input', () => {
|
||||
const result = jsonToToml('');
|
||||
expect(result.output).toBe('');
|
||||
expect(result.originalBytes).toBe(0);
|
||||
});
|
||||
|
||||
it('should calculate byte sizes', () => {
|
||||
const result = jsonToToml('{"a":1}');
|
||||
expect(result.originalBytes).toBeGreaterThan(0);
|
||||
expect(result.outputBytes).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should throw SyntaxError for invalid JSON', () => {
|
||||
expect(() => jsonToToml('{invalid}')).toThrow(SyntaxError);
|
||||
});
|
||||
|
||||
it('should throw Error for non-object top-level value', () => {
|
||||
expect(() => jsonToToml('"hello"')).toThrow(
|
||||
'TOML requires the top-level value to be an object',
|
||||
);
|
||||
expect(() => jsonToToml('[1,2]')).toThrow('TOML requires the top-level value to be an object');
|
||||
expect(() => jsonToToml('null')).toThrow('TOML requires the top-level value to be an object');
|
||||
});
|
||||
|
||||
it('should escape special characters in strings', () => {
|
||||
const result = jsonToToml('{"path":"C:\\\\Users\\\\test"}');
|
||||
expect(result.output).toContain('"C:\\\\Users\\\\test"');
|
||||
});
|
||||
|
||||
it('should handle keys with special characters', () => {
|
||||
const result = jsonToToml('{"key with spaces":"value"}');
|
||||
expect(result.output).toContain('"key with spaces"');
|
||||
});
|
||||
|
||||
it('should handle empty arrays of primitives', () => {
|
||||
const result = jsonToToml('{"items":[]}');
|
||||
expect(result.output).toContain('items = []');
|
||||
});
|
||||
|
||||
it('should separate table sections with blank lines', () => {
|
||||
const result = jsonToToml('{"a":{"x":1},"b":{"y":2}}');
|
||||
expect(result.output).toContain('\n\n');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { jsonToYaml } from '../jsonToYaml';
|
||||
|
||||
describe('jsonToYaml', () => {
|
||||
it('should convert a simple object', () => {
|
||||
const result = jsonToYaml('{"name":"test","value":123}');
|
||||
expect(result.output).toBe('name: test\nvalue: 123');
|
||||
});
|
||||
|
||||
it('should convert a nested object', () => {
|
||||
const result = jsonToYaml('{"user":{"name":"Alice","age":30}}');
|
||||
expect(result.output).toBe('user:\n name: Alice\n age: 30');
|
||||
});
|
||||
|
||||
it('should convert an array of primitives', () => {
|
||||
const result = jsonToYaml('[1,2,3]');
|
||||
expect(result.output).toBe('- 1\n- 2\n- 3');
|
||||
});
|
||||
|
||||
it('should convert an array of objects', () => {
|
||||
const result = jsonToYaml('[{"name":"A"},{"name":"B"}]');
|
||||
expect(result.output).toContain('name: A');
|
||||
expect(result.output).toContain('name: B');
|
||||
expect(result.output).toContain('- name: A');
|
||||
expect(result.output).toContain('- name: B');
|
||||
});
|
||||
|
||||
it('should handle null values', () => {
|
||||
const result = jsonToYaml('{"key":null}');
|
||||
expect(result.output).toBe('key: null');
|
||||
});
|
||||
|
||||
it('should handle boolean values', () => {
|
||||
const result = jsonToYaml('{"active":true,"deleted":false}');
|
||||
expect(result.output).toBe('active: true\ndeleted: false');
|
||||
});
|
||||
|
||||
it('should handle number values', () => {
|
||||
const result = jsonToYaml('{"count":42,"price":3.14}');
|
||||
expect(result.output).toBe('count: 42\nprice: 3.14');
|
||||
});
|
||||
|
||||
it('should handle empty object', () => {
|
||||
const result = jsonToYaml('{}');
|
||||
expect(result.output).toBe('{}');
|
||||
});
|
||||
|
||||
it('should handle empty array', () => {
|
||||
const result = jsonToYaml('[]');
|
||||
expect(result.output).toContain('[]');
|
||||
});
|
||||
|
||||
it('should handle empty string', () => {
|
||||
const result = jsonToYaml('');
|
||||
expect(result.output).toBe('');
|
||||
expect(result.originalBytes).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle whitespace-only input', () => {
|
||||
const result = jsonToYaml(' ');
|
||||
expect(result.output).toBe('');
|
||||
});
|
||||
|
||||
it('should quote strings with special characters', () => {
|
||||
const result = jsonToYaml('{"key":"value: with colon"}');
|
||||
expect(result.output).toContain('"value: with colon"');
|
||||
});
|
||||
|
||||
it('should quote strings that look like YAML keywords', () => {
|
||||
const result = jsonToYaml('{"key":"null"}');
|
||||
expect(result.output).toContain('"null"');
|
||||
});
|
||||
|
||||
it('should handle deeply nested objects', () => {
|
||||
const result = jsonToYaml('{"a":{"b":{"c":1}}}');
|
||||
expect(result.output).toBe('a:\n b:\n c: 1');
|
||||
});
|
||||
|
||||
it('should calculate byte sizes', () => {
|
||||
const result = jsonToYaml('{"a":1}');
|
||||
expect(result.originalBytes).toBeGreaterThan(0);
|
||||
expect(result.outputBytes).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should throw SyntaxError for invalid JSON', () => {
|
||||
expect(() => jsonToYaml('{invalid}')).toThrow(SyntaxError);
|
||||
});
|
||||
|
||||
it('should handle keys with special characters', () => {
|
||||
const result = jsonToYaml('{"key with spaces":"value"}');
|
||||
expect(result.output).toContain('"key with spaces"');
|
||||
});
|
||||
|
||||
it('should handle nested arrays in objects', () => {
|
||||
const result = jsonToYaml('{"items":[1,2,3]}');
|
||||
expect(result.output).toContain('items:');
|
||||
expect(result.output).toContain('- 1');
|
||||
expect(result.output).toContain('- 2');
|
||||
expect(result.output).toContain('- 3');
|
||||
});
|
||||
|
||||
it('should handle primitive top-level values', () => {
|
||||
expect(jsonToYaml('"hello"').output).toBe('hello');
|
||||
expect(jsonToYaml('42').output).toBe('42');
|
||||
expect(jsonToYaml('true').output).toBe('true');
|
||||
expect(jsonToYaml('null').output).toBe('null');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import {
|
||||
markdownToHtml,
|
||||
wrapHtmlDocument,
|
||||
downloadHtmlFile,
|
||||
SAMPLE_MARKDOWN,
|
||||
} from '@/utils/markdownToHtml';
|
||||
|
||||
describe('markdownToHtml', () => {
|
||||
it('应该转换基础 Markdown 标题', () => {
|
||||
const result = markdownToHtml('# Hello World');
|
||||
expect(result.hasError).toBe(false);
|
||||
expect(result.html).toContain('<h1');
|
||||
expect(result.html).toContain('Hello World');
|
||||
expect(result.originalLength).toBe(13);
|
||||
expect(result.htmlLength).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('应该转换粗体和斜体', () => {
|
||||
const result = markdownToHtml('**bold** and *italic*');
|
||||
expect(result.hasError).toBe(false);
|
||||
expect(result.html).toContain('<strong>bold</strong>');
|
||||
expect(result.html).toContain('<em>italic</em>');
|
||||
});
|
||||
|
||||
it('应该转换链接', () => {
|
||||
const result = markdownToHtml('[Google](https://google.com)');
|
||||
expect(result.hasError).toBe(false);
|
||||
expect(result.html).toContain('<a');
|
||||
expect(result.html).toContain('href="https://google.com"');
|
||||
expect(result.html).toContain('Google');
|
||||
});
|
||||
|
||||
it('应该转换无序列表', () => {
|
||||
const result = markdownToHtml('- item 1\n- item 2');
|
||||
expect(result.hasError).toBe(false);
|
||||
expect(result.html).toContain('<ul>');
|
||||
expect(result.html).toContain('<li>item 1</li>');
|
||||
});
|
||||
|
||||
it('应该转换有序列表', () => {
|
||||
const result = markdownToHtml('1. first\n2. second');
|
||||
expect(result.hasError).toBe(false);
|
||||
expect(result.html).toContain('<ol>');
|
||||
expect(result.html).toContain('<li>first</li>');
|
||||
});
|
||||
|
||||
it('应该转换代码块', () => {
|
||||
const result = markdownToHtml('```js\nconst x = 1;\n```');
|
||||
expect(result.hasError).toBe(false);
|
||||
expect(result.html).toContain('<pre>');
|
||||
expect(result.html).toContain('<code');
|
||||
expect(result.html).toContain('const x = 1;');
|
||||
});
|
||||
|
||||
it('应该转换行内代码', () => {
|
||||
const result = markdownToHtml('use `npm install` command');
|
||||
expect(result.hasError).toBe(false);
|
||||
expect(result.html).toContain('<code>npm install</code>');
|
||||
});
|
||||
|
||||
it('应该转换引用块', () => {
|
||||
const result = markdownToHtml('> This is a quote');
|
||||
expect(result.hasError).toBe(false);
|
||||
expect(result.html).toContain('<blockquote>');
|
||||
expect(result.html).toContain('This is a quote');
|
||||
});
|
||||
|
||||
it('应该转换表格', () => {
|
||||
const md = '| A | B |\n|---|---|\n| 1 | 2 |';
|
||||
const result = markdownToHtml(md);
|
||||
expect(result.hasError).toBe(false);
|
||||
expect(result.html).toContain('<table>');
|
||||
expect(result.html).toContain('<th>A</th>');
|
||||
expect(result.html).toContain('<td>1</td>');
|
||||
});
|
||||
|
||||
it('应该转换任务列表', () => {
|
||||
const result = markdownToHtml('- [x] done\n- [ ] todo');
|
||||
expect(result.hasError).toBe(false);
|
||||
expect(result.html).toContain('<input');
|
||||
expect(result.html).toContain('checked');
|
||||
});
|
||||
|
||||
it('应该转换删除线', () => {
|
||||
const result = markdownToHtml('~~deleted~~');
|
||||
expect(result.hasError).toBe(false);
|
||||
expect(result.html).toContain('<del>deleted</del>');
|
||||
});
|
||||
|
||||
it('应该处理空字符串', () => {
|
||||
const result = markdownToHtml('');
|
||||
expect(result.hasError).toBe(false);
|
||||
expect(result.html).toBe('');
|
||||
expect(result.originalLength).toBe(0);
|
||||
expect(result.htmlLength).toBe(0);
|
||||
});
|
||||
|
||||
it('应该处理空白字符串', () => {
|
||||
const result = markdownToHtml(' \n ');
|
||||
expect(result.hasError).toBe(false);
|
||||
expect(result.html).toBe('');
|
||||
});
|
||||
|
||||
it('应该处理中文内容', () => {
|
||||
const result = markdownToHtml('# 你好世界\n\n这是**中文**内容。');
|
||||
expect(result.hasError).toBe(false);
|
||||
expect(result.html).toContain('你好世界');
|
||||
expect(result.html).toContain('<strong>中文</strong>');
|
||||
});
|
||||
|
||||
it('应该转换示例 Markdown', () => {
|
||||
const result = markdownToHtml(SAMPLE_MARKDOWN);
|
||||
expect(result.hasError).toBe(false);
|
||||
expect(result.html).toContain('<h1');
|
||||
expect(result.html).toContain('<h2');
|
||||
expect(result.html).toContain('<table>');
|
||||
expect(result.html).toContain('<code>');
|
||||
expect(result.originalLength).toBe(SAMPLE_MARKDOWN.length);
|
||||
expect(result.htmlLength).toBeGreaterThan(result.originalLength);
|
||||
});
|
||||
});
|
||||
|
||||
describe('wrapHtmlDocument', () => {
|
||||
it('应该生成完整的 HTML 文档', () => {
|
||||
const doc = wrapHtmlDocument('<p>Hello</p>', 'Test Title');
|
||||
expect(doc).toContain('<!DOCTYPE html>');
|
||||
expect(doc).toContain('<html');
|
||||
expect(doc).toContain('<head>');
|
||||
expect(doc).toContain('<title>Test Title</title>');
|
||||
expect(doc).toContain('<body>');
|
||||
expect(doc).toContain('<p>Hello</p>');
|
||||
expect(doc).toContain('</html>');
|
||||
});
|
||||
|
||||
it('应该转义标题中的特殊字符', () => {
|
||||
const doc = wrapHtmlDocument('<p>test</p>', 'Title <script>');
|
||||
expect(doc).toContain('Title <script>');
|
||||
expect(doc).not.toContain('<script>');
|
||||
});
|
||||
|
||||
it('应该使用默认标题', () => {
|
||||
const doc = wrapHtmlDocument('<p>test</p>');
|
||||
expect(doc).toContain('<title>Markdown Export</title>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadHtmlFile', () => {
|
||||
it('应该创建下载链接并触发下载', () => {
|
||||
const createObjectURLSpy = vi.fn(() => 'blob:test');
|
||||
const revokeObjectURLSpy = vi.fn();
|
||||
(globalThis as any).URL = {
|
||||
createObjectURL: createObjectURLSpy,
|
||||
revokeObjectURL: revokeObjectURLSpy,
|
||||
};
|
||||
|
||||
const clickSpy = vi.fn();
|
||||
const mockAnchor = document.createElement('a');
|
||||
mockAnchor.click = clickSpy;
|
||||
|
||||
const createElementSpy = vi.spyOn(document, 'createElement').mockReturnValue(mockAnchor);
|
||||
const appendChildSpy = vi.spyOn(document.body, 'appendChild').mockReturnValue(mockAnchor);
|
||||
const removeChildSpy = vi.spyOn(document.body, 'removeChild').mockReturnValue(mockAnchor);
|
||||
|
||||
downloadHtmlFile('<p>test</p>', 'test.html');
|
||||
|
||||
expect(clickSpy).toHaveBeenCalled();
|
||||
expect(appendChildSpy).toHaveBeenCalled();
|
||||
expect(removeChildSpy).toHaveBeenCalled();
|
||||
expect(createObjectURLSpy).toHaveBeenCalled();
|
||||
expect(revokeObjectURLSpy).toHaveBeenCalled();
|
||||
|
||||
createElementSpy.mockRestore();
|
||||
appendChildSpy.mockRestore();
|
||||
removeChildSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { useStorageState } from '@/utils/useStorageState';
|
||||
import { storageUtil } from '@/utils/chromeStorage';
|
||||
import type { JsonToolsPageMode } from '@/types/storage';
|
||||
|
||||
// Mock storageUtil
|
||||
vi.mock('@/utils/chromeStorage', () => ({
|
||||
storageUtil: {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(() => Promise.resolve()),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('useStorageState', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('应该使用默认值初始化', () => {
|
||||
(storageUtil.get as any).mockImplementation((_key: string, defaultValue: any) =>
|
||||
Promise.resolve(defaultValue),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useStorageState('qrCode/urlExpanded', true));
|
||||
|
||||
// 初始值应为默认值(无快照时)
|
||||
expect(result.current[0]).toBe(true);
|
||||
});
|
||||
|
||||
it('应该从 localStorage 快照同步恢复初始值', () => {
|
||||
localStorage.setItem('snapshot/qrCode/urlExpanded', JSON.stringify(false));
|
||||
|
||||
(storageUtil.get as any).mockImplementation((_key: string, defaultValue: any) =>
|
||||
Promise.resolve(defaultValue),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useStorageState('qrCode/urlExpanded', true));
|
||||
|
||||
// 初始值应从快照恢复,而非默认值
|
||||
expect(result.current[0]).toBe(false);
|
||||
});
|
||||
|
||||
it('应该从 Chrome Storage 异步加载并覆盖初始值', async () => {
|
||||
(storageUtil.get as any).mockImplementation((_key: string, _defaultValue: any) =>
|
||||
Promise.resolve(false),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useStorageState('qrCode/urlExpanded', true));
|
||||
|
||||
// 初始值为默认值(无快照)
|
||||
expect(result.current[0]).toBe(true);
|
||||
|
||||
await waitFor(() => {
|
||||
// 异步加载后应覆盖为存储值
|
||||
expect(result.current[0]).toBe(false);
|
||||
expect(result.current[2]).toBe(true); // isInitialized
|
||||
});
|
||||
});
|
||||
|
||||
it('状态变化时应该保存到 Chrome Storage 和 localStorage 快照', async () => {
|
||||
(storageUtil.get as any).mockImplementation((_key: string, defaultValue: any) =>
|
||||
Promise.resolve(defaultValue),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useStorageState('qrCode/urlExpanded', true));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[2]).toBe(true); // isInitialized
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
result.current[1](false);
|
||||
});
|
||||
|
||||
expect(result.current[0]).toBe(false);
|
||||
expect(storageUtil.set).toHaveBeenCalledWith('qrCode/urlExpanded', false);
|
||||
|
||||
// 应同时写入 localStorage 快照
|
||||
expect(localStorage.getItem('snapshot/qrCode/urlExpanded')).toBe(JSON.stringify(false));
|
||||
});
|
||||
|
||||
it('应该支持验证器 - 合法值通过', async () => {
|
||||
const validator = (val: unknown): val is JsonToolsPageMode =>
|
||||
typeof val === 'string' &&
|
||||
(['diff', 'format', 'yaml', 'toml', 'minify'] as string[]).includes(val);
|
||||
|
||||
(storageUtil.get as any).mockImplementation(() => Promise.resolve('yaml'));
|
||||
|
||||
const { result } = renderHook(() => useStorageState('jsonTools/pageMode', 'diff', validator));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe('yaml');
|
||||
});
|
||||
});
|
||||
|
||||
it('应该支持验证器 - 非法值回退到默认值', async () => {
|
||||
const validator = (val: unknown): val is JsonToolsPageMode =>
|
||||
typeof val === 'string' &&
|
||||
(['diff', 'format', 'yaml', 'toml', 'minify'] as string[]).includes(val);
|
||||
|
||||
(storageUtil.get as any).mockImplementation(() => Promise.resolve('invalidMode'));
|
||||
|
||||
const { result } = renderHook(() => useStorageState('jsonTools/pageMode', 'diff', validator));
|
||||
|
||||
await waitFor(() => {
|
||||
// 非法值应回退到默认值
|
||||
expect(result.current[0]).toBe('diff');
|
||||
});
|
||||
});
|
||||
|
||||
it('快照中的非法值应被验证器拒绝,回退到默认值', () => {
|
||||
const validator = (val: unknown): val is JsonToolsPageMode =>
|
||||
typeof val === 'string' &&
|
||||
(['diff', 'format', 'yaml', 'toml', 'minify'] as string[]).includes(val);
|
||||
|
||||
// 在 localStorage 中存入一个非法值
|
||||
localStorage.setItem('snapshot/jsonTools/pageMode', JSON.stringify('invalidMode'));
|
||||
|
||||
(storageUtil.get as any).mockImplementation((_key: string, defaultValue: any) =>
|
||||
Promise.resolve(defaultValue),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useStorageState('jsonTools/pageMode', 'diff', validator));
|
||||
|
||||
// 快照中的非法值应被拒绝,使用默认值
|
||||
expect(result.current[0]).toBe('diff');
|
||||
});
|
||||
|
||||
it('快照中的合法值应被验证器接受', () => {
|
||||
const validator = (val: unknown): val is JsonToolsPageMode =>
|
||||
typeof val === 'string' &&
|
||||
(['diff', 'format', 'yaml', 'toml', 'minify'] as string[]).includes(val);
|
||||
|
||||
// 在 localStorage 中存入一个合法值
|
||||
localStorage.setItem('snapshot/jsonTools/pageMode', JSON.stringify('minify'));
|
||||
|
||||
(storageUtil.get as any).mockImplementation((_key: string, defaultValue: any) =>
|
||||
Promise.resolve(defaultValue),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useStorageState('jsonTools/pageMode', 'diff', validator));
|
||||
|
||||
// 快照中的合法值应被接受
|
||||
expect(result.current[0]).toBe('minify');
|
||||
});
|
||||
|
||||
it('快照中损坏的 JSON 应被忽略,回退到默认值', () => {
|
||||
localStorage.setItem('snapshot/qrCode/urlExpanded', 'not-valid-json{');
|
||||
|
||||
(storageUtil.get as any).mockImplementation((_key: string, defaultValue: any) =>
|
||||
Promise.resolve(defaultValue),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useStorageState('qrCode/urlExpanded', true));
|
||||
|
||||
// 损坏的快照应被忽略
|
||||
expect(result.current[0]).toBe(true);
|
||||
});
|
||||
|
||||
it('isInitialized 在异步加载完成前应为 false', () => {
|
||||
// 让 Chrome Storage 永远不 resolve
|
||||
(storageUtil.get as any).mockImplementation(() => new Promise(() => {}));
|
||||
|
||||
const { result } = renderHook(() => useStorageState('qrCode/urlExpanded', true));
|
||||
|
||||
expect(result.current[2]).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user