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:
fullex 2025-09-15 17:36:14 +08:00
parent 04ef5edea2
commit 6079961f44
9 changed files with 2317 additions and 154 deletions

View File

@ -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

View File

@ -1,6 +1,6 @@
# PreferenceService 测试窗口
# 数据重构项目测试窗口
专用于测试 PreferenceService 和 usePreference hooks 功能的独立测试窗口系统
专用于测试数据重构项目各项功能的独立测试窗口系统,包括 PreferenceService、CacheService、DataApiService 和相关 React hooks
## 🎯 当前实现
@ -18,6 +18,8 @@
## 测试组件
## PreferenceService 测试模块
### 1. PreferenceService 基础测试
- 直接测试服务层APIget, 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机制和用户友好的错误提示
- **内存管理**: 自动清理、定时器管理和资源释放

View File

@ -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)' }}>
PreferenceServiceDataApiService React hooks
PreferenceServiceCacheServiceDataApiService 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/PersistTTL过期
</Text>
<Text style={{ color: 'var(--color-tertiary)', fontWeight: 'bold' }}>
🚀 API测试CRUDReact 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);
`

View File

@ -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);
`