mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-28 05:11:24 +08:00
feat(preferences): integrate PreferenceService and enhance testing capabilities
This commit refactors the PreferenceService to use named exports for better consistency and updates the DbService import accordingly. It introduces a testing mechanism for the PreferenceService by creating test windows to facilitate cross-window preference synchronization testing. Additionally, improvements are made to the usePreference hook for better performance and stability, ensuring efficient preference management in the application.
This commit is contained in:
parent
c02f93e6b9
commit
b219e96544
@ -1,3 +1,4 @@
|
||||
import { dbService } from '@data/db/DbService'
|
||||
import { loggerService } from '@logger'
|
||||
import { DefaultPreferences } from '@shared/data/preferences'
|
||||
import type { PreferenceDefaultScopeType, PreferenceKeyType } from '@shared/data/types'
|
||||
@ -5,7 +6,6 @@ import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { BrowserWindow } from 'electron'
|
||||
|
||||
import dbService from './db/DbService'
|
||||
import { preferenceTable } from './db/schemas/preference'
|
||||
|
||||
const logger = loggerService.withContext('PreferenceService')
|
||||
@ -296,4 +296,3 @@ export class PreferenceService {
|
||||
|
||||
// Export singleton instance
|
||||
export const preferenceService = PreferenceService.getInstance()
|
||||
export default preferenceService
|
||||
|
||||
@ -67,6 +67,4 @@ class DbService {
|
||||
}
|
||||
|
||||
// Export a singleton instance
|
||||
const dbService = DbService.getInstance()
|
||||
|
||||
export default dbService
|
||||
export const dbService = DbService.getInstance()
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import dbService from '@data/db/DbService'
|
||||
import { dbService } from '@data/db/DbService'
|
||||
import { appStateTable } from '@data/db/schemas/appState'
|
||||
import { loggerService } from '@logger'
|
||||
import { isDev } from '@main/constant'
|
||||
@ -48,6 +48,7 @@ interface MigrationResult {
|
||||
export class DataRefactorMigrateService {
|
||||
private static instance: DataRefactorMigrateService | null = null
|
||||
private migrateWindow: BrowserWindow | null = null
|
||||
private testWindows: BrowserWindow[] = []
|
||||
private backupManager: BackupManager
|
||||
private db = dbService.getDb()
|
||||
private currentProgress: MigrationProgress = {
|
||||
@ -302,6 +303,14 @@ export class DataRefactorMigrateService {
|
||||
return DataRefactorMigrateService.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient static method to open test window
|
||||
*/
|
||||
public static openTestWindow(): BrowserWindow {
|
||||
const instance = DataRefactorMigrateService.getInstance()
|
||||
return instance.createTestWindow()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if migration is needed
|
||||
*/
|
||||
@ -862,6 +871,112 @@ export class DataRefactorMigrateService {
|
||||
// Don't throw - still allow restart
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and show test window for testing PreferenceService and usePreference functionality
|
||||
*/
|
||||
public createTestWindow(): BrowserWindow {
|
||||
const windowNumber = this.testWindows.length + 1
|
||||
|
||||
const testWindow = new BrowserWindow({
|
||||
width: 1000,
|
||||
height: 700,
|
||||
minWidth: 800,
|
||||
minHeight: 600,
|
||||
resizable: true,
|
||||
maximizable: true,
|
||||
minimizable: true,
|
||||
show: false,
|
||||
frame: true,
|
||||
autoHideMenuBar: true,
|
||||
title: `Data Refactor Test Window #${windowNumber} - PreferenceService Testing`,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
contextIsolation: true
|
||||
}
|
||||
})
|
||||
|
||||
// Add to test windows array
|
||||
this.testWindows.push(testWindow)
|
||||
|
||||
// Load the test window
|
||||
if (isDev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
testWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/dataRefactorTest.html')
|
||||
// Open DevTools in development mode for easier testing
|
||||
testWindow.webContents.openDevTools()
|
||||
} else {
|
||||
testWindow.loadFile(join(__dirname, '../renderer/dataRefactorTest.html'))
|
||||
}
|
||||
|
||||
testWindow.once('ready-to-show', () => {
|
||||
testWindow?.show()
|
||||
testWindow?.focus()
|
||||
})
|
||||
|
||||
testWindow.on('closed', () => {
|
||||
// Remove from test windows array when closed
|
||||
const index = this.testWindows.indexOf(testWindow)
|
||||
if (index > -1) {
|
||||
this.testWindows.splice(index, 1)
|
||||
}
|
||||
})
|
||||
|
||||
logger.info(`Test window #${windowNumber} created for PreferenceService testing`)
|
||||
return testWindow
|
||||
}
|
||||
|
||||
/**
|
||||
* Get test window instance (first one)
|
||||
*/
|
||||
public getTestWindow(): BrowserWindow | null {
|
||||
return this.testWindows.length > 0 ? this.testWindows[0] : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all test windows
|
||||
*/
|
||||
public getTestWindows(): BrowserWindow[] {
|
||||
return this.testWindows.filter((window) => !window.isDestroyed())
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all test windows
|
||||
*/
|
||||
public closeTestWindows(): void {
|
||||
this.testWindows.forEach((window) => {
|
||||
if (!window.isDestroyed()) {
|
||||
window.close()
|
||||
}
|
||||
})
|
||||
this.testWindows = []
|
||||
logger.info('All test windows closed')
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a specific test window
|
||||
*/
|
||||
public closeTestWindow(window?: BrowserWindow): void {
|
||||
if (window) {
|
||||
if (!window.isDestroyed()) {
|
||||
window.close()
|
||||
}
|
||||
} else {
|
||||
// Close first window if no specific window provided
|
||||
const firstWindow = this.getTestWindow()
|
||||
if (firstWindow && !firstWindow.isDestroyed()) {
|
||||
firstWindow.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any test windows are open
|
||||
*/
|
||||
public isTestWindowOpen(): boolean {
|
||||
return this.testWindows.some((window) => !window.isDestroyed())
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import dbService from '@data/db/DbService'
|
||||
import { dbService } from '@data/db/DbService'
|
||||
import { preferenceTable } from '@data/db/schemas/preference'
|
||||
import { loggerService } from '@logger'
|
||||
import { DefaultPreferences } from '@shared/data/preferences'
|
||||
|
||||
@ -7,7 +7,8 @@ import '@main/config'
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||
import dbService from '@data/db/DbService'
|
||||
import { dbService } from '@data/db/DbService'
|
||||
import { preferenceService } from '@data/PreferenceService'
|
||||
import { replaceDevtoolsFont } from '@main/utils/windowUtil'
|
||||
import { app, dialog } from 'electron'
|
||||
import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||
@ -154,6 +155,22 @@ if (!app.requestSingleInstanceLock()) {
|
||||
return
|
||||
}
|
||||
|
||||
//************FOR TESTING ONLY START****************/
|
||||
|
||||
await preferenceService.initialize()
|
||||
|
||||
// Create two test windows for cross-window preference sync testing
|
||||
logger.info('Creating test windows for PreferenceService cross-window sync testing')
|
||||
const testWindow1 = dataRefactorMigrateService.createTestWindow()
|
||||
const testWindow2 = dataRefactorMigrateService.createTestWindow()
|
||||
|
||||
// Position windows to avoid overlap
|
||||
testWindow1.once('ready-to-show', () => {
|
||||
const [x, y] = testWindow1.getPosition()
|
||||
testWindow2.setPosition(x + 50, y + 50)
|
||||
})
|
||||
/************FOR TESTING ONLY END****************/
|
||||
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import fs from 'node:fs'
|
||||
import { arch } from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
import { preferenceService } from '@data/PreferenceService'
|
||||
import { loggerService } from '@logger'
|
||||
import { isLinux, isMac, isPortable, isWin } from '@main/constant'
|
||||
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
|
||||
@ -14,7 +15,6 @@ import { FileMetadata, Provider, Shortcut, ThemeMode } from '@types'
|
||||
import { BrowserWindow, dialog, ipcMain, ProxyConfig, session, shell, systemPreferences, webContents } from 'electron'
|
||||
import { Notification } from 'src/renderer/src/types/notification'
|
||||
|
||||
import preferenceService from './data/PreferenceService'
|
||||
import appService from './services/AppService'
|
||||
import AppUpdater from './services/AppUpdater'
|
||||
import BackupManager from './services/BackupManager'
|
||||
|
||||
63
src/renderer/dataRefactorTest.html
Normal file
63
src/renderer/dataRefactorTest.html
Normal file
@ -0,0 +1,63 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Data Refactor Test Window - PreferenceService Testing</title>
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:;" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</head>
|
||||
|
||||
<body id="root" theme-mode="light">
|
||||
<script type="module" src="/src/windows/dataRefactorTest/entryPoint.tsx"></script>
|
||||
<style>
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
#root {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Custom button styles */
|
||||
.ant-btn-primary {
|
||||
background-color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.ant-btn-primary:hover {
|
||||
background-color: var(--color-primary-soft) !important;
|
||||
border-color: var(--color-primary-soft) !important;
|
||||
}
|
||||
|
||||
.ant-btn-primary:active,
|
||||
.ant-btn-primary:focus {
|
||||
background-color: var(--color-primary) !important;
|
||||
border-color: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
/* Non-primary button hover styles */
|
||||
.ant-btn:not(.ant-btn-primary):hover {
|
||||
border-color: var(--color-primary-soft) !important;
|
||||
color: var(--color-primary) !important;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,6 +1,6 @@
|
||||
import { loggerService } from '@logger'
|
||||
import type { PreferenceDefaultScopeType, PreferenceKeyType } from '@shared/data/types'
|
||||
import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'
|
||||
|
||||
import { preferenceService } from '../PreferenceService'
|
||||
|
||||
@ -18,8 +18,8 @@ export function usePreference<K extends PreferenceKeyType>(
|
||||
): [PreferenceDefaultScopeType[K] | undefined, (value: PreferenceDefaultScopeType[K]) => Promise<void>] {
|
||||
// Subscribe to changes for this specific preference
|
||||
const value = useSyncExternalStore(
|
||||
preferenceService.subscribeToKey(key),
|
||||
preferenceService.getSnapshot(key),
|
||||
useCallback((callback) => preferenceService.subscribeToKey(key)(callback), [key]),
|
||||
useCallback(() => preferenceService.getCachedValue(key), [key]),
|
||||
() => undefined // SSR snapshot (not used in Electron context)
|
||||
)
|
||||
|
||||
@ -61,12 +61,16 @@ export function usePreferences<T extends Record<string, PreferenceKeyType>>(
|
||||
{ [P in keyof T]: PreferenceDefaultScopeType[T[P]] | undefined },
|
||||
(updates: Partial<{ [P in keyof T]: PreferenceDefaultScopeType[T[P]] }>) => Promise<void>
|
||||
] {
|
||||
// Track changes to any of the specified keys
|
||||
// Create stable key dependencies
|
||||
const keyList = useMemo(() => Object.values(keys), [keys])
|
||||
const keyListString = keyList.join(',')
|
||||
const keysStringified = useMemo(() => JSON.stringify(keys), [keys])
|
||||
|
||||
// Cache the last snapshot to avoid infinite loops
|
||||
const lastSnapshotRef = useRef<Record<string, any>>({})
|
||||
|
||||
const allValues = useSyncExternalStore(
|
||||
useCallback(
|
||||
(callback) => {
|
||||
(callback: () => void) => {
|
||||
// Subscribe to all keys and aggregate the unsubscribe functions
|
||||
const unsubscribeFunctions = keyList.map((key) => preferenceService.subscribeToKey(key)(callback))
|
||||
|
||||
@ -74,17 +78,30 @@ export function usePreferences<T extends Record<string, PreferenceKeyType>>(
|
||||
unsubscribeFunctions.forEach((unsubscribe) => unsubscribe())
|
||||
}
|
||||
},
|
||||
[keyList]
|
||||
[keysStringified]
|
||||
),
|
||||
|
||||
useCallback(() => {
|
||||
// Return current snapshot of all values
|
||||
const snapshot: Record<string, any> = {}
|
||||
// Check if any values have actually changed
|
||||
let hasChanged = Object.keys(lastSnapshotRef.current).length === 0 // First time
|
||||
const newSnapshot: Record<string, any> = {}
|
||||
|
||||
for (const [localKey, prefKey] of Object.entries(keys)) {
|
||||
snapshot[localKey] = preferenceService.getCachedValue(prefKey)
|
||||
const currentValue = preferenceService.getCachedValue(prefKey)
|
||||
newSnapshot[localKey] = currentValue
|
||||
|
||||
if (!hasChanged && lastSnapshotRef.current[localKey] !== currentValue) {
|
||||
hasChanged = true
|
||||
}
|
||||
}
|
||||
return snapshot
|
||||
}, [keys]),
|
||||
|
||||
// Only create new object if data actually changed
|
||||
if (hasChanged) {
|
||||
lastSnapshotRef.current = newSnapshot
|
||||
}
|
||||
|
||||
return lastSnapshotRef.current
|
||||
}, [keysStringified]),
|
||||
|
||||
() => ({}) // SSR snapshot
|
||||
)
|
||||
@ -98,7 +115,7 @@ export function usePreferences<T extends Record<string, PreferenceKeyType>>(
|
||||
logger.error('Failed to load initial preferences:', error as Error)
|
||||
})
|
||||
}
|
||||
}, [keyList, keyListString])
|
||||
}, [keysStringified])
|
||||
|
||||
// Memoized batch update function
|
||||
const updateValues = useCallback(
|
||||
@ -119,7 +136,7 @@ export function usePreferences<T extends Record<string, PreferenceKeyType>>(
|
||||
throw error
|
||||
}
|
||||
},
|
||||
[keys]
|
||||
[keysStringified]
|
||||
)
|
||||
|
||||
// Type-cast the values to the expected shape
|
||||
@ -135,12 +152,12 @@ export function usePreferences<T extends Record<string, PreferenceKeyType>>(
|
||||
* @param keys - Array of preference keys to preload
|
||||
*/
|
||||
export function usePreferencePreload(keys: PreferenceKeyType[]): void {
|
||||
const keysString = keys.join(',')
|
||||
const keysString = useMemo(() => keys.join(','), [keys])
|
||||
useEffect(() => {
|
||||
preferenceService.preload(keys).catch((error) => {
|
||||
logger.error('Failed to preload preferences:', error as Error)
|
||||
})
|
||||
}, [keys, keysString])
|
||||
}, [keysString])
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
118
src/renderer/src/windows/dataRefactorTest/README.md
Normal file
118
src/renderer/src/windows/dataRefactorTest/README.md
Normal file
@ -0,0 +1,118 @@
|
||||
# PreferenceService 测试窗口
|
||||
|
||||
专用于测试 PreferenceService 和 usePreference hooks 功能的独立测试窗口系统。
|
||||
|
||||
## 🎯 当前实现
|
||||
|
||||
✅ **已完成的功能**:
|
||||
- 专用的测试窗口 (DataRefactorTestWindow)
|
||||
- **双窗口启动**:应用启动时会同时打开主窗口和两个测试窗口
|
||||
- **跨窗口同步测试**:两个测试窗口可以相互验证偏好设置的实时同步
|
||||
- 完整的测试界面,包含4个专业测试组件
|
||||
- 自动窗口定位,避免重叠
|
||||
- 窗口编号标识,便于区分
|
||||
|
||||
## 测试组件
|
||||
|
||||
### 1. PreferenceService 基础测试
|
||||
- 直接测试服务层API:get, set, getCachedValue, isCached, preload, getMultiple
|
||||
- 支持各种数据类型:字符串、数字、布尔值、JSON对象
|
||||
- 实时显示操作结果
|
||||
|
||||
### 2. usePreference Hook 测试
|
||||
- 测试单个偏好设置的React hooks
|
||||
- 支持的测试键:
|
||||
- `app.theme.mode` - 主题模式
|
||||
- `app.language` - 语言设置
|
||||
- `app.spell_check.enabled` - 拼写检查
|
||||
- `app.zoom_factor` - 缩放因子
|
||||
- 实时值更新和类型转换
|
||||
|
||||
### 3. usePreferences 批量操作测试
|
||||
- 测试多个偏好设置的批量管理
|
||||
- 4种预设场景:基础设置、UI设置、用户设置、自定义组合
|
||||
- 批量更新功能,支持JSON格式输入
|
||||
- 快速切换操作
|
||||
|
||||
### 4. Hook 高级功能测试
|
||||
- 预加载机制测试
|
||||
- 订阅机制验证
|
||||
- 缓存管理测试
|
||||
- 性能测试
|
||||
- 多个hook实例同步测试
|
||||
|
||||
## 启动方式
|
||||
|
||||
**自动启动**:应用正常启动时会自动创建两个测试窗口,窗口会自动错位显示避免重叠
|
||||
|
||||
**手动启动**:
|
||||
```javascript
|
||||
// 在开发者控制台中执行 - 创建单个测试窗口
|
||||
const { dataRefactorMigrateService } = require('./out/main/data/migrate/dataRefactor/DataRefactorMigrateService')
|
||||
dataRefactorMigrateService.createTestWindow()
|
||||
|
||||
// 创建多个测试窗口
|
||||
dataRefactorMigrateService.createTestWindow() // 第二个窗口
|
||||
dataRefactorMigrateService.createTestWindow() // 第三个窗口...
|
||||
|
||||
// 关闭所有测试窗口
|
||||
dataRefactorMigrateService.closeTestWindows()
|
||||
```
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
src/renderer/src/windows/dataRefactorTest/
|
||||
├── entryPoint.tsx # 窗口入口
|
||||
├── TestApp.tsx # 主应用组件
|
||||
└── components/
|
||||
├── PreferenceServiceTests.tsx # 服务层测试
|
||||
├── PreferenceBasicTests.tsx # 基础Hook测试
|
||||
├── PreferenceHookTests.tsx # 高级Hook测试
|
||||
└── PreferenceMultipleTests.tsx # 批量操作测试
|
||||
```
|
||||
|
||||
## 跨窗口同步测试
|
||||
|
||||
🔄 **测试场景**:
|
||||
1. **实时同步验证**:在窗口#1中修改某个偏好设置,立即观察窗口#2是否同步更新
|
||||
2. **并发修改测试**:在两个窗口中快速连续修改同一设置,验证数据一致性
|
||||
3. **批量操作同步**:在一个窗口中批量更新多个设置,观察另一个窗口的同步表现
|
||||
4. **Hook实例同步**:验证多个usePreference hook实例是否正确同步
|
||||
|
||||
📋 **测试步骤**:
|
||||
1. 同时打开两个测试窗口(自动启动)
|
||||
2. 选择相同的偏好设置键进行测试
|
||||
3. 在窗口#1中修改值,观察窗口#2的反应
|
||||
4. 检查"Hook 高级功能测试"中的订阅触发次数是否增加
|
||||
5. 验证缓存状态和实时数据的一致性
|
||||
|
||||
## 注意事项
|
||||
|
||||
⚠️ **所有测试操作都会影响真实的数据库存储!**
|
||||
|
||||
- 测试使用真实的偏好设置系统
|
||||
- 修改的值会同步到主应用和所有测试窗口
|
||||
- 可以在主应用、测试窗口#1、测试窗口#2之间看到实时同步效果
|
||||
|
||||
## 开发模式特性
|
||||
|
||||
- 自动打开DevTools便于调试
|
||||
- 支持热重载
|
||||
- 完整的TypeScript类型检查
|
||||
- React DevTools支持
|
||||
|
||||
## 💡 快速开始
|
||||
|
||||
1. **启动应用** - 自动打开2个测试窗口
|
||||
2. **选择测试** - 在"usePreference Hook 测试"中选择要测试的偏好设置键
|
||||
3. **跨窗口验证** - 在窗口#1中修改值,观察窗口#2是否同步
|
||||
4. **高级测试** - 使用"Hook 高级功能测试"验证订阅和缓存机制
|
||||
5. **批量测试** - 使用"usePreferences 批量操作测试"进行多项设置测试
|
||||
|
||||
## 🔧 技术实现
|
||||
|
||||
- **窗口管理**: DataRefactorMigrateService 单例管理多个测试窗口
|
||||
- **数据同步**: 基于真实的 PreferenceService 和 IPC 通信
|
||||
- **UI框架**: Ant Design + styled-components + React 18
|
||||
- **类型安全**: 完整的 TypeScript 类型检查和偏好设置键约束
|
||||
153
src/renderer/src/windows/dataRefactorTest/TestApp.tsx
Normal file
153
src/renderer/src/windows/dataRefactorTest/TestApp.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
import { AppLogo } from '@renderer/config/env'
|
||||
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'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import PreferenceBasicTests from './components/PreferenceBasicTests'
|
||||
import PreferenceHookTests from './components/PreferenceHookTests'
|
||||
import PreferenceMultipleTests from './components/PreferenceMultipleTests'
|
||||
import PreferenceServiceTests from './components/PreferenceServiceTests'
|
||||
|
||||
const { Header, Content } = Layout
|
||||
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'
|
||||
|
||||
return (
|
||||
<Layout style={{ height: '100vh' }}>
|
||||
<Header style={{ background: '#fff', borderBottom: '1px solid #f0f0f0', padding: '0 24px' }}>
|
||||
<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>
|
||||
</Space>
|
||||
<Space>
|
||||
<FlaskConical size={20} color="var(--color-primary)" />
|
||||
<Text type="secondary">Cross-Window Sync Testing</Text>
|
||||
</Space>
|
||||
</HeaderContent>
|
||||
</Header>
|
||||
|
||||
<Content style={{ padding: '24px', overflow: 'auto' }}>
|
||||
<Container>
|
||||
<Row gutter={[24, 24]}>
|
||||
{/* Introduction Card */}
|
||||
<Col span={24}>
|
||||
<Card>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Space align="center">
|
||||
<TestTube size={24} color="var(--color-primary)" />
|
||||
<Title level={3} style={{ margin: 0 }}>
|
||||
PreferenceService 功能测试 (窗口 #{windowNumber})
|
||||
</Title>
|
||||
</Space>
|
||||
<Text type="secondary">
|
||||
此测试窗口用于验证 PreferenceService 和 usePreference hooks
|
||||
的各项功能,包括单个偏好设置的读写、多个偏好设置的批量操作、跨窗口数据同步等。
|
||||
</Text>
|
||||
<Text type="secondary">测试使用的都是真实的偏好设置系统,所有操作都会影响实际的数据库存储。</Text>
|
||||
<Text type="secondary" style={{ color: 'var(--color-primary)', fontWeight: 'bold' }}>
|
||||
📋 跨窗口测试指南:在一个窗口中修改偏好设置,观察其他窗口是否实时同步更新。
|
||||
</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* PreferenceService Basic Tests */}
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Database size={18} />
|
||||
<span>PreferenceService 基础测试</span>
|
||||
</Space>
|
||||
}
|
||||
size="small">
|
||||
<PreferenceServiceTests />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* Basic Hook Tests */}
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Settings size={18} />
|
||||
<span>usePreference Hook 测试</span>
|
||||
</Space>
|
||||
}
|
||||
size="small">
|
||||
<PreferenceBasicTests />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* Hook Tests */}
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Settings size={18} />
|
||||
<span>Hook 高级功能测试</span>
|
||||
</Space>
|
||||
}
|
||||
size="small">
|
||||
<PreferenceHookTests />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* Multiple Preferences Tests */}
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Database size={18} />
|
||||
<span>usePreferences 批量操作测试</span>
|
||||
</Space>
|
||||
}
|
||||
size="small">
|
||||
<PreferenceMultipleTests />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Row justify="center">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
logger.info('Closing test window')
|
||||
window.close()
|
||||
}}>
|
||||
关闭测试窗口
|
||||
</Button>
|
||||
</Row>
|
||||
</Container>
|
||||
</Content>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
const HeaderContent = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const Container = styled.div`
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
`
|
||||
|
||||
export default TestApp
|
||||
@ -0,0 +1,213 @@
|
||||
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 React, { useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const { Text } = Typography
|
||||
const { Option } = Select
|
||||
|
||||
/**
|
||||
* Basic usePreference hook testing component
|
||||
* Tests single preference management with React hooks
|
||||
*/
|
||||
const PreferenceBasicTests: React.FC = () => {
|
||||
const [selectedKey, setSelectedKey] = useState<PreferenceKeyType>('app.theme.mode')
|
||||
|
||||
// Use the hook with the selected key
|
||||
const [value, setValue] = usePreference(selectedKey)
|
||||
|
||||
const [inputValue, setInputValue] = useState<string>('')
|
||||
|
||||
const handleSetValue = async () => {
|
||||
try {
|
||||
let parsedValue: any = inputValue
|
||||
|
||||
// Try to parse as JSON if it looks like an object/array/boolean/number
|
||||
if (
|
||||
inputValue.startsWith('{') ||
|
||||
inputValue.startsWith('[') ||
|
||||
inputValue === 'true' ||
|
||||
inputValue === 'false' ||
|
||||
!isNaN(Number(inputValue))
|
||||
) {
|
||||
try {
|
||||
parsedValue = JSON.parse(inputValue)
|
||||
} catch {
|
||||
// Keep as string if JSON parsing fails
|
||||
}
|
||||
}
|
||||
|
||||
await setValue(parsedValue)
|
||||
message.success('设置成功')
|
||||
setInputValue('')
|
||||
} catch (error) {
|
||||
message.error(`设置失败: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
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' }
|
||||
]
|
||||
|
||||
return (
|
||||
<TestContainer>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
{/* Key Selection */}
|
||||
<div>
|
||||
<Text strong>选择测试的偏好设置键:</Text>
|
||||
<Select
|
||||
value={selectedKey}
|
||||
onChange={setSelectedKey}
|
||||
style={{ width: '100%', marginTop: 4 }}
|
||||
placeholder="选择偏好设置键">
|
||||
{testCases.map((testCase) => (
|
||||
<Option key={testCase.key} value={testCase.key}>
|
||||
{testCase.label} ({testCase.key})
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Current Value Display */}
|
||||
<CurrentValueContainer>
|
||||
<Text strong>当前值:</Text>
|
||||
<ValueDisplay>
|
||||
{value !== undefined ? (
|
||||
typeof value === 'object' ? (
|
||||
JSON.stringify(value, null, 2)
|
||||
) : (
|
||||
String(value)
|
||||
)
|
||||
) : (
|
||||
<Text type="secondary">undefined (未设置或未加载)</Text>
|
||||
)}
|
||||
</ValueDisplay>
|
||||
</CurrentValueContainer>
|
||||
|
||||
{/* Set New Value */}
|
||||
<div>
|
||||
<Text strong>设置新值:</Text>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 4 }}>
|
||||
<Input
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
placeholder="输入新值 (JSON格式用于对象/数组)"
|
||||
onPressEnter={handleSetValue}
|
||||
/>
|
||||
<Button type="primary" onClick={handleSetValue}>
|
||||
设置
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div>
|
||||
<Text strong>快速操作:</Text>
|
||||
<Space wrap style={{ marginTop: 8 }}>
|
||||
{/* Theme Toggle */}
|
||||
{selectedKey === 'app.theme.mode' && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => setValue(value === 'ThemeMode.dark' ? 'ThemeMode.light' : 'ThemeMode.dark')}>
|
||||
切换主题 ({value === 'ThemeMode.dark' ? 'light' : 'dark'})
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Boolean Toggle */}
|
||||
{selectedKey === 'app.spell_check.enabled' && (
|
||||
<Switch
|
||||
checked={value === true}
|
||||
onChange={(checked) => setValue(checked)}
|
||||
checkedChildren="启用"
|
||||
unCheckedChildren="禁用"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Language Switch */}
|
||||
{selectedKey === 'app.language' && (
|
||||
<>
|
||||
<Button size="small" onClick={() => setValue('zh-CN')}>
|
||||
中文
|
||||
</Button>
|
||||
<Button size="small" onClick={() => setValue('en-US')}>
|
||||
English
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Sample Values */}
|
||||
<Button
|
||||
size="small"
|
||||
type="dashed"
|
||||
onClick={() => {
|
||||
const testCase = testCases.find((tc) => tc.key === selectedKey)
|
||||
if (testCase) {
|
||||
setInputValue(testCase.sampleValue)
|
||||
}
|
||||
}}>
|
||||
示例值
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* Hook Status Info */}
|
||||
<StatusContainer>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
Hook状态: 当前监听 "{selectedKey}", 值类型: {typeof value}, 是否已定义: {value !== undefined ? '是' : '否'}
|
||||
</Text>
|
||||
</StatusContainer>
|
||||
</Space>
|
||||
</TestContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const TestContainer = styled.div`
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
`
|
||||
|
||||
const CurrentValueContainer = styled.div`
|
||||
padding: 12px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid var(--color-primary);
|
||||
`
|
||||
|
||||
const ValueDisplay = styled.pre`
|
||||
margin: 8px 0 0 0;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
`
|
||||
|
||||
const StatusContainer = styled.div`
|
||||
padding: 8px;
|
||||
background: #e6f7ff;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #91d5ff;
|
||||
`
|
||||
|
||||
export default PreferenceBasicTests
|
||||
@ -0,0 +1,177 @@
|
||||
import { usePreference, usePreferencePreload, usePreferenceService } from '@renderer/data/hooks/usePreference'
|
||||
import type { PreferenceKeyType } from '@shared/data/types'
|
||||
import { Button, Card, message, Space, Typography } from 'antd'
|
||||
import React, { useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
/**
|
||||
* 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
|
||||
const [theme1] = usePreference('app.theme.mode')
|
||||
const [theme2] = usePreference('app.theme.mode')
|
||||
const [language] = usePreference('app.language')
|
||||
|
||||
// Preload test
|
||||
usePreferencePreload(['app.theme.mode', 'app.language', 'app.zoom_factor'])
|
||||
|
||||
// Use useRef to track render count without causing re-renders
|
||||
const renderCountRef = React.useRef(0)
|
||||
renderCountRef.current += 1
|
||||
|
||||
const testSubscriptions = () => {
|
||||
// Test subscription behavior
|
||||
const unsubscribe = preferenceService.subscribeToKey('app.theme.mode')(() => {
|
||||
setSubscriptionCount(prev => prev + 1)
|
||||
})
|
||||
|
||||
message.info('已添加订阅,修改app.theme.mode将触发计数')
|
||||
|
||||
// Clean up after 10 seconds
|
||||
setTimeout(() => {
|
||||
unsubscribe()
|
||||
message.info('订阅已取消')
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
const testCacheWarming = async () => {
|
||||
try {
|
||||
const keys: PreferenceKeyType[] = ['app.theme.mode', 'app.language', 'app.zoom_factor', 'app.spell_check.enabled']
|
||||
await preferenceService.preload(keys)
|
||||
|
||||
const cachedStates = keys.map(key => ({
|
||||
key,
|
||||
isCached: preferenceService.isCached(key),
|
||||
value: preferenceService.getCachedValue(key)
|
||||
}))
|
||||
|
||||
message.success(`预加载完成。缓存状态: ${cachedStates.filter(s => s.isCached).length}/${keys.length}`)
|
||||
console.log('Cache states:', cachedStates)
|
||||
} catch (error) {
|
||||
message.error(`预加载失败: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const testBatchOperations = async () => {
|
||||
try {
|
||||
const keys: PreferenceKeyType[] = ['app.theme.mode', 'app.language']
|
||||
const result = await preferenceService.getMultiple(keys)
|
||||
|
||||
message.success(`批量获取成功: ${Object.keys(result).length} 项`)
|
||||
console.log('Batch get result:', result)
|
||||
|
||||
// Test batch set
|
||||
await preferenceService.setMultiple({
|
||||
'app.theme.mode': theme1 === 'ThemeMode.dark' ? 'ThemeMode.light' : 'ThemeMode.dark',
|
||||
'app.language': language === 'zh-CN' ? 'en-US' : 'zh-CN'
|
||||
})
|
||||
|
||||
message.success('批量设置成功')
|
||||
} catch (error) {
|
||||
message.error(`批量操作失败: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const performanceTest = async () => {
|
||||
const start = performance.now()
|
||||
const iterations = 100
|
||||
|
||||
try {
|
||||
// Test rapid reads
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
preferenceService.getCachedValue('app.theme.mode')
|
||||
}
|
||||
|
||||
const readTime = performance.now() - start
|
||||
|
||||
// Test rapid writes
|
||||
const writeStart = performance.now()
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await preferenceService.set('app.theme.mode', `ThemeMode.test_${i}`)
|
||||
}
|
||||
const writeTime = performance.now() - writeStart
|
||||
|
||||
message.success(`性能测试完成: 读取${iterations}次耗时${readTime.toFixed(2)}ms, 写入10次耗时${writeTime.toFixed(2)}ms`)
|
||||
} catch (error) {
|
||||
message.error(`性能测试失败: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TestContainer>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
{/* Hook State Display */}
|
||||
<Card size="small" title="Hook 状态监控">
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Text>组件渲染次数: <Text strong>{renderCountRef.current}</Text></Text>
|
||||
<Text>订阅触发次数: <Text strong>{subscriptionCount}</Text></Text>
|
||||
<Text>Theme Hook 1: <Text code>{String(theme1)}</Text></Text>
|
||||
<Text>Theme Hook 2: <Text code>{String(theme2)}</Text></Text>
|
||||
<Text>Language Hook: <Text code>{String(language)}</Text></Text>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
注意: 相同key的多个hook应该返回相同值
|
||||
</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{/* Test Actions */}
|
||||
<Space wrap>
|
||||
<Button onClick={testSubscriptions}>
|
||||
测试订阅机制
|
||||
</Button>
|
||||
<Button onClick={testCacheWarming}>
|
||||
测试缓存预热
|
||||
</Button>
|
||||
<Button onClick={testBatchOperations}>
|
||||
测试批量操作
|
||||
</Button>
|
||||
<Button onClick={performanceTest}>
|
||||
性能测试
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
{/* Service Information */}
|
||||
<Card size="small" title="Service 信息">
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Text>Service实例: <Text code>{preferenceService ? '已连接' : '未连接'}</Text></Text>
|
||||
<Text>预加载Keys: app.theme.mode, app.language, app.zoom_factor</Text>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
usePreferenceService() 返回的是同一个单例实例
|
||||
</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{/* Hook Behavior Tests */}
|
||||
<Card size="small" title="Hook 行为测试">
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Text strong>实时同步测试:</Text>
|
||||
<Text type="secondary">
|
||||
1. 在其他测试组件中修改 app.theme.mode 或 app.language
|
||||
</Text>
|
||||
<Text type="secondary">
|
||||
2. 观察此组件中的值是否实时更新
|
||||
</Text>
|
||||
<Text type="secondary">
|
||||
3. 检查订阅触发次数是否增加
|
||||
</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
</Space>
|
||||
</TestContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const TestContainer = styled.div`
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
`
|
||||
|
||||
export default PreferenceHookTests
|
||||
@ -0,0 +1,286 @@
|
||||
import { usePreferences } from '@renderer/data/hooks/usePreference'
|
||||
import { Button, Card, Input, message, Select, Space, Table, Typography } from 'antd'
|
||||
import { ColumnType } from 'antd/es/table'
|
||||
import React, { useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const { Text } = Typography
|
||||
const { Option } = Select
|
||||
|
||||
/**
|
||||
* usePreferences hook testing component
|
||||
* Tests multiple preferences management with batch operations
|
||||
*/
|
||||
const PreferenceMultipleTests: React.FC = () => {
|
||||
// Define different test scenarios
|
||||
const [scenario, setScenario] = useState<string>('basic')
|
||||
|
||||
const scenarios = {
|
||||
basic: {
|
||||
theme: 'app.theme.mode',
|
||||
language: 'app.language',
|
||||
zoom: 'app.zoom_factor'
|
||||
},
|
||||
ui: {
|
||||
theme: 'app.theme.mode',
|
||||
zoom: 'app.zoom_factor',
|
||||
spell: 'app.spell_check.enabled'
|
||||
},
|
||||
user: {
|
||||
tray: 'app.tray.enabled',
|
||||
userName: 'app.user.name',
|
||||
devMode: 'app.developer_mode.enabled'
|
||||
},
|
||||
custom: {
|
||||
key1: 'app.theme.mode',
|
||||
key2: 'app.language',
|
||||
key3: 'app.zoom_factor',
|
||||
key4: 'app.spell_check.enabled'
|
||||
}
|
||||
} as const
|
||||
|
||||
const currentKeys = scenarios[scenario as keyof typeof scenarios]
|
||||
const [values, updateValues] = usePreferences(currentKeys)
|
||||
|
||||
const [batchInput, setBatchInput] = useState<string>('')
|
||||
|
||||
const handleBatchUpdate = async () => {
|
||||
try {
|
||||
const updates = JSON.parse(batchInput)
|
||||
await updateValues(updates)
|
||||
message.success('批量更新成功')
|
||||
setBatchInput('')
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
message.error('JSON格式错误')
|
||||
} else {
|
||||
message.error(`更新失败: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuickUpdate = async (key: string, value: any) => {
|
||||
try {
|
||||
await updateValues({ [key]: value })
|
||||
message.success(`${key} 更新成功`)
|
||||
} catch (error) {
|
||||
message.error(`更新失败: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const generateSampleBatch = () => {
|
||||
const sampleUpdates: Record<string, any> = {}
|
||||
Object.keys(currentKeys).forEach((localKey, index) => {
|
||||
const prefKey = currentKeys[localKey as keyof typeof currentKeys]
|
||||
|
||||
switch (prefKey) {
|
||||
case 'app.theme.mode':
|
||||
sampleUpdates[localKey] = values[localKey] === 'ThemeMode.dark' ? 'ThemeMode.light' : 'ThemeMode.dark'
|
||||
break
|
||||
case 'app.language':
|
||||
sampleUpdates[localKey] = values[localKey] === 'zh-CN' ? 'en-US' : 'zh-CN'
|
||||
break
|
||||
case 'app.zoom_factor':
|
||||
sampleUpdates[localKey] = 1.0 + (index * 0.1)
|
||||
break
|
||||
case 'app.spell_check.enabled':
|
||||
sampleUpdates[localKey] = !values[localKey]
|
||||
break
|
||||
default:
|
||||
sampleUpdates[localKey] = `sample_value_${index}`
|
||||
}
|
||||
})
|
||||
|
||||
setBatchInput(JSON.stringify(sampleUpdates, null, 2))
|
||||
}
|
||||
|
||||
// Table columns for displaying values
|
||||
const columns: ColumnType<any>[] = [
|
||||
{
|
||||
title: '本地键名',
|
||||
dataIndex: 'localKey',
|
||||
key: 'localKey',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '偏好设置键',
|
||||
dataIndex: 'prefKey',
|
||||
key: 'prefKey',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '当前值',
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
render: (value: any) => (
|
||||
<ValueDisplay>
|
||||
{value !== undefined ? (
|
||||
typeof value === 'object' ? JSON.stringify(value) : String(value)
|
||||
) : (
|
||||
<Text type="secondary">undefined</Text>
|
||||
)}
|
||||
</ValueDisplay>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 80,
|
||||
render: (type: string) => <Text code>{type}</Text>
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 150,
|
||||
render: (_, record) => (
|
||||
<Space size="small">
|
||||
{record.prefKey === 'app.theme.mode' && (
|
||||
<Button size="small" onClick={() => handleQuickUpdate(record.localKey, record.value === 'ThemeMode.dark' ? 'ThemeMode.light' : 'ThemeMode.dark')}>
|
||||
切换
|
||||
</Button>
|
||||
)}
|
||||
{record.prefKey === 'app.spell_check.enabled' && (
|
||||
<Button size="small" onClick={() => handleQuickUpdate(record.localKey, !record.value)}>
|
||||
切换
|
||||
</Button>
|
||||
)}
|
||||
{record.prefKey === 'app.language' && (
|
||||
<Button size="small" onClick={() => handleQuickUpdate(record.localKey, record.value === 'zh-CN' ? 'en-US' : 'zh-CN')}>
|
||||
切换
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
// Transform data for table
|
||||
const tableData = Object.keys(currentKeys).map((localKey, index) => ({
|
||||
key: index,
|
||||
localKey,
|
||||
prefKey: currentKeys[localKey as keyof typeof currentKeys],
|
||||
value: values[localKey],
|
||||
type: typeof values[localKey]
|
||||
}))
|
||||
|
||||
return (
|
||||
<TestContainer>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
{/* Scenario Selection */}
|
||||
<Card size="small" title="测试场景选择">
|
||||
<Space align="center" wrap>
|
||||
<Text>选择测试场景:</Text>
|
||||
<Select value={scenario} onChange={setScenario} style={{ width: 200 }}>
|
||||
<Option value="basic">基础设置 (theme, language, zoom)</Option>
|
||||
<Option value="ui">UI设置 (theme, zoom, spell)</Option>
|
||||
<Option value="user">用户设置 (tray, userName, devMode)</Option>
|
||||
<Option value="custom">自定义组合 (4项设置)</Option>
|
||||
</Select>
|
||||
<Text type="secondary">当前映射: {Object.keys(currentKeys).length} 项</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{/* Current Values Table */}
|
||||
<Card size="small" title="当前值状态">
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
pagination={false}
|
||||
size="small"
|
||||
bordered
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Batch Update */}
|
||||
<Card size="small" title="批量更新">
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Space>
|
||||
<Button onClick={generateSampleBatch}>
|
||||
生成示例更新
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleBatchUpdate} disabled={!batchInput.trim()}>
|
||||
执行批量更新
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Input.TextArea
|
||||
value={batchInput}
|
||||
onChange={(e) => setBatchInput(e.target.value)}
|
||||
placeholder="输入JSON格式的批量更新数据,例如: {"theme": "dark", "language": "en-US"}"
|
||||
rows={6}
|
||||
style={{ fontFamily: 'monospace' }}
|
||||
/>
|
||||
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
格式: {"localKey": "newValue", ...} - 使用本地键名,不是完整的偏好设置键名
|
||||
</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<Card size="small" title="快速操作">
|
||||
<Space wrap>
|
||||
<Button
|
||||
onClick={() => updateValues(Object.fromEntries(Object.keys(currentKeys).map(key => [key, 'test_value'])))}>
|
||||
设置所有为测试值
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => updateValues(Object.fromEntries(Object.keys(currentKeys).map(key => [key, undefined])))}>
|
||||
清空所有值
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const toggles: Record<string, any> = {}
|
||||
Object.entries(currentKeys).forEach(([localKey, prefKey]) => {
|
||||
if (prefKey === 'app.theme.mode') {
|
||||
toggles[localKey] = values[localKey] === 'ThemeMode.dark' ? 'ThemeMode.light' : 'ThemeMode.dark'
|
||||
} else if (prefKey === 'app.spell_check.enabled') {
|
||||
toggles[localKey] = !values[localKey]
|
||||
}
|
||||
})
|
||||
if (Object.keys(toggles).length > 0) {
|
||||
await updateValues(toggles)
|
||||
message.success('切换操作完成')
|
||||
}
|
||||
}}>
|
||||
切换布尔/主题值
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{/* Hook Info */}
|
||||
<Card size="small" title="Hook 信息">
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Text>
|
||||
本地键映射: <Text code>{JSON.stringify(currentKeys, null, 2)}</Text>
|
||||
</Text>
|
||||
<Text>
|
||||
返回值数量: <Text strong>{Object.keys(values).length}</Text>
|
||||
</Text>
|
||||
<Text>
|
||||
已定义值: <Text strong>{Object.values(values).filter(v => v !== undefined).length}</Text>
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
usePreferences 返回的值对象使用本地键名,内部自动映射到实际的偏好设置键
|
||||
</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
</Space>
|
||||
</TestContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const TestContainer = styled.div`
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
`
|
||||
|
||||
const ValueDisplay = styled.span`
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
`
|
||||
|
||||
export default PreferenceMultipleTests
|
||||
@ -0,0 +1,218 @@
|
||||
import { preferenceService } from '@renderer/data/PreferenceService'
|
||||
import type { PreferenceKeyType } from '@shared/data/types'
|
||||
import { Button, Input, message, Space, Typography } from 'antd'
|
||||
import React, { useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
/**
|
||||
* PreferenceService direct API testing component
|
||||
* Tests the service layer functionality without React hooks
|
||||
*/
|
||||
const PreferenceServiceTests: React.FC = () => {
|
||||
const [testKey, setTestKey] = useState<string>('app.theme.mode')
|
||||
const [testValue, setTestValue] = useState<string>('ThemeMode.dark')
|
||||
const [getResult, setGetResult] = useState<any>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleGet = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const result = await preferenceService.get(testKey as PreferenceKeyType)
|
||||
setGetResult(result)
|
||||
message.success('获取成功')
|
||||
} catch (error) {
|
||||
message.error(`获取失败: ${(error as Error).message}`)
|
||||
setGetResult(`Error: ${(error as Error).message}`)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSet = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
let parsedValue: any = testValue
|
||||
|
||||
// Try to parse as JSON if it looks like an object/array/boolean/number
|
||||
if (testValue.startsWith('{') || testValue.startsWith('[') || testValue === 'true' || testValue === 'false' || !isNaN(Number(testValue))) {
|
||||
try {
|
||||
parsedValue = JSON.parse(testValue)
|
||||
} catch {
|
||||
// Keep as string if JSON parsing fails
|
||||
}
|
||||
}
|
||||
|
||||
await preferenceService.set(testKey as PreferenceKeyType, parsedValue)
|
||||
message.success('设置成功')
|
||||
// Automatically get the updated value
|
||||
await handleGet()
|
||||
} catch (error) {
|
||||
message.error(`设置失败: ${(error as Error).message}`)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleGetCached = () => {
|
||||
try {
|
||||
const result = preferenceService.getCachedValue(testKey as PreferenceKeyType)
|
||||
setGetResult(result !== undefined ? result : 'undefined (not cached)')
|
||||
message.info('获取缓存值成功')
|
||||
} catch (error) {
|
||||
message.error(`获取缓存失败: ${(error as Error).message}`)
|
||||
setGetResult(`Error: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleIsCached = () => {
|
||||
try {
|
||||
const result = preferenceService.isCached(testKey as PreferenceKeyType)
|
||||
setGetResult(result ? 'true (已缓存)' : 'false (未缓存)')
|
||||
message.info('检查缓存状态成功')
|
||||
} catch (error) {
|
||||
message.error(`检查缓存失败: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePreload = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
await preferenceService.preload([testKey as PreferenceKeyType])
|
||||
message.success('预加载成功')
|
||||
await handleGet()
|
||||
} catch (error) {
|
||||
message.error(`预加载失败: ${(error as Error).message}`)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
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('获取示例偏好设置成功')
|
||||
} catch (error) {
|
||||
message.error(`获取偏好设置失败: ${(error as Error).message}`)
|
||||
setGetResult(`Error: ${(error as Error).message}`)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TestContainer>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
{/* Input Controls */}
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<div>
|
||||
<Text strong>Preference Key:</Text>
|
||||
<Input
|
||||
value={testKey}
|
||||
onChange={(e) => setTestKey(e.target.value)}
|
||||
placeholder="Enter preference key (e.g., app.theme)"
|
||||
style={{ marginTop: 4 }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong>Value:</Text>
|
||||
<Input
|
||||
value={testValue}
|
||||
onChange={(e) => setTestValue(e.target.value)}
|
||||
placeholder="Enter value (JSON format for objects/arrays)"
|
||||
style={{ marginTop: 4 }}
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Space wrap>
|
||||
<Button type="primary" onClick={handleGet} loading={loading}>
|
||||
Get
|
||||
</Button>
|
||||
<Button onClick={handleSet} loading={loading}>
|
||||
Set
|
||||
</Button>
|
||||
<Button onClick={handleGetCached}>
|
||||
Get Cached
|
||||
</Button>
|
||||
<Button onClick={handleIsCached}>
|
||||
Is Cached
|
||||
</Button>
|
||||
<Button onClick={handlePreload} loading={loading}>
|
||||
Preload
|
||||
</Button>
|
||||
<Button onClick={handleGetAll} loading={loading}>
|
||||
Get All
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
{/* Result Display */}
|
||||
{getResult !== null && (
|
||||
<ResultContainer>
|
||||
<Text strong>Result:</Text>
|
||||
<ResultText>{typeof getResult === 'object' ? JSON.stringify(getResult, null, 2) : String(getResult)}</ResultText>
|
||||
</ResultContainer>
|
||||
)}
|
||||
|
||||
{/* Quick Test Buttons */}
|
||||
<Space wrap>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setTestKey('app.theme.mode')
|
||||
setTestValue('ThemeMode.dark')
|
||||
}}>
|
||||
Test: app.theme.mode
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setTestKey('app.language')
|
||||
setTestValue('zh-CN')
|
||||
}}>
|
||||
Test: app.language
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setTestKey('app.spell_check.enabled')
|
||||
setTestValue('true')
|
||||
}}>
|
||||
Test: app.spell_check.enabled
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
</TestContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const TestContainer = styled.div`
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
`
|
||||
|
||||
const ResultContainer = styled.div`
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid var(--color-primary);
|
||||
`
|
||||
|
||||
const ResultText = styled.pre`
|
||||
margin: 8px 0 0 0;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
`
|
||||
|
||||
export default PreferenceServiceTests
|
||||
13
src/renderer/src/windows/dataRefactorTest/entryPoint.tsx
Normal file
13
src/renderer/src/windows/dataRefactorTest/entryPoint.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import '@ant-design/v5-patch-for-react-19'
|
||||
import '@renderer/assets/styles/index.scss'
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
|
||||
import TestApp from './TestApp'
|
||||
|
||||
loggerService.initWindowSource('DataRefactorTestWindow')
|
||||
|
||||
const root = createRoot(document.getElementById('root') as HTMLElement)
|
||||
|
||||
root.render(<TestApp />)
|
||||
Loading…
Reference in New Issue
Block a user