mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 03:31:24 +08:00
feat: enhance testing framework for CacheService and PreferenceService
- Updated README.md to reflect the expanded testing framework for CacheService, including detailed test modules and scenarios. - Added CacheService tests for direct API, hooks, advanced features, and stress testing. - Refactored TestApp to incorporate CacheService tests alongside existing PreferenceService tests, improving organization and accessibility. - Adjusted component styling to support dark mode and ensure consistent UI across test components.
This commit is contained in:
parent
04ef5edea2
commit
6079961f44
@ -182,6 +182,7 @@ if (!app.requestSingleInstanceLock()) {
|
||||
// const [x, y] = testWindow1.getPosition()
|
||||
// testWindow2.setPosition(x + 50, y + 50)
|
||||
// })
|
||||
|
||||
/************FOR TESTING ONLY END****************/
|
||||
|
||||
// Set app user model id for windows
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# PreferenceService 测试窗口
|
||||
# 数据重构项目测试窗口
|
||||
|
||||
专用于测试 PreferenceService 和 usePreference hooks 功能的独立测试窗口系统。
|
||||
专用于测试数据重构项目各项功能的独立测试窗口系统,包括 PreferenceService、CacheService、DataApiService 和相关 React hooks。
|
||||
|
||||
## 🎯 当前实现
|
||||
|
||||
@ -18,6 +18,8 @@
|
||||
|
||||
## 测试组件
|
||||
|
||||
## PreferenceService 测试模块
|
||||
|
||||
### 1. PreferenceService 基础测试
|
||||
|
||||
- 直接测试服务层API:get, set, getCachedValue, isCached, preload, getMultiple
|
||||
@ -53,6 +55,77 @@
|
||||
- 性能测试
|
||||
- 多个hook实例同步测试
|
||||
|
||||
## CacheService 测试模块
|
||||
|
||||
### 1. CacheService 直接API测试
|
||||
|
||||
- **三层缓存架构测试**:Memory cache、Shared cache、Persist cache
|
||||
- **基础操作**: get, set, has, delete 方法的完整测试
|
||||
- **TTL支持**: 可配置的过期时间测试(2s、5s、10s)
|
||||
- **跨窗口同步**: Shared cache 和 Persist cache 的实时同步验证
|
||||
- **数据类型支持**: 字符串、数字、对象、数组等多种数据类型
|
||||
- **性能优化**: 显示操作计数和自动刷新机制
|
||||
|
||||
### 2. Cache Hooks 基础测试
|
||||
|
||||
- **useCache Hook**: 测试内存缓存的React集成
|
||||
- 默认值自动设置
|
||||
- 实时值更新和类型安全
|
||||
- Hook生命周期管理
|
||||
- **useSharedCache Hook**: 测试跨窗口缓存同步
|
||||
- 跨窗口实时同步验证
|
||||
- 广播机制测试
|
||||
- 并发更新处理
|
||||
- **usePersistCache Hook**: 测试持久化缓存
|
||||
- 类型安全的预定义Schema
|
||||
- localStorage持久化
|
||||
- 默认值回退机制
|
||||
- **数据类型测试**:
|
||||
- 数字类型滑块控制
|
||||
- 复杂对象结构更新
|
||||
- 实时渲染统计
|
||||
|
||||
### 3. Cache 高级功能测试
|
||||
|
||||
- **TTL过期机制**:
|
||||
- 实时倒计时进度条
|
||||
- 自动过期验证
|
||||
- 懒加载清理机制
|
||||
- **Hook引用保护**:
|
||||
- 活跃Hook的key删除保护
|
||||
- 引用计数验证
|
||||
- 错误处理测试
|
||||
- **深度相等性优化**:
|
||||
- 相同引用跳过测试
|
||||
- 相同内容深度比较
|
||||
- 性能优化验证
|
||||
- **性能测试**:
|
||||
- 快速更新测试(100次/秒)
|
||||
- 订阅触发统计
|
||||
- 渲染次数监控
|
||||
- **多Hook同步**:
|
||||
- 同一key的多个hook实例
|
||||
- 跨缓存类型同步测试
|
||||
|
||||
### 4. Cache 压力测试
|
||||
|
||||
- **快速操作测试**:
|
||||
- 1000次操作/10秒高频测试
|
||||
- 每秒操作数统计
|
||||
- 错误率监控
|
||||
- **并发更新测试**:
|
||||
- 多个Hook同时更新
|
||||
- 跨窗口并发处理
|
||||
- 数据一致性验证
|
||||
- **大数据测试**:
|
||||
- 10KB、100KB、1MB对象存储
|
||||
- 内存使用估算
|
||||
- 存储限制警告
|
||||
- **存储限制测试**:
|
||||
- localStorage容量测试
|
||||
- 缓存大小监控
|
||||
- 性能影响评估
|
||||
|
||||
## 启动方式
|
||||
|
||||
**自动启动**:应用正常启动时会自动创建两个测试窗口,窗口会自动错位显示避免重叠
|
||||
@ -79,36 +152,75 @@ src/renderer/src/windows/dataRefactorTest/
|
||||
├── entryPoint.tsx # 窗口入口
|
||||
├── TestApp.tsx # 主应用组件
|
||||
└── components/
|
||||
# PreferenceService 测试组件
|
||||
├── PreferenceServiceTests.tsx # 服务层测试
|
||||
├── PreferenceBasicTests.tsx # 基础Hook测试
|
||||
├── PreferenceHookTests.tsx # 高级Hook测试
|
||||
└── PreferenceMultipleTests.tsx # 批量操作测试
|
||||
├── PreferenceMultipleTests.tsx # 批量操作测试
|
||||
|
||||
# CacheService 测试组件
|
||||
├── CacheServiceTests.tsx # 直接API测试
|
||||
├── CacheBasicTests.tsx # Hook基础测试
|
||||
├── CacheAdvancedTests.tsx # 高级功能测试
|
||||
├── CacheStressTests.tsx # 压力测试
|
||||
|
||||
# DataApiService 测试组件
|
||||
├── DataApiBasicTests.tsx # 基础CRUD测试
|
||||
├── DataApiAdvancedTests.tsx # 高级功能测试
|
||||
├── DataApiHookTests.tsx # React Hooks测试
|
||||
└── DataApiStressTests.tsx # 压力测试
|
||||
```
|
||||
|
||||
## 跨窗口同步测试
|
||||
|
||||
🔄 **测试场景**:
|
||||
|
||||
### PreferenceService 跨窗口同步
|
||||
1. **实时同步验证**:在窗口#1中修改某个偏好设置,立即观察窗口#2是否同步更新
|
||||
2. **并发修改测试**:在两个窗口中快速连续修改同一设置,验证数据一致性
|
||||
3. **批量操作同步**:在一个窗口中批量更新多个设置,观察另一个窗口的同步表现
|
||||
4. **Hook实例同步**:验证多个usePreference hook实例是否正确同步
|
||||
|
||||
### CacheService 跨窗口同步
|
||||
1. **Shared Cache同步**:在窗口#1中设置共享缓存,观察窗口#2的实时更新
|
||||
2. **Persist Cache同步**:修改持久化缓存,验证所有窗口的localStorage同步
|
||||
3. **TTL跨窗口验证**:在一个窗口设置TTL,观察其他窗口的过期行为
|
||||
4. **并发缓存操作**:多窗口同时操作同一缓存key,验证数据一致性
|
||||
5. **Hook引用保护**:在一个窗口尝试删除其他窗口正在使用的缓存key
|
||||
|
||||
📋 **测试步骤**:
|
||||
|
||||
### PreferenceService 测试步骤
|
||||
1. 同时打开两个测试窗口(自动启动)
|
||||
2. 选择相同的偏好设置键进行测试
|
||||
3. 在窗口#1中修改值,观察窗口#2的反应
|
||||
4. 检查"Hook 高级功能测试"中的订阅触发次数是否增加
|
||||
5. 验证缓存状态和实时数据的一致性
|
||||
|
||||
### CacheService 测试步骤
|
||||
1. 同时打开两个测试窗口(自动启动)
|
||||
2. **Memory Cache测试**:仅在当前窗口有效,其他窗口不受影响
|
||||
3. **Shared Cache测试**:在窗口#1设置共享缓存,立即检查窗口#2是否同步
|
||||
4. **Persist Cache测试**:修改持久化缓存,验证localStorage和跨窗口同步
|
||||
5. **TTL测试**:设置带过期时间的缓存,观察倒计时和跨窗口过期行为
|
||||
6. **压力测试**:运行高频操作,监控性能指标和错误率
|
||||
7. **引用保护测试**:在Hook活跃时尝试删除key,验证保护机制
|
||||
|
||||
## 注意事项
|
||||
|
||||
⚠️ **所有测试操作都会影响真实的数据库存储!**
|
||||
⚠️ **重要警告**:
|
||||
|
||||
- 测试使用真实的偏好设置系统
|
||||
- 修改的值会同步到主应用和所有测试窗口
|
||||
- 可以在主应用、测试窗口#1、测试窗口#2之间看到实时同步效果
|
||||
### PreferenceService 警告
|
||||
- **真实数据库存储**:测试使用真实的偏好设置系统
|
||||
- **跨应用同步**:修改的值会同步到主应用和所有测试窗口
|
||||
- **持久化影响**:所有更改都会持久化到SQLite数据库
|
||||
|
||||
### CacheService 警告
|
||||
- **内存占用**:压力测试可能消耗大量内存,影响浏览器性能
|
||||
- **localStorage影响**:大数据测试会占用浏览器存储空间(最大5-10MB)
|
||||
- **性能影响**:高频操作测试可能短暂影响UI响应性
|
||||
- **跨窗口影响**:Shared和Persist缓存会影响所有打开的窗口
|
||||
- **TTL清理**:过期缓存会自动清理,可能影响其他功能的测试数据
|
||||
|
||||
## 开发模式特性
|
||||
|
||||
@ -119,6 +231,7 @@ src/renderer/src/windows/dataRefactorTest/
|
||||
|
||||
## 💡 快速开始
|
||||
|
||||
### PreferenceService 快速测试
|
||||
1. **启动应用** - 自动打开2个测试窗口
|
||||
2. **选择测试** - 在"usePreference Hook 测试"中选择要测试的偏好设置键
|
||||
3. **🎛️ Slider联动测试** - 选择数值类型偏好设置,拖动Slider观察实时变化
|
||||
@ -126,11 +239,34 @@ src/renderer/src/windows/dataRefactorTest/
|
||||
5. **批量Slider测试** - 切换到"数值设置场景",同时拖动多个滑块测试批量同步
|
||||
6. **高级测试** - 使用"Hook 高级功能测试"验证订阅和缓存机制
|
||||
|
||||
### CacheService 快速测试
|
||||
1. **基础操作** - 使用"CacheService 直接API测试"进行get/set/delete操作
|
||||
2. **Hook测试** - 在"Cache Hooks 基础测试"中测试不同数据类型和默认值
|
||||
3. **TTL验证** - 设置2秒TTL缓存,观察实时倒计时和自动过期
|
||||
4. **跨窗口同步** - 设置Shared Cache,在另一窗口验证实时同步
|
||||
5. **持久化测试** - 修改Persist Cache,刷新页面验证localStorage持久化
|
||||
6. **压力测试** - 运行"快速操作测试",观察高频操作的性能表现
|
||||
7. **引用保护** - 启用Hook后尝试删除key,验证保护机制
|
||||
|
||||
## 🔧 技术实现
|
||||
|
||||
### 基础架构
|
||||
- **窗口管理**: DataRefactorMigrateService 单例管理多个测试窗口
|
||||
- **数据同步**: 基于真实的 PreferenceService 和 IPC 通信
|
||||
- **实时主题**: 使用 useSyncExternalStore 实现主题、缩放等设置的实时UI响应
|
||||
- **跨窗口识别**: 多源窗口编号支持,确保每个窗口都有唯一标识
|
||||
- **UI框架**: Ant Design + styled-components + React 18
|
||||
- **类型安全**: 完整的 TypeScript 类型检查和偏好设置键约束
|
||||
- **类型安全**: 完整的 TypeScript 类型检查和类型约束
|
||||
|
||||
### PreferenceService 技术实现
|
||||
- **数据同步**: 基于真实的 PreferenceService 和 IPC 通信
|
||||
- **实时主题**: 使用 useSyncExternalStore 实现主题、缩放等设置的实时UI响应
|
||||
- **类型约束**: 偏好设置键的完整TypeScript类型检查
|
||||
|
||||
### CacheService 技术实现
|
||||
- **三层缓存**: Memory (Map) + Shared (Map + IPC) + Persist (Map + localStorage)
|
||||
- **React集成**: useSyncExternalStore 实现外部状态订阅
|
||||
- **性能优化**: Object.is() 浅比较 + 深度相等性检查,跳过无效更新
|
||||
- **TTL管理**: 懒加载过期检查,基于时间戳的精确控制
|
||||
- **IPC同步**: 跨进程消息广播,支持批量操作和增量更新
|
||||
- **引用跟踪**: Set-based Hook引用计数,防止意外删除
|
||||
- **错误处理**: 完善的try-catch机制和用户友好的错误提示
|
||||
- **内存管理**: 自动清理、定时器管理和资源释放
|
||||
|
||||
@ -2,11 +2,15 @@ import { AppLogo } from '@renderer/config/env'
|
||||
import { usePreference } from '@renderer/data/hooks/usePreference'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
|
||||
import { Button, Card, Col, Divider, Layout, Row, Space, Typography } from 'antd'
|
||||
import { Button, Card, Col, Divider, Layout, Row, Space, Typography, Tabs } from 'antd'
|
||||
import { Activity, AlertTriangle, Database, FlaskConical, Settings, TestTube, TrendingUp, Zap } from 'lucide-react'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import CacheAdvancedTests from './components/CacheAdvancedTests'
|
||||
import CacheBasicTests from './components/CacheBasicTests'
|
||||
import CacheServiceTests from './components/CacheServiceTests'
|
||||
import CacheStressTests from './components/CacheStressTests'
|
||||
import DataApiAdvancedTests from './components/DataApiAdvancedTests'
|
||||
import DataApiBasicTests from './components/DataApiBasicTests'
|
||||
import DataApiHookTests from './components/DataApiHookTests'
|
||||
@ -100,149 +104,256 @@ const TestApp: React.FC = () => {
|
||||
</Title>
|
||||
</Space>
|
||||
<Text style={{ color: isDarkTheme ? '#d9d9d9' : 'rgba(0, 0, 0, 0.45)' }}>
|
||||
此测试窗口用于验证数据重构项目的各项功能,包括 PreferenceService、DataApiService 和相关 React hooks
|
||||
此测试窗口用于验证数据重构项目的各项功能,包括 PreferenceService、CacheService、DataApiService 和相关 React hooks
|
||||
的完整测试套件。
|
||||
</Text>
|
||||
<Text style={{ color: isDarkTheme ? '#d9d9d9' : 'rgba(0, 0, 0, 0.45)' }}>
|
||||
PreferenceService 测试使用真实的偏好设置系统,DataApiService 测试使用专用的测试路由和假数据。
|
||||
PreferenceService 测试使用真实的偏好设置系统,CacheService 测试使用三层缓存架构,DataApiService 测试使用专用的测试路由和假数据。
|
||||
</Text>
|
||||
<Text style={{ color: 'var(--color-primary)', fontWeight: 'bold' }}>
|
||||
📋 跨窗口测试指南:在一个窗口中修改偏好设置,观察其他窗口是否实时同步更新。
|
||||
</Text>
|
||||
<Text style={{ color: 'var(--color-secondary)', fontWeight: 'bold' }}>
|
||||
🗄️ 缓存系统测试:三层缓存架构(Memory/Shared/Persist),支持跨窗口同步、TTL过期、性能优化。
|
||||
</Text>
|
||||
<Text style={{ color: 'var(--color-tertiary)', fontWeight: 'bold' }}>
|
||||
🚀 数据API测试:包含基础CRUD、高级功能、React hooks和压力测试,全面验证数据请求架构。
|
||||
</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* PreferenceService Basic Tests */}
|
||||
{/* Main Content Tabs */}
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Database size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>PreferenceService 基础测试</span>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<PreferenceServiceTests />
|
||||
</Card>
|
||||
</Col>
|
||||
<StyledTabs
|
||||
defaultActiveKey="preference"
|
||||
size="large"
|
||||
$isDark={isDarkTheme}
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderRadius: 8,
|
||||
padding: '0 16px',
|
||||
border: `1px solid ${borderColor}`
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
key: 'preference',
|
||||
label: (
|
||||
<Space>
|
||||
<Settings size={16} />
|
||||
<span>PreferenceService 测试</span>
|
||||
</Space>
|
||||
),
|
||||
children: (
|
||||
<Row gutter={[24, 24]}>
|
||||
{/* PreferenceService Basic Tests */}
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Database size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>PreferenceService 基础测试</span>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<PreferenceServiceTests />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* Basic Hook Tests */}
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Settings size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>usePreference Hook 测试</span>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<PreferenceBasicTests />
|
||||
</Card>
|
||||
</Col>
|
||||
{/* Basic Hook Tests */}
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Settings size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>usePreference Hook 测试</span>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<PreferenceBasicTests />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* Hook Tests */}
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Settings size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>Hook 高级功能测试</span>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<PreferenceHookTests />
|
||||
</Card>
|
||||
</Col>
|
||||
{/* Hook Tests */}
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Settings size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>Hook 高级功能测试</span>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<PreferenceHookTests />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* Multiple Preferences Tests */}
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Database size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>usePreferences 批量操作测试</span>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<PreferenceMultipleTests />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
{/* Multiple Preferences Tests */}
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Database size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>usePreferences 批量操作测试</span>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<PreferenceMultipleTests />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'cache',
|
||||
label: (
|
||||
<Space>
|
||||
<Database size={16} />
|
||||
<span>CacheService 测试</span>
|
||||
</Space>
|
||||
),
|
||||
children: (
|
||||
<Row gutter={[24, 24]}>
|
||||
{/* Cache Service Tests */}
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Database size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>CacheService 直接API测试</span>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<CacheServiceTests />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Divider orientation="left" style={{ color: textColor }}>
|
||||
<Space>
|
||||
<Zap size={20} color="var(--color-primary)" />
|
||||
<Text style={{ color: textColor, fontSize: 16, fontWeight: 600 }}>DataApiService 功能测试</Text>
|
||||
</Space>
|
||||
</Divider>
|
||||
{/* Cache Basic Tests */}
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Settings size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>Cache Hooks 基础测试</span>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<CacheBasicTests />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Row gutter={[24, 24]}>
|
||||
{/* DataApi Basic Tests */}
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Database size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>DataApi 基础功能测试 (CRUD操作)</span>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<DataApiBasicTests />
|
||||
</Card>
|
||||
</Col>
|
||||
{/* Cache Advanced Tests */}
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Activity size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>Cache 高级功能测试</span>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<CacheAdvancedTests />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* DataApi Advanced Tests */}
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Activity size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>DataApi 高级功能测试 (取消、重试、批量)</span>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<DataApiAdvancedTests />
|
||||
</Card>
|
||||
</Col>
|
||||
{/* Cache Stress Tests */}
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<AlertTriangle size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>Cache 压力测试</span>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<CacheStressTests />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'dataapi',
|
||||
label: (
|
||||
<Space>
|
||||
<Zap size={16} />
|
||||
<span>DataApiService 测试</span>
|
||||
</Space>
|
||||
),
|
||||
children: (
|
||||
<Row gutter={[24, 24]}>
|
||||
{/* DataApi Basic Tests */}
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Database size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>DataApi 基础功能测试 (CRUD操作)</span>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<DataApiBasicTests />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* DataApi Hook Tests */}
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<TrendingUp size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>DataApi React Hooks 测试</span>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<DataApiHookTests />
|
||||
</Card>
|
||||
</Col>
|
||||
{/* DataApi Advanced Tests */}
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Activity size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>DataApi 高级功能测试 (取消、重试、批量)</span>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<DataApiAdvancedTests />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* DataApi Stress Tests */}
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<AlertTriangle size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>DataApi 压力测试 (性能与错误处理)</span>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<DataApiStressTests />
|
||||
</Card>
|
||||
{/* DataApi Hook Tests */}
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<TrendingUp size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>DataApi React Hooks 测试</span>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<DataApiHookTests />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* DataApi Stress Tests */}
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<AlertTriangle size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>DataApi 压力测试 (性能与错误处理)</span>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<DataApiStressTests />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@ -290,4 +401,42 @@ const Container = styled.div`
|
||||
margin: 0 auto;
|
||||
`
|
||||
|
||||
const StyledTabs = styled(Tabs)<{ $isDark: boolean }>`
|
||||
.ant-tabs-nav {
|
||||
background: ${props => props.$isDark ? '#262626' : '#fafafa'};
|
||||
border-radius: 6px 6px 0 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ant-tabs-tab {
|
||||
color: ${props => props.$isDark ? '#d9d9d9' : '#666'} !important;
|
||||
|
||||
&:hover {
|
||||
color: ${props => props.$isDark ? '#fff' : '#000'} !important;
|
||||
}
|
||||
|
||||
&.ant-tabs-tab-active {
|
||||
color: ${props => props.$isDark ? '#1890ff' : '#1890ff'} !important;
|
||||
|
||||
.ant-tabs-tab-btn {
|
||||
color: ${props => props.$isDark ? '#1890ff' : '#1890ff'} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-ink-bar {
|
||||
background: ${props => props.$isDark ? '#1890ff' : '#1890ff'};
|
||||
}
|
||||
|
||||
.ant-tabs-content {
|
||||
background: ${props => props.$isDark ? '#1f1f1f' : '#fff'};
|
||||
border-radius: 0 0 6px 6px;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.ant-tabs-tabpane {
|
||||
color: ${props => props.$isDark ? '#fff' : '#000'};
|
||||
}
|
||||
`
|
||||
|
||||
export default TestApp
|
||||
|
||||
@ -0,0 +1,472 @@
|
||||
import { cacheService } from '@renderer/data/CacheService'
|
||||
import { useCache, useSharedCache } from '@renderer/data/hooks/useCache'
|
||||
import { usePreference } from '@renderer/data/hooks/usePreference'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
|
||||
import { Button, Input, message, Space, Typography, Card, Row, Col, Divider, Progress, Badge, Tag } from 'antd'
|
||||
import { Clock, Shield, Zap, Activity, AlertTriangle, CheckCircle, XCircle, Timer } from 'lucide-react'
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const { Text, Title } = Typography
|
||||
|
||||
const logger = loggerService.withContext('CacheAdvancedTests')
|
||||
|
||||
/**
|
||||
* Advanced cache testing component
|
||||
* Tests TTL expiration, hook reference tracking, deep equality, performance
|
||||
*/
|
||||
const CacheAdvancedTests: React.FC = () => {
|
||||
const [currentTheme] = usePreference('ui.theme_mode')
|
||||
const isDarkTheme = currentTheme === ThemeMode.dark
|
||||
|
||||
// TTL Testing
|
||||
const [ttlKey] = useState('test-ttl-cache')
|
||||
const [ttlValue, setTtlValue] = useCache(ttlKey)
|
||||
const [ttlExpireTime, setTtlExpireTime] = useState<number | null>(null)
|
||||
const [ttlProgress, setTtlProgress] = useState(0)
|
||||
|
||||
// Hook Reference Tracking
|
||||
const [protectedKey] = useState('test-protected-cache')
|
||||
const [protectedValue, setProtectedValue] = useCache(protectedKey, 'protected-value')
|
||||
const [deleteAttemptResult, setDeleteAttemptResult] = useState<string>('')
|
||||
|
||||
// Deep Equality Testing
|
||||
const [deepEqualKey] = useState('test-deep-equal')
|
||||
const [objectValue, setObjectValue] = useCache(deepEqualKey, { nested: { count: 0 }, tags: ['initial'] })
|
||||
const [updateSkipCount, setUpdateSkipCount] = useState(0)
|
||||
|
||||
// Performance Testing
|
||||
const [perfKey] = useState('test-performance')
|
||||
const [perfValue, setPerfValue] = useCache(perfKey, 0)
|
||||
const [rapidUpdateCount, setRapidUpdateCount] = useState(0)
|
||||
const [subscriptionTriggers, setSubscriptionTriggers] = useState(0)
|
||||
const renderCountRef = useRef(0)
|
||||
const [displayRenderCount, setDisplayRenderCount] = useState(0)
|
||||
|
||||
// Multi-hook testing
|
||||
const [multiKey] = useState('test-multi-hook')
|
||||
const [value1] = useCache(multiKey, 'hook-1-default')
|
||||
const [value2] = useCache(multiKey, 'hook-2-default')
|
||||
const [value3] = useSharedCache(multiKey, 'hook-3-shared')
|
||||
|
||||
const intervalRef = useRef<NodeJS.Timeout>()
|
||||
const performanceTestRef = useRef<NodeJS.Timeout>()
|
||||
|
||||
// Update render count without causing re-renders
|
||||
renderCountRef.current += 1
|
||||
|
||||
// Track subscription changes
|
||||
useEffect(() => {
|
||||
const unsubscribe = cacheService.subscribe(perfKey, () => {
|
||||
setSubscriptionTriggers(prev => prev + 1)
|
||||
})
|
||||
return unsubscribe
|
||||
}, [perfKey])
|
||||
|
||||
// TTL Testing Functions
|
||||
const startTTLTest = useCallback((ttlMs: number) => {
|
||||
const testValue = { message: 'TTL Test', timestamp: Date.now() }
|
||||
cacheService.set(ttlKey, testValue, ttlMs)
|
||||
setTtlValue(testValue)
|
||||
|
||||
const expireAt = Date.now() + ttlMs
|
||||
setTtlExpireTime(expireAt)
|
||||
|
||||
// Clear previous interval
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
}
|
||||
|
||||
// Update progress every 100ms
|
||||
intervalRef.current = setInterval(() => {
|
||||
const now = Date.now()
|
||||
const remaining = Math.max(0, expireAt - now)
|
||||
const progress = Math.max(0, 100 - (remaining / ttlMs) * 100)
|
||||
|
||||
setTtlProgress(progress)
|
||||
|
||||
if (remaining <= 0) {
|
||||
clearInterval(intervalRef.current!)
|
||||
setTtlExpireTime(null)
|
||||
message.info('TTL expired, checking value...')
|
||||
|
||||
// Check if value is actually expired
|
||||
setTimeout(() => {
|
||||
const currentValue = cacheService.get(ttlKey)
|
||||
if (currentValue === undefined) {
|
||||
message.success('TTL expiration working correctly!')
|
||||
} else {
|
||||
message.warning('TTL expiration may have failed')
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
}, 100)
|
||||
|
||||
message.info(`TTL test started: ${ttlMs}ms`)
|
||||
logger.info('TTL test started', { key: ttlKey, ttl: ttlMs, expireAt })
|
||||
}, [ttlKey, setTtlValue])
|
||||
|
||||
// Hook Reference Tracking Test
|
||||
const testDeleteProtection = () => {
|
||||
try {
|
||||
const deleted = cacheService.delete(protectedKey)
|
||||
setDeleteAttemptResult(deleted ? 'Deleted (unexpected!)' : 'Protected (expected)')
|
||||
logger.info('Delete protection test', { key: protectedKey, deleted })
|
||||
} catch (error) {
|
||||
setDeleteAttemptResult(`Error: ${(error as Error).message}`)
|
||||
logger.error('Delete protection test error', error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
// Deep Equality Testing
|
||||
const testDeepEquality = (operation: string) => {
|
||||
const currentCount = updateSkipCount
|
||||
|
||||
switch (operation) {
|
||||
case 'same-reference':
|
||||
// Set same reference - should skip
|
||||
setObjectValue(objectValue)
|
||||
break
|
||||
|
||||
case 'same-content':
|
||||
// Set same content but different reference - should skip with deep comparison
|
||||
setObjectValue({ nested: { count: objectValue?.nested?.count || 0 }, tags: [...(objectValue?.tags || [])] })
|
||||
break
|
||||
|
||||
case 'different-content':
|
||||
// Set different content - should update
|
||||
setObjectValue({
|
||||
nested: { count: (objectValue?.nested?.count || 0) + 1 },
|
||||
tags: [...(objectValue?.tags || []), `update-${Date.now()}`]
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
// Check if update count changed
|
||||
setTimeout(() => {
|
||||
if (currentCount === updateSkipCount) {
|
||||
message.success('Update skipped due to equality check')
|
||||
} else {
|
||||
message.info('Update applied due to content change')
|
||||
}
|
||||
}, 100)
|
||||
|
||||
logger.info('Deep equality test', { operation, currentCount, objectValue })
|
||||
}
|
||||
|
||||
// Performance Testing
|
||||
const startRapidUpdates = () => {
|
||||
let count = 0
|
||||
const startTime = Date.now()
|
||||
|
||||
performanceTestRef.current = setInterval(() => {
|
||||
count++
|
||||
setPerfValue(count)
|
||||
setRapidUpdateCount(count)
|
||||
|
||||
if (count >= 100) {
|
||||
clearInterval(performanceTestRef.current!)
|
||||
const duration = Date.now() - startTime
|
||||
message.success(`Rapid updates test completed: ${count} updates in ${duration}ms`)
|
||||
logger.info('Rapid updates test completed', { count, duration })
|
||||
}
|
||||
}, 10) // Update every 10ms
|
||||
}
|
||||
|
||||
const stopRapidUpdates = () => {
|
||||
if (performanceTestRef.current) {
|
||||
clearInterval(performanceTestRef.current)
|
||||
message.info('Rapid updates test stopped')
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
}
|
||||
if (performanceTestRef.current) {
|
||||
clearInterval(performanceTestRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<TestContainer $isDark={isDarkTheme}>
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Space>
|
||||
<Text type="secondary">Advanced Features • Renders: {displayRenderCount || renderCountRef.current} • Subscriptions: {subscriptionTriggers}</Text>
|
||||
<Button size="small" onClick={() => {
|
||||
renderCountRef.current = 0
|
||||
setDisplayRenderCount(0)
|
||||
setSubscriptionTriggers(0)
|
||||
}}>Reset Stats</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
{/* TTL Testing */}
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Timer size={16} />
|
||||
<Text>TTL Expiration Testing</Text>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>Key: <code>{ttlKey}</code></Text>
|
||||
|
||||
<Space wrap>
|
||||
<Button size="small" onClick={() => startTTLTest(2000)} icon={<Clock size={12} />}>
|
||||
2s TTL
|
||||
</Button>
|
||||
<Button size="small" onClick={() => startTTLTest(5000)} icon={<Clock size={12} />}>
|
||||
5s TTL
|
||||
</Button>
|
||||
<Button size="small" onClick={() => startTTLTest(10000)} icon={<Clock size={12} />}>
|
||||
10s TTL
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
{ttlExpireTime && (
|
||||
<div>
|
||||
<Text>Expiration Progress:</Text>
|
||||
<Progress
|
||||
percent={Math.round(ttlProgress)}
|
||||
status={ttlProgress >= 100 ? 'success' : 'active'}
|
||||
strokeColor={isDarkTheme ? '#1890ff' : undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ResultDisplay $isDark={isDarkTheme}>
|
||||
<Text strong>Current Value:</Text>
|
||||
<pre>{ttlValue ? JSON.stringify(ttlValue, null, 2) : 'undefined'}</pre>
|
||||
</ResultDisplay>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* Hook Reference Tracking */}
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Shield size={16} />
|
||||
<Text>Hook Reference Protection</Text>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>Key: <code>{protectedKey}</code></Text>
|
||||
<Badge
|
||||
status="processing"
|
||||
text="This hook is actively using the cache key"
|
||||
/>
|
||||
|
||||
<Button
|
||||
danger
|
||||
onClick={testDeleteProtection}
|
||||
icon={<AlertTriangle size={12} />}
|
||||
>
|
||||
Attempt to Delete Key
|
||||
</Button>
|
||||
|
||||
{deleteAttemptResult && (
|
||||
<Tag color={deleteAttemptResult.includes('Protected') ? 'green' : 'red'}>
|
||||
{deleteAttemptResult}
|
||||
</Tag>
|
||||
)}
|
||||
|
||||
<ResultDisplay $isDark={isDarkTheme}>
|
||||
<Text strong>Current Value:</Text>
|
||||
<pre>{JSON.stringify(protectedValue, null, 2)}</pre>
|
||||
</ResultDisplay>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
{/* Deep Equality Testing */}
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<CheckCircle size={16} />
|
||||
<Text>Deep Equality Optimization</Text>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>Key: <code>{deepEqualKey}</code></Text>
|
||||
<Text>Skip Count: <Badge count={updateSkipCount} /></Text>
|
||||
|
||||
<Space direction="vertical">
|
||||
<Button size="small" onClick={() => testDeepEquality('same-reference')} icon={<XCircle size={12} />}>
|
||||
Set Same Reference
|
||||
</Button>
|
||||
<Button size="small" onClick={() => testDeepEquality('same-content')} icon={<CheckCircle size={12} />}>
|
||||
Set Same Content
|
||||
</Button>
|
||||
<Button size="small" onClick={() => testDeepEquality('different-content')} icon={<Zap size={12} />}>
|
||||
Set Different Content
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<ResultDisplay $isDark={isDarkTheme}>
|
||||
<Text strong>Current Object:</Text>
|
||||
<pre>{JSON.stringify(objectValue, null, 2)}</pre>
|
||||
</ResultDisplay>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* Performance Testing */}
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Activity size={16} />
|
||||
<Text>Performance Testing</Text>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>Key: <code>{perfKey}</code></Text>
|
||||
<Text>Updates: <Badge count={rapidUpdateCount} /></Text>
|
||||
|
||||
<Space>
|
||||
<Button type="primary" onClick={startRapidUpdates} icon={<Zap size={12} />}>
|
||||
Start Rapid Updates
|
||||
</Button>
|
||||
<Button onClick={stopRapidUpdates} icon={<XCircle size={12} />}>
|
||||
Stop
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<ResultDisplay $isDark={isDarkTheme}>
|
||||
<Text strong>Performance Value:</Text>
|
||||
<pre>{JSON.stringify(perfValue, null, 2)}</pre>
|
||||
</ResultDisplay>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Multi-Hook Synchronization */}
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Activity size={16} />
|
||||
<Text>Multi-Hook Synchronization Test</Text>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>Testing multiple hooks using the same key: <code>{multiKey}</code></Text>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Card size="small" title="useCache Hook #1">
|
||||
<ResultDisplay $isDark={isDarkTheme}>
|
||||
<pre>{JSON.stringify(value1, null, 2)}</pre>
|
||||
</ResultDisplay>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card size="small" title="useCache Hook #2">
|
||||
<ResultDisplay $isDark={isDarkTheme}>
|
||||
<pre>{JSON.stringify(value2, null, 2)}</pre>
|
||||
</ResultDisplay>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card size="small" title="useSharedCache Hook #3">
|
||||
<ResultDisplay $isDark={isDarkTheme}>
|
||||
<pre>{JSON.stringify(value3, null, 2)}</pre>
|
||||
</ResultDisplay>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Space>
|
||||
<Button
|
||||
onClick={() => cacheService.set(multiKey, `Updated at ${new Date().toLocaleTimeString()}`)}
|
||||
>
|
||||
Update via CacheService
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => cacheService.setShared(multiKey, `Shared update at ${new Date().toLocaleTimeString()}`)}
|
||||
>
|
||||
Update via Shared Cache
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
💡 高级功能测试: TTL过期机制、Hook引用保护、深度相等性优化、性能测试、多Hook同步验证
|
||||
</Text>
|
||||
</div>
|
||||
</Space>
|
||||
</TestContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const TestContainer = styled.div<{ $isDark: boolean }>`
|
||||
color: ${props => props.$isDark ? '#fff' : '#000'};
|
||||
`
|
||||
|
||||
const ResultDisplay = styled.div<{ $isDark: boolean }>`
|
||||
background: ${props => props.$isDark ? '#0d1117' : '#f6f8fa'};
|
||||
border: 1px solid ${props => props.$isDark ? '#30363d' : '#d0d7de'};
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
font-size: 11px;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
color: ${props => props.$isDark ? '#e6edf3' : '#1f2328'};
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
}
|
||||
`
|
||||
|
||||
export default CacheAdvancedTests
|
||||
@ -0,0 +1,438 @@
|
||||
import { useCache, useSharedCache, usePersistCache } from '@renderer/data/hooks/useCache'
|
||||
import { usePreference } from '@renderer/data/hooks/usePreference'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import type { PersistCacheKey } from '@shared/data/cache/cacheSchemas'
|
||||
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
|
||||
import { Button, Input, message, Select, Space, Typography, Card, Row, Col, Divider, Slider } from 'antd'
|
||||
import { Zap, Database, Eye, Edit, RefreshCw, Users, HardDrive } from 'lucide-react'
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const { Text, Title } = Typography
|
||||
const { Option } = Select
|
||||
const { TextArea } = Input
|
||||
|
||||
const logger = loggerService.withContext('CacheBasicTests')
|
||||
|
||||
/**
|
||||
* Basic cache hooks testing component
|
||||
* Tests useCache, useSharedCache, and usePersistCache hooks
|
||||
*/
|
||||
const CacheBasicTests: React.FC = () => {
|
||||
const [currentTheme] = usePreference('ui.theme_mode')
|
||||
const isDarkTheme = currentTheme === ThemeMode.dark
|
||||
|
||||
// useCache testing
|
||||
const [memoryCacheKey, setMemoryCacheKey] = useState('test-hook-memory-1')
|
||||
const [memoryCacheDefault, setMemoryCacheDefault] = useState('default-memory-value')
|
||||
const [newMemoryValue, setNewMemoryValue] = useState('')
|
||||
const [memoryValue, setMemoryValue] = useCache(memoryCacheKey, memoryCacheDefault)
|
||||
|
||||
// useSharedCache testing
|
||||
const [sharedCacheKey, setSharedCacheKey] = useState('test-hook-shared-1')
|
||||
const [sharedCacheDefault, setSharedCacheDefault] = useState('default-shared-value')
|
||||
const [newSharedValue, setNewSharedValue] = useState('')
|
||||
const [sharedValue, setSharedValue] = useSharedCache(sharedCacheKey, sharedCacheDefault)
|
||||
|
||||
// usePersistCache testing
|
||||
const [persistCacheKey, setPersistCacheKey] = useState<PersistCacheKey>('example-1')
|
||||
const [newPersistValue, setNewPersistValue] = useState('')
|
||||
const [persistValue, setPersistValue] = usePersistCache(persistCacheKey)
|
||||
|
||||
// Testing different data types
|
||||
const [numberKey] = useState('test-number-cache')
|
||||
const [numberValue, setNumberValue] = useCache(numberKey, 42)
|
||||
|
||||
const [objectKey] = useState('test-object-cache')
|
||||
const [objectValue, setObjectValue] = useCache(objectKey, { name: 'test', count: 0, active: true })
|
||||
|
||||
// Stats
|
||||
const renderCountRef = useRef(0)
|
||||
const [displayRenderCount, setDisplayRenderCount] = useState(0)
|
||||
const [updateCount, setUpdateCount] = useState(0)
|
||||
|
||||
// Available persist keys
|
||||
const persistKeys: PersistCacheKey[] = ['example-1', 'example-2', 'example-3', 'example-4']
|
||||
|
||||
// Update render count without causing re-renders
|
||||
renderCountRef.current += 1
|
||||
|
||||
const parseValue = (value: string): any => {
|
||||
if (!value) return undefined
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
const formatValue = (value: any): string => {
|
||||
if (value === undefined) return 'undefined'
|
||||
if (value === null) return 'null'
|
||||
if (typeof value === 'string') return `"${value}"`
|
||||
return JSON.stringify(value, null, 2)
|
||||
}
|
||||
|
||||
// Memory cache operations
|
||||
const handleMemoryUpdate = () => {
|
||||
try {
|
||||
const parsed = parseValue(newMemoryValue)
|
||||
setMemoryValue(parsed)
|
||||
setNewMemoryValue('')
|
||||
setUpdateCount(prev => prev + 1)
|
||||
message.success(`Memory cache updated: ${memoryCacheKey}`)
|
||||
logger.info('Memory cache updated via hook', { key: memoryCacheKey, value: parsed })
|
||||
} catch (error) {
|
||||
message.error(`Memory cache update failed: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Shared cache operations
|
||||
const handleSharedUpdate = () => {
|
||||
try {
|
||||
const parsed = parseValue(newSharedValue)
|
||||
setSharedValue(parsed)
|
||||
setNewSharedValue('')
|
||||
setUpdateCount(prev => prev + 1)
|
||||
message.success(`Shared cache updated: ${sharedCacheKey} (broadcasted to other windows)`)
|
||||
logger.info('Shared cache updated via hook', { key: sharedCacheKey, value: parsed })
|
||||
} catch (error) {
|
||||
message.error(`Shared cache update failed: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Persist cache operations
|
||||
const handlePersistUpdate = () => {
|
||||
try {
|
||||
let parsed: any
|
||||
// Handle different types based on schema
|
||||
if (persistCacheKey === 'example-1') {
|
||||
parsed = newPersistValue // string
|
||||
} else if (persistCacheKey === 'example-2') {
|
||||
parsed = parseInt(newPersistValue) || 0 // number
|
||||
} else if (persistCacheKey === 'example-3') {
|
||||
parsed = newPersistValue === 'true' // boolean
|
||||
} else if (persistCacheKey === 'example-4') {
|
||||
parsed = parseValue(newPersistValue) // object
|
||||
}
|
||||
|
||||
setPersistValue(parsed as any)
|
||||
setNewPersistValue('')
|
||||
setUpdateCount(prev => prev + 1)
|
||||
message.success(`Persist cache updated: ${persistCacheKey} (saved + broadcasted)`)
|
||||
logger.info('Persist cache updated via hook', { key: persistCacheKey, value: parsed })
|
||||
} catch (error) {
|
||||
message.error(`Persist cache update failed: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Test different data types
|
||||
const handleNumberUpdate = (newValue: number) => {
|
||||
setNumberValue(newValue)
|
||||
setUpdateCount(prev => prev + 1)
|
||||
logger.info('Number cache updated', { value: newValue })
|
||||
}
|
||||
|
||||
const handleObjectUpdate = (field: string, value: any) => {
|
||||
setObjectValue(prev => ({ ...prev, [field]: value }))
|
||||
setUpdateCount(prev => prev + 1)
|
||||
logger.info('Object cache updated', { field, value })
|
||||
}
|
||||
|
||||
return (
|
||||
<TestContainer $isDark={isDarkTheme}>
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Space>
|
||||
<Text type="secondary">React Hook Tests • Renders: {displayRenderCount || renderCountRef.current} • Updates: {updateCount}</Text>
|
||||
<Button size="small" onClick={() => {
|
||||
renderCountRef.current = 0
|
||||
setDisplayRenderCount(0)
|
||||
setUpdateCount(0)
|
||||
}}>Reset Stats</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
{/* useCache Testing */}
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Zap size={16} />
|
||||
<Text>useCache Hook</Text>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Input
|
||||
placeholder="Cache Key"
|
||||
value={memoryCacheKey}
|
||||
onChange={(e) => setMemoryCacheKey(e.target.value)}
|
||||
prefix={<Database size={14} />}
|
||||
/>
|
||||
|
||||
<Input
|
||||
placeholder="Default Value"
|
||||
value={memoryCacheDefault}
|
||||
onChange={(e) => setMemoryCacheDefault(e.target.value)}
|
||||
prefix={<Eye size={14} />}
|
||||
/>
|
||||
|
||||
<Input
|
||||
placeholder="New Value"
|
||||
value={newMemoryValue}
|
||||
onChange={(e) => setNewMemoryValue(e.target.value)}
|
||||
onPressEnter={handleMemoryUpdate}
|
||||
prefix={<Edit size={14} />}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleMemoryUpdate}
|
||||
disabled={!newMemoryValue}
|
||||
block
|
||||
>
|
||||
Update Memory Cache
|
||||
</Button>
|
||||
|
||||
<ResultDisplay $isDark={isDarkTheme}>
|
||||
<Text strong>Current Value:</Text>
|
||||
<pre>{formatValue(memoryValue)}</pre>
|
||||
</ResultDisplay>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* useSharedCache Testing */}
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Users size={16} />
|
||||
<Text>useSharedCache Hook</Text>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Input
|
||||
placeholder="Cache Key"
|
||||
value={sharedCacheKey}
|
||||
onChange={(e) => setSharedCacheKey(e.target.value)}
|
||||
prefix={<Database size={14} />}
|
||||
/>
|
||||
|
||||
<Input
|
||||
placeholder="Default Value"
|
||||
value={sharedCacheDefault}
|
||||
onChange={(e) => setSharedCacheDefault(e.target.value)}
|
||||
prefix={<Eye size={14} />}
|
||||
/>
|
||||
|
||||
<Input
|
||||
placeholder="New Value"
|
||||
value={newSharedValue}
|
||||
onChange={(e) => setNewSharedValue(e.target.value)}
|
||||
onPressEnter={handleSharedUpdate}
|
||||
prefix={<Edit size={14} />}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSharedUpdate}
|
||||
disabled={!newSharedValue}
|
||||
block
|
||||
>
|
||||
Update Shared Cache
|
||||
</Button>
|
||||
|
||||
<ResultDisplay $isDark={isDarkTheme}>
|
||||
<Text strong>Current Value:</Text>
|
||||
<pre>{formatValue(sharedValue)}</pre>
|
||||
</ResultDisplay>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* usePersistCache Testing */}
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<HardDrive size={16} />
|
||||
<Text>usePersistCache Hook</Text>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Select
|
||||
value={persistCacheKey}
|
||||
onChange={setPersistCacheKey}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="Select persist key"
|
||||
>
|
||||
{persistKeys.map(key => (
|
||||
<Option key={key} value={key}>{key}</Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Input
|
||||
placeholder="New Value"
|
||||
value={newPersistValue}
|
||||
onChange={(e) => setNewPersistValue(e.target.value)}
|
||||
onPressEnter={handlePersistUpdate}
|
||||
prefix={<Edit size={14} />}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handlePersistUpdate}
|
||||
disabled={!newPersistValue}
|
||||
block
|
||||
>
|
||||
Update Persist Cache
|
||||
</Button>
|
||||
|
||||
<ResultDisplay $isDark={isDarkTheme}>
|
||||
<Text strong>Current Value:</Text>
|
||||
<pre>{formatValue(persistValue)}</pre>
|
||||
</ResultDisplay>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Data Type Testing */}
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<RefreshCw size={16} />
|
||||
<Text>Number Type Testing</Text>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>Key: <code>{numberKey}</code></Text>
|
||||
<Text>Current Value: <strong>{numberValue}</strong></Text>
|
||||
|
||||
<Slider
|
||||
min={0}
|
||||
max={100}
|
||||
value={typeof numberValue === 'number' ? numberValue : 42}
|
||||
onChange={handleNumberUpdate}
|
||||
/>
|
||||
|
||||
<Space>
|
||||
<Button size="small" onClick={() => handleNumberUpdate(0)}>Reset to 0</Button>
|
||||
<Button size="small" onClick={() => handleNumberUpdate(Math.floor(Math.random() * 100))}>
|
||||
Random
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Database size={16} />
|
||||
<Text>Object Type Testing</Text>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>Key: <code>{objectKey}</code></Text>
|
||||
|
||||
<Space>
|
||||
<Input
|
||||
placeholder="Name"
|
||||
value={objectValue?.name || ''}
|
||||
onChange={(e) => handleObjectUpdate('name', e.target.value)}
|
||||
style={{ width: 120 }}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Count"
|
||||
type="number"
|
||||
value={objectValue?.count || 0}
|
||||
onChange={(e) => handleObjectUpdate('count', parseInt(e.target.value) || 0)}
|
||||
style={{ width: 80 }}
|
||||
/>
|
||||
<Button
|
||||
type={objectValue?.active ? 'primary' : 'default'}
|
||||
onClick={() => handleObjectUpdate('active', !objectValue?.active)}
|
||||
>
|
||||
{objectValue?.active ? 'Active' : 'Inactive'}
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<ResultDisplay $isDark={isDarkTheme}>
|
||||
<pre>{formatValue(objectValue)}</pre>
|
||||
</ResultDisplay>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
💡 提示: useCache 仅在当前窗口有效 • useSharedCache 跨窗口实时同步 • usePersistCache 类型安全的持久化存储
|
||||
</Text>
|
||||
</div>
|
||||
</Space>
|
||||
</TestContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const TestContainer = styled.div<{ $isDark: boolean }>`
|
||||
color: ${props => props.$isDark ? '#fff' : '#000'};
|
||||
`
|
||||
|
||||
const ResultDisplay = styled.div<{ $isDark: boolean }>`
|
||||
background: ${props => props.$isDark ? '#0d1117' : '#f6f8fa'};
|
||||
border: 1px solid ${props => props.$isDark ? '#30363d' : '#d0d7de'};
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
font-size: 11px;
|
||||
max-height: 100px;
|
||||
overflow-y: auto;
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
color: ${props => props.$isDark ? '#e6edf3' : '#1f2328'};
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
}
|
||||
`
|
||||
|
||||
export default CacheBasicTests
|
||||
@ -0,0 +1,443 @@
|
||||
import { cacheService } from '@renderer/data/CacheService'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import type { PersistCacheKey, PersistCacheSchema } from '@shared/data/cache/cacheSchemas'
|
||||
import { usePreference } from '@renderer/data/hooks/usePreference'
|
||||
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
|
||||
import { Button, Input, message, Select, Space, Typography, Card, Row, Col, Divider } from 'antd'
|
||||
import { Database, Clock, Trash2, Eye, Edit, Zap } from 'lucide-react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const { Text, Title } = Typography
|
||||
const { Option } = Select
|
||||
const { TextArea } = Input
|
||||
|
||||
const logger = loggerService.withContext('CacheServiceTests')
|
||||
|
||||
/**
|
||||
* Direct CacheService API testing component
|
||||
* Tests memory, shared, and persist cache operations
|
||||
*/
|
||||
const CacheServiceTests: React.FC = () => {
|
||||
const [currentTheme] = usePreference('ui.theme_mode')
|
||||
const isDarkTheme = currentTheme === ThemeMode.dark
|
||||
|
||||
// State for test operations
|
||||
const [memoryKey, setMemoryKey] = useState('test-memory-1')
|
||||
const [memoryValue, setMemoryValue] = useState('{"type": "memory", "data": "test"}')
|
||||
const [memoryTTL, setMemoryTTL] = useState<string>('5000')
|
||||
|
||||
const [sharedKey, setSharedKey] = useState('test-shared-1')
|
||||
const [sharedValue, setSharedValue] = useState('{"type": "shared", "data": "cross-window"}')
|
||||
const [sharedTTL, setSharedTTL] = useState<string>('10000')
|
||||
|
||||
const [persistKey, setPersistKey] = useState<PersistCacheKey>('example-1')
|
||||
const [persistValue, setPersistValue] = useState('updated-example-value')
|
||||
|
||||
// Display states
|
||||
const [memoryResult, setMemoryResult] = useState<any>(null)
|
||||
const [sharedResult, setSharedResult] = useState<any>(null)
|
||||
const [persistResult, setPersistResult] = useState<any>(null)
|
||||
|
||||
const [updateCount, setUpdateCount] = useState(0)
|
||||
|
||||
// Available persist keys from schema
|
||||
const persistKeys: PersistCacheKey[] = ['example-1', 'example-2', 'example-3', 'example-4']
|
||||
|
||||
const parseValue = (value: string): any => {
|
||||
if (!value) return undefined
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch {
|
||||
return value // Return as string if not valid JSON
|
||||
}
|
||||
}
|
||||
|
||||
const formatValue = (value: any): string => {
|
||||
if (value === undefined) return 'undefined'
|
||||
if (value === null) return 'null'
|
||||
if (typeof value === 'string') return `"${value}"`
|
||||
return JSON.stringify(value, null, 2)
|
||||
}
|
||||
|
||||
// Memory Cache Operations
|
||||
const handleMemorySet = () => {
|
||||
try {
|
||||
const parsed = parseValue(memoryValue)
|
||||
const ttl = memoryTTL ? parseInt(memoryTTL) : undefined
|
||||
cacheService.set(memoryKey, parsed, ttl)
|
||||
message.success(`Memory cache set: ${memoryKey}`)
|
||||
setUpdateCount(prev => prev + 1)
|
||||
logger.info('Memory cache set', { key: memoryKey, value: parsed, ttl })
|
||||
} catch (error) {
|
||||
message.error(`Memory cache set failed: ${(error as Error).message}`)
|
||||
logger.error('Memory cache set failed', error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMemoryGet = () => {
|
||||
try {
|
||||
const result = cacheService.get(memoryKey)
|
||||
setMemoryResult(result)
|
||||
message.info(`Memory cache get: ${memoryKey}`)
|
||||
logger.info('Memory cache get', { key: memoryKey, result })
|
||||
} catch (error) {
|
||||
message.error(`Memory cache get failed: ${(error as Error).message}`)
|
||||
logger.error('Memory cache get failed', error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMemoryHas = () => {
|
||||
try {
|
||||
const exists = cacheService.has(memoryKey)
|
||||
message.info(`Memory cache has ${memoryKey}: ${exists}`)
|
||||
logger.info('Memory cache has', { key: memoryKey, exists })
|
||||
} catch (error) {
|
||||
message.error(`Memory cache has failed: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMemoryDelete = () => {
|
||||
try {
|
||||
const deleted = cacheService.delete(memoryKey)
|
||||
message.info(`Memory cache delete ${memoryKey}: ${deleted}`)
|
||||
setMemoryResult(undefined)
|
||||
logger.info('Memory cache delete', { key: memoryKey, deleted })
|
||||
} catch (error) {
|
||||
message.error(`Memory cache delete failed: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Shared Cache Operations
|
||||
const handleSharedSet = () => {
|
||||
try {
|
||||
const parsed = parseValue(sharedValue)
|
||||
const ttl = sharedTTL ? parseInt(sharedTTL) : undefined
|
||||
cacheService.setShared(sharedKey, parsed, ttl)
|
||||
message.success(`Shared cache set: ${sharedKey} (broadcasted to other windows)`)
|
||||
setUpdateCount(prev => prev + 1)
|
||||
logger.info('Shared cache set', { key: sharedKey, value: parsed, ttl })
|
||||
} catch (error) {
|
||||
message.error(`Shared cache set failed: ${(error as Error).message}`)
|
||||
logger.error('Shared cache set failed', error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSharedGet = () => {
|
||||
try {
|
||||
const result = cacheService.getShared(sharedKey)
|
||||
setSharedResult(result)
|
||||
message.info(`Shared cache get: ${sharedKey}`)
|
||||
logger.info('Shared cache get', { key: sharedKey, result })
|
||||
} catch (error) {
|
||||
message.error(`Shared cache get failed: ${(error as Error).message}`)
|
||||
logger.error('Shared cache get failed', error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSharedHas = () => {
|
||||
try {
|
||||
const exists = cacheService.hasShared(sharedKey)
|
||||
message.info(`Shared cache has ${sharedKey}: ${exists}`)
|
||||
logger.info('Shared cache has', { key: sharedKey, exists })
|
||||
} catch (error) {
|
||||
message.error(`Shared cache has failed: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSharedDelete = () => {
|
||||
try {
|
||||
const deleted = cacheService.deleteShared(sharedKey)
|
||||
message.info(`Shared cache delete ${sharedKey}: ${deleted} (broadcasted to other windows)`)
|
||||
setSharedResult(undefined)
|
||||
logger.info('Shared cache delete', { key: sharedKey, deleted })
|
||||
} catch (error) {
|
||||
message.error(`Shared cache delete failed: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Persist Cache Operations
|
||||
const handlePersistSet = () => {
|
||||
try {
|
||||
let parsed: any
|
||||
// Handle different types based on the schema
|
||||
if (persistKey === 'example-1') {
|
||||
parsed = persistValue // string
|
||||
} else if (persistKey === 'example-2') {
|
||||
parsed = parseInt(persistValue) || 0 // number
|
||||
} else if (persistKey === 'example-3') {
|
||||
parsed = persistValue === 'true' // boolean
|
||||
} else if (persistKey === 'example-4') {
|
||||
parsed = parseValue(persistValue) // object
|
||||
}
|
||||
|
||||
cacheService.setPersist(persistKey, parsed as PersistCacheSchema[typeof persistKey])
|
||||
message.success(`Persist cache set: ${persistKey} (saved to localStorage + broadcasted)`)
|
||||
setUpdateCount(prev => prev + 1)
|
||||
logger.info('Persist cache set', { key: persistKey, value: parsed })
|
||||
} catch (error) {
|
||||
message.error(`Persist cache set failed: ${(error as Error).message}`)
|
||||
logger.error('Persist cache set failed', error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePersistGet = () => {
|
||||
try {
|
||||
const result = cacheService.getPersist(persistKey)
|
||||
setPersistResult(result)
|
||||
message.info(`Persist cache get: ${persistKey}`)
|
||||
logger.info('Persist cache get', { key: persistKey, result })
|
||||
} catch (error) {
|
||||
message.error(`Persist cache get failed: ${(error as Error).message}`)
|
||||
logger.error('Persist cache get failed', error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePersistHas = () => {
|
||||
try {
|
||||
const exists = cacheService.hasPersist(persistKey)
|
||||
message.info(`Persist cache has ${persistKey}: ${exists}`)
|
||||
logger.info('Persist cache has', { key: persistKey, exists })
|
||||
} catch (error) {
|
||||
message.error(`Persist cache has failed: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-refresh results
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
// Auto-get current values for display
|
||||
try {
|
||||
const memResult = cacheService.get(memoryKey)
|
||||
const sharedResult = cacheService.getShared(sharedKey)
|
||||
const persistResult = cacheService.getPersist(persistKey)
|
||||
|
||||
setMemoryResult(memResult)
|
||||
setSharedResult(sharedResult)
|
||||
setPersistResult(persistResult)
|
||||
} catch (error) {
|
||||
logger.error('Auto-refresh failed', error as Error)
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [memoryKey, sharedKey, persistKey])
|
||||
|
||||
return (
|
||||
<TestContainer $isDark={isDarkTheme}>
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Text type="secondary">
|
||||
直接测试 CacheService API • Updates: {updateCount} • Auto-refresh: 1s
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
{/* Memory Cache Section */}
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Zap size={16} />
|
||||
<Text>Memory Cache</Text>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Input
|
||||
placeholder="Cache Key"
|
||||
value={memoryKey}
|
||||
onChange={(e) => setMemoryKey(e.target.value)}
|
||||
prefix={<Database size={14} />}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
placeholder='Value (JSON or string)'
|
||||
value={memoryValue}
|
||||
onChange={(e) => setMemoryValue(e.target.value)}
|
||||
rows={2}
|
||||
/>
|
||||
|
||||
<Input
|
||||
placeholder="TTL (ms, optional)"
|
||||
value={memoryTTL}
|
||||
onChange={(e) => setMemoryTTL(e.target.value)}
|
||||
prefix={<Clock size={14} />}
|
||||
/>
|
||||
|
||||
<Space size="small" wrap>
|
||||
<Button size="small" type="primary" onClick={handleMemorySet} icon={<Edit size={12} />}>
|
||||
Set
|
||||
</Button>
|
||||
<Button size="small" onClick={handleMemoryGet} icon={<Eye size={12} />}>
|
||||
Get
|
||||
</Button>
|
||||
<Button size="small" onClick={handleMemoryHas} icon={<Database size={12} />}>
|
||||
Has
|
||||
</Button>
|
||||
<Button size="small" danger onClick={handleMemoryDelete} icon={<Trash2 size={12} />}>
|
||||
Delete
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<ResultDisplay $isDark={isDarkTheme}>
|
||||
<Text strong>Result:</Text>
|
||||
<pre>{formatValue(memoryResult)}</pre>
|
||||
</ResultDisplay>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* Shared Cache Section */}
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Database size={16} />
|
||||
<Text>Shared Cache</Text>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Input
|
||||
placeholder="Cache Key"
|
||||
value={sharedKey}
|
||||
onChange={(e) => setSharedKey(e.target.value)}
|
||||
prefix={<Database size={14} />}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
placeholder='Value (JSON or string)'
|
||||
value={sharedValue}
|
||||
onChange={(e) => setSharedValue(e.target.value)}
|
||||
rows={2}
|
||||
/>
|
||||
|
||||
<Input
|
||||
placeholder="TTL (ms, optional)"
|
||||
value={sharedTTL}
|
||||
onChange={(e) => setSharedTTL(e.target.value)}
|
||||
prefix={<Clock size={14} />}
|
||||
/>
|
||||
|
||||
<Space size="small" wrap>
|
||||
<Button size="small" type="primary" onClick={handleSharedSet} icon={<Edit size={12} />}>
|
||||
Set
|
||||
</Button>
|
||||
<Button size="small" onClick={handleSharedGet} icon={<Eye size={12} />}>
|
||||
Get
|
||||
</Button>
|
||||
<Button size="small" onClick={handleSharedHas} icon={<Database size={12} />}>
|
||||
Has
|
||||
</Button>
|
||||
<Button size="small" danger onClick={handleSharedDelete} icon={<Trash2 size={12} />}>
|
||||
Delete
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<ResultDisplay $isDark={isDarkTheme}>
|
||||
<Text strong>Result:</Text>
|
||||
<pre>{formatValue(sharedResult)}</pre>
|
||||
</ResultDisplay>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* Persist Cache Section */}
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Eye size={16} />
|
||||
<Text>Persist Cache</Text>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Select
|
||||
value={persistKey}
|
||||
onChange={setPersistKey}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="Select persist key"
|
||||
>
|
||||
{persistKeys.map(key => (
|
||||
<Option key={key} value={key}>{key}</Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<TextArea
|
||||
placeholder='Value (type depends on key)'
|
||||
value={persistValue}
|
||||
onChange={(e) => setPersistValue(e.target.value)}
|
||||
rows={2}
|
||||
/>
|
||||
|
||||
<Space size="small" wrap>
|
||||
<Button size="small" type="primary" onClick={handlePersistSet} icon={<Edit size={12} />}>
|
||||
Set
|
||||
</Button>
|
||||
<Button size="small" onClick={handlePersistGet} icon={<Eye size={12} />}>
|
||||
Get
|
||||
</Button>
|
||||
<Button size="small" onClick={handlePersistHas} icon={<Database size={12} />}>
|
||||
Has
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<ResultDisplay $isDark={isDarkTheme}>
|
||||
<Text strong>Result:</Text>
|
||||
<pre>{formatValue(persistResult)}</pre>
|
||||
</ResultDisplay>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
💡 提示: Memory cache 仅在当前窗口有效 • Shared cache 跨窗口同步 • Persist cache 持久化到 localStorage
|
||||
</Text>
|
||||
</div>
|
||||
</Space>
|
||||
</TestContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const TestContainer = styled.div<{ $isDark: boolean }>`
|
||||
color: ${props => props.$isDark ? '#fff' : '#000'};
|
||||
`
|
||||
|
||||
const ResultDisplay = styled.div<{ $isDark: boolean }>`
|
||||
background: ${props => props.$isDark ? '#0d1117' : '#f6f8fa'};
|
||||
border: 1px solid ${props => props.$isDark ? '#30363d' : '#d0d7de'};
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
font-size: 11px;
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
color: ${props => props.$isDark ? '#e6edf3' : '#1f2328'};
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
}
|
||||
`
|
||||
|
||||
export default CacheServiceTests
|
||||
@ -0,0 +1,524 @@
|
||||
import { cacheService } from '@renderer/data/CacheService'
|
||||
import { useCache, useSharedCache } from '@renderer/data/hooks/useCache'
|
||||
import { usePreference } from '@renderer/data/hooks/usePreference'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
|
||||
import { Button, Input, message, Space, Typography, Card, Row, Col, Progress, Statistic, Tag, Alert } from 'antd'
|
||||
import { Zap, AlertTriangle, TrendingUp, HardDrive, Users, Clock, Database } from 'lucide-react'
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const { Text, Title } = Typography
|
||||
|
||||
const logger = loggerService.withContext('CacheStressTests')
|
||||
|
||||
/**
|
||||
* Cache stress testing component
|
||||
* Tests performance limits, memory usage, concurrent operations
|
||||
*/
|
||||
const CacheStressTests: React.FC = () => {
|
||||
const [currentTheme] = usePreference('ui.theme_mode')
|
||||
const isDarkTheme = currentTheme === ThemeMode.dark
|
||||
|
||||
// Test States
|
||||
const [isRunning, setIsRunning] = useState(false)
|
||||
const [testProgress, setTestProgress] = useState(0)
|
||||
const [testResults, setTestResults] = useState<any>({})
|
||||
|
||||
// Performance Metrics
|
||||
const [operationsPerSecond, setOperationsPerSecond] = useState(0)
|
||||
const [totalOperations, setTotalOperations] = useState(0)
|
||||
const [memoryUsage, setMemoryUsage] = useState(0)
|
||||
const renderCountRef = useRef(0)
|
||||
const [displayRenderCount, setDisplayRenderCount] = useState(0)
|
||||
const [errorCount, setErrorCount] = useState(0)
|
||||
|
||||
// Concurrent Testing
|
||||
const [concurrentValue1, setConcurrentValue1] = useCache('concurrent-test-1', 0)
|
||||
const [concurrentValue2, setConcurrentValue2] = useCache('concurrent-test-2', 0)
|
||||
const [concurrentShared, setConcurrentShared] = useSharedCache('concurrent-shared', 0)
|
||||
|
||||
// Large Data Testing
|
||||
const [largeDataKey] = useState('large-data-test')
|
||||
const [largeDataSize, setLargeDataSize] = useState(0)
|
||||
const [largeDataValue, setLargeDataValue] = useCache(largeDataKey)
|
||||
|
||||
// Timers and refs
|
||||
const testTimerRef = useRef<NodeJS.Timeout>()
|
||||
const metricsTimerRef = useRef<NodeJS.Timeout>()
|
||||
const concurrentTimerRef = useRef<NodeJS.Timeout>()
|
||||
|
||||
// Update render count without causing re-renders
|
||||
renderCountRef.current += 1
|
||||
|
||||
// Memory Usage Estimation
|
||||
const estimateMemoryUsage = useCallback(() => {
|
||||
try {
|
||||
// Rough estimation based on localStorage size and objects
|
||||
const persistSize = localStorage.getItem('cs_cache_persist')?.length || 0
|
||||
const estimatedSize = persistSize + (totalOperations * 50) // Rough estimate
|
||||
setMemoryUsage(estimatedSize)
|
||||
} catch (error) {
|
||||
logger.error('Memory usage estimation failed', error as Error)
|
||||
}
|
||||
}, [totalOperations])
|
||||
|
||||
useEffect(() => {
|
||||
estimateMemoryUsage()
|
||||
}, [totalOperations, estimateMemoryUsage])
|
||||
|
||||
// Rapid Fire Test
|
||||
const runRapidFireTest = useCallback(async () => {
|
||||
setIsRunning(true)
|
||||
setTestProgress(0)
|
||||
setTotalOperations(0)
|
||||
setErrorCount(0)
|
||||
|
||||
const startTime = Date.now()
|
||||
const testDuration = 10000 // 10 seconds
|
||||
const targetOperations = 1000
|
||||
|
||||
let operationCount = 0
|
||||
let errors = 0
|
||||
let shouldContinue = true // Use local variable instead of state
|
||||
|
||||
const performOperation = () => {
|
||||
if (!shouldContinue) return // Check local flag
|
||||
|
||||
try {
|
||||
const key = `rapid-test-${operationCount % 100}`
|
||||
const value = { id: operationCount, timestamp: Date.now(), data: Math.random().toString(36) }
|
||||
|
||||
// Alternate between different cache types
|
||||
if (operationCount % 3 === 0) {
|
||||
cacheService.set(key, value)
|
||||
} else if (operationCount % 3 === 1) {
|
||||
cacheService.setShared(key, value)
|
||||
} else {
|
||||
cacheService.get(key)
|
||||
}
|
||||
|
||||
operationCount++
|
||||
setTotalOperations(operationCount)
|
||||
setTestProgress((operationCount / targetOperations) * 100)
|
||||
|
||||
// Calculate operations per second
|
||||
const elapsed = Date.now() - startTime
|
||||
setOperationsPerSecond(Math.round((operationCount / elapsed) * 1000))
|
||||
|
||||
if (operationCount >= targetOperations || elapsed >= testDuration) {
|
||||
shouldContinue = false
|
||||
setIsRunning(false)
|
||||
setTestResults({
|
||||
duration: elapsed,
|
||||
operations: operationCount,
|
||||
opsPerSecond: Math.round((operationCount / elapsed) * 1000),
|
||||
errors
|
||||
})
|
||||
message.success(`Rapid fire test completed: ${operationCount} operations in ${elapsed}ms`)
|
||||
logger.info('Rapid fire test completed', { operationCount, elapsed, errors })
|
||||
return
|
||||
}
|
||||
|
||||
// Schedule next operation
|
||||
setTimeout(performOperation, 1)
|
||||
} catch (error) {
|
||||
errors++
|
||||
setErrorCount(errors)
|
||||
logger.error('Rapid fire test operation failed', error as Error)
|
||||
if (shouldContinue) {
|
||||
setTimeout(performOperation, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start the test
|
||||
performOperation()
|
||||
|
||||
// Store a reference to the shouldContinue flag for stopping
|
||||
testTimerRef.current = setTimeout(() => {
|
||||
// This timer will be cleared if the test is stopped early
|
||||
shouldContinue = false
|
||||
setIsRunning(false)
|
||||
}, testDuration)
|
||||
}, [])
|
||||
|
||||
// Concurrent Updates Test
|
||||
const startConcurrentTest = () => {
|
||||
let count1 = 0
|
||||
let count2 = 0
|
||||
let sharedCount = 0
|
||||
|
||||
concurrentTimerRef.current = setInterval(() => {
|
||||
// Simulate concurrent updates from different sources
|
||||
setConcurrentValue1(++count1)
|
||||
setConcurrentValue2(++count2)
|
||||
setConcurrentShared(++sharedCount)
|
||||
|
||||
if (count1 >= 100) {
|
||||
clearInterval(concurrentTimerRef.current!)
|
||||
message.success('Concurrent updates test completed')
|
||||
}
|
||||
}, 50) // Update every 50ms
|
||||
|
||||
message.info('Concurrent updates test started')
|
||||
}
|
||||
|
||||
const stopConcurrentTest = () => {
|
||||
if (concurrentTimerRef.current) {
|
||||
clearInterval(concurrentTimerRef.current)
|
||||
message.info('Concurrent updates test stopped')
|
||||
}
|
||||
}
|
||||
|
||||
// Large Data Test
|
||||
const generateLargeData = (sizeKB: number) => {
|
||||
const targetSize = sizeKB * 1024
|
||||
const baseString = 'a'.repeat(1024) // 1KB string
|
||||
const chunks = Math.floor(targetSize / 1024)
|
||||
|
||||
const largeObject = {
|
||||
id: Date.now(),
|
||||
size: sizeKB,
|
||||
chunks: chunks,
|
||||
data: Array(chunks).fill(baseString),
|
||||
metadata: {
|
||||
created: new Date().toISOString(),
|
||||
type: 'stress-test',
|
||||
description: `Large data test object of ${sizeKB}KB`
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
setLargeDataValue(largeObject)
|
||||
setLargeDataSize(sizeKB)
|
||||
message.success(`Large data test: ${sizeKB}KB object stored`)
|
||||
logger.info('Large data test completed', { sizeKB, chunks })
|
||||
} catch (error) {
|
||||
message.error(`Large data test failed: ${(error as Error).message}`)
|
||||
logger.error('Large data test failed', error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
// LocalStorage Limit Test
|
||||
const testLocalStorageLimit = async () => {
|
||||
try {
|
||||
let testSize = 1
|
||||
let maxSize = 0
|
||||
|
||||
while (testSize <= 10240) { // Test up to 10MB
|
||||
try {
|
||||
const testData = 'x'.repeat(testSize * 1024) // testSize KB
|
||||
localStorage.setItem('storage-limit-test', testData)
|
||||
localStorage.removeItem('storage-limit-test')
|
||||
maxSize = testSize
|
||||
testSize *= 2
|
||||
} catch (error) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
message.info(`LocalStorage limit test: ~${maxSize}KB available`)
|
||||
logger.info('LocalStorage limit test completed', { maxSize })
|
||||
} catch (error) {
|
||||
message.error(`LocalStorage limit test failed: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop all tests
|
||||
const stopAllTests = () => {
|
||||
setIsRunning(false)
|
||||
if (testTimerRef.current) {
|
||||
clearTimeout(testTimerRef.current)
|
||||
testTimerRef.current = undefined
|
||||
}
|
||||
if (concurrentTimerRef.current) {
|
||||
clearInterval(concurrentTimerRef.current)
|
||||
concurrentTimerRef.current = undefined
|
||||
}
|
||||
message.info('All tests stopped')
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (testTimerRef.current) clearTimeout(testTimerRef.current)
|
||||
if (metricsTimerRef.current) clearInterval(metricsTimerRef.current)
|
||||
if (concurrentTimerRef.current) clearInterval(concurrentTimerRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<TestContainer $isDark={isDarkTheme}>
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Space>
|
||||
<Text type="secondary">Stress Testing • Renders: {displayRenderCount || renderCountRef.current} • Errors: {errorCount}</Text>
|
||||
<Button size="small" onClick={() => {
|
||||
renderCountRef.current = 0
|
||||
setDisplayRenderCount(0)
|
||||
setErrorCount(0)
|
||||
}}>Reset Stats</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* Performance Metrics */}
|
||||
<Row gutter={[16, 8]}>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Operations/Second"
|
||||
value={operationsPerSecond}
|
||||
prefix={<TrendingUp size={16} />}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Total Operations"
|
||||
value={totalOperations}
|
||||
prefix={<Database size={16} />}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Memory Usage (bytes)"
|
||||
value={memoryUsage}
|
||||
prefix={<HardDrive size={16} />}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Error Count"
|
||||
value={errorCount}
|
||||
valueStyle={{ color: errorCount > 0 ? '#ff4d4f' : undefined }}
|
||||
prefix={<AlertTriangle size={16} />}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
{/* Rapid Fire Test */}
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Zap size={16} />
|
||||
<Text>Rapid Fire Operations</Text>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>High-frequency cache operations test (1000 ops in 10s)</Text>
|
||||
|
||||
<Progress
|
||||
percent={Math.round(testProgress)}
|
||||
status={isRunning ? 'active' : testProgress > 0 ? 'success' : 'normal'}
|
||||
strokeColor={isDarkTheme ? '#1890ff' : undefined}
|
||||
/>
|
||||
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={runRapidFireTest}
|
||||
disabled={isRunning}
|
||||
icon={<Zap size={12} />}
|
||||
>
|
||||
Start Rapid Fire Test
|
||||
</Button>
|
||||
<Button onClick={stopAllTests} disabled={!isRunning} danger>
|
||||
Stop All Tests
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
{testResults.operations && (
|
||||
<ResultDisplay $isDark={isDarkTheme}>
|
||||
<Text strong>Test Results:</Text>
|
||||
<pre>{JSON.stringify(testResults, null, 2)}</pre>
|
||||
</ResultDisplay>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* Concurrent Updates Test */}
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Users size={16} />
|
||||
<Text>Concurrent Updates</Text>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>Multiple hooks updating simultaneously</Text>
|
||||
|
||||
<Row gutter={8}>
|
||||
<Col span={8}>
|
||||
<Card size="small" title="Hook 1">
|
||||
<Statistic value={concurrentValue1} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card size="small" title="Hook 2">
|
||||
<Statistic value={concurrentValue2} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card size="small" title="Shared">
|
||||
<Statistic value={concurrentShared} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={startConcurrentTest}
|
||||
icon={<Users size={12} />}
|
||||
>
|
||||
Start Concurrent Test
|
||||
</Button>
|
||||
<Button onClick={stopConcurrentTest}>
|
||||
Stop
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
{/* Large Data Test */}
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<HardDrive size={16} />
|
||||
<Text>Large Data Storage</Text>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>Test cache with large objects</Text>
|
||||
|
||||
<Space wrap>
|
||||
<Button size="small" onClick={() => generateLargeData(10)}>
|
||||
10KB Object
|
||||
</Button>
|
||||
<Button size="small" onClick={() => generateLargeData(100)}>
|
||||
100KB Object
|
||||
</Button>
|
||||
<Button size="small" onClick={() => generateLargeData(1024)}>
|
||||
1MB Object
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
{largeDataSize > 0 && (
|
||||
<Alert
|
||||
message={`Large data test completed`}
|
||||
description={`Successfully stored ${largeDataSize}KB object in cache`}
|
||||
type="success"
|
||||
showIcon
|
||||
/>
|
||||
)}
|
||||
|
||||
<ResultDisplay $isDark={isDarkTheme}>
|
||||
<Text strong>Large Data Key: </Text><code>{largeDataKey}</code>
|
||||
<br />
|
||||
<Text strong>Current Size: </Text>{largeDataSize}KB
|
||||
</ResultDisplay>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* Storage Limits Test */}
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<AlertTriangle size={16} />
|
||||
<Text>Storage Limits</Text>
|
||||
</Space>
|
||||
}
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>Test localStorage capacity and limits</Text>
|
||||
|
||||
<Alert
|
||||
message="Warning"
|
||||
description="This test may temporarily consume significant browser storage"
|
||||
type="warning"
|
||||
showIcon
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={testLocalStorageLimit}
|
||||
icon={<Database size={12} />}
|
||||
>
|
||||
Test Storage Limits
|
||||
</Button>
|
||||
|
||||
<Space direction="vertical">
|
||||
<Tag color="blue">Persist Cache Size Check</Tag>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
Current persist cache: ~{Math.round(JSON.stringify(localStorage.getItem('cs_cache_persist')).length / 1024)}KB
|
||||
</Text>
|
||||
</Space>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
⚠️ 压力测试: 高频操作、并发更新、大数据存储、存储限制测试 • 可能会影响浏览器性能
|
||||
</Text>
|
||||
</div>
|
||||
</Space>
|
||||
</TestContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const TestContainer = styled.div<{ $isDark: boolean }>`
|
||||
color: ${props => props.$isDark ? '#fff' : '#000'};
|
||||
`
|
||||
|
||||
const ResultDisplay = styled.div<{ $isDark: boolean }>`
|
||||
background: ${props => props.$isDark ? '#0d1117' : '#f6f8fa'};
|
||||
border: 1px solid ${props => props.$isDark ? '#30363d' : '#d0d7de'};
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
font-size: 11px;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
color: ${props => props.$isDark ? '#e6edf3' : '#1f2328'};
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
}
|
||||
`
|
||||
|
||||
export default CacheStressTests
|
||||
@ -77,7 +77,7 @@ const PreferenceBasicTests: React.FC = () => {
|
||||
]
|
||||
|
||||
return (
|
||||
<TestContainer isDark={isDarkTheme}>
|
||||
<TestContainer $isDark={isDarkTheme}>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
{/* Key Selection */}
|
||||
<div>
|
||||
@ -96,7 +96,7 @@ const PreferenceBasicTests: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Current Value Display */}
|
||||
<CurrentValueContainer isDark={isDarkTheme}>
|
||||
<CurrentValueContainer $isDark={isDarkTheme}>
|
||||
<Text strong>当前值:</Text>
|
||||
<ValueDisplay>
|
||||
{value !== undefined ? (
|
||||
@ -319,31 +319,31 @@ const PreferenceBasicTests: React.FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const TestContainer = styled.div<{ isDark: boolean }>`
|
||||
const TestContainer = styled.div<{ $isDark: boolean }>`
|
||||
padding: 16px;
|
||||
background: ${(props) => (props.isDark ? '#262626' : '#fafafa')};
|
||||
background: ${(props) => (props.$isDark ? '#262626' : '#fafafa')};
|
||||
border-radius: 8px;
|
||||
|
||||
.ant-typography {
|
||||
color: ${(props) => (props.isDark ? '#fff' : 'inherit')} !important;
|
||||
color: ${(props) => (props.$isDark ? '#fff' : 'inherit')} !important;
|
||||
}
|
||||
|
||||
.ant-select-selector {
|
||||
background-color: ${(props) => (props.isDark ? '#1f1f1f' : '#fff')} !important;
|
||||
border-color: ${(props) => (props.isDark ? '#434343' : '#d9d9d9')} !important;
|
||||
color: ${(props) => (props.isDark ? '#fff' : '#000')} !important;
|
||||
background-color: ${(props) => (props.$isDark ? '#1f1f1f' : '#fff')} !important;
|
||||
border-color: ${(props) => (props.$isDark ? '#434343' : '#d9d9d9')} !important;
|
||||
color: ${(props) => (props.$isDark ? '#fff' : '#000')} !important;
|
||||
}
|
||||
|
||||
.ant-input {
|
||||
background-color: ${(props) => (props.isDark ? '#1f1f1f' : '#fff')} !important;
|
||||
border-color: ${(props) => (props.isDark ? '#434343' : '#d9d9d9')} !important;
|
||||
color: ${(props) => (props.isDark ? '#fff' : '#000')} !important;
|
||||
background-color: ${(props) => (props.$isDark ? '#1f1f1f' : '#fff')} !important;
|
||||
border-color: ${(props) => (props.$isDark ? '#434343' : '#d9d9d9')} !important;
|
||||
color: ${(props) => (props.$isDark ? '#fff' : '#000')} !important;
|
||||
}
|
||||
`
|
||||
|
||||
const CurrentValueContainer = styled.div<{ isDark?: boolean }>`
|
||||
const CurrentValueContainer = styled.div<{ $isDark?: boolean }>`
|
||||
padding: 12px;
|
||||
background: ${(props) => (props.isDark ? '#1f1f1f' : '#f0f0f0')};
|
||||
background: ${(props) => (props.$isDark ? '#1f1f1f' : '#f0f0f0')};
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid var(--color-primary);
|
||||
`
|
||||
|
||||
@ -116,7 +116,7 @@ const PreferenceServiceTests: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<TestContainer isDark={isDarkTheme}>
|
||||
<TestContainer $isDark={isDarkTheme}>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
{/* Input Controls */}
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
@ -160,7 +160,7 @@ const PreferenceServiceTests: React.FC = () => {
|
||||
|
||||
{/* Result Display */}
|
||||
{getResult !== null && (
|
||||
<ResultContainer isDark={isDarkTheme}>
|
||||
<ResultContainer $isDark={isDarkTheme}>
|
||||
<Text strong>Result:</Text>
|
||||
<ResultText>
|
||||
{typeof getResult === 'object' ? JSON.stringify(getResult, null, 2) : String(getResult)}
|
||||
@ -200,26 +200,26 @@ const PreferenceServiceTests: React.FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const TestContainer = styled.div<{ isDark: boolean }>`
|
||||
const TestContainer = styled.div<{ $isDark: boolean }>`
|
||||
padding: 16px;
|
||||
background: ${(props) => (props.isDark ? '#262626' : '#fafafa')};
|
||||
background: ${(props) => (props.$isDark ? '#262626' : '#fafafa')};
|
||||
border-radius: 8px;
|
||||
|
||||
.ant-typography {
|
||||
color: ${(props) => (props.isDark ? '#fff' : 'inherit')} !important;
|
||||
color: ${(props) => (props.$isDark ? '#fff' : 'inherit')} !important;
|
||||
}
|
||||
|
||||
.ant-input {
|
||||
background-color: ${(props) => (props.isDark ? '#1f1f1f' : '#fff')} !important;
|
||||
border-color: ${(props) => (props.isDark ? '#434343' : '#d9d9d9')} !important;
|
||||
color: ${(props) => (props.isDark ? '#fff' : '#000')} !important;
|
||||
background-color: ${(props) => (props.$isDark ? '#1f1f1f' : '#fff')} !important;
|
||||
border-color: ${(props) => (props.$isDark ? '#434343' : '#d9d9d9')} !important;
|
||||
color: ${(props) => (props.$isDark ? '#fff' : '#000')} !important;
|
||||
}
|
||||
`
|
||||
|
||||
const ResultContainer = styled.div<{ isDark?: boolean }>`
|
||||
const ResultContainer = styled.div<{ $isDark?: boolean }>`
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
background: ${(props) => (props.isDark ? '#1f1f1f' : '#f0f0f0')};
|
||||
background: ${(props) => (props.$isDark ? '#1f1f1f' : '#f0f0f0')};
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid var(--color-primary);
|
||||
`
|
||||
|
||||
Loading…
Reference in New Issue
Block a user