diff --git a/src/main/data/PreferenceService.ts b/src/main/data/PreferenceService.ts index fd14c1bdca..0c2f52294d 100644 --- a/src/main/data/PreferenceService.ts +++ b/src/main/data/PreferenceService.ts @@ -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 diff --git a/src/main/data/db/DbService.ts b/src/main/data/db/DbService.ts index 00d87a6ba0..6f136a58a6 100644 --- a/src/main/data/db/DbService.ts +++ b/src/main/data/db/DbService.ts @@ -67,6 +67,4 @@ class DbService { } // Export a singleton instance -const dbService = DbService.getInstance() - -export default dbService +export const dbService = DbService.getInstance() diff --git a/src/main/data/migrate/dataRefactor/DataRefactorMigrateService.ts b/src/main/data/migrate/dataRefactor/DataRefactorMigrateService.ts index db922bd613..eb748b7667 100644 --- a/src/main/data/migrate/dataRefactor/DataRefactorMigrateService.ts +++ b/src/main/data/migrate/dataRefactor/DataRefactorMigrateService.ts @@ -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 diff --git a/src/main/data/migrate/dataRefactor/migrators/PreferencesMigrator.ts b/src/main/data/migrate/dataRefactor/migrators/PreferencesMigrator.ts index 3fff882706..a6c9581fd3 100644 --- a/src/main/data/migrate/dataRefactor/migrators/PreferencesMigrator.ts +++ b/src/main/data/migrate/dataRefactor/migrators/PreferencesMigrator.ts @@ -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' diff --git a/src/main/index.ts b/src/main/index.ts index 52bc54e78e..3900a61971 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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') diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 20450f82e8..7c05b60ecd 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -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' diff --git a/src/renderer/dataRefactorTest.html b/src/renderer/dataRefactorTest.html new file mode 100644 index 0000000000..20309b3526 --- /dev/null +++ b/src/renderer/dataRefactorTest.html @@ -0,0 +1,63 @@ + + + + + + Data Refactor Test Window - PreferenceService Testing + + + + + + + + + + \ No newline at end of file diff --git a/src/renderer/src/data/hooks/usePreference.ts b/src/renderer/src/data/hooks/usePreference.ts index 0e2ecb851b..1324099053 100644 --- a/src/renderer/src/data/hooks/usePreference.ts +++ b/src/renderer/src/data/hooks/usePreference.ts @@ -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( ): [PreferenceDefaultScopeType[K] | undefined, (value: PreferenceDefaultScopeType[K]) => Promise] { // 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>( { [P in keyof T]: PreferenceDefaultScopeType[T[P]] | undefined }, (updates: Partial<{ [P in keyof T]: PreferenceDefaultScopeType[T[P]] }>) => Promise ] { - // 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>({}) + 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>( unsubscribeFunctions.forEach((unsubscribe) => unsubscribe()) } }, - [keyList] + [keysStringified] ), useCallback(() => { - // Return current snapshot of all values - const snapshot: Record = {} + // Check if any values have actually changed + let hasChanged = Object.keys(lastSnapshotRef.current).length === 0 // First time + const newSnapshot: Record = {} + 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>( 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>( throw error } }, - [keys] + [keysStringified] ) // Type-cast the values to the expected shape @@ -135,12 +152,12 @@ export function usePreferences>( * @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]) } /** diff --git a/src/renderer/src/windows/dataRefactorTest/README.md b/src/renderer/src/windows/dataRefactorTest/README.md new file mode 100644 index 0000000000..798c72f0c1 --- /dev/null +++ b/src/renderer/src/windows/dataRefactorTest/README.md @@ -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 类型检查和偏好设置键约束 \ No newline at end of file diff --git a/src/renderer/src/windows/dataRefactorTest/TestApp.tsx b/src/renderer/src/windows/dataRefactorTest/TestApp.tsx new file mode 100644 index 0000000000..8e41e86bf8 --- /dev/null +++ b/src/renderer/src/windows/dataRefactorTest/TestApp.tsx @@ -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 ( + +
+ + + Logo + + Test Window #{windowNumber} + + + + + Cross-Window Sync Testing + + +
+ + + + + {/* Introduction Card */} + + + + + + + PreferenceService 功能测试 (窗口 #{windowNumber}) + + + + 此测试窗口用于验证 PreferenceService 和 usePreference hooks + 的各项功能,包括单个偏好设置的读写、多个偏好设置的批量操作、跨窗口数据同步等。 + + 测试使用的都是真实的偏好设置系统,所有操作都会影响实际的数据库存储。 + + 📋 跨窗口测试指南:在一个窗口中修改偏好设置,观察其他窗口是否实时同步更新。 + + + + + + {/* PreferenceService Basic Tests */} + + + + PreferenceService 基础测试 + + } + size="small"> + + + + + {/* Basic Hook Tests */} + + + + usePreference Hook 测试 + + } + size="small"> + + + + + {/* Hook Tests */} + + + + Hook 高级功能测试 + + } + size="small"> + + + + + {/* Multiple Preferences Tests */} + + + + usePreferences 批量操作测试 + + } + size="small"> + + + + + + + + + + + + +
+ ) +} + +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 diff --git a/src/renderer/src/windows/dataRefactorTest/components/PreferenceBasicTests.tsx b/src/renderer/src/windows/dataRefactorTest/components/PreferenceBasicTests.tsx new file mode 100644 index 0000000000..b302e787e4 --- /dev/null +++ b/src/renderer/src/windows/dataRefactorTest/components/PreferenceBasicTests.tsx @@ -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('app.theme.mode') + + // Use the hook with the selected key + const [value, setValue] = usePreference(selectedKey) + + const [inputValue, setInputValue] = useState('') + + 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 ( + + + {/* Key Selection */} +
+ 选择测试的偏好设置键: + +
+ + {/* Current Value Display */} + + 当前值: + + {value !== undefined ? ( + typeof value === 'object' ? ( + JSON.stringify(value, null, 2) + ) : ( + String(value) + ) + ) : ( + undefined (未设置或未加载) + )} + + + + {/* Set New Value */} +
+ 设置新值: + + setInputValue(e.target.value)} + placeholder="输入新值 (JSON格式用于对象/数组)" + onPressEnter={handleSetValue} + /> + + +
+ + {/* Quick Actions */} +
+ 快速操作: + + {/* Theme Toggle */} + {selectedKey === 'app.theme.mode' && ( + + )} + + {/* Boolean Toggle */} + {selectedKey === 'app.spell_check.enabled' && ( + setValue(checked)} + checkedChildren="启用" + unCheckedChildren="禁用" + /> + )} + + {/* Language Switch */} + {selectedKey === 'app.language' && ( + <> + + + + )} + + {/* Zoom Factor */} + {selectedKey === 'app.zoom_factor' && ( + <> + + + + + )} + + {/* Sample Values */} + + +
+ + {/* Hook Status Info */} + + + Hook状态: 当前监听 "{selectedKey}", 值类型: {typeof value}, 是否已定义: {value !== undefined ? '是' : '否'} + + +
+
+ ) +} + +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 diff --git a/src/renderer/src/windows/dataRefactorTest/components/PreferenceHookTests.tsx b/src/renderer/src/windows/dataRefactorTest/components/PreferenceHookTests.tsx new file mode 100644 index 0000000000..a7235a3235 --- /dev/null +++ b/src/renderer/src/windows/dataRefactorTest/components/PreferenceHookTests.tsx @@ -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 ( + + + {/* Hook State Display */} + + + 组件渲染次数: {renderCountRef.current} + 订阅触发次数: {subscriptionCount} + Theme Hook 1: {String(theme1)} + Theme Hook 2: {String(theme2)} + Language Hook: {String(language)} + + 注意: 相同key的多个hook应该返回相同值 + + + + + {/* Test Actions */} + + + + + + + + {/* Service Information */} + + + Service实例: {preferenceService ? '已连接' : '未连接'} + 预加载Keys: app.theme.mode, app.language, app.zoom_factor + + usePreferenceService() 返回的是同一个单例实例 + + + + + {/* Hook Behavior Tests */} + + + 实时同步测试: + + 1. 在其他测试组件中修改 app.theme.mode 或 app.language + + + 2. 观察此组件中的值是否实时更新 + + + 3. 检查订阅触发次数是否增加 + + + + + + ) +} + +const TestContainer = styled.div` + padding: 16px; + background: #fafafa; + border-radius: 8px; +` + +export default PreferenceHookTests \ No newline at end of file diff --git a/src/renderer/src/windows/dataRefactorTest/components/PreferenceMultipleTests.tsx b/src/renderer/src/windows/dataRefactorTest/components/PreferenceMultipleTests.tsx new file mode 100644 index 0000000000..cb65eb6951 --- /dev/null +++ b/src/renderer/src/windows/dataRefactorTest/components/PreferenceMultipleTests.tsx @@ -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('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('') + + 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 = {} + 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[] = [ + { + title: '本地键名', + dataIndex: 'localKey', + key: 'localKey', + width: 120 + }, + { + title: '偏好设置键', + dataIndex: 'prefKey', + key: 'prefKey', + width: 200 + }, + { + title: '当前值', + dataIndex: 'value', + key: 'value', + render: (value: any) => ( + + {value !== undefined ? ( + typeof value === 'object' ? JSON.stringify(value) : String(value) + ) : ( + undefined + )} + + ) + }, + { + title: '类型', + dataIndex: 'type', + key: 'type', + width: 80, + render: (type: string) => {type} + }, + { + title: '操作', + key: 'actions', + width: 150, + render: (_, record) => ( + + {record.prefKey === 'app.theme.mode' && ( + + )} + {record.prefKey === 'app.spell_check.enabled' && ( + + )} + {record.prefKey === 'app.language' && ( + + )} + + ) + } + ] + + // 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 ( + + + {/* Scenario Selection */} + + + 选择测试场景: + + 当前映射: {Object.keys(currentKeys).length} 项 + + + + {/* Current Values Table */} + + + + + {/* Batch Update */} + + + + + + + + setBatchInput(e.target.value)} + placeholder="输入JSON格式的批量更新数据,例如: {"theme": "dark", "language": "en-US"}" + rows={6} + style={{ fontFamily: 'monospace' }} + /> + + + 格式: {"localKey": "newValue", ...} - 使用本地键名,不是完整的偏好设置键名 + + + + + {/* Quick Actions */} + + + + + + + + + {/* Hook Info */} + + + + 本地键映射: {JSON.stringify(currentKeys, null, 2)} + + + 返回值数量: {Object.keys(values).length} + + + 已定义值: {Object.values(values).filter(v => v !== undefined).length} + + + usePreferences 返回的值对象使用本地键名,内部自动映射到实际的偏好设置键 + + + + + + ) +} + +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 \ No newline at end of file diff --git a/src/renderer/src/windows/dataRefactorTest/components/PreferenceServiceTests.tsx b/src/renderer/src/windows/dataRefactorTest/components/PreferenceServiceTests.tsx new file mode 100644 index 0000000000..d21f7505f7 --- /dev/null +++ b/src/renderer/src/windows/dataRefactorTest/components/PreferenceServiceTests.tsx @@ -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('app.theme.mode') + const [testValue, setTestValue] = useState('ThemeMode.dark') + const [getResult, setGetResult] = useState(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 ( + + + {/* Input Controls */} + +
+ Preference Key: + setTestKey(e.target.value)} + placeholder="Enter preference key (e.g., app.theme)" + style={{ marginTop: 4 }} + /> +
+
+ Value: + setTestValue(e.target.value)} + placeholder="Enter value (JSON format for objects/arrays)" + style={{ marginTop: 4 }} + /> +
+
+ + {/* Action Buttons */} + + + + + + + + + + {/* Result Display */} + {getResult !== null && ( + + Result: + {typeof getResult === 'object' ? JSON.stringify(getResult, null, 2) : String(getResult)} + + )} + + {/* Quick Test Buttons */} + + + + + +
+
+ ) +} + +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 \ No newline at end of file diff --git a/src/renderer/src/windows/dataRefactorTest/entryPoint.tsx b/src/renderer/src/windows/dataRefactorTest/entryPoint.tsx new file mode 100644 index 0000000000..ac164184e8 --- /dev/null +++ b/src/renderer/src/windows/dataRefactorTest/entryPoint.tsx @@ -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() \ No newline at end of file