mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-28 05:11:24 +08:00
feat(preferences): enhance testing framework with real-time UI synchronization and slider controls
This commit updates the dataRefactorTest README to include new features such as real-time UI linkage for theme, language, and zoom preferences, as well as interactive slider controls for various settings. The TestApp component is modified to support dynamic window identification and theme monitoring. Additionally, the PreferenceBasicTests, PreferenceMultipleTests, and PreferenceServiceTests components are enhanced with slider functionality for numerical preferences, improving the testing capabilities for cross-window synchronization and user experience.
This commit is contained in:
parent
85bdcdc206
commit
df876651b9
@ -8,6 +8,9 @@
|
||||
- 专用的测试窗口 (DataRefactorTestWindow)
|
||||
- **双窗口启动**:应用启动时会同时打开主窗口和两个测试窗口
|
||||
- **跨窗口同步测试**:两个测试窗口可以相互验证偏好设置的实时同步
|
||||
- **实时UI联动**:主题、语言、缩放等偏好设置变化会立即反映在UI上
|
||||
- **🎛️ Slider联动测试**:多个交互式滑块控制数值类型偏好设置,支持跨窗口实时同步
|
||||
- **多源窗口编号**:支持URL参数、窗口标题、窗口名称等多种方式确定窗口编号
|
||||
- 完整的测试界面,包含4个专业测试组件
|
||||
- 自动窗口定位,避免重叠
|
||||
- 窗口编号标识,便于区分
|
||||
@ -25,12 +28,16 @@
|
||||
- `app.theme.mode` - 主题模式
|
||||
- `app.language` - 语言设置
|
||||
- `app.spell_check.enabled` - 拼写检查
|
||||
- `app.zoom_factor` - 缩放因子
|
||||
- `app.zoom_factor` - 缩放因子 (🎛️ 支持Slider)
|
||||
- `chat.message.font_size` - 消息字体大小 (🎛️ 支持Slider)
|
||||
- `feature.selection.action_window_opacity` - 选择窗口透明度 (🎛️ 支持Slider)
|
||||
- 实时值更新和类型转换
|
||||
- **Slider联动控制**:数值类型偏好设置提供交互式滑块,支持实时拖拽调整
|
||||
|
||||
### 3. usePreferences 批量操作测试
|
||||
- 测试多个偏好设置的批量管理
|
||||
- 4种预设场景:基础设置、UI设置、用户设置、自定义组合
|
||||
- 5种预设场景:基础设置、UI设置、用户设置、🎛️数值设置、自定义组合
|
||||
- **🎛️ 数值设置场景**:专门的Slider联动控制区域,包含缩放、字体、选择窗口透明度三个滑块
|
||||
- 批量更新功能,支持JSON格式输入
|
||||
- 快速切换操作
|
||||
|
||||
@ -106,13 +113,16 @@ src/renderer/src/windows/dataRefactorTest/
|
||||
|
||||
1. **启动应用** - 自动打开2个测试窗口
|
||||
2. **选择测试** - 在"usePreference Hook 测试"中选择要测试的偏好设置键
|
||||
3. **跨窗口验证** - 在窗口#1中修改值,观察窗口#2是否同步
|
||||
4. **高级测试** - 使用"Hook 高级功能测试"验证订阅和缓存机制
|
||||
5. **批量测试** - 使用"usePreferences 批量操作测试"进行多项设置测试
|
||||
3. **🎛️ Slider联动测试** - 选择数值类型偏好设置,拖动Slider观察实时变化
|
||||
4. **跨窗口验证** - 在窗口#1中修改值,观察窗口#2是否同步
|
||||
5. **批量Slider测试** - 切换到"数值设置场景",同时拖动多个滑块测试批量同步
|
||||
6. **高级测试** - 使用"Hook 高级功能测试"验证订阅和缓存机制
|
||||
|
||||
## 🔧 技术实现
|
||||
|
||||
- **窗口管理**: DataRefactorMigrateService 单例管理多个测试窗口
|
||||
- **数据同步**: 基于真实的 PreferenceService 和 IPC 通信
|
||||
- **实时主题**: 使用 useSyncExternalStore 实现主题、缩放等设置的实时UI响应
|
||||
- **跨窗口识别**: 多源窗口编号支持,确保每个窗口都有唯一标识
|
||||
- **UI框架**: Ant Design + styled-components + React 18
|
||||
- **类型安全**: 完整的 TypeScript 类型检查和偏好设置键约束
|
||||
@ -1,4 +1,5 @@
|
||||
import { AppLogo } from '@renderer/config/env'
|
||||
import { usePreference } from '@renderer/data/hooks/usePreference'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import { Button, Card, Col, Divider, Layout, Row, Space, Typography } from 'antd'
|
||||
import { Database, FlaskConical, Settings, TestTube } from 'lucide-react'
|
||||
@ -16,47 +17,91 @@ const { Title, Text } = Typography
|
||||
const logger = loggerService.withContext('TestApp')
|
||||
|
||||
const TestApp: React.FC = () => {
|
||||
// Get window title to determine window number
|
||||
const windowTitle = document.title
|
||||
const windowMatch = windowTitle.match(/#(\d+)/)
|
||||
const windowNumber = windowMatch ? windowMatch[1] : '1'
|
||||
// Get window number from multiple sources
|
||||
const getWindowNumber = () => {
|
||||
// Try URL search params first
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const windowParam = urlParams.get('window')
|
||||
if (windowParam) {
|
||||
return windowParam
|
||||
}
|
||||
|
||||
// Try document title
|
||||
const windowTitle = document.title
|
||||
const windowMatch = windowTitle.match(/#(\d+)/)
|
||||
if (windowMatch) {
|
||||
return windowMatch[1]
|
||||
}
|
||||
|
||||
// Try window name
|
||||
if (window.name && window.name.includes('#')) {
|
||||
const nameMatch = window.name.match(/#(\d+)/)
|
||||
if (nameMatch) {
|
||||
return nameMatch[1]
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: generate based on window creation time
|
||||
return Math.floor(Date.now() / 1000) % 100
|
||||
}
|
||||
|
||||
const windowNumber = getWindowNumber()
|
||||
|
||||
// Add theme preference monitoring for UI changes
|
||||
const [theme, setTheme] = usePreference('app.theme.mode')
|
||||
const [language] = usePreference('app.language')
|
||||
const [zoomFactor] = usePreference('app.zoom_factor')
|
||||
|
||||
// Apply theme-based styling
|
||||
const isDarkTheme = theme === 'ThemeMode.dark'
|
||||
const headerBg = isDarkTheme ? '#141414' : '#fff'
|
||||
const borderColor = isDarkTheme ? '#303030' : '#f0f0f0'
|
||||
const textColor = isDarkTheme ? '#fff' : '#000'
|
||||
|
||||
// Apply zoom factor
|
||||
const zoomValue = typeof zoomFactor === 'number' ? zoomFactor : 1.0
|
||||
|
||||
return (
|
||||
<Layout style={{ height: '100vh' }}>
|
||||
<Header style={{ background: '#fff', borderBottom: '1px solid #f0f0f0', padding: '0 24px' }}>
|
||||
<Layout style={{ height: '100vh', transform: `scale(${zoomValue})`, transformOrigin: 'top left' }}>
|
||||
<Header
|
||||
style={{ background: headerBg, borderBottom: `1px solid ${borderColor}`, padding: '0 24px', color: textColor }}>
|
||||
<HeaderContent>
|
||||
<Space align="center">
|
||||
<img src={AppLogo} alt="Logo" style={{ width: 28, height: 28, borderRadius: 6 }} />
|
||||
<Title level={4} style={{ margin: 0, color: 'var(--color-primary)' }}>
|
||||
Test Window #{windowNumber}
|
||||
<Title level={4} style={{ margin: 0, color: textColor }}>
|
||||
Test Window #{windowNumber} {isDarkTheme ? '🌙' : '☀️'}
|
||||
</Title>
|
||||
</Space>
|
||||
<Space>
|
||||
<FlaskConical size={20} color="var(--color-primary)" />
|
||||
<Text type="secondary">Cross-Window Sync Testing</Text>
|
||||
<FlaskConical size={20} color={isDarkTheme ? '#fff' : 'var(--color-primary)'} />
|
||||
<Text style={{ color: textColor }}>
|
||||
Cross-Window Sync Testing | {language || 'en-US'} | Zoom: {Math.round(zoomValue * 100)}%
|
||||
</Text>
|
||||
</Space>
|
||||
</HeaderContent>
|
||||
</Header>
|
||||
|
||||
<Content style={{ padding: '24px', overflow: 'auto' }}>
|
||||
<Content style={{ padding: '24px', overflow: 'auto', backgroundColor: isDarkTheme ? '#000' : '#f5f5f5' }}>
|
||||
<Container>
|
||||
<Row gutter={[24, 24]}>
|
||||
{/* Introduction Card */}
|
||||
<Col span={24}>
|
||||
<Card>
|
||||
<Card style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Space align="center">
|
||||
<TestTube size={24} color="var(--color-primary)" />
|
||||
<Title level={3} style={{ margin: 0 }}>
|
||||
<Title level={3} style={{ margin: 0, color: textColor }}>
|
||||
PreferenceService 功能测试 (窗口 #{windowNumber})
|
||||
</Title>
|
||||
</Space>
|
||||
<Text type="secondary">
|
||||
<Text style={{ color: isDarkTheme ? '#d9d9d9' : 'rgba(0, 0, 0, 0.45)' }}>
|
||||
此测试窗口用于验证 PreferenceService 和 usePreference hooks
|
||||
的各项功能,包括单个偏好设置的读写、多个偏好设置的批量操作、跨窗口数据同步等。
|
||||
</Text>
|
||||
<Text type="secondary">测试使用的都是真实的偏好设置系统,所有操作都会影响实际的数据库存储。</Text>
|
||||
<Text type="secondary" style={{ color: 'var(--color-primary)', fontWeight: 'bold' }}>
|
||||
<Text style={{ color: isDarkTheme ? '#d9d9d9' : 'rgba(0, 0, 0, 0.45)' }}>
|
||||
测试使用的都是真实的偏好设置系统,所有操作都会影响实际的数据库存储。
|
||||
</Text>
|
||||
<Text style={{ color: 'var(--color-primary)', fontWeight: 'bold' }}>
|
||||
📋 跨窗口测试指南:在一个窗口中修改偏好设置,观察其他窗口是否实时同步更新。
|
||||
</Text>
|
||||
</Space>
|
||||
@ -68,11 +113,12 @@ const TestApp: React.FC = () => {
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Database size={18} />
|
||||
<span>PreferenceService 基础测试</span>
|
||||
<Database size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>PreferenceService 基础测试</span>
|
||||
</Space>
|
||||
}
|
||||
size="small">
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<PreferenceServiceTests />
|
||||
</Card>
|
||||
</Col>
|
||||
@ -82,11 +128,12 @@ const TestApp: React.FC = () => {
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Settings size={18} />
|
||||
<span>usePreference Hook 测试</span>
|
||||
<Settings size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>usePreference Hook 测试</span>
|
||||
</Space>
|
||||
}
|
||||
size="small">
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<PreferenceBasicTests />
|
||||
</Card>
|
||||
</Col>
|
||||
@ -96,11 +143,12 @@ const TestApp: React.FC = () => {
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Settings size={18} />
|
||||
<span>Hook 高级功能测试</span>
|
||||
<Settings size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>Hook 高级功能测试</span>
|
||||
</Space>
|
||||
}
|
||||
size="small">
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<PreferenceHookTests />
|
||||
</Card>
|
||||
</Col>
|
||||
@ -110,11 +158,12 @@ const TestApp: React.FC = () => {
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Database size={18} />
|
||||
<span>usePreferences 批量操作测试</span>
|
||||
<Database size={18} color={isDarkTheme ? '#fff' : '#000'} />
|
||||
<span style={{ color: textColor }}>usePreferences 批量操作测试</span>
|
||||
</Space>
|
||||
}
|
||||
size="small">
|
||||
size="small"
|
||||
style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}>
|
||||
<PreferenceMultipleTests />
|
||||
</Card>
|
||||
</Col>
|
||||
@ -123,14 +172,28 @@ const TestApp: React.FC = () => {
|
||||
<Divider />
|
||||
|
||||
<Row justify="center">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
logger.info('Closing test window')
|
||||
window.close()
|
||||
}}>
|
||||
关闭测试窗口
|
||||
</Button>
|
||||
<Space>
|
||||
<Button
|
||||
icon={isDarkTheme ? '☀️' : '🌙'}
|
||||
onClick={async () => {
|
||||
await setTheme(isDarkTheme ? 'ThemeMode.light' : 'ThemeMode.dark')
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#434343' : '#f0f0f0',
|
||||
borderColor: borderColor,
|
||||
color: textColor
|
||||
}}>
|
||||
{isDarkTheme ? '切换到亮色主题' : '切换到暗色主题'}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
logger.info('Closing test window')
|
||||
window.close()
|
||||
}}>
|
||||
关闭测试窗口
|
||||
</Button>
|
||||
</Space>
|
||||
</Row>
|
||||
</Container>
|
||||
</Content>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { usePreference } from '@renderer/data/hooks/usePreference'
|
||||
import type { PreferenceKeyType } from '@shared/data/types'
|
||||
import { Button, Input, message, Select, Space, Switch, Typography } from 'antd'
|
||||
import { Button, Input, message, Select, Slider, Space, Switch, Typography } from 'antd'
|
||||
import React, { useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@ -19,6 +19,10 @@ const PreferenceBasicTests: React.FC = () => {
|
||||
|
||||
const [inputValue, setInputValue] = useState<string>('')
|
||||
|
||||
// Add theme monitoring for visual changes
|
||||
const [currentTheme] = usePreference('app.theme.mode')
|
||||
const isDarkTheme = currentTheme === 'ThemeMode.dark'
|
||||
|
||||
const handleSetValue = async () => {
|
||||
try {
|
||||
let parsedValue: any = inputValue
|
||||
@ -47,15 +51,33 @@ const PreferenceBasicTests: React.FC = () => {
|
||||
}
|
||||
|
||||
const testCases = [
|
||||
{ key: 'app.theme.mode', label: 'App Theme Mode', sampleValue: 'ThemeMode.dark' },
|
||||
{ key: 'app.language', label: 'App Language', sampleValue: 'zh-CN' },
|
||||
{ key: 'app.spell_check.enabled', label: 'Spell Check', sampleValue: 'true' },
|
||||
{ key: 'app.zoom_factor', label: 'Zoom Factor', sampleValue: '1.2' },
|
||||
{ key: 'app.tray.enabled', label: 'Tray Enabled', sampleValue: 'true' }
|
||||
{ key: 'app.theme.mode', label: 'App Theme Mode', sampleValue: 'ThemeMode.dark', type: 'enum' },
|
||||
{ key: 'app.language', label: 'App Language', sampleValue: 'zh-CN', type: 'enum' },
|
||||
{ key: 'app.spell_check.enabled', label: 'Spell Check', sampleValue: 'true', type: 'boolean' },
|
||||
{ key: 'app.zoom_factor', label: 'Zoom Factor', sampleValue: '1.2', type: 'number', min: 0.5, max: 2.0, step: 0.1 },
|
||||
{ key: 'app.tray.enabled', label: 'Tray Enabled', sampleValue: 'true', type: 'boolean' },
|
||||
{
|
||||
key: 'chat.message.font_size',
|
||||
label: 'Message Font Size',
|
||||
sampleValue: '14',
|
||||
type: 'number',
|
||||
min: 8,
|
||||
max: 72,
|
||||
step: 1
|
||||
},
|
||||
{
|
||||
key: 'feature.selection.action_window_opacity',
|
||||
label: 'Selection Window Opacity',
|
||||
sampleValue: '95',
|
||||
type: 'number',
|
||||
min: 10,
|
||||
max: 100,
|
||||
step: 5
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<TestContainer>
|
||||
<TestContainer isDark={isDarkTheme}>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
{/* Key Selection */}
|
||||
<div>
|
||||
@ -74,7 +96,7 @@ const PreferenceBasicTests: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Current Value Display */}
|
||||
<CurrentValueContainer>
|
||||
<CurrentValueContainer isDark={isDarkTheme}>
|
||||
<Text strong>当前值:</Text>
|
||||
<ValueDisplay>
|
||||
{value !== undefined ? (
|
||||
@ -109,12 +131,14 @@ const PreferenceBasicTests: React.FC = () => {
|
||||
<div>
|
||||
<Text strong>快速操作:</Text>
|
||||
<Space wrap style={{ marginTop: 8 }}>
|
||||
{/* Theme Toggle */}
|
||||
{/* Theme Toggle with Visual Feedback */}
|
||||
{selectedKey === 'app.theme.mode' && (
|
||||
<Button
|
||||
size="small"
|
||||
type={isDarkTheme ? 'default' : 'primary'}
|
||||
icon={isDarkTheme ? '🌙' : '☀️'}
|
||||
onClick={() => setValue(value === 'ThemeMode.dark' ? 'ThemeMode.light' : 'ThemeMode.dark')}>
|
||||
切换主题 ({value === 'ThemeMode.dark' ? 'light' : 'dark'})
|
||||
切换主题 ({value === 'ThemeMode.dark' ? '→ Light' : '→ Dark'})
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@ -140,20 +164,134 @@ const PreferenceBasicTests: React.FC = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Zoom Factor */}
|
||||
{selectedKey === 'app.zoom_factor' && (
|
||||
<>
|
||||
<Button size="small" onClick={() => setValue(0.8)}>
|
||||
80%
|
||||
</Button>
|
||||
<Button size="small" onClick={() => setValue(1.0)}>
|
||||
100%
|
||||
</Button>
|
||||
<Button size="small" onClick={() => setValue(1.2)}>
|
||||
120%
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{/* Number Type Sliders */}
|
||||
{(() => {
|
||||
const currentTestCase = testCases.find((tc) => tc.key === selectedKey)
|
||||
if (currentTestCase?.type === 'number') {
|
||||
const numValue =
|
||||
typeof value === 'number'
|
||||
? value
|
||||
: typeof value === 'string'
|
||||
? parseFloat(value)
|
||||
: currentTestCase.min || 0
|
||||
const min = currentTestCase.min || 0
|
||||
const max = currentTestCase.max || 100
|
||||
const step = currentTestCase.step || 1
|
||||
|
||||
const getDisplayValue = () => {
|
||||
if (selectedKey === 'app.zoom_factor') {
|
||||
return `${Math.round(numValue * 100)}%`
|
||||
} else if (selectedKey === 'feature.selection.action_window_opacity') {
|
||||
return `${Math.round(numValue)}%`
|
||||
} else {
|
||||
return numValue.toString()
|
||||
}
|
||||
}
|
||||
|
||||
const getMarks = () => {
|
||||
if (selectedKey === 'app.zoom_factor') {
|
||||
return {
|
||||
0.5: '50%',
|
||||
0.8: '80%',
|
||||
1.0: '100%',
|
||||
1.2: '120%',
|
||||
1.5: '150%',
|
||||
2.0: '200%'
|
||||
}
|
||||
} else if (selectedKey === 'chat.message.font_size') {
|
||||
return {
|
||||
8: '8px',
|
||||
12: '12px',
|
||||
14: '14px',
|
||||
16: '16px',
|
||||
18: '18px',
|
||||
24: '24px',
|
||||
36: '36px',
|
||||
72: '72px'
|
||||
}
|
||||
} else if (selectedKey === 'feature.selection.action_window_opacity') {
|
||||
return {
|
||||
10: '10%',
|
||||
30: '30%',
|
||||
50: '50%',
|
||||
70: '70%',
|
||||
90: '90%',
|
||||
100: '100%'
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%', marginTop: 8 }}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>
|
||||
{currentTestCase.label}: <strong>{getDisplayValue()}</strong>
|
||||
</Text>
|
||||
<Slider
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={numValue}
|
||||
onChange={(val) => setValue(val)}
|
||||
marks={getMarks()}
|
||||
tooltip={{
|
||||
formatter: (val) => {
|
||||
if (selectedKey === 'app.zoom_factor') {
|
||||
return `${Math.round((val || 0) * 100)}%`
|
||||
} else if (selectedKey === 'feature.selection.action_window_opacity') {
|
||||
return `${Math.round(val || 0)}%`
|
||||
}
|
||||
return val?.toString() || '0'
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%', marginBottom: 8 }}
|
||||
/>
|
||||
{selectedKey === 'app.zoom_factor' && (
|
||||
<Space>
|
||||
<Button size="small" onClick={() => setValue(0.8)}>
|
||||
80%
|
||||
</Button>
|
||||
<Button size="small" onClick={() => setValue(1.0)}>
|
||||
100%
|
||||
</Button>
|
||||
<Button size="small" onClick={() => setValue(1.2)}>
|
||||
120%
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
{selectedKey === 'chat.message.font_size' && (
|
||||
<Space>
|
||||
<Button size="small" onClick={() => setValue(12)}>
|
||||
Small
|
||||
</Button>
|
||||
<Button size="small" onClick={() => setValue(14)}>
|
||||
Normal
|
||||
</Button>
|
||||
<Button size="small" onClick={() => setValue(16)}>
|
||||
Large
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
{selectedKey === 'feature.selection.action_window_opacity' && (
|
||||
<Space>
|
||||
<Button size="small" onClick={() => setValue(50)}>
|
||||
50%
|
||||
</Button>
|
||||
<Button size="small" onClick={() => setValue(80)}>
|
||||
80%
|
||||
</Button>
|
||||
<Button size="small" onClick={() => setValue(100)}>
|
||||
100%
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})()}
|
||||
|
||||
{/* Sample Values */}
|
||||
<Button
|
||||
@ -181,15 +319,31 @@ const PreferenceBasicTests: React.FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const TestContainer = styled.div`
|
||||
const TestContainer = styled.div<{ isDark: boolean }>`
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
background: ${(props) => (props.isDark ? '#262626' : '#fafafa')};
|
||||
border-radius: 8px;
|
||||
|
||||
.ant-typography {
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
`
|
||||
|
||||
const CurrentValueContainer = styled.div`
|
||||
const CurrentValueContainer = styled.div<{ isDark?: boolean }>`
|
||||
padding: 12px;
|
||||
background: #f0f0f0;
|
||||
background: ${(props) => (props.isDark ? '#1f1f1f' : '#f0f0f0')};
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid var(--color-primary);
|
||||
`
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { usePreference, usePreferencePreload, usePreferenceService } from '@renderer/data/hooks/usePreference'
|
||||
import { usePreference } from '@renderer/data/hooks/usePreference'
|
||||
import { preferenceService } from '@renderer/data/PreferenceService'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import type { PreferenceKeyType } from '@shared/data/types'
|
||||
import { Button, Card, message, Space, Typography } from 'antd'
|
||||
import React, { useState } from 'react'
|
||||
@ -6,12 +8,13 @@ import styled from 'styled-components'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
const logger = loggerService.withContext('PreferenceHookTests')
|
||||
|
||||
/**
|
||||
* Advanced usePreference hook testing component
|
||||
* Tests preloading, service access, and hook behavior
|
||||
*/
|
||||
const PreferenceHookTests: React.FC = () => {
|
||||
const preferenceService = usePreferenceService()
|
||||
const [subscriptionCount, setSubscriptionCount] = useState(0)
|
||||
|
||||
// Test multiple hooks with same key
|
||||
@ -19,8 +22,13 @@ const PreferenceHookTests: React.FC = () => {
|
||||
const [theme2] = usePreference('app.theme.mode')
|
||||
const [language] = usePreference('app.language')
|
||||
|
||||
// Preload test
|
||||
usePreferencePreload(['app.theme.mode', 'app.language', 'app.zoom_factor'])
|
||||
// Manual preload implementation using useEffect
|
||||
React.useEffect(() => {
|
||||
const preloadKeys: PreferenceKeyType[] = ['app.theme.mode', 'app.language', 'app.zoom_factor']
|
||||
preferenceService.preload(preloadKeys).catch((error) => {
|
||||
logger.error('Failed to preload preferences:', error as Error)
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Use useRef to track render count without causing re-renders
|
||||
const renderCountRef = React.useRef(0)
|
||||
@ -28,7 +36,7 @@ const PreferenceHookTests: React.FC = () => {
|
||||
|
||||
const testSubscriptions = () => {
|
||||
// Test subscription behavior
|
||||
const unsubscribe = preferenceService.subscribeToKey('app.theme.mode')(() => {
|
||||
const unsubscribe = preferenceService.subscribeKeyChange('app.theme.mode')(() => {
|
||||
setSubscriptionCount((prev) => prev + 1)
|
||||
})
|
||||
|
||||
@ -53,7 +61,7 @@ const PreferenceHookTests: React.FC = () => {
|
||||
}))
|
||||
|
||||
message.success(`预加载完成。缓存状态: ${cachedStates.filter((s) => s.isCached).length}/${keys.length}`)
|
||||
console.log('Cache states:', cachedStates)
|
||||
logger.debug('Cache states:', { cachedStates })
|
||||
} catch (error) {
|
||||
message.error(`预加载失败: ${(error as Error).message}`)
|
||||
}
|
||||
@ -65,7 +73,7 @@ const PreferenceHookTests: React.FC = () => {
|
||||
const result = await preferenceService.getMultiple(keys)
|
||||
|
||||
message.success(`批量获取成功: ${Object.keys(result).length} 项`)
|
||||
console.log('Batch get result:', result)
|
||||
logger.debug('Batch get result:', { result })
|
||||
|
||||
// Test batch set
|
||||
await preferenceService.setMultiple({
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useMultiplePreferences } from '@renderer/data/hooks/usePreference'
|
||||
import { Button, Card, Input, message, Select, Space, Table, Typography } from 'antd'
|
||||
import { Button, Card, Input, message, Select, Slider, Space, Table, Typography } from 'antd'
|
||||
import { ColumnType } from 'antd/es/table'
|
||||
import React, { useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
@ -31,6 +31,11 @@ const PreferenceMultipleTests: React.FC = () => {
|
||||
userName: 'app.user.name',
|
||||
devMode: 'app.developer_mode.enabled'
|
||||
},
|
||||
numbers: {
|
||||
zoomFactor: 'app.zoom_factor',
|
||||
fontSize: 'chat.message.font_size',
|
||||
opacity: 'feature.selection.action_window_opacity'
|
||||
},
|
||||
custom: {
|
||||
key1: 'app.theme.mode',
|
||||
key2: 'app.language',
|
||||
@ -86,6 +91,12 @@ const PreferenceMultipleTests: React.FC = () => {
|
||||
case 'app.spell_check.enabled':
|
||||
sampleUpdates[localKey] = !values[localKey]
|
||||
break
|
||||
case 'chat.message.font_size':
|
||||
sampleUpdates[localKey] = 14 + index * 2
|
||||
break
|
||||
case 'feature.selection.action_window_opacity':
|
||||
sampleUpdates[localKey] = 80 + index * 10
|
||||
break
|
||||
default:
|
||||
sampleUpdates[localKey] = `sample_value_${index}`
|
||||
}
|
||||
@ -184,10 +195,11 @@ const PreferenceMultipleTests: React.FC = () => {
|
||||
<Card size="small" title="测试场景选择">
|
||||
<Space align="center" wrap>
|
||||
<Text>选择测试场景:</Text>
|
||||
<Select value={scenario} onChange={setScenario} style={{ width: 200 }}>
|
||||
<Select value={scenario} onChange={setScenario} style={{ width: 250 }}>
|
||||
<Option value="basic">基础设置 (theme, language, zoom)</Option>
|
||||
<Option value="ui">UI设置 (theme, zoom, spell)</Option>
|
||||
<Option value="user">用户设置 (tray, userName, devMode)</Option>
|
||||
<Option value="numbers">🎛️ 数值设置 (zoom, fontSize, selection opacity)</Option>
|
||||
<Option value="custom">自定义组合 (4项设置)</Option>
|
||||
</Select>
|
||||
<Text type="secondary">当前映射: {Object.keys(currentKeys).length} 项</Text>
|
||||
@ -199,6 +211,86 @@ const PreferenceMultipleTests: React.FC = () => {
|
||||
<Table columns={columns} dataSource={tableData} pagination={false} size="small" bordered />
|
||||
</Card>
|
||||
|
||||
{/* Interactive Slider Controls for Numbers Scenario */}
|
||||
{scenario === 'numbers' && (
|
||||
<Card size="small" title="🎛️ 实时Slider联动控制" style={{ backgroundColor: '#f0f8ff' }}>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Text type="secondary">拖动任一滑块,观察其他窗口中相同偏好设置的实时同步变化</Text>
|
||||
|
||||
{/* Zoom Factor Slider */}
|
||||
<div>
|
||||
<Text strong>
|
||||
缩放因子: {Math.round((typeof (values as any).zoomFactor === 'number' ? (values as any).zoomFactor : 1.0) * 100)}%
|
||||
</Text>
|
||||
<Slider
|
||||
min={0.5}
|
||||
max={2.0}
|
||||
step={0.1}
|
||||
value={typeof (values as any).zoomFactor === 'number' ? (values as any).zoomFactor : 1.0}
|
||||
onChange={(val) => updateValues({ zoomFactor: val } as any)}
|
||||
marks={{
|
||||
0.5: '50%',
|
||||
1.0: '100%',
|
||||
1.5: '150%',
|
||||
2.0: '200%'
|
||||
}}
|
||||
tooltip={{ formatter: (val) => `${Math.round((val || 1) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Font Size Slider */}
|
||||
<div>
|
||||
<Text strong>字体大小: {typeof (values as any).fontSize === 'number' ? (values as any).fontSize : 14}px</Text>
|
||||
<Slider
|
||||
min={8}
|
||||
max={72}
|
||||
step={1}
|
||||
value={typeof (values as any).fontSize === 'number' ? (values as any).fontSize : 14}
|
||||
onChange={(val) => updateValues({ fontSize: val } as any)}
|
||||
marks={{
|
||||
8: '8px',
|
||||
12: '12px',
|
||||
16: '16px',
|
||||
24: '24px',
|
||||
36: '36px',
|
||||
72: '72px'
|
||||
}}
|
||||
tooltip={{ formatter: (val) => `${val}px` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Selection Window Opacity Slider */}
|
||||
<div>
|
||||
<Text strong>
|
||||
选择窗口透明度: {Math.round(typeof (values as any).opacity === 'number' ? (values as any).opacity : 100)}%
|
||||
</Text>
|
||||
<Slider
|
||||
min={10}
|
||||
max={100}
|
||||
step={5}
|
||||
value={typeof (values as any).opacity === 'number' ? (values as any).opacity : 100}
|
||||
onChange={(val) => updateValues({ opacity: val } as any)}
|
||||
marks={{
|
||||
10: '10%',
|
||||
30: '30%',
|
||||
50: '50%',
|
||||
70: '70%',
|
||||
90: '90%',
|
||||
100: '100%'
|
||||
}}
|
||||
tooltip={{ formatter: (val) => `${Math.round(val || 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '8px', backgroundColor: '#e6f7ff', borderRadius: '4px' }}>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
💡 测试提示:同时拖动多个滑块,观察另一个测试窗口中的同步效果!
|
||||
</Text>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Batch Update */}
|
||||
<Card size="small" title="批量更新">
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { usePreference } from '@renderer/data/hooks/usePreference'
|
||||
import { preferenceService } from '@renderer/data/PreferenceService'
|
||||
import type { PreferenceKeyType } from '@shared/data/types'
|
||||
import { Button, Input, message, Space, Typography } from 'antd'
|
||||
@ -16,6 +17,10 @@ const PreferenceServiceTests: React.FC = () => {
|
||||
const [getResult, setGetResult] = useState<any>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// Theme monitoring for visual changes
|
||||
const [currentTheme] = usePreference('app.theme.mode')
|
||||
const isDarkTheme = currentTheme === 'ThemeMode.dark'
|
||||
|
||||
const handleGet = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
@ -98,17 +103,10 @@ const PreferenceServiceTests: React.FC = () => {
|
||||
const handleGetAll = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
// Get multiple keys to simulate getAll functionality
|
||||
const sampleKeys = [
|
||||
'app.theme.mode',
|
||||
'app.language',
|
||||
'app.zoom_factor',
|
||||
'app.spell_check.enabled',
|
||||
'app.user.name'
|
||||
] as PreferenceKeyType[]
|
||||
const result = await preferenceService.getMultiple(sampleKeys)
|
||||
setGetResult(`Sample preferences (${Object.keys(result).length} keys):\n${JSON.stringify(result, null, 2)}`)
|
||||
message.success('获取示例偏好设置成功')
|
||||
// Use loadAll to get all preferences at once
|
||||
const result = await preferenceService.loadAll()
|
||||
setGetResult(`All preferences (${Object.keys(result).length} keys):\n${JSON.stringify(result, null, 2)}`)
|
||||
message.success('获取所有偏好设置成功')
|
||||
} catch (error) {
|
||||
message.error(`获取偏好设置失败: ${(error as Error).message}`)
|
||||
setGetResult(`Error: ${(error as Error).message}`)
|
||||
@ -118,7 +116,7 @@ const PreferenceServiceTests: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<TestContainer>
|
||||
<TestContainer isDark={isDarkTheme}>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
{/* Input Controls */}
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
@ -156,13 +154,13 @@ const PreferenceServiceTests: React.FC = () => {
|
||||
Preload
|
||||
</Button>
|
||||
<Button onClick={handleGetAll} loading={loading}>
|
||||
Get All
|
||||
Load All
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
{/* Result Display */}
|
||||
{getResult !== null && (
|
||||
<ResultContainer>
|
||||
<ResultContainer isDark={isDarkTheme}>
|
||||
<Text strong>Result:</Text>
|
||||
<ResultText>
|
||||
{typeof getResult === 'object' ? JSON.stringify(getResult, null, 2) : String(getResult)}
|
||||
@ -202,16 +200,26 @@ const PreferenceServiceTests: React.FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const TestContainer = styled.div`
|
||||
const TestContainer = styled.div<{ isDark: boolean }>`
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
background: ${(props) => (props.isDark ? '#262626' : '#fafafa')};
|
||||
border-radius: 8px;
|
||||
|
||||
.ant-typography {
|
||||
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;
|
||||
}
|
||||
`
|
||||
|
||||
const ResultContainer = styled.div`
|
||||
const ResultContainer = styled.div<{ isDark?: boolean }>`
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
background: #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