mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-11 08:19:01 +08:00
test: update tests to use usePreference hook and improve snapshot consistency
- Refactored tests in MainTextBlock and ThinkingBlock to utilize the usePreference hook for managing user settings. - Updated snapshots in DraggableVirtualList test to reflect changes in class names. - Enhanced export tests to ensure proper handling of markdown formatting and citation footnotes. - Mocked additional dependencies globally for improved test reliability.
This commit is contained in:
parent
8cc6b08831
commit
8353f331f1
@ -20,8 +20,7 @@ exports[`DraggableVirtualList > snapshot > should match snapshot with custom sty
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="virtual-scroller"
|
class="ScrollBarContainer-eGlIoO jgKpou virtual-scroller"
|
||||||
data-testid="scrollbar"
|
|
||||||
style="height: 100%; width: 100%; overflow-y: auto; position: relative;"
|
style="height: 100%; width: 100%; overflow-y: auto; position: relative;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
305
src/renderer/src/data/README.md
Normal file
305
src/renderer/src/data/README.md
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
# Data Layer - Renderer Process
|
||||||
|
|
||||||
|
This directory contains the unified data access layer for Cherry Studio's renderer process, providing type-safe interfaces for data operations, preference management, and caching.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The `src/renderer/src/data` directory implements the new data architecture as part of the ongoing database refactoring project. It provides three core services that handle all data operations in the renderer process:
|
||||||
|
|
||||||
|
- **DataApiService**: RESTful-style API for communication with the main process
|
||||||
|
- **PreferenceService**: Unified preference/configuration management with real-time sync
|
||||||
|
- **CacheService**: Three-tier caching system for optimal performance
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ React Components│
|
||||||
|
└─────────┬───────┘
|
||||||
|
│
|
||||||
|
┌─────────▼───────┐
|
||||||
|
│ React Hooks │ ← useDataApi, usePreference, useCache
|
||||||
|
└─────────┬───────┘
|
||||||
|
│
|
||||||
|
┌─────────▼───────┐
|
||||||
|
│ Services │ ← DataApiService, PreferenceService, CacheService
|
||||||
|
└─────────┬───────┘
|
||||||
|
│
|
||||||
|
┌─────────▼───────┐
|
||||||
|
│ IPC Layer │ ← Main Process Communication
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Data API Operations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useQuery, useMutation } from '@data/hooks/useDataApi'
|
||||||
|
|
||||||
|
// Fetch data with auto-retry and caching
|
||||||
|
const { data, loading, error } = useQuery('/topics')
|
||||||
|
|
||||||
|
// Create/update data with optimistic updates
|
||||||
|
const { trigger: createTopic } = useMutation('/topics', 'POST')
|
||||||
|
await createTopic({ title: 'New Topic', content: 'Hello World' })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Preference Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { usePreference } from '@data/hooks/usePreference'
|
||||||
|
|
||||||
|
// Manage user preferences with real-time sync
|
||||||
|
const [theme, setTheme] = usePreference('app.theme.mode')
|
||||||
|
const [fontSize, setFontSize] = usePreference('chat.message.font_size')
|
||||||
|
|
||||||
|
// Optimistic updates (default)
|
||||||
|
await setTheme('dark') // UI updates immediately, syncs to database
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cache Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useCache, useSharedCache, usePersistCache } from '@data/hooks/useCache'
|
||||||
|
|
||||||
|
// Component-level cache (lost on app restart)
|
||||||
|
const [count, setCount] = useCache('ui.counter')
|
||||||
|
|
||||||
|
// Cross-window cache (shared between all windows)
|
||||||
|
const [windowState, setWindowState] = useSharedCache('window.layout')
|
||||||
|
|
||||||
|
// Persistent cache (survives app restarts)
|
||||||
|
const [recentFiles, setRecentFiles] = usePersistCache('app.recent_files')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Services
|
||||||
|
|
||||||
|
### DataApiService
|
||||||
|
|
||||||
|
**Purpose**: Type-safe communication with the main process using RESTful-style APIs.
|
||||||
|
|
||||||
|
**Key Features**:
|
||||||
|
- Type-safe request/response handling
|
||||||
|
- Automatic retry with exponential backoff
|
||||||
|
- Batch operations and transactions
|
||||||
|
- Real-time subscriptions
|
||||||
|
- Request cancellation and timeout handling
|
||||||
|
|
||||||
|
**Basic Usage**:
|
||||||
|
```typescript
|
||||||
|
import { dataApiService } from '@data/DataApiService'
|
||||||
|
|
||||||
|
// Simple GET request
|
||||||
|
const topics = await dataApiService.get('/topics')
|
||||||
|
|
||||||
|
// POST with body
|
||||||
|
const newTopic = await dataApiService.post('/topics', {
|
||||||
|
body: { title: 'Hello', content: 'World' }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Batch operations
|
||||||
|
const responses = await dataApiService.batch([
|
||||||
|
{ method: 'GET', path: '/topics' },
|
||||||
|
{ method: 'GET', path: '/messages' }
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
### PreferenceService
|
||||||
|
|
||||||
|
**Purpose**: Centralized preference/configuration management with cross-window synchronization.
|
||||||
|
|
||||||
|
**Key Features**:
|
||||||
|
- Optimistic and pessimistic update strategies
|
||||||
|
- Real-time cross-window synchronization
|
||||||
|
- Local caching for performance
|
||||||
|
- Race condition handling
|
||||||
|
- Batch operations for multiple preferences
|
||||||
|
|
||||||
|
**Basic Usage**:
|
||||||
|
```typescript
|
||||||
|
import { preferenceService } from '@data/PreferenceService'
|
||||||
|
|
||||||
|
// Get single preference
|
||||||
|
const theme = await preferenceService.get('app.theme.mode')
|
||||||
|
|
||||||
|
// Set with optimistic updates (default)
|
||||||
|
await preferenceService.set('app.theme.mode', 'dark')
|
||||||
|
|
||||||
|
// Set with pessimistic updates
|
||||||
|
await preferenceService.set('api.key', 'secret', { optimistic: false })
|
||||||
|
|
||||||
|
// Batch operations
|
||||||
|
await preferenceService.setMultiple({
|
||||||
|
'app.theme.mode': 'dark',
|
||||||
|
'chat.message.font_size': 14
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### CacheService
|
||||||
|
|
||||||
|
**Purpose**: Three-tier caching system for different data persistence needs.
|
||||||
|
|
||||||
|
**Cache Tiers**:
|
||||||
|
1. **Memory Cache**: Component-level, lost on app restart
|
||||||
|
2. **Shared Cache**: Cross-window, lost on app restart
|
||||||
|
3. **Persist Cache**: Cross-window + localStorage, survives restarts
|
||||||
|
|
||||||
|
**Key Features**:
|
||||||
|
- TTL (Time To Live) support
|
||||||
|
- Hook reference tracking (prevents deletion of active data)
|
||||||
|
- Cross-window synchronization
|
||||||
|
- Type-safe cache schemas
|
||||||
|
- Automatic default value handling
|
||||||
|
|
||||||
|
**Basic Usage**:
|
||||||
|
```typescript
|
||||||
|
import { cacheService } from '@data/CacheService'
|
||||||
|
|
||||||
|
// Memory cache (component-level)
|
||||||
|
cacheService.set('temp.calculation', result, 30000) // 30s TTL
|
||||||
|
const result = cacheService.get('temp.calculation')
|
||||||
|
|
||||||
|
// Shared cache (cross-window)
|
||||||
|
cacheService.setShared('window.layout', layoutConfig)
|
||||||
|
const layout = cacheService.getShared('window.layout')
|
||||||
|
|
||||||
|
// Persist cache (survives restarts)
|
||||||
|
cacheService.setPersist('app.recent_files', recentFiles)
|
||||||
|
const files = cacheService.getPersist('app.recent_files')
|
||||||
|
```
|
||||||
|
|
||||||
|
## React Hooks
|
||||||
|
|
||||||
|
### useDataApi
|
||||||
|
|
||||||
|
Type-safe data fetching with SWR integration.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useQuery, useMutation } from '@data/hooks/useDataApi'
|
||||||
|
|
||||||
|
// GET requests with auto-caching
|
||||||
|
const { data, loading, error, mutate } = useQuery('/topics', {
|
||||||
|
query: { page: 1, limit: 20 }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mutations with optimistic updates
|
||||||
|
const { trigger: updateTopic, isMutating } = useMutation('/topics/123', 'PUT')
|
||||||
|
await updateTopic({ title: 'Updated Title' })
|
||||||
|
```
|
||||||
|
|
||||||
|
### usePreference
|
||||||
|
|
||||||
|
Reactive preference management with automatic synchronization.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { usePreference } from '@data/hooks/usePreference'
|
||||||
|
|
||||||
|
// Basic usage with optimistic updates
|
||||||
|
const [theme, setTheme] = usePreference('app.theme.mode')
|
||||||
|
|
||||||
|
// Pessimistic updates for critical settings
|
||||||
|
const [apiKey, setApiKey] = usePreference('api.key', { optimistic: false })
|
||||||
|
|
||||||
|
// Handle updates
|
||||||
|
const handleThemeChange = async (newTheme) => {
|
||||||
|
try {
|
||||||
|
await setTheme(newTheme) // Auto-rollback on failure
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Theme update failed:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### useCache Hooks
|
||||||
|
|
||||||
|
Component-friendly cache management with automatic lifecycle handling.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useCache, useSharedCache, usePersistCache } from '@data/hooks/useCache'
|
||||||
|
|
||||||
|
// Memory cache (useState-like, but shared between components)
|
||||||
|
const [counter, setCounter] = useCache('ui.counter', 0)
|
||||||
|
|
||||||
|
// Shared cache (cross-window)
|
||||||
|
const [layout, setLayout] = useSharedCache('window.layout')
|
||||||
|
|
||||||
|
// Persistent cache (survives restarts)
|
||||||
|
const [recentFiles, setRecentFiles] = usePersistCache('app.recent_files')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### When to Use Which Service
|
||||||
|
|
||||||
|
- **DataApiService**: For database operations, API calls, and any data that needs to persist
|
||||||
|
- **PreferenceService**: For user settings, app configuration, and preferences
|
||||||
|
- **CacheService**: For temporary data, computed results, and performance optimization
|
||||||
|
|
||||||
|
### Performance Guidelines
|
||||||
|
|
||||||
|
1. **Prefer React Hooks**: Use `useQuery`, `usePreference`, `useCache` for component integration
|
||||||
|
2. **Batch Operations**: Use `setMultiple()` for updating multiple preferences
|
||||||
|
3. **Cache Strategically**: Use appropriate cache tiers based on data lifetime needs
|
||||||
|
4. **Optimize Re-renders**: SWR and useSyncExternalStore minimize unnecessary re-renders
|
||||||
|
|
||||||
|
### Common Patterns
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Loading states with error handling
|
||||||
|
const { data, loading, error } = useQuery('/topics')
|
||||||
|
if (loading) return <Loading />
|
||||||
|
if (error) return <Error error={error} />
|
||||||
|
|
||||||
|
// Form handling with preferences
|
||||||
|
const [fontSize, setFontSize] = usePreference('chat.message.font_size')
|
||||||
|
const handleChange = (e) => setFontSize(Number(e.target.value))
|
||||||
|
|
||||||
|
// Temporary state with caching
|
||||||
|
const [searchQuery, setSearchQuery] = useCache('search.current_query', '')
|
||||||
|
const [searchResults, setSearchResults] = useCache('search.results', [])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type Safety
|
||||||
|
|
||||||
|
All services provide full TypeScript support with auto-completion and type checking:
|
||||||
|
|
||||||
|
- **API Types**: Defined in `@shared/data/api/`
|
||||||
|
- **Preference Types**: Defined in `@shared/data/preference/`
|
||||||
|
- **Cache Types**: Defined in `@shared/data/cache/`
|
||||||
|
|
||||||
|
Type definitions are automatically inferred, providing:
|
||||||
|
- Request/response type safety
|
||||||
|
- Preference key validation
|
||||||
|
- Cache schema enforcement
|
||||||
|
- Auto-completion in IDEs
|
||||||
|
|
||||||
|
## Migration from Legacy Systems
|
||||||
|
|
||||||
|
This new data layer replaces multiple legacy systems:
|
||||||
|
- Redux-persist slices → PreferenceService
|
||||||
|
- localStorage direct access → CacheService
|
||||||
|
- Direct IPC calls → DataApiService
|
||||||
|
- Dexie database operations → DataApiService
|
||||||
|
|
||||||
|
For migration guidelines, see the project's `.claude/` directory documentation.
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/renderer/src/data/
|
||||||
|
├── DataApiService.ts # User Data API querying service
|
||||||
|
├── PreferenceService.ts # Preferences management
|
||||||
|
├── CacheService.ts # Three-tier caching system
|
||||||
|
└── hooks/
|
||||||
|
├── useDataApi.ts # React hooks for user data operations
|
||||||
|
├── usePreference.ts # React hooks for preferences
|
||||||
|
└── useCache.ts # React hooks for caching
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- **API Schemas**: `packages/shared/data/` - Type definitions and API contracts
|
||||||
|
- **Architecture Design**: `.claude/data-architecture.md` - Detailed system design
|
||||||
|
- **Migration Guide**: `.claude/migration-planning.md` - Legacy system migration
|
||||||
|
- **Project Overview**: `CLAUDE.local.md` - Complete refactoring context
|
||||||
@ -12,12 +12,17 @@ import MainTextBlock from '../MainTextBlock'
|
|||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
const mockUseSettings = vi.fn()
|
const mockUseSettings = vi.fn()
|
||||||
const mockUseSelector = vi.fn()
|
const mockUseSelector = vi.fn()
|
||||||
|
let mockUsePreference: any
|
||||||
|
|
||||||
// Mock hooks
|
// Mock hooks
|
||||||
vi.mock('@renderer/hooks/useSettings', () => ({
|
vi.mock('@renderer/hooks/useSettings', () => ({
|
||||||
useSettings: () => mockUseSettings()
|
useSettings: () => mockUseSettings()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@data/hooks/usePreference', () => ({
|
||||||
|
usePreference: vi.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('react-redux', async () => {
|
vi.mock('react-redux', async () => {
|
||||||
const actual = await import('react-redux')
|
const actual = await import('react-redux')
|
||||||
return {
|
return {
|
||||||
@ -107,12 +112,15 @@ describe('MainTextBlock', () => {
|
|||||||
// Get the mocked functions
|
// Get the mocked functions
|
||||||
const { getModelUniqId } = await import('@renderer/services/ModelService')
|
const { getModelUniqId } = await import('@renderer/services/ModelService')
|
||||||
const { withCitationTags, determineCitationSource } = await import('@renderer/utils/citation')
|
const { withCitationTags, determineCitationSource } = await import('@renderer/utils/citation')
|
||||||
|
const { usePreference } = await import('@data/hooks/usePreference')
|
||||||
mockGetModelUniqId = getModelUniqId as any
|
mockGetModelUniqId = getModelUniqId as any
|
||||||
mockWithCitationTags = withCitationTags as any
|
mockWithCitationTags = withCitationTags as any
|
||||||
mockDetermineCitationSource = determineCitationSource as any
|
mockDetermineCitationSource = determineCitationSource as any
|
||||||
|
mockUsePreference = usePreference as any
|
||||||
|
|
||||||
// Default mock implementations
|
// Default mock implementations
|
||||||
mockUseSettings.mockReturnValue({ renderInputMessageAsMarkdown: false })
|
mockUseSettings.mockReturnValue({ renderInputMessageAsMarkdown: false })
|
||||||
|
mockUsePreference.mockReturnValue([false, vi.fn()]) // usePreference returns [value, setter]
|
||||||
mockUseSelector.mockReturnValue([]) // Empty citations by default
|
mockUseSelector.mockReturnValue([]) // Empty citations by default
|
||||||
mockGetModelUniqId.mockImplementation((model: Model) => `${model.id}-${model.name}`)
|
mockGetModelUniqId.mockImplementation((model: Model) => `${model.id}-${model.name}`)
|
||||||
})
|
})
|
||||||
@ -167,7 +175,7 @@ describe('MainTextBlock', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should render in plain text mode for user messages when setting disabled', () => {
|
it('should render in plain text mode for user messages when setting disabled', () => {
|
||||||
mockUseSettings.mockReturnValue({ renderInputMessageAsMarkdown: false })
|
mockUsePreference.mockReturnValue([false, vi.fn()])
|
||||||
const block = createMainTextBlock({ content: 'User message\nWith line breaks' })
|
const block = createMainTextBlock({ content: 'User message\nWith line breaks' })
|
||||||
renderMainTextBlock({ block, role: 'user' })
|
renderMainTextBlock({ block, role: 'user' })
|
||||||
|
|
||||||
@ -182,7 +190,7 @@ describe('MainTextBlock', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should render user messages as markdown when setting enabled', () => {
|
it('should render user messages as markdown when setting enabled', () => {
|
||||||
mockUseSettings.mockReturnValue({ renderInputMessageAsMarkdown: true })
|
mockUsePreference.mockReturnValue([true, vi.fn()])
|
||||||
const block = createMainTextBlock({ content: 'User **bold** content' })
|
const block = createMainTextBlock({ content: 'User **bold** content' })
|
||||||
renderMainTextBlock({ block, role: 'user' })
|
renderMainTextBlock({ block, role: 'user' })
|
||||||
|
|
||||||
@ -191,7 +199,7 @@ describe('MainTextBlock', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should preserve complex formatting in plain text mode', () => {
|
it('should preserve complex formatting in plain text mode', () => {
|
||||||
mockUseSettings.mockReturnValue({ renderInputMessageAsMarkdown: false })
|
mockUsePreference.mockReturnValue([false, vi.fn()])
|
||||||
const complexContent = `Line 1
|
const complexContent = `Line 1
|
||||||
Indented line
|
Indented line
|
||||||
**Bold not parsed**
|
**Bold not parsed**
|
||||||
@ -417,13 +425,13 @@ describe('MainTextBlock', () => {
|
|||||||
const block = createMainTextBlock({ content: 'Settings test content' })
|
const block = createMainTextBlock({ content: 'Settings test content' })
|
||||||
|
|
||||||
// Test with markdown enabled
|
// Test with markdown enabled
|
||||||
mockUseSettings.mockReturnValue({ renderInputMessageAsMarkdown: true })
|
mockUsePreference.mockReturnValue([true, vi.fn()])
|
||||||
const { unmount } = renderMainTextBlock({ block, role: 'user' })
|
const { unmount } = renderMainTextBlock({ block, role: 'user' })
|
||||||
expect(getRenderedMarkdown()).toBeInTheDocument()
|
expect(getRenderedMarkdown()).toBeInTheDocument()
|
||||||
unmount()
|
unmount()
|
||||||
|
|
||||||
// Test with markdown disabled
|
// Test with markdown disabled
|
||||||
mockUseSettings.mockReturnValue({ renderInputMessageAsMarkdown: false })
|
mockUsePreference.mockReturnValue([false, vi.fn()])
|
||||||
renderMainTextBlock({ block, role: 'user' })
|
renderMainTextBlock({ block, role: 'user' })
|
||||||
expect(getRenderedPlainText()).toBeInTheDocument()
|
expect(getRenderedPlainText()).toBeInTheDocument()
|
||||||
expect(getRenderedMarkdown()).not.toBeInTheDocument()
|
expect(getRenderedMarkdown()).not.toBeInTheDocument()
|
||||||
|
|||||||
@ -8,12 +8,17 @@ import ThinkingBlock from '../ThinkingBlock'
|
|||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
const mockUseSettings = vi.fn()
|
const mockUseSettings = vi.fn()
|
||||||
const mockUseTranslation = vi.fn()
|
const mockUseTranslation = vi.fn()
|
||||||
|
let mockUsePreference: any
|
||||||
|
|
||||||
// Mock hooks
|
// Mock hooks
|
||||||
vi.mock('@renderer/hooks/useSettings', () => ({
|
vi.mock('@renderer/hooks/useSettings', () => ({
|
||||||
useSettings: () => mockUseSettings()
|
useSettings: () => mockUseSettings()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@data/hooks/usePreference', () => ({
|
||||||
|
usePreference: vi.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('react-i18next', () => ({
|
vi.mock('react-i18next', () => ({
|
||||||
useTranslation: () => mockUseTranslation()
|
useTranslation: () => mockUseTranslation()
|
||||||
}))
|
}))
|
||||||
@ -120,6 +125,10 @@ describe('ThinkingBlock', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vi.useFakeTimers()
|
vi.useFakeTimers()
|
||||||
|
|
||||||
|
// Get the mocked functions
|
||||||
|
const { usePreference } = await import('@data/hooks/usePreference')
|
||||||
|
mockUsePreference = usePreference as any
|
||||||
|
|
||||||
// Default mock implementations
|
// Default mock implementations
|
||||||
mockUseSettings.mockReturnValue({
|
mockUseSettings.mockReturnValue({
|
||||||
messageFont: 'sans-serif',
|
messageFont: 'sans-serif',
|
||||||
@ -127,6 +136,23 @@ describe('ThinkingBlock', () => {
|
|||||||
thoughtAutoCollapse: false
|
thoughtAutoCollapse: false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Mock usePreference calls - component uses these hooks:
|
||||||
|
// - usePreference('chat.message.font')
|
||||||
|
// - usePreference('chat.message.font_size')
|
||||||
|
// - usePreference('chat.message.thought.auto_collapse')
|
||||||
|
mockUsePreference.mockImplementation((key: string) => {
|
||||||
|
switch (key) {
|
||||||
|
case 'chat.message.font':
|
||||||
|
return ['sans-serif', vi.fn()]
|
||||||
|
case 'chat.message.font_size':
|
||||||
|
return [14, vi.fn()]
|
||||||
|
case 'chat.message.thought.auto_collapse':
|
||||||
|
return [false, vi.fn()]
|
||||||
|
default:
|
||||||
|
return [undefined, vi.fn()]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
mockUseTranslation.mockReturnValue({
|
mockUseTranslation.mockReturnValue({
|
||||||
t: (key: string, params?: any) => {
|
t: (key: string, params?: any) => {
|
||||||
if (key === 'chat.thinking' && params?.seconds) {
|
if (key === 'chat.thinking' && params?.seconds) {
|
||||||
@ -275,10 +301,17 @@ describe('ThinkingBlock', () => {
|
|||||||
unmount()
|
unmount()
|
||||||
|
|
||||||
// Test collapsed by default (auto-collapse enabled)
|
// Test collapsed by default (auto-collapse enabled)
|
||||||
mockUseSettings.mockReturnValue({
|
mockUsePreference.mockImplementation((key: string) => {
|
||||||
messageFont: 'sans-serif',
|
switch (key) {
|
||||||
fontSize: 14,
|
case 'chat.message.font':
|
||||||
thoughtAutoCollapse: true
|
return ['sans-serif', vi.fn()]
|
||||||
|
case 'chat.message.font_size':
|
||||||
|
return [14, vi.fn()]
|
||||||
|
case 'chat.message.thought.auto_collapse':
|
||||||
|
return [true, vi.fn()] // Enable auto-collapse
|
||||||
|
default:
|
||||||
|
return [undefined, vi.fn()]
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
renderThinkingBlock(block)
|
renderThinkingBlock(block)
|
||||||
@ -288,10 +321,17 @@ describe('ThinkingBlock', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should auto-collapse when thinking completes if setting enabled', () => {
|
it('should auto-collapse when thinking completes if setting enabled', () => {
|
||||||
mockUseSettings.mockReturnValue({
|
mockUsePreference.mockImplementation((key: string) => {
|
||||||
messageFont: 'sans-serif',
|
switch (key) {
|
||||||
fontSize: 14,
|
case 'chat.message.font':
|
||||||
thoughtAutoCollapse: true
|
return ['sans-serif', vi.fn()]
|
||||||
|
case 'chat.message.font_size':
|
||||||
|
return [14, vi.fn()]
|
||||||
|
case 'chat.message.thought.auto_collapse':
|
||||||
|
return [true, vi.fn()] // Enable auto-collapse
|
||||||
|
default:
|
||||||
|
return [undefined, vi.fn()]
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const streamingBlock = createThinkingBlock({ status: MessageBlockStatus.STREAMING })
|
const streamingBlock = createThinkingBlock({ status: MessageBlockStatus.STREAMING })
|
||||||
@ -325,9 +365,17 @@ describe('ThinkingBlock', () => {
|
|||||||
]
|
]
|
||||||
|
|
||||||
testCases.forEach(({ settings, expectedFont, expectedSize }) => {
|
testCases.forEach(({ settings, expectedFont, expectedSize }) => {
|
||||||
mockUseSettings.mockReturnValue({
|
mockUsePreference.mockImplementation((key: string) => {
|
||||||
...settings,
|
switch (key) {
|
||||||
thoughtAutoCollapse: false
|
case 'chat.message.font':
|
||||||
|
return [settings.messageFont, vi.fn()]
|
||||||
|
case 'chat.message.font_size':
|
||||||
|
return [settings.fontSize, vi.fn()]
|
||||||
|
case 'chat.message.thought.auto_collapse':
|
||||||
|
return [false, vi.fn()] // Keep expanded to test styling
|
||||||
|
default:
|
||||||
|
return [undefined, vi.fn()]
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const block = createThinkingBlock()
|
const block = createThinkingBlock()
|
||||||
|
|||||||
@ -99,7 +99,8 @@ vi.mock('@renderer/utils', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@shared/config/prompts', () => ({
|
vi.mock('@shared/config/prompts', () => ({
|
||||||
WEB_SEARCH_PROMPT_FOR_OPENROUTER: 'mock-prompt'
|
WEB_SEARCH_PROMPT_FOR_OPENROUTER: 'mock-prompt',
|
||||||
|
TRANSLATE_PROMPT: 'You are a translation expert. Your only task is to translate text enclosed with <translate_input> from input language to {{target_language}}, provide the translation result directly without any explanation, without `TRANSLATE` and keep original format.'
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@renderer/config/systemModels', () => ({
|
vi.mock('@renderer/config/systemModels', () => ({
|
||||||
|
|||||||
@ -35,6 +35,11 @@ vi.mock('@renderer/i18n', () => ({
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Mock getProviderLabel
|
||||||
|
vi.mock('@renderer/i18n/label', () => ({
|
||||||
|
getProviderLabel: vi.fn((providerId: string) => providerId || 'Unknown Provider')
|
||||||
|
}))
|
||||||
|
|
||||||
// Mock the find utility functions - crucial for the test
|
// Mock the find utility functions - crucial for the test
|
||||||
vi.mock('@renderer/utils/messageUtils/find', () => ({
|
vi.mock('@renderer/utils/messageUtils/find', () => ({
|
||||||
// Provide type safety for mocked message
|
// Provide type safety for mocked message
|
||||||
@ -66,11 +71,13 @@ vi.mock('@renderer/hooks/useTopic', () => ({
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// PreferenceService is now mocked globally in tests/renderer.setup.ts
|
||||||
|
|
||||||
vi.mock('@renderer/utils/markdown', async (importOriginal) => {
|
vi.mock('@renderer/utils/markdown', async (importOriginal) => {
|
||||||
const actual = await importOriginal()
|
const actual = await importOriginal()
|
||||||
return {
|
return {
|
||||||
...(actual as any),
|
...(actual as any),
|
||||||
markdownToPlainText: vi.fn((str) => str) // Simple pass-through for testing export logic
|
markdownToPlainText: vi.fn((str: string) => str) // Simple pass-through for testing export logic
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -311,7 +318,7 @@ describe('export', () => {
|
|||||||
const markdown = await messageToMarkdown(msgWithCitation)
|
const markdown = await messageToMarkdown(msgWithCitation)
|
||||||
expect(markdown).toContain('## 🤖 Assistant')
|
expect(markdown).toContain('## 🤖 Assistant')
|
||||||
expect(markdown).toContain('Main content')
|
expect(markdown).toContain('Main content')
|
||||||
expect(markdown).toContain('[1] [https://example1.com](Example Citation 1)')
|
expect(markdown).toContain('[^1]: [https://example1.com](Example Citation 1)')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -352,34 +359,34 @@ describe('export', () => {
|
|||||||
expect(sections.length).toBeGreaterThanOrEqual(2)
|
expect(sections.length).toBeGreaterThanOrEqual(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle <think> tag and replace newlines with <br> in reasoning', () => {
|
it('should handle <think> tag and replace newlines with <br> in reasoning', async () => {
|
||||||
const msg = mockedMessages.find((m) => m.id === 'a3')
|
const msg = mockedMessages.find((m) => m.id === 'a3')
|
||||||
expect(msg).toBeDefined()
|
expect(msg).toBeDefined()
|
||||||
const markdown = messageToMarkdownWithReasoning(msg!)
|
const markdown = await messageToMarkdownWithReasoning(msg!)
|
||||||
expect(markdown).toContain('Answer B')
|
expect(markdown).toContain('Answer B')
|
||||||
expect(markdown).toContain('<details')
|
expect(markdown).toContain('<details')
|
||||||
expect(markdown).toContain('Line1<br>Line2')
|
expect(markdown).toContain('Line1<br>Line2')
|
||||||
expect(markdown).not.toContain('<think>')
|
expect(markdown).not.toContain('<think>')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not include details section if no thinking block exists', () => {
|
it('should not include details section if no thinking block exists', async () => {
|
||||||
const msg = mockedMessages.find((m) => m.id === 'a4')
|
const msg = mockedMessages.find((m) => m.id === 'a4')
|
||||||
expect(msg).toBeDefined()
|
expect(msg).toBeDefined()
|
||||||
const markdown = messageToMarkdownWithReasoning(msg!)
|
const markdown = await messageToMarkdownWithReasoning(msg!)
|
||||||
expect(markdown).toContain('## 🤖 Assistant')
|
expect(markdown).toContain('## 🤖 Assistant')
|
||||||
expect(markdown).toContain('Simple Answer')
|
expect(markdown).toContain('Simple Answer')
|
||||||
expect(markdown).not.toContain('<details')
|
expect(markdown).not.toContain('<details')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should include both reasoning and citation content', () => {
|
it('should include both reasoning and citation content', async () => {
|
||||||
const msg = mockedMessages.find((m) => m.id === 'a5')
|
const msg = mockedMessages.find((m) => m.id === 'a5')
|
||||||
expect(msg).toBeDefined()
|
expect(msg).toBeDefined()
|
||||||
const markdown = messageToMarkdownWithReasoning(msg!)
|
const markdown = await messageToMarkdownWithReasoning(msg!)
|
||||||
expect(markdown).toContain('## 🤖 Assistant')
|
expect(markdown).toContain('## 🤖 Assistant')
|
||||||
expect(markdown).toContain('Answer with citation')
|
expect(markdown).toContain('Answer with citation')
|
||||||
expect(markdown).toContain('<details')
|
expect(markdown).toContain('<details')
|
||||||
expect(markdown).toContain('Some thinking')
|
expect(markdown).toContain('Some thinking')
|
||||||
expect(markdown).toContain('[1] [https://example1.com](Example Citation 1)')
|
expect(markdown).toContain('[^1]: [https://example1.com](Example Citation 1)')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should format citations as footnotes when standardize citations is enabled', () => {
|
it('should format citations as footnotes when standardize citations is enabled', () => {
|
||||||
@ -415,7 +422,7 @@ describe('export', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should handle an empty array of messages', async () => {
|
it('should handle an empty array of messages', async () => {
|
||||||
expect(messagesToMarkdown([])).toBe('')
|
expect(await messagesToMarkdown([])).toBe('')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle a single message without separator', async () => {
|
it('should handle a single message without separator', async () => {
|
||||||
@ -458,7 +465,7 @@ describe('export', () => {
|
|||||||
const { TopicManager } = await import('@renderer/hooks/useTopic')
|
const { TopicManager } = await import('@renderer/hooks/useTopic')
|
||||||
;(TopicManager.getTopicMessages as any).mockResolvedValue([userMsg, assistantMsg])
|
;(TopicManager.getTopicMessages as any).mockResolvedValue([userMsg, assistantMsg])
|
||||||
// Specific mock for this test to check formatting
|
// Specific mock for this test to check formatting
|
||||||
;(markdownToPlainText as any).mockImplementation(async (str: string) => str.replace(/[#*]/g, ''))
|
;(markdownToPlainText as any).mockImplementation((str: string) => str.replace(/[#*]/g, ''))
|
||||||
|
|
||||||
const plainText = await topicToPlainText(testTopic)
|
const plainText = await topicToPlainText(testTopic)
|
||||||
|
|
||||||
@ -475,7 +482,7 @@ describe('export', () => {
|
|||||||
const testMessage = createMessage({ role: 'user', id: 'single_msg_plain' }, [
|
const testMessage = createMessage({ role: 'user', id: 'single_msg_plain' }, [
|
||||||
{ type: MessageBlockType.MAIN_TEXT, content: '### Single Message Content' }
|
{ type: MessageBlockType.MAIN_TEXT, content: '### Single Message Content' }
|
||||||
])
|
])
|
||||||
;(markdownToPlainText as any).mockImplementation(async (str: string) => str.replace(/[#*_]/g, ''))
|
;(markdownToPlainText as any).mockImplementation((str: string) => str.replace(/[#*_]/g, ''))
|
||||||
|
|
||||||
const result = await messageToPlainText(testMessage)
|
const result = await messageToPlainText(testMessage)
|
||||||
expect(result).toBe('Single Message Content')
|
expect(result).toBe('Single Message Content')
|
||||||
@ -1002,7 +1009,7 @@ describe('Citation formatting in Markdown export', () => {
|
|||||||
expect(processedContent).not.toContain('<sup')
|
expect(processedContent).not.toContain('<sup')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should properly test formatCitationsAsFootnotes through messageToMarkdown', () => {
|
test('should properly test formatCitationsAsFootnotes through messageToMarkdown', async () => {
|
||||||
const msgWithCitations = createMessage({ role: 'assistant', id: 'test_footnotes' }, [
|
const msgWithCitations = createMessage({ role: 'assistant', id: 'test_footnotes' }, [
|
||||||
{
|
{
|
||||||
type: MessageBlockType.MAIN_TEXT,
|
type: MessageBlockType.MAIN_TEXT,
|
||||||
@ -1012,13 +1019,13 @@ describe('Citation formatting in Markdown export', () => {
|
|||||||
])
|
])
|
||||||
|
|
||||||
// This tests the complete flow including formatCitationsAsFootnotes
|
// This tests the complete flow including formatCitationsAsFootnotes
|
||||||
const markdown = messageToMarkdown(msgWithCitations)
|
const markdown = await messageToMarkdown(msgWithCitations)
|
||||||
|
|
||||||
// Should contain the title and content
|
// Should contain the title and content
|
||||||
expect(markdown).toContain('## 🤖 Assistant')
|
expect(markdown).toContain('## 🤖 Assistant')
|
||||||
expect(markdown).toContain('Content with citations')
|
expect(markdown).toContain('Content with citations')
|
||||||
|
|
||||||
// Should include citation content (mocked by getCitationContent)
|
// Should include citation content (mocked by getCitationContent)
|
||||||
expect(markdown).toContain('[1] [https://example1.com](Example Citation 1)')
|
expect(markdown).toContain('[^1]: [https://example1.com](Example Citation 1)')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,8 +1,20 @@
|
|||||||
import type { Model, Provider, SystemProvider } from '@renderer/types'
|
import type { Model, Provider, SystemProvider } from '@renderer/types'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
import { includeKeywords, matchKeywordsInModel, matchKeywordsInProvider, matchKeywordsInString } from '../match'
|
import { includeKeywords, matchKeywordsInModel, matchKeywordsInProvider, matchKeywordsInString } from '../match'
|
||||||
|
|
||||||
|
// Mock i18n to return English provider labels
|
||||||
|
vi.mock('@renderer/i18n/label', () => ({
|
||||||
|
getProviderLabel: vi.fn((id: string) => {
|
||||||
|
const labelMap: Record<string, string> = {
|
||||||
|
'dashscope': 'Alibaba Cloud',
|
||||||
|
'openai': 'OpenAI',
|
||||||
|
'anthropic': 'Anthropic'
|
||||||
|
}
|
||||||
|
return labelMap[id] || id
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
describe('match', () => {
|
describe('match', () => {
|
||||||
const provider = {
|
const provider = {
|
||||||
id: '12345',
|
id: '12345',
|
||||||
|
|||||||
@ -1,5 +1,17 @@
|
|||||||
import { Provider, SystemProvider } from '@renderer/types'
|
import { Provider, SystemProvider } from '@renderer/types'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
// Mock i18n to return English provider labels
|
||||||
|
vi.mock('@renderer/i18n/label', () => ({
|
||||||
|
getProviderLabel: vi.fn((id: string) => {
|
||||||
|
const labelMap: Record<string, string> = {
|
||||||
|
'dashscope': 'Alibaba Cloud',
|
||||||
|
'openai': 'OpenAI',
|
||||||
|
'anthropic': 'Anthropic'
|
||||||
|
}
|
||||||
|
return labelMap[id] || id
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
import {
|
import {
|
||||||
firstLetter,
|
firstLetter,
|
||||||
|
|||||||
@ -270,7 +270,7 @@ const createBaseMarkdown = async (
|
|||||||
normalizeCitations: boolean = true
|
normalizeCitations: boolean = true
|
||||||
): Promise<{ titleSection: string; reasoningSection: string; contentSection: string; citation: string }> => {
|
): Promise<{ titleSection: string; reasoningSection: string; contentSection: string; citation: string }> => {
|
||||||
const forceDollarMathInMarkdown = await preferenceService.get('data.export.markdown.force_dollar_math')
|
const forceDollarMathInMarkdown = await preferenceService.get('data.export.markdown.force_dollar_math')
|
||||||
const roleText = getRoleText(message.role, message.model?.name, message.model?.provider)
|
const roleText = await getRoleText(message.role, message.model?.name, message.model?.provider)
|
||||||
const titleSection = `## ${roleText}`
|
const titleSection = `## ${roleText}`
|
||||||
let reasoningSection = ''
|
let reasoningSection = ''
|
||||||
|
|
||||||
|
|||||||
609
tests/__mocks__/README.md
Normal file
609
tests/__mocks__/README.md
Normal file
@ -0,0 +1,609 @@
|
|||||||
|
# Test Mocks
|
||||||
|
|
||||||
|
这个目录包含了项目中使用的统一测试模拟(mocks)。这些模拟按照进程类型组织,避免重名冲突,并在相应的测试设置文件中全局配置。
|
||||||
|
|
||||||
|
## 🎯 统一模拟概述
|
||||||
|
|
||||||
|
### 已实现的统一模拟
|
||||||
|
|
||||||
|
#### Renderer Process Mocks
|
||||||
|
- ✅ **PreferenceService** - 渲染进程偏好设置服务模拟
|
||||||
|
- ✅ **DataApiService** - 渲染进程数据API服务模拟
|
||||||
|
- ✅ **CacheService** - 渲染进程三层缓存服务模拟
|
||||||
|
- ✅ **useDataApi hooks** - 数据API钩子模拟 (useQuery, useMutation, usePaginatedQuery, etc.)
|
||||||
|
- ✅ **usePreference hooks** - 偏好设置钩子模拟 (usePreference, useMultiplePreferences)
|
||||||
|
- ✅ **useCache hooks** - 缓存钩子模拟 (useCache, useSharedCache, usePersistCache)
|
||||||
|
|
||||||
|
#### Main Process Mocks
|
||||||
|
- ✅ **PreferenceService** - 主进程偏好设置服务模拟
|
||||||
|
- ✅ **DataApiService** - 主进程数据API服务模拟
|
||||||
|
- ✅ **CacheService** - 主进程缓存服务模拟
|
||||||
|
|
||||||
|
### 🌟 核心优势
|
||||||
|
|
||||||
|
- **进程分离**: 按照renderer/main分开组织,避免重名冲突
|
||||||
|
- **自动应用**: 无需在每个测试文件中单独模拟
|
||||||
|
- **完整API覆盖**: 实现了所有服务和钩子的完整API
|
||||||
|
- **类型安全**: 完全支持 TypeScript,保持与真实服务的类型兼容性
|
||||||
|
- **现实行为**: 模拟提供现实的默认值和行为模式
|
||||||
|
- **高度可定制**: 支持为特定测试定制行为
|
||||||
|
- **测试工具**: 内置丰富的测试工具函数
|
||||||
|
|
||||||
|
### 📁 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/__mocks__/
|
||||||
|
├── README.md # 本文档
|
||||||
|
├── renderer/ # 渲染进程模拟
|
||||||
|
│ ├── PreferenceService.ts # 渲染进程偏好设置服务模拟
|
||||||
|
│ ├── DataApiService.ts # 渲染进程数据API服务模拟
|
||||||
|
│ ├── CacheService.ts # 渲染进程缓存服务模拟
|
||||||
|
│ ├── useDataApi.ts # 数据API钩子模拟
|
||||||
|
│ ├── usePreference.ts # 偏好设置钩子模拟
|
||||||
|
│ └── useCache.ts # 缓存钩子模拟
|
||||||
|
├── main/ # 主进程模拟
|
||||||
|
│ ├── PreferenceService.ts # 主进程偏好设置服务模拟
|
||||||
|
│ ├── DataApiService.ts # 主进程数据API服务模拟
|
||||||
|
│ └── CacheService.ts # 主进程缓存服务模拟
|
||||||
|
├── RendererLoggerService.ts # 渲染进程日志服务模拟
|
||||||
|
└── MainLoggerService.ts # 主进程日志服务模拟
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔧 测试设置
|
||||||
|
|
||||||
|
#### Renderer Process Tests
|
||||||
|
在 `tests/renderer.setup.ts` 中配置了所有渲染进程模拟:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 自动加载 renderer/ 目录下的模拟
|
||||||
|
vi.mock('@data/PreferenceService', async () => {
|
||||||
|
const { MockPreferenceService } = await import('./__mocks__/renderer/PreferenceService')
|
||||||
|
return MockPreferenceService
|
||||||
|
})
|
||||||
|
// ... 其他渲染进程模拟
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Main Process Tests
|
||||||
|
在 `tests/main.setup.ts` 中配置了所有主进程模拟:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 自动加载 main/ 目录下的模拟
|
||||||
|
vi.mock('@main/data/PreferenceService', async () => {
|
||||||
|
const { MockMainPreferenceServiceExport } = await import('./__mocks__/main/PreferenceService')
|
||||||
|
return MockMainPreferenceServiceExport
|
||||||
|
})
|
||||||
|
// ... 其他主进程模拟
|
||||||
|
```
|
||||||
|
|
||||||
|
## PreferenceService Mock
|
||||||
|
|
||||||
|
### 简介
|
||||||
|
|
||||||
|
`PreferenceService.ts` 提供了 PreferenceService 的统一模拟实现,用于所有渲染进程测试。这个模拟:
|
||||||
|
|
||||||
|
- ✅ **自动应用**:在 `renderer.setup.ts` 中全局配置,无需在每个测试文件中单独模拟
|
||||||
|
- ✅ **完整API**:实现了 PreferenceService 的所有方法(get, getMultiple, set, etc.)
|
||||||
|
- ✅ **合理默认值**:提供了常用偏好设置的默认值
|
||||||
|
- ✅ **可定制**:支持为特定测试定制默认值
|
||||||
|
- ✅ **类型安全**:完全支持 TypeScript 类型检查
|
||||||
|
|
||||||
|
### 默认值
|
||||||
|
|
||||||
|
模拟提供了以下默认偏好设置:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 导出偏好设置
|
||||||
|
'data.export.markdown.force_dollar_math': false
|
||||||
|
'data.export.markdown.exclude_citations': false
|
||||||
|
'data.export.markdown.standardize_citations': true
|
||||||
|
'data.export.markdown.show_model_name': false
|
||||||
|
'data.export.markdown.show_model_provider': false
|
||||||
|
|
||||||
|
// UI偏好设置
|
||||||
|
'ui.language': 'en'
|
||||||
|
'ui.theme': 'light'
|
||||||
|
'ui.font_size': 14
|
||||||
|
|
||||||
|
// AI偏好设置
|
||||||
|
'ai.default_model': 'gpt-4'
|
||||||
|
'ai.temperature': 0.7
|
||||||
|
'ai.max_tokens': 2000
|
||||||
|
|
||||||
|
// 功能开关
|
||||||
|
'feature.web_search': true
|
||||||
|
'feature.reasoning': false
|
||||||
|
'feature.tool_calling': true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 基本使用
|
||||||
|
|
||||||
|
由于模拟已经全局配置,大多数测试可以直接使用 PreferenceService,无需额外设置:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { preferenceService } from '@data/PreferenceService'
|
||||||
|
|
||||||
|
describe('MyComponent', () => {
|
||||||
|
it('should use preference values', async () => {
|
||||||
|
// PreferenceService 已经被自动模拟
|
||||||
|
const value = await preferenceService.get('ui.theme')
|
||||||
|
expect(value).toBe('light') // 使用默认值
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 高级使用
|
||||||
|
|
||||||
|
#### 1. 修改单个测试的偏好值
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { preferenceService } from '@data/PreferenceService'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
|
||||||
|
describe('Custom preferences', () => {
|
||||||
|
it('should work with custom preference values', async () => {
|
||||||
|
// 为这个测试修改特定值
|
||||||
|
;(preferenceService.get as any).mockImplementation((key: string) => {
|
||||||
|
if (key === 'ui.theme') return Promise.resolve('dark')
|
||||||
|
// 其他键使用默认模拟行为
|
||||||
|
return vi.fn().mockResolvedValue(null)()
|
||||||
|
})
|
||||||
|
|
||||||
|
const theme = await preferenceService.get('ui.theme')
|
||||||
|
expect(theme).toBe('dark')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 重置模拟状态
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { preferenceService } from '@data/PreferenceService'
|
||||||
|
|
||||||
|
describe('Mock state management', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// 重置模拟到初始状态
|
||||||
|
if ('_resetMockState' in preferenceService) {
|
||||||
|
;(preferenceService as any)._resetMockState()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 检查模拟内部状态
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { preferenceService } from '@data/PreferenceService'
|
||||||
|
|
||||||
|
describe('Mock inspection', () => {
|
||||||
|
it('should allow inspecting mock state', () => {
|
||||||
|
// 查看当前模拟状态
|
||||||
|
if ('_getMockState' in preferenceService) {
|
||||||
|
const state = (preferenceService as any)._getMockState()
|
||||||
|
console.log('Current mock state:', state)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 为整个测试套件定制默认值
|
||||||
|
|
||||||
|
如果需要为特定的测试文件定制默认值,可以在该文件中重新模拟:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
|
||||||
|
// 重写全局模拟,添加自定义默认值
|
||||||
|
vi.mock('@data/PreferenceService', async () => {
|
||||||
|
const { createMockPreferenceService } = await import('tests/__mocks__/PreferenceService')
|
||||||
|
|
||||||
|
// 定制默认值
|
||||||
|
const customDefaults = {
|
||||||
|
'my.custom.setting': 'custom_value',
|
||||||
|
'ui.theme': 'dark' // 覆盖默认值
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
preferenceService: createMockPreferenceService(customDefaults)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试验证
|
||||||
|
|
||||||
|
可以验证 PreferenceService 方法是否被正确调用:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { preferenceService } from '@data/PreferenceService'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
|
||||||
|
describe('Preference service calls', () => {
|
||||||
|
it('should call preference service methods', async () => {
|
||||||
|
await preferenceService.get('ui.theme')
|
||||||
|
|
||||||
|
// 验证方法调用
|
||||||
|
expect(preferenceService.get).toHaveBeenCalledWith('ui.theme')
|
||||||
|
expect(preferenceService.get).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加新的默认值
|
||||||
|
|
||||||
|
当项目中添加新的偏好设置时,请在 `PreferenceService.ts` 的 `mockPreferenceDefaults` 中添加相应的默认值:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const mockPreferenceDefaults: Record<string, any> = {
|
||||||
|
// 现有默认值...
|
||||||
|
|
||||||
|
// 新增默认值
|
||||||
|
'new.feature.enabled': true,
|
||||||
|
'new.feature.config': { option: 'value' }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这样可以确保所有测试都能使用合理的默认值,减少测试失败的可能性。
|
||||||
|
|
||||||
|
## DataApiService Mock
|
||||||
|
|
||||||
|
### 简介
|
||||||
|
|
||||||
|
`DataApiService.ts` 提供了数据API服务的统一模拟,支持所有HTTP方法和高级功能。
|
||||||
|
|
||||||
|
### 功能特性
|
||||||
|
|
||||||
|
- **完整HTTP支持**: GET, POST, PUT, PATCH, DELETE
|
||||||
|
- **批量操作**: batch() 和 transaction() 支持
|
||||||
|
- **订阅系统**: subscribe/unsubscribe 模拟
|
||||||
|
- **连接管理**: connect/disconnect/ping 方法
|
||||||
|
- **智能模拟数据**: 基于路径自动生成合理的响应
|
||||||
|
|
||||||
|
### 基本使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { dataApiService } from '@data/DataApiService'
|
||||||
|
|
||||||
|
describe('API Integration', () => {
|
||||||
|
it('should fetch topics', async () => {
|
||||||
|
// 自动模拟,返回预设的主题列表
|
||||||
|
const response = await dataApiService.get('/api/topics')
|
||||||
|
expect(response.success).toBe(true)
|
||||||
|
expect(response.data.topics).toHaveLength(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 高级使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { MockDataApiUtils } from 'tests/__mocks__/DataApiService'
|
||||||
|
|
||||||
|
describe('Custom API behavior', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
MockDataApiUtils.resetMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle custom responses', async () => {
|
||||||
|
// 设置特定路径的自定义响应
|
||||||
|
MockDataApiUtils.setCustomResponse('/api/topics', 'GET', {
|
||||||
|
topics: [{ id: 'custom', name: 'Custom Topic' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await dataApiService.get('/api/topics')
|
||||||
|
expect(response.data.topics[0].name).toBe('Custom Topic')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should simulate errors', async () => {
|
||||||
|
// 模拟错误响应
|
||||||
|
MockDataApiUtils.setErrorResponse('/api/topics', 'GET', 'Network error')
|
||||||
|
|
||||||
|
const response = await dataApiService.get('/api/topics')
|
||||||
|
expect(response.success).toBe(false)
|
||||||
|
expect(response.error?.message).toBe('Network error')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## CacheService Mock
|
||||||
|
|
||||||
|
### 简介
|
||||||
|
|
||||||
|
`CacheService.ts` 提供了三层缓存系统的完整模拟:内存缓存、共享缓存和持久化缓存。
|
||||||
|
|
||||||
|
### 功能特性
|
||||||
|
|
||||||
|
- **三层架构**: 内存、共享、持久化缓存
|
||||||
|
- **订阅系统**: 支持缓存变更订阅
|
||||||
|
- **TTL支持**: 模拟缓存过期(简化版)
|
||||||
|
- **Hook引用跟踪**: 模拟生产环境的引用管理
|
||||||
|
- **默认值**: 基于缓存schema的智能默认值
|
||||||
|
|
||||||
|
### 基本使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { cacheService } from '@data/CacheService'
|
||||||
|
|
||||||
|
describe('Cache Operations', () => {
|
||||||
|
it('should store and retrieve cache values', () => {
|
||||||
|
// 设置缓存值
|
||||||
|
cacheService.set('user.preferences', { theme: 'dark' })
|
||||||
|
|
||||||
|
// 获取缓存值
|
||||||
|
const preferences = cacheService.get('user.preferences')
|
||||||
|
expect(preferences.theme).toBe('dark')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should work with persist cache', () => {
|
||||||
|
// 持久化缓存操作
|
||||||
|
cacheService.setPersist('app.last_opened_topic', 'topic123')
|
||||||
|
const lastTopic = cacheService.getPersist('app.last_opened_topic')
|
||||||
|
expect(lastTopic).toBe('topic123')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 高级测试工具
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { MockCacheUtils } from 'tests/__mocks__/CacheService'
|
||||||
|
|
||||||
|
describe('Advanced cache testing', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
MockCacheUtils.resetMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set initial cache state', () => {
|
||||||
|
// 设置初始缓存状态
|
||||||
|
MockCacheUtils.setInitialState({
|
||||||
|
memory: [['theme', 'dark'], ['language', 'en']],
|
||||||
|
persist: [['app.version', '1.0.0']]
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(cacheService.get('theme')).toBe('dark')
|
||||||
|
expect(cacheService.getPersist('app.version')).toBe('1.0.0')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should simulate cache changes', () => {
|
||||||
|
let changeCount = 0
|
||||||
|
cacheService.subscribe('theme', () => changeCount++)
|
||||||
|
|
||||||
|
MockCacheUtils.triggerCacheChange('theme', 'light')
|
||||||
|
expect(changeCount).toBe(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## useDataApi Hooks Mock
|
||||||
|
|
||||||
|
### 简介
|
||||||
|
|
||||||
|
`useDataApi.ts` 提供了所有数据API钩子的统一模拟,包括查询、变更和分页功能。
|
||||||
|
|
||||||
|
### 支持的钩子
|
||||||
|
|
||||||
|
- `useQuery` - 数据查询钩子
|
||||||
|
- `useMutation` - 数据变更钩子
|
||||||
|
- `usePaginatedQuery` - 分页查询钩子
|
||||||
|
- `useInvalidateCache` - 缓存失效钩子
|
||||||
|
- `prefetch` - 预取函数
|
||||||
|
|
||||||
|
### 基本使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useQuery, useMutation } from '@data/hooks/useDataApi'
|
||||||
|
|
||||||
|
describe('Data API Hooks', () => {
|
||||||
|
it('should work with useQuery', () => {
|
||||||
|
const { data, isLoading, error } = useQuery('/api/topics')
|
||||||
|
|
||||||
|
// 默认返回模拟数据
|
||||||
|
expect(data).toBeDefined()
|
||||||
|
expect(data.topics).toHaveLength(2)
|
||||||
|
expect(isLoading).toBe(false)
|
||||||
|
expect(error).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should work with useMutation', async () => {
|
||||||
|
const { trigger, isMutating } = useMutation('/api/topics', 'POST')
|
||||||
|
|
||||||
|
const result = await trigger({ name: 'New Topic' })
|
||||||
|
expect(result.created).toBe(true)
|
||||||
|
expect(result.name).toBe('New Topic')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义测试行为
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { MockUseDataApiUtils } from 'tests/__mocks__/useDataApi'
|
||||||
|
|
||||||
|
describe('Custom hook behavior', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
MockUseDataApiUtils.resetMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should mock loading state', () => {
|
||||||
|
MockUseDataApiUtils.mockQueryLoading('/api/topics')
|
||||||
|
|
||||||
|
const { data, isLoading } = useQuery('/api/topics')
|
||||||
|
expect(isLoading).toBe(true)
|
||||||
|
expect(data).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should mock error state', () => {
|
||||||
|
const error = new Error('API Error')
|
||||||
|
MockUseDataApiUtils.mockQueryError('/api/topics', error)
|
||||||
|
|
||||||
|
const { data, error: queryError } = useQuery('/api/topics')
|
||||||
|
expect(queryError).toBe(error)
|
||||||
|
expect(data).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## usePreference Hooks Mock
|
||||||
|
|
||||||
|
### 简介
|
||||||
|
|
||||||
|
`usePreference.ts` 提供了偏好设置钩子的统一模拟,支持单个和批量偏好管理。
|
||||||
|
|
||||||
|
### 支持的钩子
|
||||||
|
|
||||||
|
- `usePreference` - 单个偏好设置钩子
|
||||||
|
- `useMultiplePreferences` - 多个偏好设置钩子
|
||||||
|
|
||||||
|
### 基本使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { usePreference, useMultiplePreferences } from '@data/hooks/usePreference'
|
||||||
|
|
||||||
|
describe('Preference Hooks', () => {
|
||||||
|
it('should work with usePreference', async () => {
|
||||||
|
const [theme, setTheme] = usePreference('ui.theme')
|
||||||
|
|
||||||
|
expect(theme).toBe('light') // 默认值
|
||||||
|
|
||||||
|
await setTheme('dark')
|
||||||
|
// 在测试中,可以通过工具函数验证值是否更新
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should work with multiple preferences', async () => {
|
||||||
|
const [prefs, setPrefs] = useMultiplePreferences({
|
||||||
|
theme: 'ui.theme',
|
||||||
|
lang: 'ui.language'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(prefs.theme).toBe('light')
|
||||||
|
expect(prefs.lang).toBe('en')
|
||||||
|
|
||||||
|
await setPrefs({ theme: 'dark' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 高级测试
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { MockUsePreferenceUtils } from 'tests/__mocks__/usePreference'
|
||||||
|
|
||||||
|
describe('Advanced preference testing', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
MockUsePreferenceUtils.resetMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should simulate preference changes', () => {
|
||||||
|
MockUsePreferenceUtils.setPreferenceValue('ui.theme', 'dark')
|
||||||
|
|
||||||
|
const [theme] = usePreference('ui.theme')
|
||||||
|
expect(theme).toBe('dark')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should simulate external changes', () => {
|
||||||
|
let callCount = 0
|
||||||
|
MockUsePreferenceUtils.addSubscriber('ui.theme', () => callCount++)
|
||||||
|
|
||||||
|
MockUsePreferenceUtils.simulateExternalPreferenceChange('ui.theme', 'dark')
|
||||||
|
expect(callCount).toBe(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## useCache Hooks Mock
|
||||||
|
|
||||||
|
### 简介
|
||||||
|
|
||||||
|
`useCache.ts` 提供了缓存钩子的统一模拟,支持三种缓存层级。
|
||||||
|
|
||||||
|
### 支持的钩子
|
||||||
|
|
||||||
|
- `useCache` - 内存缓存钩子
|
||||||
|
- `useSharedCache` - 共享缓存钩子
|
||||||
|
- `usePersistCache` - 持久化缓存钩子
|
||||||
|
|
||||||
|
### 基本使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useCache, useSharedCache, usePersistCache } from '@data/hooks/useCache'
|
||||||
|
|
||||||
|
describe('Cache Hooks', () => {
|
||||||
|
it('should work with useCache', () => {
|
||||||
|
const [theme, setTheme] = useCache('ui.theme', 'light')
|
||||||
|
|
||||||
|
expect(theme).toBe('light')
|
||||||
|
setTheme('dark')
|
||||||
|
// 值立即更新
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should work with different cache types', () => {
|
||||||
|
const [shared, setShared] = useSharedCache('app.window_count', 1)
|
||||||
|
const [persist, setPersist] = usePersistCache('app.last_version', '1.0.0')
|
||||||
|
|
||||||
|
expect(shared).toBe(1)
|
||||||
|
expect(persist).toBe('1.0.0')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试工具
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { MockUseCacheUtils } from 'tests/__mocks__/useCache'
|
||||||
|
|
||||||
|
describe('Cache hook testing', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
MockUseCacheUtils.resetMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set initial cache state', () => {
|
||||||
|
MockUseCacheUtils.setMultipleCacheValues({
|
||||||
|
memory: [['ui.theme', 'dark']],
|
||||||
|
shared: [['app.mode', 'development']],
|
||||||
|
persist: [['user.id', 'user123']]
|
||||||
|
})
|
||||||
|
|
||||||
|
const [theme] = useCache('ui.theme')
|
||||||
|
const [mode] = useSharedCache('app.mode')
|
||||||
|
const [userId] = usePersistCache('user.id')
|
||||||
|
|
||||||
|
expect(theme).toBe('dark')
|
||||||
|
expect(mode).toBe('development')
|
||||||
|
expect(userId).toBe('user123')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## LoggerService Mock
|
||||||
|
|
||||||
|
### 简介
|
||||||
|
|
||||||
|
项目还包含了 LoggerService 的模拟:
|
||||||
|
- `RendererLoggerService.ts` - 渲染进程日志服务模拟
|
||||||
|
- `MainLoggerService.ts` - 主进程日志服务模拟
|
||||||
|
|
||||||
|
这些模拟同样在相应的测试设置文件中全局配置。
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
1. **优先使用全局模拟**:大多数情况下应该直接使用全局配置的模拟,而不是在每个测试中单独模拟
|
||||||
|
2. **合理的默认值**:确保模拟的默认值反映实际应用的常见配置
|
||||||
|
3. **文档更新**:当添加新的模拟或修改现有模拟时,请更新相关文档
|
||||||
|
4. **类型安全**:保持模拟与实际服务的类型兼容性
|
||||||
|
5. **测试隔离**:如果需要修改模拟行为,确保在测试后恢复或在 beforeEach 中重置
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 模拟未生效
|
||||||
|
|
||||||
|
如果发现 PreferenceService 模拟未生效:
|
||||||
|
|
||||||
|
1. 确认测试运行在渲染进程环境中(`vitest.config.ts` 中的 `renderer` 项目)
|
||||||
|
2. 检查 `tests/renderer.setup.ts` 是否正确配置
|
||||||
|
3. 确认导入路径使用的是 `@data/PreferenceService` 而非相对路径
|
||||||
|
|
||||||
|
### 类型错误
|
||||||
|
|
||||||
|
如果遇到 TypeScript 类型错误:
|
||||||
|
|
||||||
|
1. 确认模拟实现与实际 PreferenceService 接口匹配
|
||||||
|
2. 在测试中使用类型断言:`(preferenceService as any)._getMockState()`
|
||||||
|
3. 检查是否需要更新模拟的类型定义
|
||||||
237
tests/__mocks__/main/CacheService.ts
Normal file
237
tests/__mocks__/main/CacheService.ts
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
import type { CacheEntry, CacheSyncMessage } from '@shared/data/cache/cacheTypes'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock CacheService for main process testing
|
||||||
|
* Simulates the complete main process CacheService functionality
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Mock cache storage
|
||||||
|
const mockMainCache = new Map<string, CacheEntry>()
|
||||||
|
|
||||||
|
// Mock broadcast tracking
|
||||||
|
const mockBroadcastCalls: Array<{ message: CacheSyncMessage; senderWindowId?: number }> = []
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock CacheService class
|
||||||
|
*/
|
||||||
|
export class MockMainCacheService {
|
||||||
|
private static instance: MockMainCacheService
|
||||||
|
private initialized = false
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
public static getInstance(): MockMainCacheService {
|
||||||
|
if (!MockMainCacheService.instance) {
|
||||||
|
MockMainCacheService.instance = new MockMainCacheService()
|
||||||
|
}
|
||||||
|
return MockMainCacheService.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock initialization
|
||||||
|
public initialize = vi.fn(async (): Promise<void> => {
|
||||||
|
this.initialized = true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock main process cache methods
|
||||||
|
public get = vi.fn(<T>(key: string): T | undefined => {
|
||||||
|
const entry = mockMainCache.get(key)
|
||||||
|
if (!entry) return undefined
|
||||||
|
|
||||||
|
// Check TTL (lazy cleanup)
|
||||||
|
if (entry.expireAt && Date.now() > entry.expireAt) {
|
||||||
|
mockMainCache.delete(key)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry.value as T
|
||||||
|
})
|
||||||
|
|
||||||
|
public set = vi.fn(<T>(key: string, value: T, ttl?: number): void => {
|
||||||
|
const entry: CacheEntry<T> = {
|
||||||
|
value,
|
||||||
|
expireAt: ttl ? Date.now() + ttl : undefined
|
||||||
|
}
|
||||||
|
mockMainCache.set(key, entry)
|
||||||
|
})
|
||||||
|
|
||||||
|
public has = vi.fn((key: string): boolean => {
|
||||||
|
const entry = mockMainCache.get(key)
|
||||||
|
if (!entry) return false
|
||||||
|
|
||||||
|
// Check TTL
|
||||||
|
if (entry.expireAt && Date.now() > entry.expireAt) {
|
||||||
|
mockMainCache.delete(key)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
public delete = vi.fn((key: string): boolean => {
|
||||||
|
return mockMainCache.delete(key)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock cleanup
|
||||||
|
public cleanup = vi.fn((): void => {
|
||||||
|
mockMainCache.clear()
|
||||||
|
mockBroadcastCalls.length = 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// Private methods exposed for testing
|
||||||
|
private broadcastSync = vi.fn((message: CacheSyncMessage, senderWindowId?: number): void => {
|
||||||
|
mockBroadcastCalls.push({ message, senderWindowId })
|
||||||
|
})
|
||||||
|
|
||||||
|
private setupIpcHandlers = vi.fn((): void => {
|
||||||
|
// Mock IPC handler setup
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock singleton instance
|
||||||
|
const mockInstance = MockMainCacheService.getInstance()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export mock service
|
||||||
|
*/
|
||||||
|
export const MockMainCacheServiceExport = {
|
||||||
|
CacheService: MockMainCacheService,
|
||||||
|
cacheService: mockInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility functions for testing
|
||||||
|
*/
|
||||||
|
export const MockMainCacheServiceUtils = {
|
||||||
|
/**
|
||||||
|
* Reset all mock call counts and state
|
||||||
|
*/
|
||||||
|
resetMocks: () => {
|
||||||
|
// Reset all method mocks
|
||||||
|
Object.values(mockInstance).forEach(method => {
|
||||||
|
if (vi.isMockFunction(method)) {
|
||||||
|
method.mockClear()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reset cache state
|
||||||
|
mockMainCache.clear()
|
||||||
|
mockBroadcastCalls.length = 0
|
||||||
|
|
||||||
|
// Reset initialized state
|
||||||
|
mockInstance['initialized'] = false
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set cache value for testing
|
||||||
|
*/
|
||||||
|
setCacheValue: <T>(key: string, value: T, ttl?: number) => {
|
||||||
|
const entry: CacheEntry<T> = {
|
||||||
|
value,
|
||||||
|
expireAt: ttl ? Date.now() + ttl : undefined
|
||||||
|
}
|
||||||
|
mockMainCache.set(key, entry)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache value for testing
|
||||||
|
*/
|
||||||
|
getCacheValue: <T>(key: string): T | undefined => {
|
||||||
|
const entry = mockMainCache.get(key)
|
||||||
|
if (!entry) return undefined
|
||||||
|
|
||||||
|
// Check TTL
|
||||||
|
if (entry.expireAt && Date.now() > entry.expireAt) {
|
||||||
|
mockMainCache.delete(key)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry.value as T
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set initialization state for testing
|
||||||
|
*/
|
||||||
|
setInitialized: (initialized: boolean) => {
|
||||||
|
mockInstance['initialized'] = initialized
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current initialization state
|
||||||
|
*/
|
||||||
|
isInitialized: (): boolean => {
|
||||||
|
return mockInstance['initialized']
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all cache entries for testing
|
||||||
|
*/
|
||||||
|
getAllCacheEntries: (): Map<string, CacheEntry> => {
|
||||||
|
return new Map(mockMainCache)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get broadcast call history for testing
|
||||||
|
*/
|
||||||
|
getBroadcastHistory: (): Array<{ message: CacheSyncMessage; senderWindowId?: number }> => {
|
||||||
|
return [...mockBroadcastCalls]
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate cache sync broadcast
|
||||||
|
*/
|
||||||
|
simulateCacheSync: (message: CacheSyncMessage, senderWindowId?: number) => {
|
||||||
|
mockBroadcastCalls.push({ message, senderWindowId })
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set multiple cache values at once
|
||||||
|
*/
|
||||||
|
setMultipleCacheValues: (values: Array<[string, any, number?]>) => {
|
||||||
|
values.forEach(([key, value, ttl]) => {
|
||||||
|
const entry: CacheEntry = {
|
||||||
|
value,
|
||||||
|
expireAt: ttl ? Date.now() + ttl : undefined
|
||||||
|
}
|
||||||
|
mockMainCache.set(key, entry)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate cache expiration for testing
|
||||||
|
*/
|
||||||
|
simulateCacheExpiration: (key: string) => {
|
||||||
|
const entry = mockMainCache.get(key)
|
||||||
|
if (entry) {
|
||||||
|
entry.expireAt = Date.now() - 1000 // Set to expired
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache statistics
|
||||||
|
*/
|
||||||
|
getCacheStats: () => ({
|
||||||
|
totalEntries: mockMainCache.size,
|
||||||
|
broadcastCalls: mockBroadcastCalls.length,
|
||||||
|
keys: Array.from(mockMainCache.keys())
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock initialization error
|
||||||
|
*/
|
||||||
|
simulateInitializationError: (error: Error) => {
|
||||||
|
mockInstance.initialize.mockRejectedValue(error)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get mock call counts for debugging
|
||||||
|
*/
|
||||||
|
getMockCallCounts: () => ({
|
||||||
|
initialize: mockInstance.initialize.mock.calls.length,
|
||||||
|
get: mockInstance.get.mock.calls.length,
|
||||||
|
set: mockInstance.set.mock.calls.length,
|
||||||
|
has: mockInstance.has.mock.calls.length,
|
||||||
|
delete: mockInstance.delete.mock.calls.length,
|
||||||
|
cleanup: mockInstance.cleanup.mock.calls.length
|
||||||
|
})
|
||||||
|
}
|
||||||
169
tests/__mocks__/main/DataApiService.ts
Normal file
169
tests/__mocks__/main/DataApiService.ts
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import { vi } from 'vitest'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock DataApiService for main process testing
|
||||||
|
* Simulates the complete main process DataApiService functionality
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock ApiServer class
|
||||||
|
*/
|
||||||
|
class MockApiServer {
|
||||||
|
public initialize = vi.fn(() => new MockApiServer())
|
||||||
|
|
||||||
|
public getSystemInfo = vi.fn(() => ({
|
||||||
|
server: 'MockApiServer',
|
||||||
|
version: '1.0.0',
|
||||||
|
handlers: ['test-handler'],
|
||||||
|
middlewares: ['test-middleware']
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock IpcAdapter class
|
||||||
|
*/
|
||||||
|
class MockIpcAdapter {
|
||||||
|
public setupHandlers = vi.fn()
|
||||||
|
public removeHandlers = vi.fn()
|
||||||
|
public isInitialized = vi.fn(() => true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock DataApiService class
|
||||||
|
*/
|
||||||
|
export class MockMainDataApiService {
|
||||||
|
private static instance: MockMainDataApiService
|
||||||
|
private initialized = false
|
||||||
|
private apiServer: MockApiServer
|
||||||
|
private ipcAdapter: MockIpcAdapter
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.apiServer = new MockApiServer()
|
||||||
|
this.ipcAdapter = new MockIpcAdapter()
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): MockMainDataApiService {
|
||||||
|
if (!MockMainDataApiService.instance) {
|
||||||
|
MockMainDataApiService.instance = new MockMainDataApiService()
|
||||||
|
}
|
||||||
|
return MockMainDataApiService.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock initialization
|
||||||
|
public initialize = vi.fn(async (): Promise<void> => {
|
||||||
|
this.initialized = true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock system status
|
||||||
|
public getSystemStatus = vi.fn(() => {
|
||||||
|
if (!this.initialized) {
|
||||||
|
return {
|
||||||
|
initialized: false,
|
||||||
|
error: 'DataApiService not initialized'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
initialized: true,
|
||||||
|
ipcInitialized: true,
|
||||||
|
server: 'MockApiServer',
|
||||||
|
version: '1.0.0',
|
||||||
|
handlers: ['test-handler'],
|
||||||
|
middlewares: ['test-middleware']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock API server access
|
||||||
|
public getApiServer = vi.fn((): MockApiServer => {
|
||||||
|
return this.apiServer
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock shutdown
|
||||||
|
public shutdown = vi.fn(async (): Promise<void> => {
|
||||||
|
this.initialized = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock singleton instance
|
||||||
|
const mockInstance = MockMainDataApiService.getInstance()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export mock service
|
||||||
|
*/
|
||||||
|
export const MockMainDataApiServiceExport = {
|
||||||
|
DataApiService: MockMainDataApiService,
|
||||||
|
dataApiService: mockInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock API components for advanced testing
|
||||||
|
*/
|
||||||
|
export const MockApiComponents = {
|
||||||
|
ApiServer: MockApiServer,
|
||||||
|
IpcAdapter: MockIpcAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility functions for testing
|
||||||
|
*/
|
||||||
|
export const MockMainDataApiServiceUtils = {
|
||||||
|
/**
|
||||||
|
* Reset all mock call counts and state
|
||||||
|
*/
|
||||||
|
resetMocks: () => {
|
||||||
|
// Reset all method mocks
|
||||||
|
Object.values(mockInstance).forEach(method => {
|
||||||
|
if (vi.isMockFunction(method)) {
|
||||||
|
method.mockClear()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reset initialized state
|
||||||
|
mockInstance['initialized'] = false
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set initialization state for testing
|
||||||
|
*/
|
||||||
|
setInitialized: (initialized: boolean) => {
|
||||||
|
mockInstance['initialized'] = initialized
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current initialization state
|
||||||
|
*/
|
||||||
|
isInitialized: (): boolean => {
|
||||||
|
return mockInstance['initialized']
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock system info for testing
|
||||||
|
*/
|
||||||
|
mockSystemInfo: (info: Record<string, any>) => {
|
||||||
|
mockInstance.getApiServer().getSystemInfo.mockReturnValue(info)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate initialization error
|
||||||
|
*/
|
||||||
|
simulateInitializationError: (error: Error) => {
|
||||||
|
mockInstance.initialize.mockRejectedValue(error)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate shutdown error
|
||||||
|
*/
|
||||||
|
simulateShutdownError: (error: Error) => {
|
||||||
|
mockInstance.shutdown.mockRejectedValue(error)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get mock call counts for debugging
|
||||||
|
*/
|
||||||
|
getMockCallCounts: () => ({
|
||||||
|
initialize: mockInstance.initialize.mock.calls.length,
|
||||||
|
shutdown: mockInstance.shutdown.mock.calls.length,
|
||||||
|
getSystemStatus: mockInstance.getSystemStatus.mock.calls.length,
|
||||||
|
getApiServer: mockInstance.getApiServer.mock.calls.length
|
||||||
|
})
|
||||||
|
}
|
||||||
286
tests/__mocks__/main/PreferenceService.ts
Normal file
286
tests/__mocks__/main/PreferenceService.ts
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
import type {
|
||||||
|
PreferenceDefaultScopeType,
|
||||||
|
PreferenceKeyType
|
||||||
|
} from '@shared/data/preference/preferenceTypes'
|
||||||
|
import { DefaultPreferences } from '@shared/data/preference/preferenceSchemas'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock PreferenceService for main process testing
|
||||||
|
* Simulates the complete main process PreferenceService functionality
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Mock preference state storage
|
||||||
|
const mockPreferenceState = new Map<PreferenceKeyType, any>()
|
||||||
|
|
||||||
|
// Initialize with defaults
|
||||||
|
Object.entries(DefaultPreferences.default).forEach(([key, value]) => {
|
||||||
|
mockPreferenceState.set(key as PreferenceKeyType, value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock subscription tracking
|
||||||
|
const mockSubscriptions = new Map<number, Set<string>>() // windowId -> Set<keys>
|
||||||
|
const mockMainSubscribers = new Map<string, Set<(newValue: any, oldValue?: any) => void>>()
|
||||||
|
|
||||||
|
// Helper function to notify main process subscribers
|
||||||
|
const notifyMainSubscribers = (key: string, newValue: any, oldValue?: any) => {
|
||||||
|
const subscribers = mockMainSubscribers.get(key)
|
||||||
|
if (subscribers) {
|
||||||
|
subscribers.forEach(callback => {
|
||||||
|
try {
|
||||||
|
callback(newValue, oldValue)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Mock PreferenceService: Main subscriber callback error:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock PreferenceService class
|
||||||
|
*/
|
||||||
|
export class MockMainPreferenceService {
|
||||||
|
private static instance: MockMainPreferenceService
|
||||||
|
private initialized = false
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
public static getInstance(): MockMainPreferenceService {
|
||||||
|
if (!MockMainPreferenceService.instance) {
|
||||||
|
MockMainPreferenceService.instance = new MockMainPreferenceService()
|
||||||
|
}
|
||||||
|
return MockMainPreferenceService.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock initialization
|
||||||
|
public initialize = vi.fn(async (): Promise<void> => {
|
||||||
|
this.initialized = true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock get method
|
||||||
|
public get = vi.fn(<K extends PreferenceKeyType>(key: K): PreferenceDefaultScopeType[K] => {
|
||||||
|
return mockPreferenceState.get(key) ?? DefaultPreferences.default[key]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock set method
|
||||||
|
public set = vi.fn(async <K extends PreferenceKeyType>(
|
||||||
|
key: K,
|
||||||
|
value: PreferenceDefaultScopeType[K]
|
||||||
|
): Promise<void> => {
|
||||||
|
const oldValue = mockPreferenceState.get(key)
|
||||||
|
mockPreferenceState.set(key, value)
|
||||||
|
notifyMainSubscribers(key, value, oldValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock getMultiple method
|
||||||
|
public getMultiple = vi.fn(<K extends PreferenceKeyType>(keys: K[]) => {
|
||||||
|
const result: any = {}
|
||||||
|
keys.forEach(key => {
|
||||||
|
result[key] = mockPreferenceState.get(key) ?? DefaultPreferences.default[key]
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock setMultiple method
|
||||||
|
public setMultiple = vi.fn(async (updates: Partial<PreferenceDefaultScopeType>): Promise<void> => {
|
||||||
|
Object.entries(updates).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
const oldValue = mockPreferenceState.get(key as PreferenceKeyType)
|
||||||
|
mockPreferenceState.set(key as PreferenceKeyType, value)
|
||||||
|
notifyMainSubscribers(key, value, oldValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock subscription methods
|
||||||
|
public subscribeForWindow = vi.fn((windowId: number, keys: string[]): void => {
|
||||||
|
if (!mockSubscriptions.has(windowId)) {
|
||||||
|
mockSubscriptions.set(windowId, new Set())
|
||||||
|
}
|
||||||
|
const windowKeys = mockSubscriptions.get(windowId)!
|
||||||
|
keys.forEach(key => windowKeys.add(key))
|
||||||
|
})
|
||||||
|
|
||||||
|
public unsubscribeForWindow = vi.fn((windowId: number): void => {
|
||||||
|
mockSubscriptions.delete(windowId)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock main process subscription methods
|
||||||
|
public subscribeChange = vi.fn(<K extends PreferenceKeyType>(
|
||||||
|
key: K,
|
||||||
|
callback: (newValue: PreferenceDefaultScopeType[K], oldValue?: PreferenceDefaultScopeType[K]) => void
|
||||||
|
): (() => void) => {
|
||||||
|
if (!mockMainSubscribers.has(key)) {
|
||||||
|
mockMainSubscribers.set(key, new Set())
|
||||||
|
}
|
||||||
|
mockMainSubscribers.get(key)!.add(callback)
|
||||||
|
|
||||||
|
// Return unsubscribe function
|
||||||
|
return () => {
|
||||||
|
const subscribers = mockMainSubscribers.get(key)
|
||||||
|
if (subscribers) {
|
||||||
|
subscribers.delete(callback)
|
||||||
|
if (subscribers.size === 0) {
|
||||||
|
mockMainSubscribers.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
public subscribeMultipleChanges = vi.fn((
|
||||||
|
keys: PreferenceKeyType[],
|
||||||
|
callback: (key: PreferenceKeyType, newValue: any, oldValue: any) => void
|
||||||
|
): (() => void) => {
|
||||||
|
const unsubscribeFunctions = keys.map(key =>
|
||||||
|
this.subscribeChange(key, (newValue, oldValue) => callback(key, newValue, oldValue))
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribeFunctions.forEach(unsubscribe => unsubscribe())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock utility methods
|
||||||
|
public getAll = vi.fn((): PreferenceDefaultScopeType => {
|
||||||
|
const result: any = {}
|
||||||
|
Object.keys(DefaultPreferences.default).forEach(key => {
|
||||||
|
result[key] = mockPreferenceState.get(key as PreferenceKeyType) ?? DefaultPreferences.default[key as PreferenceKeyType]
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
public getSubscriptions = vi.fn(() => new Map(mockSubscriptions))
|
||||||
|
|
||||||
|
public removeAllChangeListeners = vi.fn((): void => {
|
||||||
|
mockMainSubscribers.clear()
|
||||||
|
})
|
||||||
|
|
||||||
|
public getChangeListenerCount = vi.fn((): number => {
|
||||||
|
let total = 0
|
||||||
|
mockMainSubscribers.forEach(subscribers => {
|
||||||
|
total += subscribers.size
|
||||||
|
})
|
||||||
|
return total
|
||||||
|
})
|
||||||
|
|
||||||
|
public getKeyListenerCount = vi.fn((key: PreferenceKeyType): number => {
|
||||||
|
return mockMainSubscribers.get(key)?.size ?? 0
|
||||||
|
})
|
||||||
|
|
||||||
|
public getSubscribedKeys = vi.fn((): string[] => {
|
||||||
|
return Array.from(mockMainSubscribers.keys())
|
||||||
|
})
|
||||||
|
|
||||||
|
public getSubscriptionStats = vi.fn((): Record<string, number> => {
|
||||||
|
const stats: Record<string, number> = {}
|
||||||
|
mockMainSubscribers.forEach((subscribers, key) => {
|
||||||
|
stats[key] = subscribers.size
|
||||||
|
})
|
||||||
|
return stats
|
||||||
|
})
|
||||||
|
|
||||||
|
// Static methods
|
||||||
|
public static registerIpcHandler = vi.fn((): void => {
|
||||||
|
// Mock IPC handler registration
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock singleton instance
|
||||||
|
const mockInstance = MockMainPreferenceService.getInstance()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export mock service
|
||||||
|
*/
|
||||||
|
export const MockMainPreferenceServiceExport = {
|
||||||
|
PreferenceService: MockMainPreferenceService,
|
||||||
|
preferenceService: mockInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility functions for testing
|
||||||
|
*/
|
||||||
|
export const MockMainPreferenceServiceUtils = {
|
||||||
|
/**
|
||||||
|
* Reset all mock call counts and state
|
||||||
|
*/
|
||||||
|
resetMocks: () => {
|
||||||
|
// Reset all method mocks
|
||||||
|
Object.values(mockInstance).forEach(method => {
|
||||||
|
if (vi.isMockFunction(method)) {
|
||||||
|
method.mockClear()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reset state to defaults
|
||||||
|
mockPreferenceState.clear()
|
||||||
|
Object.entries(DefaultPreferences.default).forEach(([key, value]) => {
|
||||||
|
mockPreferenceState.set(key as PreferenceKeyType, value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Clear subscriptions
|
||||||
|
mockSubscriptions.clear()
|
||||||
|
mockMainSubscribers.clear()
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a preference value for testing
|
||||||
|
*/
|
||||||
|
setPreferenceValue: <K extends PreferenceKeyType>(key: K, value: PreferenceDefaultScopeType[K]) => {
|
||||||
|
const oldValue = mockPreferenceState.get(key)
|
||||||
|
mockPreferenceState.set(key, value)
|
||||||
|
notifyMainSubscribers(key, value, oldValue)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current preference value
|
||||||
|
*/
|
||||||
|
getPreferenceValue: <K extends PreferenceKeyType>(key: K): PreferenceDefaultScopeType[K] => {
|
||||||
|
return mockPreferenceState.get(key) ?? DefaultPreferences.default[key]
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set multiple preference values for testing
|
||||||
|
*/
|
||||||
|
setMultiplePreferenceValues: (values: Record<string, any>) => {
|
||||||
|
Object.entries(values).forEach(([key, value]) => {
|
||||||
|
const oldValue = mockPreferenceState.get(key as PreferenceKeyType)
|
||||||
|
mockPreferenceState.set(key as PreferenceKeyType, value)
|
||||||
|
notifyMainSubscribers(key, value, oldValue)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all current preference values
|
||||||
|
*/
|
||||||
|
getAllPreferenceValues: (): Record<string, any> => {
|
||||||
|
const result: Record<string, any> = {}
|
||||||
|
mockPreferenceState.forEach((value, key) => {
|
||||||
|
result[key] = value
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate window subscription
|
||||||
|
*/
|
||||||
|
simulateWindowSubscription: (windowId: number, keys: string[]) => {
|
||||||
|
mockInstance.subscribeForWindow(windowId, keys)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate external preference change
|
||||||
|
*/
|
||||||
|
simulateExternalPreferenceChange: <K extends PreferenceKeyType>(key: K, value: PreferenceDefaultScopeType[K]) => {
|
||||||
|
const oldValue = mockPreferenceState.get(key)
|
||||||
|
mockPreferenceState.set(key, value)
|
||||||
|
notifyMainSubscribers(key, value, oldValue)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get subscription counts for debugging
|
||||||
|
*/
|
||||||
|
getSubscriptionCounts: () => ({
|
||||||
|
windows: Array.from(mockSubscriptions.entries()).map(([windowId, keys]) => [windowId, keys.size]),
|
||||||
|
mainSubscribers: Array.from(mockMainSubscribers.entries()).map(([key, subs]) => [key, subs.size])
|
||||||
|
})
|
||||||
|
}
|
||||||
389
tests/__mocks__/renderer/CacheService.ts
Normal file
389
tests/__mocks__/renderer/CacheService.ts
Normal file
@ -0,0 +1,389 @@
|
|||||||
|
import type {
|
||||||
|
RendererPersistCacheKey,
|
||||||
|
RendererPersistCacheSchema,
|
||||||
|
UseCacheKey,
|
||||||
|
UseCacheSchema,
|
||||||
|
UseSharedCacheKey,
|
||||||
|
UseSharedCacheSchema
|
||||||
|
} from '@shared/data/cache/cacheSchemas'
|
||||||
|
import { DefaultRendererPersistCache, DefaultUseCache, DefaultUseSharedCache } from '@shared/data/cache/cacheSchemas'
|
||||||
|
import type { CacheSubscriber } from '@shared/data/cache/cacheTypes'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock CacheService for testing
|
||||||
|
* Provides a comprehensive mock of the three-layer cache system
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a mock CacheService with realistic behavior
|
||||||
|
*/
|
||||||
|
export const createMockCacheService = (options: {
|
||||||
|
initialMemoryCache?: Map<string, any>
|
||||||
|
initialSharedCache?: Map<string, any>
|
||||||
|
initialPersistCache?: Map<RendererPersistCacheKey, any>
|
||||||
|
} = {}) => {
|
||||||
|
// Mock cache storage
|
||||||
|
const memoryCache = new Map<string, any>(options.initialMemoryCache || [])
|
||||||
|
const sharedCache = new Map<string, any>(options.initialSharedCache || [])
|
||||||
|
const persistCache = new Map<RendererPersistCacheKey, any>(options.initialPersistCache || [])
|
||||||
|
|
||||||
|
// Mock subscribers
|
||||||
|
const subscribers = new Map<string, Set<CacheSubscriber>>()
|
||||||
|
|
||||||
|
// Helper function to notify subscribers
|
||||||
|
const notifySubscribers = (key: string, value: any) => {
|
||||||
|
const keySubscribers = subscribers.get(key)
|
||||||
|
if (keySubscribers) {
|
||||||
|
keySubscribers.forEach(callback => {
|
||||||
|
try {
|
||||||
|
callback()
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Mock CacheService: Subscriber callback error:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockCacheService = {
|
||||||
|
// Memory cache methods
|
||||||
|
get: vi.fn(<T>(key: string): T | null => {
|
||||||
|
if (memoryCache.has(key)) {
|
||||||
|
return memoryCache.get(key) as T
|
||||||
|
}
|
||||||
|
// Return default values for known cache keys
|
||||||
|
const defaultValue = getDefaultValueForKey(key)
|
||||||
|
return defaultValue !== undefined ? defaultValue : null
|
||||||
|
}),
|
||||||
|
|
||||||
|
set: vi.fn(<T>(key: string, value: T, ttl?: number): void => {
|
||||||
|
const oldValue = memoryCache.get(key)
|
||||||
|
memoryCache.set(key, value)
|
||||||
|
if (oldValue !== value) {
|
||||||
|
notifySubscribers(key, value)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
delete: vi.fn((key: string): boolean => {
|
||||||
|
const existed = memoryCache.has(key)
|
||||||
|
memoryCache.delete(key)
|
||||||
|
if (existed) {
|
||||||
|
notifySubscribers(key, null)
|
||||||
|
}
|
||||||
|
return existed
|
||||||
|
}),
|
||||||
|
|
||||||
|
clear: vi.fn((): void => {
|
||||||
|
const keys = Array.from(memoryCache.keys())
|
||||||
|
memoryCache.clear()
|
||||||
|
keys.forEach(key => notifySubscribers(key, null))
|
||||||
|
}),
|
||||||
|
|
||||||
|
has: vi.fn((key: string): boolean => {
|
||||||
|
return memoryCache.has(key)
|
||||||
|
}),
|
||||||
|
|
||||||
|
size: vi.fn((): number => {
|
||||||
|
return memoryCache.size
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Shared cache methods
|
||||||
|
getShared: vi.fn(<T>(key: string): T | null => {
|
||||||
|
if (sharedCache.has(key)) {
|
||||||
|
return sharedCache.get(key) as T
|
||||||
|
}
|
||||||
|
const defaultValue = getDefaultSharedValueForKey(key)
|
||||||
|
return defaultValue !== undefined ? defaultValue : null
|
||||||
|
}),
|
||||||
|
|
||||||
|
setShared: vi.fn(<T>(key: string, value: T, ttl?: number): void => {
|
||||||
|
const oldValue = sharedCache.get(key)
|
||||||
|
sharedCache.set(key, value)
|
||||||
|
if (oldValue !== value) {
|
||||||
|
notifySubscribers(`shared:${key}`, value)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
deleteShared: vi.fn((key: string): boolean => {
|
||||||
|
const existed = sharedCache.has(key)
|
||||||
|
sharedCache.delete(key)
|
||||||
|
if (existed) {
|
||||||
|
notifySubscribers(`shared:${key}`, null)
|
||||||
|
}
|
||||||
|
return existed
|
||||||
|
}),
|
||||||
|
|
||||||
|
clearShared: vi.fn((): void => {
|
||||||
|
const keys = Array.from(sharedCache.keys())
|
||||||
|
sharedCache.clear()
|
||||||
|
keys.forEach(key => notifySubscribers(`shared:${key}`, null))
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Persist cache methods
|
||||||
|
getPersist: vi.fn(<K extends RendererPersistCacheKey>(key: K): RendererPersistCacheSchema[K] => {
|
||||||
|
if (persistCache.has(key)) {
|
||||||
|
return persistCache.get(key) as RendererPersistCacheSchema[K]
|
||||||
|
}
|
||||||
|
return DefaultRendererPersistCache[key]
|
||||||
|
}),
|
||||||
|
|
||||||
|
setPersist: vi.fn(<K extends RendererPersistCacheKey>(key: K, value: RendererPersistCacheSchema[K]): void => {
|
||||||
|
const oldValue = persistCache.get(key)
|
||||||
|
persistCache.set(key, value)
|
||||||
|
if (oldValue !== value) {
|
||||||
|
notifySubscribers(`persist:${key}`, value)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
deletePersist: vi.fn(<K extends RendererPersistCacheKey>(key: K): boolean => {
|
||||||
|
const existed = persistCache.has(key)
|
||||||
|
persistCache.delete(key)
|
||||||
|
if (existed) {
|
||||||
|
notifySubscribers(`persist:${key}`, DefaultRendererPersistCache[key])
|
||||||
|
}
|
||||||
|
return existed
|
||||||
|
}),
|
||||||
|
|
||||||
|
clearPersist: vi.fn((): void => {
|
||||||
|
const keys = Array.from(persistCache.keys()) as RendererPersistCacheKey[]
|
||||||
|
persistCache.clear()
|
||||||
|
keys.forEach(key => notifySubscribers(`persist:${key}`, DefaultRendererPersistCache[key]))
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Subscription methods
|
||||||
|
subscribe: vi.fn((key: string, callback: CacheSubscriber): (() => void) => {
|
||||||
|
if (!subscribers.has(key)) {
|
||||||
|
subscribers.set(key, new Set())
|
||||||
|
}
|
||||||
|
subscribers.get(key)!.add(callback)
|
||||||
|
|
||||||
|
// Return unsubscribe function
|
||||||
|
return () => {
|
||||||
|
const keySubscribers = subscribers.get(key)
|
||||||
|
if (keySubscribers) {
|
||||||
|
keySubscribers.delete(callback)
|
||||||
|
if (keySubscribers.size === 0) {
|
||||||
|
subscribers.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
unsubscribe: vi.fn((key: string, callback?: CacheSubscriber): void => {
|
||||||
|
if (callback) {
|
||||||
|
const keySubscribers = subscribers.get(key)
|
||||||
|
if (keySubscribers) {
|
||||||
|
keySubscribers.delete(callback)
|
||||||
|
if (keySubscribers.size === 0) {
|
||||||
|
subscribers.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
subscribers.delete(key)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Hook reference tracking (for advanced cache management)
|
||||||
|
addHookReference: vi.fn((key: string): void => {
|
||||||
|
// Mock implementation - in real service this prevents cache cleanup
|
||||||
|
}),
|
||||||
|
|
||||||
|
removeHookReference: vi.fn((key: string): void => {
|
||||||
|
// Mock implementation
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Utility methods
|
||||||
|
getAllKeys: vi.fn((): string[] => {
|
||||||
|
return Array.from(memoryCache.keys())
|
||||||
|
}),
|
||||||
|
|
||||||
|
getStats: vi.fn(() => ({
|
||||||
|
memorySize: memoryCache.size,
|
||||||
|
sharedSize: sharedCache.size,
|
||||||
|
persistSize: persistCache.size,
|
||||||
|
subscriberCount: subscribers.size
|
||||||
|
})),
|
||||||
|
|
||||||
|
// Internal state access for testing
|
||||||
|
_getMockState: () => ({
|
||||||
|
memoryCache: new Map(memoryCache),
|
||||||
|
sharedCache: new Map(sharedCache),
|
||||||
|
persistCache: new Map(persistCache),
|
||||||
|
subscribers: new Map(subscribers)
|
||||||
|
}),
|
||||||
|
|
||||||
|
_resetMockState: () => {
|
||||||
|
memoryCache.clear()
|
||||||
|
sharedCache.clear()
|
||||||
|
persistCache.clear()
|
||||||
|
subscribers.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mockCacheService
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default value for cache keys based on schema
|
||||||
|
*/
|
||||||
|
function getDefaultValueForKey(key: string): any {
|
||||||
|
// Try to match against known cache schemas
|
||||||
|
if (key in DefaultUseCache) {
|
||||||
|
return DefaultUseCache[key as UseCacheKey]
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultSharedValueForKey(key: string): any {
|
||||||
|
if (key in DefaultUseSharedCache) {
|
||||||
|
return DefaultUseSharedCache[key as UseSharedCacheKey]
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default mock instance
|
||||||
|
export const mockCacheService = createMockCacheService()
|
||||||
|
|
||||||
|
// Singleton instance mock
|
||||||
|
export const MockCacheService = {
|
||||||
|
CacheService: class MockCacheService {
|
||||||
|
static getInstance() {
|
||||||
|
return mockCacheService
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegate all methods to the mock
|
||||||
|
get<T>(key: string): T | null {
|
||||||
|
return mockCacheService.get<T>(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
set<T>(key: string, value: T, ttl?: number): void {
|
||||||
|
return mockCacheService.set(key, value, ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(key: string): boolean {
|
||||||
|
return mockCacheService.delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
return mockCacheService.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
has(key: string): boolean {
|
||||||
|
return mockCacheService.has(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
size(): number {
|
||||||
|
return mockCacheService.size()
|
||||||
|
}
|
||||||
|
|
||||||
|
getShared<T>(key: string): T | null {
|
||||||
|
return mockCacheService.getShared<T>(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
setShared<T>(key: string, value: T, ttl?: number): void {
|
||||||
|
return mockCacheService.setShared(key, value, ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteShared(key: string): boolean {
|
||||||
|
return mockCacheService.deleteShared(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
clearShared(): void {
|
||||||
|
return mockCacheService.clearShared()
|
||||||
|
}
|
||||||
|
|
||||||
|
getPersist<K extends RendererPersistCacheKey>(key: K): RendererPersistCacheSchema[K] {
|
||||||
|
return mockCacheService.getPersist(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
setPersist<K extends RendererPersistCacheKey>(key: K, value: RendererPersistCacheSchema[K]): void {
|
||||||
|
return mockCacheService.setPersist(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
deletePersist<K extends RendererPersistCacheKey>(key: K): boolean {
|
||||||
|
return mockCacheService.deletePersist(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
clearPersist(): void {
|
||||||
|
return mockCacheService.clearPersist()
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(key: string, callback: CacheSubscriber): () => void {
|
||||||
|
return mockCacheService.subscribe(key, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribe(key: string, callback?: CacheSubscriber): void {
|
||||||
|
return mockCacheService.unsubscribe(key, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
addHookReference(key: string): void {
|
||||||
|
return mockCacheService.addHookReference(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeHookReference(key: string): void {
|
||||||
|
return mockCacheService.removeHookReference(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllKeys(): string[] {
|
||||||
|
return mockCacheService.getAllKeys()
|
||||||
|
}
|
||||||
|
|
||||||
|
getStats() {
|
||||||
|
return mockCacheService.getStats()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cacheService: mockCacheService
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility functions for testing
|
||||||
|
*/
|
||||||
|
export const MockCacheUtils = {
|
||||||
|
/**
|
||||||
|
* Reset all mock function call counts and state
|
||||||
|
*/
|
||||||
|
resetMocks: () => {
|
||||||
|
Object.values(mockCacheService).forEach(method => {
|
||||||
|
if (vi.isMockFunction(method)) {
|
||||||
|
method.mockClear()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if ('_resetMockState' in mockCacheService) {
|
||||||
|
;(mockCacheService as any)._resetMockState()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set initial cache state for testing
|
||||||
|
*/
|
||||||
|
setInitialState: (state: {
|
||||||
|
memory?: Array<[string, any]>
|
||||||
|
shared?: Array<[string, any]>
|
||||||
|
persist?: Array<[RendererPersistCacheKey, any]>
|
||||||
|
}) => {
|
||||||
|
if ('_resetMockState' in mockCacheService) {
|
||||||
|
;(mockCacheService as any)._resetMockState()
|
||||||
|
}
|
||||||
|
|
||||||
|
state.memory?.forEach(([key, value]) => mockCacheService.set(key, value))
|
||||||
|
state.shared?.forEach(([key, value]) => mockCacheService.setShared(key, value))
|
||||||
|
state.persist?.forEach(([key, value]) => mockCacheService.setPersist(key, value))
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current mock state for inspection
|
||||||
|
*/
|
||||||
|
getCurrentState: () => {
|
||||||
|
if ('_getMockState' in mockCacheService) {
|
||||||
|
return (mockCacheService as any)._getMockState()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate cache events for testing subscribers
|
||||||
|
*/
|
||||||
|
triggerCacheChange: (key: string, value: any) => {
|
||||||
|
mockCacheService.set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
326
tests/__mocks__/renderer/DataApiService.ts
Normal file
326
tests/__mocks__/renderer/DataApiService.ts
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
import type { ConcreteApiPaths } from '@shared/data/api/apiSchemas'
|
||||||
|
import type {
|
||||||
|
ApiClient,
|
||||||
|
BatchRequest,
|
||||||
|
BatchResponse,
|
||||||
|
DataRequest,
|
||||||
|
DataResponse,
|
||||||
|
SubscriptionCallback,
|
||||||
|
SubscriptionOptions,
|
||||||
|
TransactionRequest
|
||||||
|
} from '@shared/data/api/apiTypes'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock DataApiService for testing
|
||||||
|
* Provides a comprehensive mock of the DataApiService with realistic behavior
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Mock response utilities
|
||||||
|
const createMockResponse = <T>(data: T, success = true): DataResponse<T> => ({
|
||||||
|
success,
|
||||||
|
data,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
...(success ? {} : { error: { code: 'MOCK_ERROR', message: 'Mock error', details: {} } })
|
||||||
|
})
|
||||||
|
|
||||||
|
const createMockError = (message: string): DataResponse<never> => ({
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
code: 'MOCK_ERROR',
|
||||||
|
message,
|
||||||
|
details: {}
|
||||||
|
},
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock implementation of DataApiService
|
||||||
|
*/
|
||||||
|
export const createMockDataApiService = (customBehavior: Partial<ApiClient> = {}): ApiClient => {
|
||||||
|
const mockService: ApiClient = {
|
||||||
|
// HTTP Methods
|
||||||
|
get: vi.fn(async (path: ConcreteApiPaths, options?: any) => {
|
||||||
|
// Default mock behavior - return empty data based on path
|
||||||
|
const mockData = getMockDataForPath(path, 'GET')
|
||||||
|
return createMockResponse(mockData)
|
||||||
|
}),
|
||||||
|
|
||||||
|
post: vi.fn(async (path: ConcreteApiPaths, options?: any) => {
|
||||||
|
const mockData = getMockDataForPath(path, 'POST')
|
||||||
|
return createMockResponse(mockData)
|
||||||
|
}),
|
||||||
|
|
||||||
|
put: vi.fn(async (path: ConcreteApiPaths, options?: any) => {
|
||||||
|
const mockData = getMockDataForPath(path, 'PUT')
|
||||||
|
return createMockResponse(mockData)
|
||||||
|
}),
|
||||||
|
|
||||||
|
patch: vi.fn(async (path: ConcreteApiPaths, options?: any) => {
|
||||||
|
const mockData = getMockDataForPath(path, 'PATCH')
|
||||||
|
return createMockResponse(mockData)
|
||||||
|
}),
|
||||||
|
|
||||||
|
delete: vi.fn(async (path: ConcreteApiPaths, options?: any) => {
|
||||||
|
return createMockResponse({ deleted: true })
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Batch operations
|
||||||
|
batch: vi.fn(async (requests: BatchRequest[]): Promise<BatchResponse> => {
|
||||||
|
const responses = requests.map((request, index) => ({
|
||||||
|
id: request.id || `batch_${index}`,
|
||||||
|
success: true,
|
||||||
|
data: getMockDataForPath(request.path as ConcreteApiPaths, request.method),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}))
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
responses,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Transaction support
|
||||||
|
transaction: vi.fn(async (operations: TransactionRequest[]): Promise<DataResponse<any[]>> => {
|
||||||
|
const results = operations.map((op, index) => ({
|
||||||
|
operation: op.operation,
|
||||||
|
result: getMockDataForPath(op.path as ConcreteApiPaths, 'POST'),
|
||||||
|
success: true
|
||||||
|
}))
|
||||||
|
|
||||||
|
return createMockResponse(results)
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Subscription methods
|
||||||
|
subscribe: vi.fn((path: ConcreteApiPaths, callback: SubscriptionCallback, options?: SubscriptionOptions) => {
|
||||||
|
// Return a mock unsubscribe function
|
||||||
|
return vi.fn()
|
||||||
|
}),
|
||||||
|
|
||||||
|
unsubscribe: vi.fn((path: ConcreteApiPaths) => {
|
||||||
|
// Mock unsubscribe
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Connection management
|
||||||
|
connect: vi.fn(async () => {
|
||||||
|
return createMockResponse({ connected: true })
|
||||||
|
}),
|
||||||
|
|
||||||
|
disconnect: vi.fn(async () => {
|
||||||
|
return createMockResponse({ disconnected: true })
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
ping: vi.fn(async () => {
|
||||||
|
return createMockResponse({ pong: true, timestamp: new Date().toISOString() })
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Apply custom behavior overrides
|
||||||
|
...customBehavior
|
||||||
|
}
|
||||||
|
|
||||||
|
return mockService
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get mock data based on API path and method
|
||||||
|
* Provides realistic mock responses for common API endpoints
|
||||||
|
*/
|
||||||
|
function getMockDataForPath(path: ConcreteApiPaths, method: string): any {
|
||||||
|
// Parse path to determine data type
|
||||||
|
if (path.includes('/topics')) {
|
||||||
|
if (method === 'GET' && path.endsWith('/topics')) {
|
||||||
|
return {
|
||||||
|
topics: [
|
||||||
|
{ id: 'topic1', name: 'Mock Topic 1', createdAt: '2024-01-01T00:00:00Z' },
|
||||||
|
{ id: 'topic2', name: 'Mock Topic 2', createdAt: '2024-01-02T00:00:00Z' }
|
||||||
|
],
|
||||||
|
total: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (method === 'GET' && path.match(/\/topics\/[^/]+$/)) {
|
||||||
|
return {
|
||||||
|
id: 'topic1',
|
||||||
|
name: 'Mock Topic',
|
||||||
|
messages: [],
|
||||||
|
createdAt: '2024-01-01T00:00:00Z'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (method === 'POST' && path.endsWith('/topics')) {
|
||||||
|
return {
|
||||||
|
id: 'new_topic',
|
||||||
|
name: 'New Mock Topic',
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.includes('/messages')) {
|
||||||
|
if (method === 'GET') {
|
||||||
|
return {
|
||||||
|
messages: [
|
||||||
|
{ id: 'msg1', content: 'Mock message 1', role: 'user', timestamp: '2024-01-01T00:00:00Z' },
|
||||||
|
{ id: 'msg2', content: 'Mock message 2', role: 'assistant', timestamp: '2024-01-01T00:01:00Z' }
|
||||||
|
],
|
||||||
|
total: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (method === 'POST') {
|
||||||
|
return {
|
||||||
|
id: 'new_message',
|
||||||
|
content: 'New mock message',
|
||||||
|
role: 'user',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.includes('/preferences')) {
|
||||||
|
if (method === 'GET') {
|
||||||
|
return {
|
||||||
|
preferences: {
|
||||||
|
'ui.theme': 'light',
|
||||||
|
'ui.language': 'en',
|
||||||
|
'data.export.format': 'markdown'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (method === 'POST' || method === 'PUT') {
|
||||||
|
return { updated: true, timestamp: new Date().toISOString() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default mock data
|
||||||
|
return {
|
||||||
|
id: 'mock_id',
|
||||||
|
data: 'mock_data',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default mock instance
|
||||||
|
export const mockDataApiService = createMockDataApiService()
|
||||||
|
|
||||||
|
// Singleton instance mock
|
||||||
|
export const MockDataApiService = {
|
||||||
|
DataApiService: class MockDataApiService {
|
||||||
|
static getInstance() {
|
||||||
|
return mockDataApiService
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instance methods delegate to the mock
|
||||||
|
async get(path: ConcreteApiPaths, options?: any) {
|
||||||
|
return mockDataApiService.get(path, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
async post(path: ConcreteApiPaths, options?: any) {
|
||||||
|
return mockDataApiService.post(path, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
async put(path: ConcreteApiPaths, options?: any) {
|
||||||
|
return mockDataApiService.put(path, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
async patch(path: ConcreteApiPaths, options?: any) {
|
||||||
|
return mockDataApiService.patch(path, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(path: ConcreteApiPaths, options?: any) {
|
||||||
|
return mockDataApiService.delete(path, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
async batch(requests: BatchRequest[]) {
|
||||||
|
return mockDataApiService.batch(requests)
|
||||||
|
}
|
||||||
|
|
||||||
|
async transaction(operations: TransactionRequest[]) {
|
||||||
|
return mockDataApiService.transaction(operations)
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(path: ConcreteApiPaths, callback: SubscriptionCallback, options?: SubscriptionOptions) {
|
||||||
|
return mockDataApiService.subscribe(path, callback, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribe(path: ConcreteApiPaths) {
|
||||||
|
return mockDataApiService.unsubscribe(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
return mockDataApiService.connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnect() {
|
||||||
|
return mockDataApiService.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
async ping() {
|
||||||
|
return mockDataApiService.ping()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataApiService: mockDataApiService
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility functions for testing
|
||||||
|
*/
|
||||||
|
export const MockDataApiUtils = {
|
||||||
|
/**
|
||||||
|
* Reset all mock function call counts and implementations
|
||||||
|
*/
|
||||||
|
resetMocks: () => {
|
||||||
|
Object.values(mockDataApiService).forEach(method => {
|
||||||
|
if (vi.isMockFunction(method)) {
|
||||||
|
method.mockClear()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set custom response for a specific path and method
|
||||||
|
*/
|
||||||
|
setCustomResponse: (path: ConcreteApiPaths, method: string, response: any) => {
|
||||||
|
const methodFn = mockDataApiService[method.toLowerCase() as keyof ApiClient] as any
|
||||||
|
if (vi.isMockFunction(methodFn)) {
|
||||||
|
methodFn.mockImplementation(async (requestPath: string, options?: any) => {
|
||||||
|
if (requestPath === path) {
|
||||||
|
return createMockResponse(response)
|
||||||
|
}
|
||||||
|
// Fall back to default behavior
|
||||||
|
return createMockResponse(getMockDataForPath(requestPath as ConcreteApiPaths, method))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set error response for a specific path and method
|
||||||
|
*/
|
||||||
|
setErrorResponse: (path: ConcreteApiPaths, method: string, errorMessage: string) => {
|
||||||
|
const methodFn = mockDataApiService[method.toLowerCase() as keyof ApiClient] as any
|
||||||
|
if (vi.isMockFunction(methodFn)) {
|
||||||
|
methodFn.mockImplementation(async (requestPath: string, options?: any) => {
|
||||||
|
if (requestPath === path) {
|
||||||
|
return createMockError(errorMessage)
|
||||||
|
}
|
||||||
|
// Fall back to default behavior
|
||||||
|
return createMockResponse(getMockDataForPath(requestPath as ConcreteApiPaths, method))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get call count for a specific method
|
||||||
|
*/
|
||||||
|
getCallCount: (method: keyof ApiClient): number => {
|
||||||
|
const methodFn = mockDataApiService[method] as any
|
||||||
|
return vi.isMockFunction(methodFn) ? methodFn.mock.calls.length : 0
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get calls for a specific method
|
||||||
|
*/
|
||||||
|
getCalls: (method: keyof ApiClient): any[] => {
|
||||||
|
const methodFn = mockDataApiService[method] as any
|
||||||
|
return vi.isMockFunction(methodFn) ? methodFn.mock.calls : []
|
||||||
|
}
|
||||||
|
}
|
||||||
98
tests/__mocks__/renderer/PreferenceService.ts
Normal file
98
tests/__mocks__/renderer/PreferenceService.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { vi } from 'vitest'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock PreferenceService for testing
|
||||||
|
* Provides common preference defaults used across the application
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Default preference values used in tests
|
||||||
|
export const mockPreferenceDefaults: Record<string, any> = {
|
||||||
|
// Export preferences
|
||||||
|
'data.export.markdown.force_dollar_math': false,
|
||||||
|
'data.export.markdown.exclude_citations': false,
|
||||||
|
'data.export.markdown.standardize_citations': true,
|
||||||
|
'data.export.markdown.show_model_name': false,
|
||||||
|
'data.export.markdown.show_model_provider': false,
|
||||||
|
|
||||||
|
// UI preferences
|
||||||
|
'ui.language': 'zh-CN',
|
||||||
|
'ui.theme': 'light',
|
||||||
|
'ui.font_size': 14,
|
||||||
|
|
||||||
|
// AI preferences
|
||||||
|
'ai.default_model': 'gpt-4',
|
||||||
|
'ai.temperature': 0.7,
|
||||||
|
'ai.max_tokens': 2000,
|
||||||
|
|
||||||
|
// Feature flags
|
||||||
|
'feature.web_search': true,
|
||||||
|
'feature.reasoning': false,
|
||||||
|
'feature.tool_calling': true,
|
||||||
|
|
||||||
|
// User preferences
|
||||||
|
'user.name': 'MockUser',
|
||||||
|
|
||||||
|
// App preferences
|
||||||
|
'app.user.name': 'MockUser',
|
||||||
|
'app.language': 'zh-CN',
|
||||||
|
|
||||||
|
// Add more defaults as needed
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock implementation of PreferenceService
|
||||||
|
*/
|
||||||
|
export const createMockPreferenceService = (customDefaults: Record<string, any> = {}) => {
|
||||||
|
const mergedDefaults = { ...mockPreferenceDefaults, ...customDefaults }
|
||||||
|
|
||||||
|
return {
|
||||||
|
get: vi.fn((key: string) => {
|
||||||
|
const value = mergedDefaults[key]
|
||||||
|
return Promise.resolve(value !== undefined ? value : null)
|
||||||
|
}),
|
||||||
|
|
||||||
|
getMultiple: vi.fn((keys: Record<string, string>) => {
|
||||||
|
const result: Record<string, any> = {}
|
||||||
|
Object.entries(keys).forEach(([alias, key]) => {
|
||||||
|
const value = mergedDefaults[key]
|
||||||
|
result[alias] = value !== undefined ? value : null
|
||||||
|
})
|
||||||
|
return Promise.resolve(result)
|
||||||
|
}),
|
||||||
|
|
||||||
|
set: vi.fn((key: string, value: any) => {
|
||||||
|
mergedDefaults[key] = value
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
|
||||||
|
setMultiple: vi.fn((values: Record<string, any>) => {
|
||||||
|
Object.assign(mergedDefaults, values)
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
|
||||||
|
delete: vi.fn((key: string) => {
|
||||||
|
delete mergedDefaults[key]
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
|
||||||
|
clear: vi.fn(() => {
|
||||||
|
Object.keys(mergedDefaults).forEach(key => delete mergedDefaults[key])
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Internal state access for testing
|
||||||
|
_getMockState: () => ({ ...mergedDefaults }),
|
||||||
|
_resetMockState: () => {
|
||||||
|
Object.keys(mergedDefaults).forEach(key => delete mergedDefaults[key])
|
||||||
|
Object.assign(mergedDefaults, mockPreferenceDefaults, customDefaults)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default mock instance
|
||||||
|
export const mockPreferenceService = createMockPreferenceService()
|
||||||
|
|
||||||
|
// Export for easy mocking in individual tests
|
||||||
|
export const MockPreferenceService = {
|
||||||
|
preferenceService: mockPreferenceService
|
||||||
|
}
|
||||||
428
tests/__mocks__/renderer/useCache.ts
Normal file
428
tests/__mocks__/renderer/useCache.ts
Normal file
@ -0,0 +1,428 @@
|
|||||||
|
import type {
|
||||||
|
RendererPersistCacheKey,
|
||||||
|
RendererPersistCacheSchema,
|
||||||
|
UseCacheKey,
|
||||||
|
UseCacheSchema,
|
||||||
|
UseSharedCacheKey,
|
||||||
|
UseSharedCacheSchema
|
||||||
|
} from '@shared/data/cache/cacheSchemas'
|
||||||
|
import { DefaultRendererPersistCache, DefaultUseCache, DefaultUseSharedCache } from '@shared/data/cache/cacheSchemas'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock useCache hooks for testing
|
||||||
|
* Provides comprehensive mocks for all cache management hooks
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Mock cache state storage
|
||||||
|
const mockMemoryCache = new Map<UseCacheKey, any>()
|
||||||
|
const mockSharedCache = new Map<UseSharedCacheKey, any>()
|
||||||
|
const mockPersistCache = new Map<RendererPersistCacheKey, any>()
|
||||||
|
|
||||||
|
// Initialize caches with defaults
|
||||||
|
Object.entries(DefaultUseCache).forEach(([key, value]) => {
|
||||||
|
mockMemoryCache.set(key as UseCacheKey, value)
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.entries(DefaultUseSharedCache).forEach(([key, value]) => {
|
||||||
|
mockSharedCache.set(key as UseSharedCacheKey, value)
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.entries(DefaultRendererPersistCache).forEach(([key, value]) => {
|
||||||
|
mockPersistCache.set(key as RendererPersistCacheKey, value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock subscribers for cache changes
|
||||||
|
const mockMemorySubscribers = new Map<UseCacheKey, Set<() => void>>()
|
||||||
|
const mockSharedSubscribers = new Map<UseSharedCacheKey, Set<() => void>>()
|
||||||
|
const mockPersistSubscribers = new Map<RendererPersistCacheKey, Set<() => void>>()
|
||||||
|
|
||||||
|
// Helper functions to notify subscribers
|
||||||
|
const notifyMemorySubscribers = (key: UseCacheKey) => {
|
||||||
|
const subscribers = mockMemorySubscribers.get(key)
|
||||||
|
if (subscribers) {
|
||||||
|
subscribers.forEach(callback => {
|
||||||
|
try {
|
||||||
|
callback()
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Mock useCache: Memory subscriber callback error:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const notifySharedSubscribers = (key: UseSharedCacheKey) => {
|
||||||
|
const subscribers = mockSharedSubscribers.get(key)
|
||||||
|
if (subscribers) {
|
||||||
|
subscribers.forEach(callback => {
|
||||||
|
try {
|
||||||
|
callback()
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Mock useCache: Shared subscriber callback error:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const notifyPersistSubscribers = (key: RendererPersistCacheKey) => {
|
||||||
|
const subscribers = mockPersistSubscribers.get(key)
|
||||||
|
if (subscribers) {
|
||||||
|
subscribers.forEach(callback => {
|
||||||
|
try {
|
||||||
|
callback()
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Mock useCache: Persist subscriber callback error:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock useCache hook (memory cache)
|
||||||
|
*/
|
||||||
|
export const mockUseCache = vi.fn(<K extends UseCacheKey>(
|
||||||
|
key: K,
|
||||||
|
initValue?: UseCacheSchema[K]
|
||||||
|
): [UseCacheSchema[K], (value: UseCacheSchema[K]) => void] => {
|
||||||
|
// Get current value
|
||||||
|
let currentValue = mockMemoryCache.get(key)
|
||||||
|
if (currentValue === undefined) {
|
||||||
|
currentValue = initValue ?? DefaultUseCache[key]
|
||||||
|
if (currentValue !== undefined) {
|
||||||
|
mockMemoryCache.set(key, currentValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock setValue function
|
||||||
|
const setValue = vi.fn((value: UseCacheSchema[K]) => {
|
||||||
|
mockMemoryCache.set(key, value)
|
||||||
|
notifyMemorySubscribers(key)
|
||||||
|
})
|
||||||
|
|
||||||
|
return [currentValue, setValue]
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock useSharedCache hook (shared cache)
|
||||||
|
*/
|
||||||
|
export const mockUseSharedCache = vi.fn(<K extends UseSharedCacheKey>(
|
||||||
|
key: K,
|
||||||
|
initValue?: UseSharedCacheSchema[K]
|
||||||
|
): [UseSharedCacheSchema[K], (value: UseSharedCacheSchema[K]) => void] => {
|
||||||
|
// Get current value
|
||||||
|
let currentValue = mockSharedCache.get(key)
|
||||||
|
if (currentValue === undefined) {
|
||||||
|
currentValue = initValue ?? DefaultUseSharedCache[key]
|
||||||
|
if (currentValue !== undefined) {
|
||||||
|
mockSharedCache.set(key, currentValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock setValue function
|
||||||
|
const setValue = vi.fn((value: UseSharedCacheSchema[K]) => {
|
||||||
|
mockSharedCache.set(key, value)
|
||||||
|
notifySharedSubscribers(key)
|
||||||
|
})
|
||||||
|
|
||||||
|
return [currentValue, setValue]
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock usePersistCache hook (persistent cache)
|
||||||
|
*/
|
||||||
|
export const mockUsePersistCache = vi.fn(<K extends RendererPersistCacheKey>(
|
||||||
|
key: K,
|
||||||
|
initValue?: RendererPersistCacheSchema[K]
|
||||||
|
): [RendererPersistCacheSchema[K], (value: RendererPersistCacheSchema[K]) => void] => {
|
||||||
|
// Get current value
|
||||||
|
let currentValue = mockPersistCache.get(key)
|
||||||
|
if (currentValue === undefined) {
|
||||||
|
currentValue = initValue ?? DefaultRendererPersistCache[key]
|
||||||
|
if (currentValue !== undefined) {
|
||||||
|
mockPersistCache.set(key, currentValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock setValue function
|
||||||
|
const setValue = vi.fn((value: RendererPersistCacheSchema[K]) => {
|
||||||
|
mockPersistCache.set(key, value)
|
||||||
|
notifyPersistSubscribers(key)
|
||||||
|
})
|
||||||
|
|
||||||
|
return [currentValue, setValue]
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export all mocks as a unified module
|
||||||
|
*/
|
||||||
|
export const MockUseCache = {
|
||||||
|
useCache: mockUseCache,
|
||||||
|
useSharedCache: mockUseSharedCache,
|
||||||
|
usePersistCache: mockUsePersistCache
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility functions for testing
|
||||||
|
*/
|
||||||
|
export const MockUseCacheUtils = {
|
||||||
|
/**
|
||||||
|
* Reset all hook mock call counts and state
|
||||||
|
*/
|
||||||
|
resetMocks: () => {
|
||||||
|
mockUseCache.mockClear()
|
||||||
|
mockUseSharedCache.mockClear()
|
||||||
|
mockUsePersistCache.mockClear()
|
||||||
|
|
||||||
|
// Reset caches to defaults
|
||||||
|
mockMemoryCache.clear()
|
||||||
|
mockSharedCache.clear()
|
||||||
|
mockPersistCache.clear()
|
||||||
|
|
||||||
|
Object.entries(DefaultUseCache).forEach(([key, value]) => {
|
||||||
|
mockMemoryCache.set(key as UseCacheKey, value)
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.entries(DefaultUseSharedCache).forEach(([key, value]) => {
|
||||||
|
mockSharedCache.set(key as UseSharedCacheKey, value)
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.entries(DefaultRendererPersistCache).forEach(([key, value]) => {
|
||||||
|
mockPersistCache.set(key as RendererPersistCacheKey, value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Clear subscribers
|
||||||
|
mockMemorySubscribers.clear()
|
||||||
|
mockSharedSubscribers.clear()
|
||||||
|
mockPersistSubscribers.clear()
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set cache value for testing (memory cache)
|
||||||
|
*/
|
||||||
|
setCacheValue: <K extends UseCacheKey>(key: K, value: UseCacheSchema[K]) => {
|
||||||
|
mockMemoryCache.set(key, value)
|
||||||
|
notifyMemorySubscribers(key)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache value (memory cache)
|
||||||
|
*/
|
||||||
|
getCacheValue: <K extends UseCacheKey>(key: K): UseCacheSchema[K] => {
|
||||||
|
return mockMemoryCache.get(key) ?? DefaultUseCache[key]
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set shared cache value for testing
|
||||||
|
*/
|
||||||
|
setSharedCacheValue: <K extends UseSharedCacheKey>(key: K, value: UseSharedCacheSchema[K]) => {
|
||||||
|
mockSharedCache.set(key, value)
|
||||||
|
notifySharedSubscribers(key)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get shared cache value
|
||||||
|
*/
|
||||||
|
getSharedCacheValue: <K extends UseSharedCacheKey>(key: K): UseSharedCacheSchema[K] => {
|
||||||
|
return mockSharedCache.get(key) ?? DefaultUseSharedCache[key]
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set persist cache value for testing
|
||||||
|
*/
|
||||||
|
setPersistCacheValue: <K extends RendererPersistCacheKey>(key: K, value: RendererPersistCacheSchema[K]) => {
|
||||||
|
mockPersistCache.set(key, value)
|
||||||
|
notifyPersistSubscribers(key)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get persist cache value
|
||||||
|
*/
|
||||||
|
getPersistCacheValue: <K extends RendererPersistCacheKey>(key: K): RendererPersistCacheSchema[K] => {
|
||||||
|
return mockPersistCache.get(key) ?? DefaultRendererPersistCache[key]
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set multiple cache values at once
|
||||||
|
*/
|
||||||
|
setMultipleCacheValues: (values: {
|
||||||
|
memory?: Array<[UseCacheKey, any]>
|
||||||
|
shared?: Array<[UseSharedCacheKey, any]>
|
||||||
|
persist?: Array<[RendererPersistCacheKey, any]>
|
||||||
|
}) => {
|
||||||
|
values.memory?.forEach(([key, value]) => {
|
||||||
|
mockMemoryCache.set(key, value)
|
||||||
|
notifyMemorySubscribers(key)
|
||||||
|
})
|
||||||
|
|
||||||
|
values.shared?.forEach(([key, value]) => {
|
||||||
|
mockSharedCache.set(key, value)
|
||||||
|
notifySharedSubscribers(key)
|
||||||
|
})
|
||||||
|
|
||||||
|
values.persist?.forEach(([key, value]) => {
|
||||||
|
mockPersistCache.set(key, value)
|
||||||
|
notifyPersistSubscribers(key)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all cache values
|
||||||
|
*/
|
||||||
|
getAllCacheValues: () => ({
|
||||||
|
memory: Object.fromEntries(mockMemoryCache.entries()),
|
||||||
|
shared: Object.fromEntries(mockSharedCache.entries()),
|
||||||
|
persist: Object.fromEntries(mockPersistCache.entries())
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate cache change from external source
|
||||||
|
*/
|
||||||
|
simulateExternalCacheChange: <K extends UseCacheKey>(key: K, value: UseCacheSchema[K]) => {
|
||||||
|
mockMemoryCache.set(key, value)
|
||||||
|
notifyMemorySubscribers(key)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock cache hook to return specific value for a key
|
||||||
|
*/
|
||||||
|
mockCacheReturn: <K extends UseCacheKey>(
|
||||||
|
key: K,
|
||||||
|
value: UseCacheSchema[K],
|
||||||
|
setValue?: (value: UseCacheSchema[K]) => void
|
||||||
|
) => {
|
||||||
|
mockUseCache.mockImplementation((cacheKey, initValue) => {
|
||||||
|
if (cacheKey === key) {
|
||||||
|
return [
|
||||||
|
value,
|
||||||
|
setValue || vi.fn()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default behavior for other keys
|
||||||
|
const defaultValue = mockMemoryCache.get(cacheKey) ?? initValue ?? DefaultUseCache[cacheKey]
|
||||||
|
return [
|
||||||
|
defaultValue,
|
||||||
|
vi.fn()
|
||||||
|
]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock shared cache hook to return specific value for a key
|
||||||
|
*/
|
||||||
|
mockSharedCacheReturn: <K extends UseSharedCacheKey>(
|
||||||
|
key: K,
|
||||||
|
value: UseSharedCacheSchema[K],
|
||||||
|
setValue?: (value: UseSharedCacheSchema[K]) => void
|
||||||
|
) => {
|
||||||
|
mockUseSharedCache.mockImplementation((cacheKey, initValue) => {
|
||||||
|
if (cacheKey === key) {
|
||||||
|
return [
|
||||||
|
value,
|
||||||
|
setValue || vi.fn()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default behavior for other keys
|
||||||
|
const defaultValue = mockSharedCache.get(cacheKey) ?? initValue ?? DefaultUseSharedCache[cacheKey]
|
||||||
|
return [
|
||||||
|
defaultValue,
|
||||||
|
vi.fn()
|
||||||
|
]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock persist cache hook to return specific value for a key
|
||||||
|
*/
|
||||||
|
mockPersistCacheReturn: <K extends RendererPersistCacheKey>(
|
||||||
|
key: K,
|
||||||
|
value: RendererPersistCacheSchema[K],
|
||||||
|
setValue?: (value: RendererPersistCacheSchema[K]) => void
|
||||||
|
) => {
|
||||||
|
mockUsePersistCache.mockImplementation((cacheKey, initValue) => {
|
||||||
|
if (cacheKey === key) {
|
||||||
|
return [
|
||||||
|
value,
|
||||||
|
setValue || vi.fn()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default behavior for other keys
|
||||||
|
const defaultValue = mockPersistCache.get(cacheKey) ?? initValue ?? DefaultRendererPersistCache[cacheKey]
|
||||||
|
return [
|
||||||
|
defaultValue,
|
||||||
|
vi.fn()
|
||||||
|
]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add subscriber for cache changes (for testing subscription behavior)
|
||||||
|
*/
|
||||||
|
addMemorySubscriber: (key: UseCacheKey, callback: () => void): (() => void) => {
|
||||||
|
if (!mockMemorySubscribers.has(key)) {
|
||||||
|
mockMemorySubscribers.set(key, new Set())
|
||||||
|
}
|
||||||
|
mockMemorySubscribers.get(key)!.add(callback)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const subscribers = mockMemorySubscribers.get(key)
|
||||||
|
if (subscribers) {
|
||||||
|
subscribers.delete(callback)
|
||||||
|
if (subscribers.size === 0) {
|
||||||
|
mockMemorySubscribers.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add subscriber for shared cache changes
|
||||||
|
*/
|
||||||
|
addSharedSubscriber: (key: UseSharedCacheKey, callback: () => void): (() => void) => {
|
||||||
|
if (!mockSharedSubscribers.has(key)) {
|
||||||
|
mockSharedSubscribers.set(key, new Set())
|
||||||
|
}
|
||||||
|
mockSharedSubscribers.get(key)!.add(callback)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const subscribers = mockSharedSubscribers.get(key)
|
||||||
|
if (subscribers) {
|
||||||
|
subscribers.delete(callback)
|
||||||
|
if (subscribers.size === 0) {
|
||||||
|
mockSharedSubscribers.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add subscriber for persist cache changes
|
||||||
|
*/
|
||||||
|
addPersistSubscriber: (key: RendererPersistCacheKey, callback: () => void): (() => void) => {
|
||||||
|
if (!mockPersistSubscribers.has(key)) {
|
||||||
|
mockPersistSubscribers.set(key, new Set())
|
||||||
|
}
|
||||||
|
mockPersistSubscribers.get(key)!.add(callback)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const subscribers = mockPersistSubscribers.get(key)
|
||||||
|
if (subscribers) {
|
||||||
|
subscribers.delete(callback)
|
||||||
|
if (subscribers.size === 0) {
|
||||||
|
mockPersistSubscribers.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get subscriber counts for debugging
|
||||||
|
*/
|
||||||
|
getSubscriberCounts: () => ({
|
||||||
|
memory: Array.from(mockMemorySubscribers.entries()).map(([key, subs]) => [key, subs.size]),
|
||||||
|
shared: Array.from(mockSharedSubscribers.entries()).map(([key, subs]) => [key, subs.size]),
|
||||||
|
persist: Array.from(mockPersistSubscribers.entries()).map(([key, subs]) => [key, subs.size])
|
||||||
|
})
|
||||||
|
}
|
||||||
369
tests/__mocks__/renderer/useDataApi.ts
Normal file
369
tests/__mocks__/renderer/useDataApi.ts
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
import type { ConcreteApiPaths } from '@shared/data/api/apiSchemas'
|
||||||
|
import type { PaginatedResponse } from '@shared/data/api/apiTypes'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock useDataApi hooks for testing
|
||||||
|
* Provides comprehensive mocks for all data API hooks with realistic SWR-like behavior
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Mock SWR response interface
|
||||||
|
interface MockSWRResponse<T> {
|
||||||
|
data?: T
|
||||||
|
error?: Error
|
||||||
|
isLoading: boolean
|
||||||
|
isValidating: boolean
|
||||||
|
mutate: (data?: T | Promise<T> | ((data: T) => T)) => Promise<T | undefined>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock mutation response interface
|
||||||
|
interface MockMutationResponse<T> {
|
||||||
|
data?: T
|
||||||
|
error?: Error
|
||||||
|
isMutating: boolean
|
||||||
|
trigger: (...args: any[]) => Promise<T>
|
||||||
|
reset: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock paginated response interface
|
||||||
|
interface MockPaginatedResponse<T> extends MockSWRResponse<PaginatedResponse<T>> {
|
||||||
|
loadMore: () => void
|
||||||
|
isLoadingMore: boolean
|
||||||
|
hasMore: boolean
|
||||||
|
items: T[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create mock data based on API path
|
||||||
|
*/
|
||||||
|
function createMockDataForPath(path: ConcreteApiPaths): any {
|
||||||
|
if (path.includes('/topics')) {
|
||||||
|
if (path.endsWith('/topics')) {
|
||||||
|
return {
|
||||||
|
topics: [
|
||||||
|
{ id: 'topic1', name: 'Mock Topic 1', createdAt: '2024-01-01T00:00:00Z' },
|
||||||
|
{ id: 'topic2', name: 'Mock Topic 2', createdAt: '2024-01-02T00:00:00Z' }
|
||||||
|
],
|
||||||
|
total: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: 'topic1',
|
||||||
|
name: 'Mock Topic',
|
||||||
|
messages: [],
|
||||||
|
createdAt: '2024-01-01T00:00:00Z'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.includes('/messages')) {
|
||||||
|
return {
|
||||||
|
messages: [
|
||||||
|
{ id: 'msg1', content: 'Mock message 1', role: 'user' },
|
||||||
|
{ id: 'msg2', content: 'Mock message 2', role: 'assistant' }
|
||||||
|
],
|
||||||
|
total: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { id: 'mock_id', data: 'mock_data' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock useQuery hook
|
||||||
|
*/
|
||||||
|
export const mockUseQuery = vi.fn(<TPath extends ConcreteApiPaths>(
|
||||||
|
path: TPath | null,
|
||||||
|
query?: any,
|
||||||
|
options?: any
|
||||||
|
): MockSWRResponse<any> => {
|
||||||
|
const isLoading = options?.initialLoading ?? false
|
||||||
|
const hasError = options?.shouldError ?? false
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
return {
|
||||||
|
data: undefined,
|
||||||
|
error: new Error(`Mock error for ${path}`),
|
||||||
|
isLoading: false,
|
||||||
|
isValidating: false,
|
||||||
|
mutate: vi.fn().mockResolvedValue(undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockData = path ? createMockDataForPath(path) : undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: mockData,
|
||||||
|
error: undefined,
|
||||||
|
isLoading,
|
||||||
|
isValidating: false,
|
||||||
|
mutate: vi.fn().mockResolvedValue(mockData)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock useMutation hook
|
||||||
|
*/
|
||||||
|
export const mockUseMutation = vi.fn(<TPath extends ConcreteApiPaths, TMethod extends 'POST' | 'PUT' | 'DELETE' | 'PATCH'>(
|
||||||
|
path: TPath,
|
||||||
|
method: TMethod,
|
||||||
|
options?: any
|
||||||
|
): MockMutationResponse<any> => {
|
||||||
|
const isMutating = options?.initialMutating ?? false
|
||||||
|
const hasError = options?.shouldError ?? false
|
||||||
|
|
||||||
|
const mockTrigger = vi.fn(async (...args: any[]) => {
|
||||||
|
if (hasError) {
|
||||||
|
throw new Error(`Mock mutation error for ${method} ${path}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate different responses based on method
|
||||||
|
switch (method) {
|
||||||
|
case 'POST':
|
||||||
|
return { id: 'new_item', created: true, ...args[0] }
|
||||||
|
case 'PUT':
|
||||||
|
case 'PATCH':
|
||||||
|
return { id: 'updated_item', updated: true, ...args[0] }
|
||||||
|
case 'DELETE':
|
||||||
|
return { deleted: true }
|
||||||
|
default:
|
||||||
|
return { success: true }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: undefined,
|
||||||
|
error: undefined,
|
||||||
|
isMutating,
|
||||||
|
trigger: mockTrigger,
|
||||||
|
reset: vi.fn()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock usePaginatedQuery hook
|
||||||
|
*/
|
||||||
|
export const mockUsePaginatedQuery = vi.fn(<TPath extends ConcreteApiPaths>(
|
||||||
|
path: TPath | null,
|
||||||
|
query?: any,
|
||||||
|
options?: any
|
||||||
|
): MockPaginatedResponse<any> => {
|
||||||
|
const isLoading = options?.initialLoading ?? false
|
||||||
|
const isLoadingMore = options?.initialLoadingMore ?? false
|
||||||
|
const hasError = options?.shouldError ?? false
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
return {
|
||||||
|
data: undefined,
|
||||||
|
error: new Error(`Mock paginated error for ${path}`),
|
||||||
|
isLoading: false,
|
||||||
|
isValidating: false,
|
||||||
|
mutate: vi.fn().mockResolvedValue(undefined),
|
||||||
|
loadMore: vi.fn(),
|
||||||
|
isLoadingMore: false,
|
||||||
|
hasMore: false,
|
||||||
|
items: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockItems = path ? [
|
||||||
|
{ id: 'item1', name: 'Mock Item 1' },
|
||||||
|
{ id: 'item2', name: 'Mock Item 2' },
|
||||||
|
{ id: 'item3', name: 'Mock Item 3' }
|
||||||
|
] : []
|
||||||
|
|
||||||
|
const mockData: PaginatedResponse<any> = {
|
||||||
|
items: mockItems,
|
||||||
|
total: mockItems.length,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
hasMore: false
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: mockData,
|
||||||
|
error: undefined,
|
||||||
|
isLoading,
|
||||||
|
isValidating: false,
|
||||||
|
mutate: vi.fn().mockResolvedValue(mockData),
|
||||||
|
loadMore: vi.fn(),
|
||||||
|
isLoadingMore,
|
||||||
|
hasMore: mockData.hasMore,
|
||||||
|
items: mockItems
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock useInvalidateCache hook
|
||||||
|
*/
|
||||||
|
export const mockUseInvalidateCache = vi.fn(() => {
|
||||||
|
return {
|
||||||
|
invalidate: vi.fn(async (path?: ConcreteApiPaths) => {
|
||||||
|
// Mock cache invalidation
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
invalidateAll: vi.fn(async () => {
|
||||||
|
// Mock invalidate all caches
|
||||||
|
return Promise.resolve()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock prefetch function
|
||||||
|
*/
|
||||||
|
export const mockPrefetch = vi.fn(async <TPath extends ConcreteApiPaths>(
|
||||||
|
path: TPath,
|
||||||
|
query?: any,
|
||||||
|
options?: any
|
||||||
|
): Promise<any> => {
|
||||||
|
// Mock prefetch - return mock data
|
||||||
|
return createMockDataForPath(path)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export all mocks as a unified module
|
||||||
|
*/
|
||||||
|
export const MockUseDataApi = {
|
||||||
|
useQuery: mockUseQuery,
|
||||||
|
useMutation: mockUseMutation,
|
||||||
|
usePaginatedQuery: mockUsePaginatedQuery,
|
||||||
|
useInvalidateCache: mockUseInvalidateCache,
|
||||||
|
prefetch: mockPrefetch
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility functions for testing
|
||||||
|
*/
|
||||||
|
export const MockUseDataApiUtils = {
|
||||||
|
/**
|
||||||
|
* Reset all hook mock call counts and implementations
|
||||||
|
*/
|
||||||
|
resetMocks: () => {
|
||||||
|
mockUseQuery.mockClear()
|
||||||
|
mockUseMutation.mockClear()
|
||||||
|
mockUsePaginatedQuery.mockClear()
|
||||||
|
mockUseInvalidateCache.mockClear()
|
||||||
|
mockPrefetch.mockClear()
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up useQuery to return specific data
|
||||||
|
*/
|
||||||
|
mockQueryData: <T>(path: ConcreteApiPaths, data: T) => {
|
||||||
|
mockUseQuery.mockImplementation((queryPath, query, options) => {
|
||||||
|
if (queryPath === path) {
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
error: undefined,
|
||||||
|
isLoading: false,
|
||||||
|
isValidating: false,
|
||||||
|
mutate: vi.fn().mockResolvedValue(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Default behavior for other paths
|
||||||
|
return mockUseQuery.getMockImplementation()?.(queryPath, query, options) || {
|
||||||
|
data: undefined,
|
||||||
|
error: undefined,
|
||||||
|
isLoading: false,
|
||||||
|
isValidating: false,
|
||||||
|
mutate: vi.fn().mockResolvedValue(undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up useQuery to return loading state
|
||||||
|
*/
|
||||||
|
mockQueryLoading: (path: ConcreteApiPaths) => {
|
||||||
|
mockUseQuery.mockImplementation((queryPath, query, options) => {
|
||||||
|
if (queryPath === path) {
|
||||||
|
return {
|
||||||
|
data: undefined,
|
||||||
|
error: undefined,
|
||||||
|
isLoading: true,
|
||||||
|
isValidating: true,
|
||||||
|
mutate: vi.fn().mockResolvedValue(undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mockUseQuery.getMockImplementation()?.(queryPath, query, options) || {
|
||||||
|
data: undefined,
|
||||||
|
error: undefined,
|
||||||
|
isLoading: false,
|
||||||
|
isValidating: false,
|
||||||
|
mutate: vi.fn().mockResolvedValue(undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up useQuery to return error state
|
||||||
|
*/
|
||||||
|
mockQueryError: (path: ConcreteApiPaths, error: Error) => {
|
||||||
|
mockUseQuery.mockImplementation((queryPath, query, options) => {
|
||||||
|
if (queryPath === path) {
|
||||||
|
return {
|
||||||
|
data: undefined,
|
||||||
|
error,
|
||||||
|
isLoading: false,
|
||||||
|
isValidating: false,
|
||||||
|
mutate: vi.fn().mockResolvedValue(undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mockUseQuery.getMockImplementation()?.(queryPath, query, options) || {
|
||||||
|
data: undefined,
|
||||||
|
error: undefined,
|
||||||
|
isLoading: false,
|
||||||
|
isValidating: false,
|
||||||
|
mutate: vi.fn().mockResolvedValue(undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up useMutation to simulate success
|
||||||
|
*/
|
||||||
|
mockMutationSuccess: <T>(path: ConcreteApiPaths, method: string, result: T) => {
|
||||||
|
mockUseMutation.mockImplementation((mutationPath, mutationMethod, options) => {
|
||||||
|
if (mutationPath === path && mutationMethod === method) {
|
||||||
|
return {
|
||||||
|
data: undefined,
|
||||||
|
error: undefined,
|
||||||
|
isMutating: false,
|
||||||
|
trigger: vi.fn().mockResolvedValue(result),
|
||||||
|
reset: vi.fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mockUseMutation.getMockImplementation()?.(mutationPath, mutationMethod, options) || {
|
||||||
|
data: undefined,
|
||||||
|
error: undefined,
|
||||||
|
isMutating: false,
|
||||||
|
trigger: vi.fn().mockResolvedValue({}),
|
||||||
|
reset: vi.fn()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up useMutation to simulate error
|
||||||
|
*/
|
||||||
|
mockMutationError: (path: ConcreteApiPaths, method: string, error: Error) => {
|
||||||
|
mockUseMutation.mockImplementation((mutationPath, mutationMethod, options) => {
|
||||||
|
if (mutationPath === path && mutationMethod === method) {
|
||||||
|
return {
|
||||||
|
data: undefined,
|
||||||
|
error: undefined,
|
||||||
|
isMutating: false,
|
||||||
|
trigger: vi.fn().mockRejectedValue(error),
|
||||||
|
reset: vi.fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mockUseMutation.getMockImplementation()?.(mutationPath, mutationMethod, options) || {
|
||||||
|
data: undefined,
|
||||||
|
error: undefined,
|
||||||
|
isMutating: false,
|
||||||
|
trigger: vi.fn().mockResolvedValue({}),
|
||||||
|
reset: vi.fn()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
296
tests/__mocks__/renderer/usePreference.ts
Normal file
296
tests/__mocks__/renderer/usePreference.ts
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
import type {
|
||||||
|
PreferenceKeyType,
|
||||||
|
PreferenceUpdateOptions,
|
||||||
|
PreferenceValueType
|
||||||
|
} from '@shared/data/preference/preferenceTypes'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
|
||||||
|
import { mockPreferenceDefaults } from './PreferenceService'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock usePreference hooks for testing
|
||||||
|
* Provides comprehensive mocks for preference management hooks
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Mock preference state storage
|
||||||
|
const mockPreferenceState = new Map<PreferenceKeyType, any>()
|
||||||
|
|
||||||
|
// Initialize with defaults
|
||||||
|
Object.entries(mockPreferenceDefaults).forEach(([key, value]) => {
|
||||||
|
mockPreferenceState.set(key as PreferenceKeyType, value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock subscribers for preference changes
|
||||||
|
const mockPreferenceSubscribers = new Map<PreferenceKeyType, Set<() => void>>()
|
||||||
|
|
||||||
|
// Helper function to notify subscribers
|
||||||
|
const notifyPreferenceSubscribers = (key: PreferenceKeyType) => {
|
||||||
|
const subscribers = mockPreferenceSubscribers.get(key)
|
||||||
|
if (subscribers) {
|
||||||
|
subscribers.forEach(callback => {
|
||||||
|
try {
|
||||||
|
callback()
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Mock usePreference: Subscriber callback error:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock usePreference hook
|
||||||
|
*/
|
||||||
|
export const mockUsePreference = vi.fn(<K extends PreferenceKeyType>(
|
||||||
|
key: K,
|
||||||
|
options?: PreferenceUpdateOptions
|
||||||
|
): [PreferenceValueType<K>, (value: PreferenceValueType<K>) => Promise<void>] => {
|
||||||
|
// Get current value
|
||||||
|
const currentValue = mockPreferenceState.get(key) ?? mockPreferenceDefaults[key] ?? null
|
||||||
|
|
||||||
|
// Mock setValue function
|
||||||
|
const setValue = vi.fn(async (value: PreferenceValueType<K>) => {
|
||||||
|
const oldValue = mockPreferenceState.get(key)
|
||||||
|
|
||||||
|
// Simulate optimistic updates (default behavior)
|
||||||
|
if (options?.optimistic !== false) {
|
||||||
|
mockPreferenceState.set(key, value)
|
||||||
|
notifyPreferenceSubscribers(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate async update delay
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10))
|
||||||
|
|
||||||
|
// For pessimistic updates, update after delay
|
||||||
|
if (options?.optimistic === false) {
|
||||||
|
mockPreferenceState.set(key, value)
|
||||||
|
notifyPreferenceSubscribers(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate error scenarios if configured
|
||||||
|
if (options?.shouldError) {
|
||||||
|
// Rollback optimistic update on error
|
||||||
|
if (options.optimistic !== false) {
|
||||||
|
mockPreferenceState.set(key, oldValue)
|
||||||
|
notifyPreferenceSubscribers(key)
|
||||||
|
}
|
||||||
|
throw new Error(`Mock preference error for key: ${key}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return [currentValue, setValue]
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock useMultiplePreferences hook
|
||||||
|
*/
|
||||||
|
export const mockUseMultiplePreferences = vi.fn(<T extends Record<string, PreferenceKeyType>>(
|
||||||
|
keys: T,
|
||||||
|
options?: PreferenceUpdateOptions
|
||||||
|
): [
|
||||||
|
{ [K in keyof T]: PreferenceValueType<T[K]> },
|
||||||
|
(values: Partial<{ [K in keyof T]: PreferenceValueType<T[K]> }>) => Promise<void>
|
||||||
|
] => {
|
||||||
|
// Get current values for all keys
|
||||||
|
const currentValues = {} as { [K in keyof T]: PreferenceValueType<T[K]> }
|
||||||
|
Object.entries(keys).forEach(([alias, key]) => {
|
||||||
|
currentValues[alias as keyof T] = mockPreferenceState.get(key as PreferenceKeyType) ??
|
||||||
|
mockPreferenceDefaults[key as string] ?? null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock setValues function
|
||||||
|
const setValues = vi.fn(async (values: Partial<{ [K in keyof T]: PreferenceValueType<T[K]> }>) => {
|
||||||
|
const oldValues = { ...currentValues }
|
||||||
|
|
||||||
|
// Simulate optimistic updates
|
||||||
|
if (options?.optimistic !== false) {
|
||||||
|
Object.entries(values).forEach(([alias, value]) => {
|
||||||
|
const key = keys[alias as keyof T] as PreferenceKeyType
|
||||||
|
if (value !== undefined) {
|
||||||
|
mockPreferenceState.set(key, value)
|
||||||
|
currentValues[alias as keyof T] = value as any
|
||||||
|
notifyPreferenceSubscribers(key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate async update delay
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10))
|
||||||
|
|
||||||
|
// For pessimistic updates, update after delay
|
||||||
|
if (options?.optimistic === false) {
|
||||||
|
Object.entries(values).forEach(([alias, value]) => {
|
||||||
|
const key = keys[alias as keyof T] as PreferenceKeyType
|
||||||
|
if (value !== undefined) {
|
||||||
|
mockPreferenceState.set(key, value)
|
||||||
|
currentValues[alias as keyof T] = value as any
|
||||||
|
notifyPreferenceSubscribers(key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate error scenarios
|
||||||
|
if (options?.shouldError) {
|
||||||
|
// Rollback optimistic updates on error
|
||||||
|
if (options.optimistic !== false) {
|
||||||
|
Object.entries(oldValues).forEach(([alias, value]) => {
|
||||||
|
const key = keys[alias as keyof T] as PreferenceKeyType
|
||||||
|
mockPreferenceState.set(key, value)
|
||||||
|
currentValues[alias as keyof T] = value
|
||||||
|
notifyPreferenceSubscribers(key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
throw new Error('Mock multiple preferences error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return [currentValues, setValues]
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export all mocks as a unified module
|
||||||
|
*/
|
||||||
|
export const MockUsePreference = {
|
||||||
|
usePreference: mockUsePreference,
|
||||||
|
useMultiplePreferences: mockUseMultiplePreferences
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility functions for testing
|
||||||
|
*/
|
||||||
|
export const MockUsePreferenceUtils = {
|
||||||
|
/**
|
||||||
|
* Reset all hook mock call counts and state
|
||||||
|
*/
|
||||||
|
resetMocks: () => {
|
||||||
|
mockUsePreference.mockClear()
|
||||||
|
mockUseMultiplePreferences.mockClear()
|
||||||
|
|
||||||
|
// Reset state to defaults
|
||||||
|
mockPreferenceState.clear()
|
||||||
|
Object.entries(mockPreferenceDefaults).forEach(([key, value]) => {
|
||||||
|
mockPreferenceState.set(key as PreferenceKeyType, value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Clear subscribers
|
||||||
|
mockPreferenceSubscribers.clear()
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a preference value for testing
|
||||||
|
*/
|
||||||
|
setPreferenceValue: <K extends PreferenceKeyType>(key: K, value: PreferenceValueType<K>) => {
|
||||||
|
mockPreferenceState.set(key, value)
|
||||||
|
notifyPreferenceSubscribers(key)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current preference value
|
||||||
|
*/
|
||||||
|
getPreferenceValue: <K extends PreferenceKeyType>(key: K): PreferenceValueType<K> => {
|
||||||
|
return mockPreferenceState.get(key) ?? mockPreferenceDefaults[key] ?? null
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set multiple preference values for testing
|
||||||
|
*/
|
||||||
|
setMultiplePreferenceValues: (values: Record<string, any>) => {
|
||||||
|
Object.entries(values).forEach(([key, value]) => {
|
||||||
|
mockPreferenceState.set(key as PreferenceKeyType, value)
|
||||||
|
notifyPreferenceSubscribers(key as PreferenceKeyType)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all current preference values
|
||||||
|
*/
|
||||||
|
getAllPreferenceValues: (): Record<string, any> => {
|
||||||
|
const result: Record<string, any> = {}
|
||||||
|
mockPreferenceState.forEach((value, key) => {
|
||||||
|
result[key] = value
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate preference change from external source
|
||||||
|
*/
|
||||||
|
simulateExternalPreferenceChange: <K extends PreferenceKeyType>(key: K, value: PreferenceValueType<K>) => {
|
||||||
|
mockPreferenceState.set(key, value)
|
||||||
|
notifyPreferenceSubscribers(key)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock preference hook to return specific value for a key
|
||||||
|
*/
|
||||||
|
mockPreferenceReturn: <K extends PreferenceKeyType>(
|
||||||
|
key: K,
|
||||||
|
value: PreferenceValueType<K>,
|
||||||
|
setValue?: (value: PreferenceValueType<K>) => Promise<void>
|
||||||
|
) => {
|
||||||
|
mockUsePreference.mockImplementation((preferenceKey, options) => {
|
||||||
|
if (preferenceKey === key) {
|
||||||
|
return [
|
||||||
|
value,
|
||||||
|
setValue || vi.fn().mockResolvedValue(undefined)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default behavior for other keys
|
||||||
|
const defaultValue = mockPreferenceState.get(preferenceKey) ??
|
||||||
|
mockPreferenceDefaults[preferenceKey] ?? null
|
||||||
|
return [
|
||||||
|
defaultValue,
|
||||||
|
vi.fn().mockResolvedValue(undefined)
|
||||||
|
]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock preference hook to simulate error for a key
|
||||||
|
*/
|
||||||
|
mockPreferenceError: <K extends PreferenceKeyType>(key: K, error: Error) => {
|
||||||
|
mockUsePreference.mockImplementation((preferenceKey, options) => {
|
||||||
|
if (preferenceKey === key) {
|
||||||
|
const setValue = vi.fn().mockRejectedValue(error)
|
||||||
|
const currentValue = mockPreferenceState.get(key) ?? mockPreferenceDefaults[key] ?? null
|
||||||
|
return [currentValue, setValue]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default behavior for other keys
|
||||||
|
const defaultValue = mockPreferenceState.get(preferenceKey) ??
|
||||||
|
mockPreferenceDefaults[preferenceKey] ?? null
|
||||||
|
return [
|
||||||
|
defaultValue,
|
||||||
|
vi.fn().mockResolvedValue(undefined)
|
||||||
|
]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add subscriber for preference changes (for testing subscription behavior)
|
||||||
|
*/
|
||||||
|
addSubscriber: (key: PreferenceKeyType, callback: () => void): (() => void) => {
|
||||||
|
if (!mockPreferenceSubscribers.has(key)) {
|
||||||
|
mockPreferenceSubscribers.set(key, new Set())
|
||||||
|
}
|
||||||
|
mockPreferenceSubscribers.get(key)!.add(callback)
|
||||||
|
|
||||||
|
// Return unsubscribe function
|
||||||
|
return () => {
|
||||||
|
const subscribers = mockPreferenceSubscribers.get(key)
|
||||||
|
if (subscribers) {
|
||||||
|
subscribers.delete(callback)
|
||||||
|
if (subscribers.size === 0) {
|
||||||
|
mockPreferenceSubscribers.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get subscriber count for a preference key
|
||||||
|
*/
|
||||||
|
getSubscriberCount: (key: PreferenceKeyType): number => {
|
||||||
|
return mockPreferenceSubscribers.get(key)?.size ?? 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,6 +9,40 @@ vi.mock('@logger', async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Mock PreferenceService globally for main tests
|
||||||
|
vi.mock('@main/data/PreferenceService', async () => {
|
||||||
|
const { MockMainPreferenceServiceExport } = await import('./__mocks__/main/PreferenceService')
|
||||||
|
return MockMainPreferenceServiceExport
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock DataApiService globally for main tests
|
||||||
|
vi.mock('@main/data/DataApiService', async () => {
|
||||||
|
const { MockMainDataApiServiceExport } = await import('./__mocks__/main/DataApiService')
|
||||||
|
return MockMainDataApiServiceExport
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock CacheService globally for main tests
|
||||||
|
vi.mock('@main/data/CacheService', async () => {
|
||||||
|
const { MockMainCacheServiceExport } = await import('./__mocks__/main/CacheService')
|
||||||
|
return MockMainCacheServiceExport
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock DbService globally for main tests (if exists)
|
||||||
|
vi.mock('@main/data/db/DbService', async () => {
|
||||||
|
try {
|
||||||
|
const { MockDbService } = await import('./__mocks__/DbService')
|
||||||
|
return MockDbService
|
||||||
|
} catch {
|
||||||
|
// Return basic mock if DbService mock doesn't exist yet
|
||||||
|
return {
|
||||||
|
dbService: {
|
||||||
|
initialize: vi.fn(),
|
||||||
|
getDb: vi.fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Mock electron modules that are commonly used in main process
|
// Mock electron modules that are commonly used in main process
|
||||||
vi.mock('electron', () => ({
|
vi.mock('electron', () => ({
|
||||||
app: {
|
app: {
|
||||||
|
|||||||
@ -14,6 +14,42 @@ vi.mock('@logger', async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Mock PreferenceService globally for renderer tests
|
||||||
|
vi.mock('@data/PreferenceService', async () => {
|
||||||
|
const { MockPreferenceService } = await import('./__mocks__/renderer/PreferenceService')
|
||||||
|
return MockPreferenceService
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock DataApiService globally for renderer tests
|
||||||
|
vi.mock('@data/DataApiService', async () => {
|
||||||
|
const { MockDataApiService } = await import('./__mocks__/renderer/DataApiService')
|
||||||
|
return MockDataApiService
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock CacheService globally for renderer tests
|
||||||
|
vi.mock('@data/CacheService', async () => {
|
||||||
|
const { MockCacheService } = await import('./__mocks__/renderer/CacheService')
|
||||||
|
return MockCacheService
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock useDataApi hooks globally for renderer tests
|
||||||
|
vi.mock('@data/hooks/useDataApi', async () => {
|
||||||
|
const { MockUseDataApi } = await import('./__mocks__/renderer/useDataApi')
|
||||||
|
return MockUseDataApi
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock usePreference hooks globally for renderer tests
|
||||||
|
vi.mock('@data/hooks/usePreference', async () => {
|
||||||
|
const { MockUsePreference } = await import('./__mocks__/renderer/usePreference')
|
||||||
|
return MockUsePreference
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock useCache hooks globally for renderer tests
|
||||||
|
vi.mock('@data/hooks/useCache', async () => {
|
||||||
|
const { MockUseCache } = await import('./__mocks__/renderer/useCache')
|
||||||
|
return MockUseCache
|
||||||
|
})
|
||||||
|
|
||||||
vi.mock('axios', () => ({
|
vi.mock('axios', () => ({
|
||||||
default: {
|
default: {
|
||||||
get: vi.fn().mockResolvedValue({ data: {} }), // Mocking axios GET request
|
get: vi.fn().mockResolvedValue({ data: {} }), // Mocking axios GET request
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user