feat(test): unit test install logic main process (#449)

This commit is contained in:
Wendong-Fan 2025-10-06 13:28:08 +08:00 committed by GitHub
commit 02ac7dc0b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 5254 additions and 19 deletions

View file

@ -50,16 +50,18 @@ export default function ChatBox(): JSX.Element {
})
.catch((err) => console.error("Failed to fetch settings:", err));
proxyFetchGet("/api/configs").then((configsRes) => {
const configs = Array.isArray(configsRes) ? configsRes : [];
const _hasApiKey = configs.find(
(item) => item.config_name === "GOOGLE_API_KEY"
);
const _hasApiId = configs.find(
(item) => item.config_name === "SEARCH_ENGINE_ID"
);
if (_hasApiKey && _hasApiId) setHasSearchKey(true);
});
proxyFetchGet("/api/configs")
.then((configsRes) => {
const configs = Array.isArray(configsRes) ? configsRes : [];
const _hasApiKey = configs.find(
(item) => item.config_name === "GOOGLE_API_KEY"
);
const _hasApiId = configs.find(
(item) => item.config_name === "SEARCH_ENGINE_ID"
);
if (_hasApiKey && _hasApiId) setHasSearchKey(true);
})
.catch((err) => console.error("Failed to fetch configs:", err));
}, []);
// Refresh privacy status when dialog closes

View file

@ -41,16 +41,20 @@ export const useInstallationSetup = () => {
};
const checkBackendStatus = async() => {
// Also check if installation is currently in progress
const installationStatus = await window.electronAPI.getInstallationStatus();
console.log('[useInstallationSetup] Installation status check:', installationStatus);
if (installationStatus.success && installationStatus.isInstalling) {
console.log('[useInstallationSetup] Installation in progress, starting frontend state');
startInstallation();
try {
// Also check if installation is currently in progress
const installationStatus = await window.electronAPI.getInstallationStatus();
console.log('[useInstallationSetup] Installation status check:', installationStatus);
if (installationStatus.success && installationStatus.isInstalling) {
console.log('[useInstallationSetup] Installation in progress, starting frontend state');
startInstallation();
}
} catch (err) {
console.error('[useInstallationSetup] Failed to check installation status:', err);
}
}
checkToolInstalled();
checkBackendStatus();
}, [initState, setInitState, startInstallation]);

423
test/README.md Normal file
View file

@ -0,0 +1,423 @@
# Installation Flow Testing Environment
This comprehensive testing environment allows you to test all installation flows end-to-end with mocked `uv sync`, `uvicorn`, and Electron APIs. It simulates different system states and provides utilities to change the environment during tests.
## Overview
The testing environment consists of three main components:
1. **Electron API Mocks** (`test/mocks/electronMocks.ts`) - Mock Electron's preload APIs
2. **Environment State Mocks** (`test/mocks/environmentMocks.ts`) - Mock filesystem, processes, and system state
3. **Test Scenarios** - Predefined scenarios for different installation flows
## Quick Start
```typescript
import { setupElectronMocks, TestScenarios } from '../mocks/electronMocks'
import { setupMockEnvironment } from '../mocks/environmentMocks'
describe('My Installation Test', () => {
let electronAPI: MockedElectronAPI
let mockEnv: ReturnType<typeof setupMockEnvironment>
beforeEach(() => {
// Set up mocks
const { electronAPI: api } = setupElectronMocks()
electronAPI = api
mockEnv = setupMockEnvironment()
})
it('should handle version update', async () => {
// Apply scenario
TestScenarios.versionUpdate(electronAPI)
// Your test code here
})
})
```
## Electron API Mocks
### Available Mock Methods
- `checkAndInstallDepsOnUpdate()` - Simulates dependency installation
- `getInstallationStatus()` - Returns current installation status
- `exportLog()` - Simulates log export functionality
- Event listeners for installation events
### Simulation Functions
```typescript
// Simulate installation events
electronAPI.simulateInstallationStart()
electronAPI.simulateInstallationLog('stdout', 'Installing packages...')
electronAPI.simulateInstallationComplete(true) // or false for failure
// Simulate system changes
electronAPI.simulateVersionChange('2.0.0')
electronAPI.simulateVenvRemoval()
electronAPI.simulateUvicornStartup()
```
### Mock State Control
```typescript
// Control the mock state directly
electronAPI.mockState.venvExists = false
electronAPI.mockState.isInstalling = true
electronAPI.mockState.toolInstalled = false
```
## Environment State Mocks
### Filesystem Mock
Controls file system operations:
```typescript
// Control file existence
mockEnv.mockState.filesystem.venvExists = false
mockEnv.mockState.filesystem.versionFileExists = true
mockEnv.mockState.filesystem.installedLockExists = false
// Control file contents
mockEnv.mockState.filesystem.versionFileContent = '0.9.0'
```
### Process Mock
Controls process spawning and execution:
```typescript
// Control tool availability
mockEnv.mockState.processes.uvAvailable = false
mockEnv.mockState.processes.bunAvailable = true
mockEnv.mockState.processes.uvicornRunning = false
// Control network connectivity
mockEnv.mockState.network.canConnectToDefault = false
mockEnv.mockState.network.canConnectToMirror = true
```
## Predefined Test Scenarios
### Electron API Scenarios
Use `TestScenarios` from `electronMocks.ts`:
```typescript
// Fresh installation - no .venv, no version file
TestScenarios.freshInstall(electronAPI)
// Version update - version file exists but version changed
TestScenarios.versionUpdate(electronAPI)
// .venv removed - version file exists but .venv is missing
TestScenarios.venvRemoved(electronAPI)
// Installation in progress - when user opens app during installation
TestScenarios.installationInProgress(electronAPI)
// Installation error scenario
TestScenarios.installationError(electronAPI)
// Uvicorn startup with dependency installation
TestScenarios.uvicornDepsInstall(electronAPI)
// All good - no installation needed
TestScenarios.allGood(electronAPI)
```
### Environment Scenarios
Use `mockEnv.scenarios` from `environmentMocks.ts`:
```typescript
// Fresh installation
mockEnv.scenarios.freshInstall()
// Version update
mockEnv.scenarios.versionUpdate('0.9.0', '1.0.0')
// .venv removed
mockEnv.scenarios.venvRemoved()
// Network issues
mockEnv.scenarios.networkIssues()
// Complete failure
mockEnv.scenarios.completeFailure()
// Uvicorn startup installation
mockEnv.scenarios.uvicornStartupInstall()
// Installation in progress
mockEnv.scenarios.installationInProgress()
```
## Testing Different Installation States
### Installation Store States
Test all possible states from `installationStore.ts`:
- `'idle'` - Initial state
- `'checking-permissions'` - Checking system permissions
- `'showing-carousel'` - Showing onboarding carousel
- `'installing'` - Installation in progress
- `'error'` - Installation failed
- `'completed'` - Installation successful
```typescript
import { useInstallationStore } from '@/store/installationStore'
it('should transition through all states', () => {
const store = useInstallationStore.getState()
expect(store.state).toBe('idle')
store.startInstallation()
expect(store.state).toBe('installing')
store.setError('Installation failed')
expect(store.state).toBe('error')
store.retryInstallation()
expect(store.state).toBe('installing')
store.setSuccess()
expect(store.state).toBe('completed')
})
```
## Specific Test Cases
### 1. Testing .venv Removal
```typescript
it('should handle .venv removal', async () => {
// Simulate .venv being removed
TestScenarios.venvRemoved(electronAPI)
// or
mockEnv.scenarios.venvRemoved()
// Test your component/hook
const result = await electronAPI.checkAndInstallDepsOnUpdate()
expect(result.success).toBe(true)
})
```
### 2. Testing Version File Changes
```typescript
it('should handle version file changes', async () => {
// Simulate version change
TestScenarios.versionUpdate(electronAPI)
// or
mockEnv.scenarios.versionUpdate('0.9.0', '1.0.0')
// Your test assertions
})
```
### 3. Testing Uvicorn Startup Installation
```typescript
it('should handle uvicorn starting with dependency installation', async () => {
// Simulate uvicorn detecting missing dependencies
TestScenarios.uvicornDepsInstall(electronAPI)
// Trigger uvicorn startup
electronAPI.simulateUvicornStartup()
// Wait for installation events
await waitFor(() => {
expect(mockInstallationStore.startInstallation).toHaveBeenCalled()
})
})
```
### 4. Testing UI Installation States
```typescript
it('should show correct UI for each installation state', () => {
const { result } = renderHook(() => useInstallationStore())
// Test idle state
expect(result.current.state).toBe('idle')
expect(result.current.isVisible).toBe(false)
// Test installing state
act(() => result.current.startInstallation())
expect(result.current.state).toBe('installing')
expect(result.current.isVisible).toBe(true)
// Test error state
act(() => result.current.setError('Installation failed'))
expect(result.current.state).toBe('error')
expect(result.current.error).toBe('Installation failed')
// Test completed state
act(() => result.current.setSuccess())
expect(result.current.state).toBe('completed')
expect(result.current.progress).toBe(100)
})
```
## Advanced Testing Patterns
### Testing Event Sequences
```typescript
it('should handle complete installation flow', async () => {
const events: string[] = []
// Set up event tracking
electronAPI.onInstallDependenciesStart(() => events.push('start'))
electronAPI.onInstallDependenciesLog(() => events.push('log'))
electronAPI.onInstallDependenciesComplete(() => events.push('complete'))
// Trigger installation
await electronAPI.checkAndInstallDepsOnUpdate()
// Verify event sequence
expect(events).toEqual(['start', 'log', 'log', 'complete'])
})
```
### Testing Error Recovery
```typescript
it('should recover from installation errors', async () => {
// Set up error scenario
TestScenarios.installationError(electronAPI)
const store = useInstallationStore.getState()
// Trigger installation
await store.performInstallation()
expect(store.state).toBe('error')
// Simulate retry
TestScenarios.allGood(electronAPI) // Fix the environment
store.retryInstallation()
await waitFor(() => {
expect(store.state).toBe('completed')
})
})
```
### Testing Concurrent Operations
```typescript
it('should handle concurrent installation attempts', async () => {
const store = useInstallationStore.getState()
// Start multiple installations
const promise1 = store.performInstallation()
const promise2 = store.performInstallation()
// Should handle gracefully
const [result1, result2] = await Promise.all([promise1, promise2])
expect(store.state).toBe('completed')
})
```
## Debugging Tests
### Logging Mock State
```typescript
// Log current mock state
console.log('Electron API State:', electronAPI.mockState)
console.log('Environment State:', mockEnv.mockState)
// Check what functions were called
console.log('checkAndInstallDepsOnUpdate calls:',
electronAPI.checkAndInstallDepsOnUpdate.mock.calls)
```
### Waiting for Async Operations
```typescript
import { waitForStateChange } from '../mocks/environmentMocks'
// Wait for specific state changes
await waitForStateChange(
() => mockEnv.mockState.processes.uvSyncInProgress,
true,
1000 // timeout
)
```
## Running the Tests
```bash
# Run all installation tests
npm test test/unit/store/installationStore.test.ts
npm test test/unit/hooks/useInstallationSetup.test.ts
npm test test/unit/electron/install-deps.test.ts
# Run with coverage
npm test -- --coverage
# Run in watch mode
npm test -- --watch
```
## Common Issues and Solutions
### 1. Mock Not Applied
**Problem**: Mock functions not being called
**Solution**: Ensure mocks are set up before importing modules
```typescript
beforeEach(async () => {
setupMocks() // Set up first
const module = await import('./module') // Import after
})
```
### 2. State Not Updating
**Problem**: Mock state changes not reflected
**Solution**: Use simulation functions instead of direct state mutation
```typescript
// Don't do this
electronAPI.mockState.isInstalling = true
// Do this instead
electronAPI.simulateInstallationStart()
```
### 3. Async Operations Not Completing
**Problem**: Tests timeout waiting for async operations
**Solution**: Use proper wait functions and increase timeouts
```typescript
await vi.waitFor(() => {
expect(condition).toBe(true)
}, { timeout: 2000 })
```
## Best Practices
1. **Reset State**: Always reset mock state between tests
2. **Use Scenarios**: Prefer predefined scenarios over manual state setup
3. **Test Edge Cases**: Include error conditions and edge cases
4. **Verify Events**: Check that the correct events are emitted
5. **Test Cleanup**: Verify that resources are properly cleaned up
6. **Integration Tests**: Test the complete flow, not just individual functions
## Example Test Files
- `test/unit/store/installationStore.test.ts` - Store state management
- `test/unit/hooks/useInstallationSetup.test.ts` - Hook behavior
- `test/unit/electron/install-deps.test.ts` - Backend installation logic
These test files demonstrate all the patterns and scenarios described in this README.

467
test/mocks/electronMocks.ts Normal file
View file

@ -0,0 +1,467 @@
import { vi } from 'vitest'
export interface MockedElectronAPI {
// Mock environment state that can be controlled in tests
mockState: {
venvExists: boolean
versionFileExists: boolean
currentVersion: string
savedVersion: string
isInstalling: boolean
installedLockExists: boolean
uvicornStarting: boolean
toolInstalled: boolean
allowForceInstall: boolean
// Environment-related state
envFileExists: boolean
envContent: string
eigentDirExists: boolean
userEmail: string
mcpRemoteConfigExists: boolean
hasToken: boolean
}
// Mock implementation functions
checkAndInstallDepsOnUpdate: ReturnType<typeof vi.fn>
getInstallationStatus: ReturnType<typeof vi.fn>
exportLog: ReturnType<typeof vi.fn>
onInstallDependenciesStart: ReturnType<typeof vi.fn>
onInstallDependenciesLog: ReturnType<typeof vi.fn>
onInstallDependenciesComplete: ReturnType<typeof vi.fn>
removeAllListeners: ReturnType<typeof vi.fn>
// EnvUtil mock functions
getEnvPath: ReturnType<typeof vi.fn>
updateEnvBlock: ReturnType<typeof vi.fn>
removeEnvKey: ReturnType<typeof vi.fn>
getEmailFolderPath: ReturnType<typeof vi.fn>
parseEnvBlock: ReturnType<typeof vi.fn>
// Test utilities
simulateInstallationStart: () => void
simulateInstallationLog: (type: 'stdout' | 'stderr', data: string) => void
simulateInstallationComplete: (success: boolean, error?: string) => void
simulateVersionChange: (newVersion: string) => void
simulateVenvRemoval: () => void
simulateUvicornStartup: () => void
simulateEnvCorruption: () => void
simulateUserEmailChange: (email: string) => void
simulateMcpConfigMissing: () => void
reset: () => void
}
export interface MockedIpcRenderer {
invoke: ReturnType<typeof vi.fn>
on: ReturnType<typeof vi.fn>
removeAllListeners: ReturnType<typeof vi.fn>
}
/**
* Creates a comprehensive mock for the Electron API
* This mock can simulate all the different installation scenarios
*/
export function createElectronAPIMock(): MockedElectronAPI {
// Listeners for simulation
const installStartListeners: Array<() => void> = []
const installLogListeners: Array<(data: { type: string; data: string }) => void> = []
const installCompleteListeners: Array<(data: { success: boolean; code?: number; error?: string }) => void> = []
const mockState = {
venvExists: true,
versionFileExists: true,
currentVersion: '1.0.0',
savedVersion: '1.0.0',
isInstalling: false,
installedLockExists: true,
uvicornStarting: false,
toolInstalled: true,
allowForceInstall: false,
// Environment-related state
envFileExists: true,
envContent: 'MOCK_VAR=mock_value\n# === MCP INTEGRATION ENV START ===\nMCP_KEY=test_value\n# === MCP INTEGRATION ENV END ===',
eigentDirExists: true,
userEmail: 'test@example.com',
mcpRemoteConfigExists: true,
hasToken: true,
}
const electronAPI: MockedElectronAPI = {
mockState,
// Core API functions
checkAndInstallDepsOnUpdate: vi.fn().mockImplementation(async () => {
const { versionFileExists, currentVersion, savedVersion, allowForceInstall, venvExists, toolInstalled } = mockState
// Simulate the real implementation logic that checks:
// 1. Version file existence and version match
// 2. Virtual environment existence
// 3. Command tools installation status
const versionChanged = !versionFileExists || savedVersion !== currentVersion
const needsInstallation = allowForceInstall || versionChanged || !venvExists || !toolInstalled
if (needsInstallation) {
// Log the reason for installation
if (!toolInstalled) {
electronAPI.simulateInstallationLog('stdout', 'Command tools missing, starting installation...')
} else if (!venvExists) {
electronAPI.simulateInstallationLog('stdout', 'Virtual environment missing, starting installation...')
} else if (versionChanged) {
electronAPI.simulateInstallationLog('stdout', 'Version changed, starting installation...')
}
// Trigger installation
electronAPI.simulateInstallationStart()
// Simulate installation process with delay
setTimeout(() => {
electronAPI.simulateInstallationLog('stdout', 'Resolving dependencies...')
setTimeout(() => {
electronAPI.simulateInstallationLog('stdout', 'Installing packages...')
setTimeout(() => {
electronAPI.simulateInstallationComplete(true)
// Update state after successful installation
mockState.venvExists = true
mockState.toolInstalled = true
mockState.installedLockExists = true
}, 100)
}, 100)
}, 50)
return { success: true, message: 'Dependencies installed successfully after update' }
} else {
return { success: true, message: 'Version not changed, venv exists, and tools installed - skipped installation' }
}
}),
getInstallationStatus: vi.fn().mockImplementation(async () => {
return {
success: true,
isInstalling: mockState.isInstalling,
hasLockFile: mockState.isInstalling || mockState.installedLockExists,
installedExists: mockState.installedLockExists
}
}),
exportLog: vi.fn().mockImplementation(async () => {
return {
success: true,
savedPath: '/mock/path/to/log.txt'
}
}),
// Event listeners
onInstallDependenciesStart: vi.fn().mockImplementation((callback: () => void) => {
installStartListeners.push(callback)
}),
onInstallDependenciesLog: vi.fn().mockImplementation((callback: (data: { type: string; data: string }) => void) => {
installLogListeners.push(callback)
}),
onInstallDependenciesComplete: vi.fn().mockImplementation((callback: (data: { success: boolean; code?: number; error?: string }) => void) => {
installCompleteListeners.push(callback)
}),
removeAllListeners: vi.fn().mockImplementation(() => {
installStartListeners.length = 0
installLogListeners.length = 0
installCompleteListeners.length = 0
}),
// EnvUtil mock functions
getEnvPath: vi.fn().mockImplementation((email: string) => {
const sanitizedEmail = email.split("@")[0].replace(/[\\/*?:"<>|\s]/g, "_").replace(".", "_")
return `/mock/home/.eigent/.env.${sanitizedEmail}`
}),
updateEnvBlock: vi.fn().mockImplementation((lines: string[], kv: Record<string, string>) => {
// Mock implementation that adds/updates environment variables in the MCP block
const startMarker = '# === MCP INTEGRATION ENV START ==='
const endMarker = '# === MCP INTEGRATION ENV END ==='
let start = lines.findIndex(l => l.trim() === startMarker)
let end = lines.findIndex(l => l.trim() === endMarker)
if (start === -1 || end === -1) {
// No block exists, create one
lines.push(startMarker)
Object.entries(kv).forEach(([k, v]) => {
lines.push(`${k}=${v}`)
})
lines.push(endMarker)
return lines
}
// Update existing block
const newBlock = Object.entries(kv).map(([k, v]) => `${k}=${v}`)
return [
...lines.slice(0, start + 1),
...newBlock,
...lines.slice(end)
]
}),
removeEnvKey: vi.fn().mockImplementation((lines: string[], key: string) => {
// Mock implementation that removes a key from the MCP block
const startMarker = '# === MCP INTEGRATION ENV START ==='
const endMarker = '# === MCP INTEGRATION ENV END ==='
let start = lines.findIndex(l => l.trim() === startMarker)
let end = lines.findIndex(l => l.trim() === endMarker)
if (start === -1 || end === -1) return lines
const block = lines.slice(start + 1, end)
const newBlock = block.filter(line => !line.startsWith(key + '='))
return [
...lines.slice(0, start + 1),
...newBlock,
...lines.slice(end)
]
}),
getEmailFolderPath: vi.fn().mockImplementation((email: string) => {
const sanitizedEmail = email.split("@")[0].replace(/[\\/*?:"<>|\s]/g, "_").replace(".", "_")
return {
MCP_REMOTE_CONFIG_DIR: `/mock/home/.eigent/${sanitizedEmail}`,
MCP_CONFIG_DIR: '/mock/home/.eigent',
tempEmail: sanitizedEmail,
hasToken: mockState.hasToken
}
}),
parseEnvBlock: vi.fn().mockImplementation((content: string) => {
const lines = content.split(/\r?\n/)
const startMarker = '# === MCP INTEGRATION ENV START ==='
const endMarker = '# === MCP INTEGRATION ENV END ==='
let start = lines.findIndex(l => l.trim() === startMarker)
let end = lines.findIndex(l => l.trim() === endMarker)
if (start === -1) start = lines.length
if (end === -1) end = lines.length
return { lines, start, end }
}),
// Simulation utilities
simulateInstallationStart: () => {
mockState.isInstalling = true
installStartListeners.forEach(listener => listener())
},
simulateInstallationLog: (type: 'stdout' | 'stderr', data: string) => {
installLogListeners.forEach(listener => listener({ type, data }))
},
simulateInstallationComplete: (success: boolean, error?: string) => {
mockState.isInstalling = false
if (success) {
mockState.installedLockExists = true
}
installCompleteListeners.forEach(listener =>
listener({ success, error, code: success ? 0 : 1 })
)
},
simulateVersionChange: (newVersion: string) => {
mockState.currentVersion = newVersion
// This simulates a version mismatch scenario
},
simulateVenvRemoval: () => {
mockState.venvExists = false
mockState.installedLockExists = false
// Don't remove version file - this simulates venv being deleted but version file still existing
},
simulateUvicornStartup: () => {
mockState.uvicornStarting = true
// Simulate uvicorn detecting dependency installation need
setTimeout(() => {
electronAPI.simulateInstallationStart()
electronAPI.simulateInstallationLog('stdout', 'Uvicorn detected missing dependencies')
electronAPI.simulateInstallationLog('stdout', 'Resolving dependencies...')
setTimeout(() => {
electronAPI.simulateInstallationLog('stdout', 'Uvicorn running on http://127.0.0.1:8000')
electronAPI.simulateInstallationComplete(true)
mockState.uvicornStarting = false
}, 200)
}, 100)
},
simulateEnvCorruption: () => {
mockState.envFileExists = true
mockState.envContent = 'INVALID_ENV_CONTENT\n# === MCP INTEGRATION ENV START ===\nBROKEN'
},
simulateUserEmailChange: (email: string) => {
mockState.userEmail = email
},
simulateMcpConfigMissing: () => {
mockState.mcpRemoteConfigExists = false
},
reset: () => {
Object.assign(mockState, {
venvExists: true,
versionFileExists: true,
currentVersion: '1.0.0',
savedVersion: '1.0.0',
isInstalling: false,
installedLockExists: true,
uvicornStarting: false,
toolInstalled: true,
allowForceInstall: false,
// Reset environment-related state
envFileExists: true,
envContent: 'MOCK_VAR=mock_value\n# === MCP INTEGRATION ENV START ===\nMCP_KEY=test_value\n# === MCP INTEGRATION ENV END ===',
eigentDirExists: true,
userEmail: 'test@example.com',
mcpRemoteConfigExists: true,
hasToken: true,
})
// Clear all listeners
installStartListeners.length = 0
installLogListeners.length = 0
installCompleteListeners.length = 0
// Reset all mocks
electronAPI.checkAndInstallDepsOnUpdate.mockClear()
electronAPI.getInstallationStatus.mockClear()
electronAPI.exportLog.mockClear()
electronAPI.onInstallDependenciesStart.mockClear()
electronAPI.onInstallDependenciesLog.mockClear()
electronAPI.onInstallDependenciesComplete.mockClear()
electronAPI.removeAllListeners.mockClear()
electronAPI.getEnvPath.mockClear()
electronAPI.updateEnvBlock.mockClear()
electronAPI.removeEnvKey.mockClear()
electronAPI.getEmailFolderPath.mockClear()
electronAPI.parseEnvBlock.mockClear()
}
}
return electronAPI
}
/**
* Creates a mock for the IPC Renderer
*/
export function createIpcRendererMock(): MockedIpcRenderer {
return {
invoke: vi.fn().mockImplementation(async (channel: string, ...args: any[]) => {
if (channel === 'check-tool-installed') {
return {
success: true,
isInstalled: true // This can be controlled via the electronAPI mock
}
}
return { success: false, error: 'Unknown channel' }
}),
on: vi.fn(),
removeAllListeners: vi.fn(),
}
}
/**
* Test utility to set up all Electron mocks
*/
export function setupElectronMocks() {
const electronAPI = createElectronAPIMock()
const ipcRenderer = createIpcRendererMock()
// Set up global mocks
Object.defineProperty(window, 'electronAPI', {
value: electronAPI,
writable: true
})
Object.defineProperty(window, 'ipcRenderer', {
value: ipcRenderer,
writable: true
})
return { electronAPI, ipcRenderer }
}
/**
* Predefined test scenarios
*/
export const TestScenarios = {
/**
* Fresh installation - no venv, no version file
*/
freshInstall: (electronAPI: MockedElectronAPI) => {
electronAPI.mockState.venvExists = false
electronAPI.mockState.versionFileExists = false
electronAPI.mockState.installedLockExists = false
electronAPI.mockState.toolInstalled = false
},
/**
* Version update scenario - version file exists but version changed
*/
versionUpdate: (electronAPI: MockedElectronAPI) => {
electronAPI.mockState.versionFileExists = true
electronAPI.mockState.savedVersion = '0.9.0'
electronAPI.mockState.currentVersion = '1.0.0'
electronAPI.mockState.installedLockExists = false
},
/**
* Venv removed scenario - version file exists but .venv is missing
*/
venvRemoved: (electronAPI: MockedElectronAPI) => {
electronAPI.mockState.venvExists = false
electronAPI.mockState.versionFileExists = true
electronAPI.mockState.installedLockExists = false
},
/**
* Installation in progress - when user opens app during installation
*/
installationInProgress: (electronAPI: MockedElectronAPI) => {
electronAPI.mockState.isInstalling = true
electronAPI.mockState.installedLockExists = false
},
/**
* Installation error scenario
*/
installationError: (electronAPI: MockedElectronAPI) => {
electronAPI.checkAndInstallDepsOnUpdate.mockImplementation(async () => {
electronAPI.simulateInstallationStart()
setTimeout(() => {
electronAPI.simulateInstallationLog('stderr', 'Error: Failed to resolve dependencies')
electronAPI.simulateInstallationComplete(false, 'Installation failed')
}, 100)
return { success: false, message: 'Installation failed' }
})
},
/**
* Uvicorn startup with dependency installation
*/
uvicornDepsInstall: (electronAPI: MockedElectronAPI) => {
electronAPI.mockState.uvicornStarting = true
electronAPI.mockState.isInstalling = false
// The simulateUvicornStartup method will handle the rest
},
/**
* All good - no installation needed
*/
allGood: (electronAPI: MockedElectronAPI) => {
electronAPI.mockState.venvExists = true
electronAPI.mockState.versionFileExists = true
electronAPI.mockState.savedVersion = electronAPI.mockState.currentVersion
electronAPI.mockState.installedLockExists = true
electronAPI.mockState.isInstalling = false
electronAPI.mockState.toolInstalled = true
}
}

View file

@ -0,0 +1,687 @@
import { vi } from 'vitest'
/**
* Environment state management for testing installation flows
* This module provides utilities to simulate different system states
*/
export interface MockEnvironmentState {
filesystem: {
venvExists: boolean
versionFileExists: boolean
versionFileContent: string
installingLockExists: boolean
installedLockExists: boolean
backendPathExists: boolean
pyprojectExists: boolean
// New fields for process.ts functions
eigentDirExists: boolean
eigentBinDirExists: boolean
eigentCacheDirExists: boolean
eigentVenvsDirExists: boolean
eigentRuntimeDirExists: boolean
resourcesDirExists: boolean
binariesExist: { [name: string]: boolean }
oldVenvsExist: string[] // List of old venv directories that exist
}
processes: {
uvAvailable: boolean
bunAvailable: boolean
uvicornRunning: boolean
uvSyncInProgress: boolean
installationInProgress: boolean
}
app: {
currentVersion: string
userData: string
appPath: string
isPackaged: boolean
resourcesPath: string
}
system: {
platform: 'win32' | 'darwin' | 'linux'
homedir: string
}
network: {
canConnectToMirror: boolean
canConnectToDefault: boolean
}
}
/**
* Mock implementations for Node.js fs module
*/
export function createFileSystemMock() {
const mockState: MockEnvironmentState = {
filesystem: {
venvExists: true,
versionFileExists: true,
versionFileContent: '1.0.0',
installingLockExists: false,
installedLockExists: true,
backendPathExists: true,
pyprojectExists: true,
eigentDirExists: true,
eigentBinDirExists: true,
eigentCacheDirExists: true,
eigentVenvsDirExists: true,
eigentRuntimeDirExists: true,
resourcesDirExists: true,
binariesExist: { 'uv': true, 'bun': true },
oldVenvsExist: []
},
processes: {
uvAvailable: true,
bunAvailable: true,
uvicornRunning: false,
uvSyncInProgress: false,
installationInProgress: false,
},
app: {
currentVersion: '1.0.0',
userData: '/mock/user/data',
appPath: '/mock/app/path',
isPackaged: false,
resourcesPath: '/mock/resources/path'
},
system: {
platform: 'win32',
homedir: '/mock/home'
},
network: {
canConnectToMirror: true,
canConnectToDefault: true,
}
}
const fsMock = {
existsSync: vi.fn().mockImplementation((path: string) => {
if (!path || typeof path !== 'string') return false
if (path.includes('version.txt')) return mockState.filesystem.versionFileExists
if (path.includes('uv_installing.lock')) return mockState.filesystem.installingLockExists
if (path.includes('uv_installed.lock')) return mockState.filesystem.installedLockExists
if (path.includes('.venv')) return mockState.filesystem.venvExists
if (path.includes('backend')) return mockState.filesystem.backendPathExists
if (path.includes('pyproject.toml')) return mockState.filesystem.pyprojectExists
if (path.includes('.eigent/bin') || path.includes('.eigent\\bin')) return mockState.filesystem.eigentBinDirExists
if (path.includes('.eigent/cache') || path.includes('.eigent\\cache')) return mockState.filesystem.eigentCacheDirExists
if (path.includes('.eigent/venvs') || path.includes('.eigent\\venvs')) return mockState.filesystem.eigentVenvsDirExists
if (path.includes('.eigent/runtime') || path.includes('.eigent\\runtime')) return mockState.filesystem.eigentRuntimeDirExists
if (path.includes('.eigent') && !path.includes('bin') && !path.includes('cache') && !path.includes('venvs') && !path.includes('runtime')) {
return mockState.filesystem.eigentDirExists
}
if (path.includes('resources')) return mockState.filesystem.resourcesDirExists
// Check for specific binaries
for (const [name, exists] of Object.entries(mockState.filesystem.binariesExist)) {
if (path.includes(name + '.exe') || path.endsWith(name)) {
return exists
}
}
// Check for old venv directories
for (const oldVenv of mockState.filesystem.oldVenvsExist) {
if (path.includes(oldVenv)) return true
}
return true
}),
readFileSync: vi.fn().mockImplementation((path: string, encoding?: string) => {
if (!path || typeof path !== 'string') return ''
if (path.includes('version.txt')) {
return mockState.filesystem.versionFileContent
}
if (path.includes('pyproject.toml')) {
return `
[project]
name = "backend"
version = "1.0.0"
dependencies = ["fastapi", "uvicorn"]
`
}
return ''
}),
writeFileSync: vi.fn().mockImplementation((path: string, content: string) => {
if (!path || typeof path !== 'string') return
if (path.includes('version.txt')) {
mockState.filesystem.versionFileContent = content
mockState.filesystem.versionFileExists = true
} else if (path.includes('uv_installing.lock')) {
mockState.filesystem.installingLockExists = true
} else if (path.includes('uv_installed.lock')) {
mockState.filesystem.installedLockExists = true
}
}),
unlinkSync: vi.fn().mockImplementation((path: string) => {
if (!path || typeof path !== 'string') return
if (path.includes('uv_installing.lock')) {
mockState.filesystem.installingLockExists = false
} else if (path.includes('uv_installed.lock')) {
mockState.filesystem.installedLockExists = false
} else if (path.includes('version.txt')) {
mockState.filesystem.versionFileExists = false
}
}),
mkdirSync: vi.fn().mockImplementation((path: string, options?: any) => {
if (!path || typeof path !== 'string') return
if (path.includes('backend')) {
mockState.filesystem.backendPathExists = true
} else if (path.includes('.eigent/bin') || path.includes('.eigent\\bin')) {
mockState.filesystem.eigentBinDirExists = true
} else if (path.includes('.eigent/cache') || path.includes('.eigent\\cache')) {
mockState.filesystem.eigentCacheDirExists = true
} else if (path.includes('.eigent/venvs') || path.includes('.eigent\\venvs')) {
mockState.filesystem.eigentVenvsDirExists = true
} else if (path.includes('.eigent/runtime') || path.includes('.eigent\\runtime')) {
mockState.filesystem.eigentRuntimeDirExists = true
} else if (path.includes('.eigent')) {
mockState.filesystem.eigentDirExists = true
}
}),
rmSync: vi.fn().mockImplementation((path: string, options?: any) => {
if (!path || typeof path !== 'string') return
// Handle cleanup of old venvs
for (let i = 0; i < mockState.filesystem.oldVenvsExist.length; i++) {
if (path.includes(mockState.filesystem.oldVenvsExist[i])) {
mockState.filesystem.oldVenvsExist.splice(i, 1)
break
}
}
}),
readdirSync: vi.fn().mockImplementation((path: string, options?: any) => {
if (!path || typeof path !== 'string') return []
if (path.includes('.eigent/venvs')) {
// Return old venv directories for cleanup testing
return mockState.filesystem.oldVenvsExist.map(venv => ({
name: venv,
isDirectory: () => true
}))
}
return []
}),
// State control methods
mockState,
reset: () => {
Object.assign(mockState, {
filesystem: {
venvExists: true,
versionFileExists: true,
versionFileContent: '1.0.0',
installingLockExists: false,
installedLockExists: true,
backendPathExists: true,
pyprojectExists: true,
eigentDirExists: true,
eigentBinDirExists: true,
eigentCacheDirExists: true,
eigentVenvsDirExists: true,
eigentRuntimeDirExists: true,
resourcesDirExists: true,
binariesExist: { 'uv': true, 'bun': true },
oldVenvsExist: []
},
processes: {
uvAvailable: true,
bunAvailable: true,
uvicornRunning: false,
uvSyncInProgress: false,
installationInProgress: false,
},
app: {
currentVersion: '1.0.0',
userData: '/mock/user/data',
appPath: '/mock/app/path',
isPackaged: false,
resourcesPath: '/mock/resources/path'
},
system: {
platform: 'win32',
homedir: '/mock/home'
},
network: {
canConnectToMirror: true,
canConnectToDefault: true,
}
})
}
}
return fsMock
}
/**
* Mock implementations for child_process spawn
*/
export function createProcessMock() {
const processMock = {
spawn: vi.fn(),
mockState: {} as MockEnvironmentState,
setupSpawnMock: (mockState: MockEnvironmentState) => {
processMock.mockState = mockState
processMock.spawn.mockImplementation((command: string, args: string[], options: any) => {
// Mock process events
const mockProcess = {
stdout: {
on: vi.fn().mockImplementation((event: string, callback: (data: Buffer) => void) => {
if (event === 'data') {
// Simulate different process outputs based on command
setTimeout(() => {
if (command.includes('uv') && args.includes('sync')) {
mockState.processes.uvSyncInProgress = true
callback(Buffer.from('Resolved 10 packages in 1.2s\n'))
setTimeout(() => {
callback(Buffer.from('Installing packages...\n'))
setTimeout(() => {
callback(Buffer.from('Installation complete\n'))
mockState.processes.uvSyncInProgress = false
}, 100)
}, 50)
} else if (command.includes('uvicorn')) {
mockState.processes.uvicornRunning = true
callback(Buffer.from('Uvicorn running on http://127.0.0.1:8000\n'))
}
}, 10)
}
})
},
stderr: {
on: vi.fn().mockImplementation((event: string, callback: (data: Buffer) => void) => {
if (event === 'data') {
// Simulate error scenarios
if (!mockState.processes.uvAvailable && command.includes('uv')) {
setTimeout(() => {
callback(Buffer.from('uv: command not found\n'))
}, 10)
}
}
})
},
on: vi.fn().mockImplementation((event: string, callback: (code: number) => void) => {
if (event === 'close') {
setTimeout(() => {
if (command.includes('uv') && args.includes('sync')) {
const exitCode = mockState.processes.uvAvailable &&
mockState.network.canConnectToDefault ? 0 : 1
callback(exitCode)
} else {
callback(0)
}
}, 150)
}
}),
kill: vi.fn()
}
return mockProcess
})
},
reset: () => {
processMock.spawn.mockReset()
}
}
return processMock
}
/**
* Mock for Electron app module
*/
export function createElectronAppMock() {
const appMock = {
getVersion: vi.fn(),
getPath: vi.fn(),
getAppPath: vi.fn(),
isPackaged: false,
mockState: {} as MockEnvironmentState,
setup: (mockState: MockEnvironmentState) => {
appMock.mockState = mockState
appMock.getVersion.mockReturnValue(mockState.app.currentVersion)
appMock.getAppPath.mockReturnValue(mockState.app.appPath)
appMock.isPackaged = mockState.app.isPackaged
appMock.getPath.mockImplementation((name: string) => {
if (name === 'userData') return mockState.app.userData
return '/mock/path'
})
// Mock process.resourcesPath for packaged apps
if (mockState.app.isPackaged) {
Object.defineProperty(process, 'resourcesPath', {
value: mockState.app.resourcesPath,
configurable: true
})
}
},
reset: () => {
appMock.getVersion.mockReset()
appMock.getPath.mockReset()
appMock.getAppPath.mockReset()
}
}
return appMock
}
/**
* Mock for OS module
*/
export function createOsMock() {
const osMock = {
homedir: vi.fn().mockReturnValue('/mock/home'),
mockState: {} as MockEnvironmentState,
setup: (mockState: MockEnvironmentState) => {
osMock.mockState = mockState
osMock.homedir.mockReturnValue(mockState.system.homedir || '/mock/home')
},
reset: () => {
osMock.homedir.mockReset()
osMock.homedir.mockReturnValue('/mock/home')
}
}
return osMock
}
/**
* Mock for path module
*/
export function createPathMock() {
return {
join: vi.fn((...args) => {
const validArgs = args.filter(arg => arg != null && arg !== undefined && arg !== '')
return validArgs.length > 0 ? validArgs.join(process.platform === 'win32' ? '\\' : '/') : ''
}),
resolve: vi.fn((...args) => {
const validArgs = args.filter(arg => arg != null && arg !== undefined && arg !== '')
return validArgs.length > 0 ? validArgs.join(process.platform === 'win32' ? '\\' : '/') : ''
}),
dirname: vi.fn((path: string) => {
if (!path || typeof path !== 'string') return ''
const parts = path.split(process.platform === 'win32' ? '\\' : '/')
return parts.slice(0, -1).join(process.platform === 'win32' ? '\\' : '/')
})
}
}
/**
* Mock for process utilities from electron/main/utils/process.ts
*/
export function createProcessUtilsMock() {
const utilsMock = {
getResourcePath: vi.fn(),
getBackendPath: vi.fn(),
runInstallScript: vi.fn(),
getBinaryName: vi.fn(),
getBinaryPath: vi.fn(),
getCachePath: vi.fn(),
getVenvPath: vi.fn(),
getVenvsBaseDir: vi.fn(),
cleanupOldVenvs: vi.fn(),
isBinaryExists: vi.fn(),
mockState: {} as MockEnvironmentState,
setup: (mockState: MockEnvironmentState) => {
utilsMock.mockState = mockState
utilsMock.getResourcePath.mockReturnValue(
`${mockState.app.appPath}/resources`
)
utilsMock.getBackendPath.mockReturnValue(
mockState.app.isPackaged
? `${mockState.app.resourcesPath}/backend`
: `${mockState.app.appPath}/backend`
)
utilsMock.runInstallScript.mockImplementation(async (scriptPath: string) => {
// Simulate successful script execution and update binary state
if (scriptPath.includes('install-uv')) {
mockState.filesystem.binariesExist['uv'] = true
mockState.processes.uvAvailable = true
} else if (scriptPath.includes('install-bun')) {
mockState.filesystem.binariesExist['bun'] = true
mockState.processes.bunAvailable = true
}
return true
})
utilsMock.getBinaryName.mockImplementation(async (name: string) => {
return mockState.system.platform === 'win32' ? `${name}.exe` : name
})
utilsMock.getBinaryPath.mockImplementation(async (name?: string) => {
const binDir = `${mockState.system.homedir}/.eigent/bin`
if (!name) return binDir
const binaryName = mockState.system.platform === 'win32' ? `${name}.exe` : name
return `${binDir}/${binaryName}`
})
utilsMock.getCachePath.mockImplementation((folder: string) => {
return `${mockState.system.homedir}/.eigent/cache/${folder}`
})
utilsMock.getVenvPath.mockImplementation((version: string) => {
return `${mockState.system.homedir}/.eigent/venvs/backend-${version}`
})
utilsMock.getVenvsBaseDir.mockReturnValue(
`${mockState.system.homedir}/.eigent/venvs`
)
utilsMock.cleanupOldVenvs.mockImplementation(async (currentVersion: string) => {
// Simulate cleanup by removing old venvs from mock state
mockState.filesystem.oldVenvsExist = mockState.filesystem.oldVenvsExist.filter(
venv => venv.includes(`backend-${currentVersion}`)
)
})
utilsMock.isBinaryExists.mockImplementation(async (name: string) => {
return mockState.filesystem.binariesExist[name] || false
})
},
reset: () => {
Object.values(utilsMock).forEach(fn => {
if (typeof fn === 'function' && 'mockReset' in fn) {
fn.mockReset()
}
})
}
}
return utilsMock
}
/**
* Mock for electron-log
*/
export function createLogMock() {
return {
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
}
}
/**
* Complete environment setup for testing
* Note: vi.mock calls should be done at the top level of test files, not here
*/
export function setupMockEnvironment() {
const fsMock = createFileSystemMock()
const processMock = createProcessMock()
const appMock = createElectronAppMock()
const osMock = createOsMock()
const pathMock = createPathMock()
const processUtilsMock = createProcessUtilsMock()
const logMock = createLogMock()
// Set up the shared state
processMock.setupSpawnMock(fsMock.mockState)
appMock.setup(fsMock.mockState)
osMock.setup(fsMock.mockState)
processUtilsMock.setup(fsMock.mockState)
return {
fsMock,
processMock,
appMock,
osMock,
pathMock,
processUtilsMock,
logMock,
mockState: fsMock.mockState,
// Utility functions for test scenarios
scenarios: {
freshInstall: () => {
fsMock.mockState.filesystem.venvExists = false
fsMock.mockState.filesystem.versionFileExists = false
fsMock.mockState.filesystem.installedLockExists = false
fsMock.mockState.filesystem.binariesExist = { 'uv': false, 'bun': false }
fsMock.mockState.processes.uvAvailable = false
fsMock.mockState.processes.bunAvailable = false
},
versionUpdate: (oldVersion: string, newVersion: string) => {
fsMock.mockState.filesystem.versionFileContent = oldVersion
fsMock.mockState.app.currentVersion = newVersion
appMock.getVersion.mockReturnValue(newVersion)
},
venvRemoved: () => {
fsMock.mockState.filesystem.venvExists = false
fsMock.mockState.filesystem.installedLockExists = false
},
networkIssues: () => {
fsMock.mockState.network.canConnectToDefault = false
fsMock.mockState.network.canConnectToMirror = true
},
completeFailure: () => {
fsMock.mockState.network.canConnectToDefault = false
fsMock.mockState.network.canConnectToMirror = false
fsMock.mockState.processes.uvAvailable = false
fsMock.mockState.filesystem.binariesExist = { 'uv': false, 'bun': false }
// Note: installCommandTool is defined in the install-deps module,
// not in process utils, so it should be mocked in the test itself
},
uvicornStartupInstall: () => {
fsMock.mockState.processes.uvicornRunning = false
fsMock.mockState.filesystem.installedLockExists = false
// Uvicorn will detect missing deps and start installation
},
installationInProgress: () => {
fsMock.mockState.filesystem.installingLockExists = true
fsMock.mockState.processes.installationInProgress = true
},
// New scenarios for process.ts testing
packagedApp: () => {
fsMock.mockState.app.isPackaged = true
appMock.isPackaged = true
},
multipleOldVenvs: (currentVersion: string) => {
fsMock.mockState.filesystem.oldVenvsExist = [
'backend-0.9.0',
'backend-0.9.5',
'backend-1.0.1-beta',
`backend-${currentVersion}` // This should not be cleaned up
]
},
macOSEnvironment: () => {
fsMock.mockState.system.platform = 'darwin'
Object.defineProperty(process, 'platform', { value: 'darwin', configurable: true })
},
linuxEnvironment: () => {
fsMock.mockState.system.platform = 'linux'
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true })
},
missingEigentDirectories: () => {
fsMock.mockState.filesystem.eigentDirExists = false
fsMock.mockState.filesystem.eigentBinDirExists = false
fsMock.mockState.filesystem.eigentCacheDirExists = false
fsMock.mockState.filesystem.eigentVenvsDirExists = false
fsMock.mockState.filesystem.eigentRuntimeDirExists = false
}
},
reset: () => {
fsMock.reset()
processMock.reset()
appMock.reset()
osMock.reset()
processUtilsMock.reset()
// Reset process.platform to original
Object.defineProperty(process, 'platform', {
value: 'win32',
configurable: true
})
}
}
}
/**
* Factory functions for creating mocks that can be used in vi.mock calls
* These should be called at the top level of test files
*/
export function createMockFactories() {
return {
fs: () => createFileSystemMock(),
childProcess: () => createProcessMock(),
os: () => ({ default: createOsMock() }),
path: () => ({ default: createPathMock() }),
electron: () => ({
app: createElectronAppMock(),
BrowserWindow: vi.fn()
}),
electronLog: () => ({ default: createLogMock() }),
processUtils: () => createProcessUtilsMock()
}
}
/**
* Test utility to wait for async state changes
*/
export function waitForStateChange<T>(
stateGetter: () => T,
expectedValue: T,
timeout: number = 1000
): Promise<void> {
return new Promise((resolve, reject) => {
const startTime = Date.now()
const check = () => {
if (stateGetter() === expectedValue) {
resolve()
} else if (Date.now() - startTime > timeout) {
reject(new Error(`Timeout waiting for state change. Expected: ${expectedValue}, got: ${stateGetter()}`))
} else {
setTimeout(check, 10)
}
}
check()
})
}

282
test/mocks/testUtils.ts Normal file
View file

@ -0,0 +1,282 @@
import { setupElectronMocks, TestScenarios, type MockedElectronAPI } from './electronMocks'
import { setupMockEnvironment } from './environmentMocks'
/**
* Complete test setup utility that combines all mocks and provides
* easy-to-use functions for testing installation flows
*/
export function createTestEnvironment() {
const { electronAPI, ipcRenderer } = setupElectronMocks()
const mockEnv = setupMockEnvironment()
return {
electronAPI,
ipcRenderer,
mockEnv,
// Quick scenario setups
scenarios: {
/**
* Fresh installation - no .venv, no version file, tools not installed
*/
freshInstall: () => {
TestScenarios.freshInstall(electronAPI)
mockEnv.scenarios.freshInstall()
},
/**
* Version update - version file exists but version changed
*/
versionUpdate: (oldVersion: string = '0.9.0', newVersion: string = '1.0.0') => {
TestScenarios.versionUpdate(electronAPI)
mockEnv.scenarios.versionUpdate(oldVersion, newVersion)
},
/**
* .venv removed - version file exists but .venv is missing
*/
venvRemoved: () => {
TestScenarios.venvRemoved(electronAPI)
mockEnv.scenarios.venvRemoved()
},
/**
* Installation in progress - when user opens app during installation
*/
installationInProgress: () => {
TestScenarios.installationInProgress(electronAPI)
mockEnv.scenarios.installationInProgress()
},
/**
* Installation error - installation fails
*/
installationError: () => {
TestScenarios.installationError(electronAPI)
mockEnv.scenarios.completeFailure()
},
/**
* Uvicorn startup with dependency installation
*/
uvicornDepsInstall: () => {
TestScenarios.uvicornDepsInstall(electronAPI)
mockEnv.scenarios.uvicornStartupInstall()
},
/**
* Network issues - default mirror fails, backup succeeds
*/
networkIssues: () => {
TestScenarios.allGood(electronAPI)
mockEnv.scenarios.networkIssues()
},
/**
* All good - no installation needed
*/
allGood: () => {
TestScenarios.allGood(electronAPI)
// Use default mockEnv state (all good)
}
},
// Simulation utilities
simulate: {
/**
* Simulate a complete successful installation flow
*/
successfulInstallation: async (delay: number = 100) => {
electronAPI.simulateInstallationStart()
setTimeout(() => {
electronAPI.simulateInstallationLog('stdout', 'Resolving dependencies...')
}, delay)
setTimeout(() => {
electronAPI.simulateInstallationLog('stdout', 'Downloading packages...')
}, delay * 2)
setTimeout(() => {
electronAPI.simulateInstallationLog('stdout', 'Installing packages...')
}, delay * 3)
setTimeout(() => {
electronAPI.simulateInstallationComplete(true)
}, delay * 4)
},
/**
* Simulate a failed installation flow
*/
failedInstallation: async (delay: number = 100, errorMessage: string = 'Installation failed') => {
electronAPI.simulateInstallationStart()
setTimeout(() => {
electronAPI.simulateInstallationLog('stdout', 'Resolving dependencies...')
}, delay)
setTimeout(() => {
electronAPI.simulateInstallationLog('stderr', `Error: ${errorMessage}`)
}, delay * 2)
setTimeout(() => {
electronAPI.simulateInstallationComplete(false, errorMessage)
}, delay * 3)
},
/**
* Simulate uvicorn startup that detects missing dependencies
*/
uvicornWithDeps: async (delay: number = 100) => {
setTimeout(() => {
electronAPI.simulateInstallationLog('stdout', 'Uvicorn detected missing dependencies')
}, delay)
setTimeout(() => {
electronAPI.simulateInstallationStart()
}, delay * 2)
setTimeout(() => {
electronAPI.simulateInstallationLog('stdout', 'Resolving dependencies...')
}, delay * 3)
setTimeout(() => {
electronAPI.simulateInstallationLog('stdout', 'Uvicorn running on http://127.0.0.1:8000')
electronAPI.simulateInstallationComplete(true)
}, delay * 4)
}
},
// State inspection utilities
inspect: {
/**
* Get current installation state summary
*/
getInstallationState: () => ({
electronState: electronAPI.mockState,
envState: mockEnv.mockState,
isInstalling: electronAPI.mockState.isInstalling || mockEnv.mockState.processes.installationInProgress,
hasLockFiles: mockEnv.mockState.filesystem.installingLockExists || mockEnv.mockState.filesystem.installedLockExists,
toolsAvailable: mockEnv.mockState.processes.uvAvailable && mockEnv.mockState.processes.bunAvailable,
venvExists: electronAPI.mockState.venvExists && mockEnv.mockState.filesystem.venvExists,
}),
/**
* Check if environment is in expected state for a scenario
*/
verifyScenario: (scenarioName: string) => {
const state = mockEnv.mockState
const electronState = electronAPI.mockState
switch (scenarioName) {
case 'freshInstall':
return !state.filesystem.venvExists &&
!state.filesystem.versionFileExists &&
!electronState.toolInstalled
case 'versionUpdate':
return state.filesystem.versionFileExists &&
state.app.currentVersion !== state.filesystem.versionFileContent
case 'venvRemoved':
return !state.filesystem.venvExists &&
state.filesystem.versionFileExists
case 'installationInProgress':
return state.filesystem.installingLockExists ||
electronState.isInstalling
default:
return false
}
}
},
// Reset everything
reset: () => {
electronAPI.reset()
mockEnv.reset()
}
}
}
/**
* Helper function to wait for installation state changes
*/
export async function waitForInstallationState(
getState: () => any,
expectedState: string,
timeout: number = 1000
): Promise<void> {
return new Promise((resolve, reject) => {
const startTime = Date.now()
const check = () => {
if (getState().state === expectedState) {
resolve()
} else if (Date.now() - startTime > timeout) {
reject(new Error(`Timeout waiting for state ${expectedState}, current: ${getState().state}`))
} else {
setTimeout(check, 10)
}
}
check()
})
}
/**
* Helper function to wait for multiple logs
*/
export async function waitForLogs(
getLogs: () => any[],
expectedCount: number,
timeout: number = 1000
): Promise<void> {
return new Promise((resolve, reject) => {
const startTime = Date.now()
const check = () => {
if (getLogs().length >= expectedCount) {
resolve()
} else if (Date.now() - startTime > timeout) {
reject(new Error(`Timeout waiting for ${expectedCount} logs, got: ${getLogs().length}`))
} else {
setTimeout(check, 10)
}
}
check()
})
}
/**
* Example usage in tests:
*
* ```typescript
* import { createTestEnvironment, waitForInstallationState } from '../mocks/testUtils'
*
* describe('Installation Flow', () => {
* let testEnv: ReturnType<typeof createTestEnvironment>
*
* beforeEach(() => {
* testEnv = createTestEnvironment()
* })
*
* it('should handle fresh installation', async () => {
* testEnv.scenarios.freshInstall()
*
* // Verify scenario setup
* expect(testEnv.inspect.verifyScenario('freshInstall')).toBe(true)
*
* // Simulate installation
* await testEnv.simulate.successfulInstallation()
*
* // Verify result
* const state = testEnv.inspect.getInstallationState()
* expect(state.isInstalling).toBe(false)
* })
* })
* ```
*/

View file

@ -2,11 +2,48 @@
import { vi } from 'vitest'
import '@testing-library/jest-dom'
// Mock react-i18next
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => {
// Map translation keys to English text
const translations: Record<string, string> = {
'chat.welcome-to-eigent': 'Welcome to Eigent',
'chat.how-can-i-help-you': 'How can I help you today?',
'chat.palm-springs-tennis-trip-planner': 'Palm Springs Tennis Trip Planner',
'chat.bank-transfer-csv-analysis-and-visualization': 'Bank Transfer CSV Analysis and Visualization',
'chat.find-duplicate-files-in-downloads-folder': 'Find Duplicate Files in Downloads Folder',
'setting.search-mcp': 'Search MCPs',
'chat.by-messaging-eigent': 'By messaging Eigent, you agree to our',
'chat.terms-of-use': 'Terms of Use',
'chat.and': 'and',
'chat.privacy-policy': 'Privacy Policy',
'chat.palm-springs-tennis-trip-planner-message': 'Plan a tennis trip to Palm Springs',
'chat.bank-transfer-csv-analysis-and-visualization-message': 'Analyze and visualize bank transfer CSV',
'chat.find-duplicate-files-in-downloads-folder-message': 'Find duplicate files in Downloads folder',
'chat.no-reply-received-task-continue': 'No reply received, task will continue',
}
return translations[key] || key
},
i18n: {
language: 'en',
changeLanguage: vi.fn(),
},
}),
}))
// Mock Electron APIs if needed
global.electronAPI = {
// Add mock implementations for electron preload APIs
}
// Mock ipcRenderer
global.ipcRenderer = {
invoke: vi.fn(),
on: vi.fn(),
removeAllListeners: vi.fn(),
}
// Mock environment variables
process.env.NODE_ENV = 'test'
@ -22,6 +59,13 @@ global.waitFor = async (callback: () => boolean, timeout = 5000) => {
throw new Error(`Timeout waiting for condition after ${timeout}ms`)
}
// Add type declarations for globals
declare global {
var electronAPI: any
var ipcRenderer: any
var waitFor: (callback: () => boolean, timeout?: number) => Promise<void>
}
// Setup DOM environment
Object.defineProperty(window, 'matchMedia', {
writable: true,

View file

@ -737,11 +737,21 @@ describe('ChatBox Component', () => {
})
it('should handle privacy fetch errors', async () => {
// Mock console.error to suppress expected error logs
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
// Mock the fetch to reject properly for testing error handling
mockProxyFetchGet.mockRejectedValue(new Error('Privacy fetch failed'))
// Rendering should not throw even with fetch error
expect(() => renderChatBox()).not.toThrow()
// Wait for the promise to settle
await waitFor(() => {
expect(consoleErrorSpy).toHaveBeenCalled()
})
consoleErrorSpy.mockRestore()
})
})
})

View file

@ -0,0 +1,609 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { setupMockEnvironment } from '../../mocks/environmentMocks'
// Set up global mock environment before any imports
const globalMockEnv = setupMockEnvironment()
// Mock all dependencies at the top level
vi.mock('node:fs', () => ({
...globalMockEnv.fsMock,
default: globalMockEnv.fsMock
}))
vi.mock('node:path', () => ({
...globalMockEnv.pathMock,
default: globalMockEnv.pathMock
}))
vi.mock('child_process', () => ({
...globalMockEnv.processMock,
default: globalMockEnv.processMock
}))
vi.mock('electron-log', () => ({ default: globalMockEnv.logMock }))
vi.mock('electron', () => ({
app: globalMockEnv.appMock,
BrowserWindow: vi.fn()
}))
vi.mock('../../../electron/main/utils/process', () => globalMockEnv.processUtilsMock)
vi.mock('../../../electron/main/init', () => ({
getMainWindow: vi.fn().mockReturnValue({
webContents: { send: vi.fn() },
isDestroyed: vi.fn().mockReturnValue(false)
})
}))
vi.mock('../../../electron/main/utils/safeWebContentsSend', () => ({
safeMainWindowSend: vi.fn().mockReturnValue(true)
}))
// Import the module under test after mocking
let installDeps: any
describe('Install Deps Module', () => {
let mockEnv: ReturnType<typeof setupMockEnvironment>
beforeEach(async () => {
// Reset the mock environment state for each test
mockEnv = globalMockEnv
mockEnv.reset()
// Set up the shared state
mockEnv.processMock.setupSpawnMock(mockEnv.mockState)
mockEnv.appMock.setup(mockEnv.mockState)
mockEnv.osMock.setup(mockEnv.mockState)
mockEnv.processUtilsMock.setup(mockEnv.mockState)
// Import the module under test fresh for each test
installDeps = await import('../../../electron/main/install-deps')
})
afterEach(() => {
vi.clearAllMocks()
mockEnv.reset()
})
describe('checkAndInstallDepsOnUpdate', () => {
it('should skip installation when version has not changed and tools are installed', async () => {
// Set up scenario where version is the same and tools exist
mockEnv.mockState.filesystem.versionFileExists = true
mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
mockEnv.mockState.app.currentVersion = '1.0.0'
mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': true }
const mockWin = {
webContents: { send: vi.fn() },
isDestroyed: vi.fn().mockReturnValue(false)
}
const result = await installDeps.checkAndInstallDepsOnUpdate({
win: mockWin,
forceInstall: false
})
expect(result.success).toBe(true)
expect(result.message).toContain('Version not changed')
expect(mockEnv.fsMock.writeFileSync).not.toHaveBeenCalledWith(
expect.stringContaining('version.txt'),
expect.any(String)
)
})
it('should install dependencies when version file does not exist', async () => {
// Set up fresh installation scenario
mockEnv.scenarios.freshInstall()
const mockWin = {
webContents: { send: vi.fn() },
isDestroyed: vi.fn().mockReturnValue(false)
}
const result = await installDeps.checkAndInstallDepsOnUpdate({
win: mockWin,
forceInstall: false
})
console.log(result);
expect(result.success).toBe(true)
expect(result.message).toContain('Dependencies installed successfully')
expect(mockWin.webContents.send).toHaveBeenCalledWith(
'update-notification',
expect.objectContaining({
type: 'version-update',
reason: 'version file not exist'
})
)
})
it('should install dependencies when version has changed', async () => {
// Set up version update scenario
mockEnv.scenarios.versionUpdate('0.9.0', '1.0.0')
const mockWin = {
webContents: { send: vi.fn() },
isDestroyed: vi.fn().mockReturnValue(false)
}
const result = await installDeps.checkAndInstallDepsOnUpdate({
win: mockWin,
forceInstall: false
})
expect(result.success).toBe(true)
expect(result.message).toContain('Dependencies installed successfully')
expect(mockWin.webContents.send).toHaveBeenCalledWith(
'update-notification',
expect.objectContaining({
type: 'version-update',
currentVersion: '1.0.0',
previousVersion: '0.9.0',
reason: 'version not match'
})
)
})
it('should install when command tools are missing', async () => {
// Same version but tools missing
mockEnv.mockState.filesystem.versionFileExists = true
mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
mockEnv.mockState.app.currentVersion = '1.0.0'
mockEnv.mockState.filesystem.binariesExist = { 'uv': false, 'bun': true }
const mockWin = {
webContents: { send: vi.fn() },
isDestroyed: vi.fn().mockReturnValue(false)
}
const result = await installDeps.checkAndInstallDepsOnUpdate({
win: mockWin,
forceInstall: false
})
expect(result.success).toBe(true)
expect(result.message).toContain('Dependencies installed successfully')
})
it('should force install when forceInstall is true', async () => {
// Set up scenario where normally no installation would be needed
mockEnv.mockState.filesystem.versionFileExists = true
mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
mockEnv.mockState.app.currentVersion = '1.0.0'
mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': true }
const mockWin = {
webContents: { send: vi.fn() },
isDestroyed: vi.fn().mockReturnValue(false)
}
const result = await installDeps.checkAndInstallDepsOnUpdate({
win: mockWin,
forceInstall: true
})
expect(result.success).toBe(true)
expect(result.message).toContain('Dependencies installed successfully')
})
it('should handle installation failure', async () => {
// Set up failure scenario
mockEnv.scenarios.completeFailure()
const mockWin = {
webContents: { send: vi.fn() },
isDestroyed: vi.fn().mockReturnValue(false)
}
const result = await installDeps.checkAndInstallDepsOnUpdate({
win: mockWin,
forceInstall: true
})
expect(result.success).toBe(false)
expect(result.message).toContain('Install dependencies failed')
})
it('should handle window being destroyed', async () => {
const mockWin = {
webContents: { send: vi.fn() },
isDestroyed: vi.fn().mockReturnValue(true)
}
mockEnv.scenarios.versionUpdate('0.9.0', '1.0.0')
const result = await installDeps.checkAndInstallDepsOnUpdate({
win: mockWin,
forceInstall: false
})
// Should still complete successfully
expect(result.success).toBe(true)
// Should not try to send notifications to destroyed window
expect(mockWin.webContents.send).not.toHaveBeenCalled()
})
it('should handle null window gracefully', async () => {
mockEnv.scenarios.versionUpdate('0.9.0', '1.0.0')
const result = await installDeps.checkAndInstallDepsOnUpdate({
win: null,
forceInstall: false
})
expect(result.success).toBe(true)
// Should not crash when window is null
})
})
describe('installCommandTool', () => {
it('should install uv and bun when not available', async () => {
// Set up scenario where tools are not available initially
mockEnv.mockState.filesystem.binariesExist = { 'uv': false, 'bun': false }
// Simulate successful installation
let uvCallCount = 0
let bunCallCount = 0
mockEnv.processUtilsMock.isBinaryExists.mockImplementation(async (name: string) => {
if (name === 'uv') {
uvCallCount++
return uvCallCount > 1 // False first time, true after "installation"
}
if (name === 'bun') {
bunCallCount++
return bunCallCount > 1 // False first time, true after "installation"
}
return false
})
const result = await installDeps.installCommandTool()
expect(result.success).toBe(true)
expect(result.message).toContain('Command tools installed successfully')
expect(mockEnv.processUtilsMock.runInstallScript).toHaveBeenCalledTimes(2) // uv and bun
})
it('should skip installation when tools are already available', async () => {
// Tools are available by default in mockState
mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': true }
const result = await installDeps.installCommandTool()
expect(result.success).toBe(true)
expect(result.message).toContain('Command tools installed successfully')
expect(mockEnv.processUtilsMock.runInstallScript).not.toHaveBeenCalled()
})
it('should handle uv installation failure', async () => {
// Mock uv installation failure
mockEnv.mockState.filesystem.binariesExist = { 'uv': false, 'bun': true }
// uv remains unavailable even after installation attempt
mockEnv.processUtilsMock.isBinaryExists.mockImplementation(async (name: string) => {
if (name === 'uv') return false // Always fails
if (name === 'bun') return true
return false
})
const result = await installDeps.installCommandTool()
expect(result.success).toBe(false)
expect(result.message).toContain('uv installation failed')
})
it('should handle bun installation failure', async () => {
// Mock bun installation failure
mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': false }
// bun remains unavailable even after installation attempt
mockEnv.processUtilsMock.isBinaryExists.mockImplementation(async (name: string) => {
if (name === 'uv') return true
if (name === 'bun') return false // Always fails
return false
})
const result = await installDeps.installCommandTool()
expect(result.success).toBe(false)
expect(result.message).toContain('bun installation failed')
})
})
})
// describe('getInstallationStatus', () => {
// it('should return correct status when installation is in progress', async () => {
// mockEnv.scenarios.installationInProgress()
// const status = await installDeps.getInstallationStatus()
// expect(status.isInstalling).toBe(true)
// expect(status.hasLockFile).toBe(true)
// expect(status.installedExists).toBe(false)
// })
// it('should return correct status when installation is completed', async () => {
// // Default state has installation completed
// mockEnv.mockState.filesystem.installingLockExists = false
// mockEnv.mockState.filesystem.installedLockExists = true
// const status = await installDeps.getInstallationStatus()
// expect(status.isInstalling).toBe(false)
// expect(status.hasLockFile).toBe(true)
// expect(status.installedExists).toBe(true)
// })
// it('should return correct status when no installation has occurred', async () => {
// mockEnv.mockState.filesystem.installingLockExists = false
// mockEnv.mockState.filesystem.installedLockExists = false
// const status = await installDeps.getInstallationStatus()
// expect(status.isInstalling).toBe(false)
// expect(status.hasLockFile).toBe(false)
// expect(status.installedExists).toBe(false)
// })
// it('should handle file system errors gracefully', async () => {
// // Mock fs.existsSync to throw an error
// mockEnv.fsMock.existsSync.mockImplementation(() => {
// throw new Error('File system error')
// })
// const status = await installDeps.getInstallationStatus()
// expect(status.isInstalling).toBe(false)
// expect(status.hasLockFile).toBe(false)
// expect(status.installedExists).toBe(false)
// })
// })
// describe('installDependencies', () => {
// it('should successfully install dependencies with default settings', async () => {
// // Set up successful installation scenario
// mockEnv.mockState.processes.uvAvailable = true
// mockEnv.mockState.network.canConnectToDefault = true
// const result = await installDeps.installDependencies('1.0.0')
// expect(result.success).toBe(true)
// expect(result.message).toContain('Dependencies installed successfully')
// expect(mockEnv.fsMock.writeFileSync).toHaveBeenCalledWith(
// expect.stringContaining('uv_installed.lock'),
// ''
// )
// })
// it('should fall back to mirror when default fails', async () => {
// // Set up network issues scenario - first install fails, mirror succeeds
// mockEnv.scenarios.networkIssues()
// const result = await installDeps.installDependencies('1.0.0')
// expect(result.success).toBe(true)
// expect(result.message).toContain('Dependencies installed successfully with mirror')
// })
// it('should fail when both default and mirror fail', async () => {
// mockEnv.scenarios.completeFailure()
// const result = await installDeps.installDependencies('1.0.0')
// expect(result.success).toBe(false)
// expect(result.message).toContain('Both default and mirror install failed')
// })
// it('should handle command tool installation failure', async () => {
// // Set up scenario where command tool installation fails
// mockEnv.mockState.filesystem.binariesExist = { 'uv': false, 'bun': false }
// // Mock tools to remain unavailable
// mockEnv.processUtilsMock.isBinaryExists.mockResolvedValue(false)
// const result = await installDeps.installDependencies('1.0.0')
// expect(result.success).toBe(false)
// expect(result.message).toContain('Command tool installation failed')
// })
// it('should create and clean up lock files correctly', async () => {
// mockEnv.mockState.processes.uvAvailable = true
// mockEnv.mockState.network.canConnectToDefault = true
// await installDeps.installDependencies('1.0.0')
// // Verify that installation lock is created and then cleaned up
// expect(mockEnv.fsMock.writeFileSync).toHaveBeenCalledWith(
// expect.stringContaining('uv_installing.lock'),
// ''
// )
// expect(mockEnv.fsMock.unlinkSync).toHaveBeenCalledWith(
// expect.stringContaining('uv_installing.lock')
// )
// expect(mockEnv.fsMock.writeFileSync).toHaveBeenCalledWith(
// expect.stringContaining('uv_installed.lock'),
// ''
// )
// })
// it('should clean up old virtual environments after successful installation', async () => {
// mockEnv.scenarios.multipleOldVenvs('1.0.0')
// mockEnv.mockState.processes.uvAvailable = true
// mockEnv.mockState.network.canConnectToDefault = true
// await installDeps.installDependencies('1.0.0')
// expect(mockEnv.processUtilsMock.cleanupOldVenvs).toHaveBeenCalledWith('1.0.0')
// })
// })
// describe('detectInstallationLogs', () => {
// beforeEach(() => {
// // Reset the module-level state variables
// vi.resetModules()
// })
// it('should detect UV dependency installation patterns', () => {
// const installationPatterns = [
// 'Resolved 10 packages in 1.2s',
// 'Downloaded package xyz',
// 'Installing numpy==1.21.0',
// 'Built wheel for package',
// 'Prepared virtual environment',
// 'Syncing dependencies',
// 'Creating virtualenv at .venv',
// 'Updating package index',
// 'Audited 15 packages'
// ]
// installationPatterns.forEach(pattern => {
// // The function has side effects, so we can't easily test return values
// // Instead, we test that it doesn't throw and processes the input
// expect(() => installDeps.detectInstallationLogs(pattern)).not.toThrow()
// })
// })
// it('should handle uvicorn startup messages', () => {
// const uvicornMessages = [
// 'Uvicorn running on http://127.0.0.1:8000',
// 'Application startup complete',
// 'Server started successfully'
// ]
// uvicornMessages.forEach(message => {
// expect(() => installDeps.detectInstallationLogs(message)).not.toThrow()
// })
// })
// it('should handle installation failure messages', () => {
// const failureMessages = [
// '× No solution found when resolving dependencies',
// 'failed to resolve dependencies',
// 'installation failed'
// ]
// failureMessages.forEach(message => {
// expect(() => installDeps.detectInstallationLogs(message)).not.toThrow()
// })
// })
// })
// describe('Error Handling and Edge Cases', () => {
// it('should handle file system permission errors gracefully', async () => {
// // Mock filesystem error
// mockEnv.fsMock.writeFileSync.mockImplementation((path: string) => {
// if (path.includes('version.txt')) {
// throw new Error('Permission denied')
// }
// })
// const mockWin = {
// webContents: { send: vi.fn() },
// isDestroyed: vi.fn().mockReturnValue(false)
// }
// // The function should handle errors gracefully
// const result = await installDeps.checkAndInstallDepsOnUpdate({
// win: mockWin,
// forceInstall: true
// })
// // Should still return a result, even if there are file system errors
// expect(result).toBeDefined()
// expect(typeof result.success).toBe('boolean')
// expect(typeof result.message).toBe('string')
// })
// it('should handle timezone-based mirror selection for China', async () => {
// // Mock Intl.DateTimeFormat for China timezone
// const originalDateTimeFormat = global.Intl.DateTimeFormat
// const mockDateTimeFormat = vi.fn().mockImplementation(() => ({
// resolvedOptions: () => ({ timeZone: 'Asia/Shanghai' })
// })) as any
// global.Intl.DateTimeFormat = mockDateTimeFormat
// try {
// // Set up scenario where default fails but mirror succeeds
// mockEnv.scenarios.networkIssues()
// const result = await installDeps.installDependencies('1.0.0')
// expect(result.success).toBe(true)
// expect(result.message).toContain('Dependencies installed successfully with mirror')
// } finally {
// // Restore original
// global.Intl.DateTimeFormat = originalDateTimeFormat
// }
// })
// it('should handle invalid version strings', async () => {
// const result = await installDeps.installDependencies('')
// // Should handle empty version string gracefully
// expect(result).toBeDefined()
// expect(typeof result.success).toBe('boolean')
// })
// it('should handle missing backend directory', async () => {
// mockEnv.mockState.filesystem.backendPathExists = false
// const result = await installDeps.installDependencies('1.0.0')
// // Should create the directory and continue
// expect(mockEnv.fsMock.mkdirSync).toHaveBeenCalledWith(
// expect.stringContaining('backend'),
// { recursive: true }
// )
// expect(result).toBeDefined()
// })
// })
// describe('Integration Tests', () => {
// it('should handle complete fresh installation workflow', async () => {
// // Set up completely fresh system
// mockEnv.scenarios.freshInstall()
// const mockWin = {
// webContents: { send: vi.fn() },
// isDestroyed: vi.fn().mockReturnValue(false)
// }
// const result = await installDeps.checkAndInstallDepsOnUpdate({
// win: mockWin,
// forceInstall: false
// })
// expect(result.success).toBe(true)
// expect(mockWin.webContents.send).toHaveBeenCalledWith(
// 'update-notification',
// expect.objectContaining({
// type: 'version-update',
// reason: 'version file not exist'
// })
// )
// })
// it('should handle version update with missing tools', async () => {
// // Version file exists but tools are missing
// mockEnv.mockState.filesystem.versionFileExists = true
// mockEnv.mockState.filesystem.versionFileContent = '0.9.0'
// mockEnv.mockState.app.currentVersion = '1.0.0'
// mockEnv.mockState.filesystem.binariesExist = { 'uv': false, 'bun': false }
// const mockWin = {
// webContents: { send: vi.fn() },
// isDestroyed: vi.fn().mockReturnValue(false)
// }
// const result = await installDeps.checkAndInstallDepsOnUpdate({
// win: mockWin,
// forceInstall: false
// })
// expect(result.success).toBe(true)
// expect(mockWin.webContents.send).toHaveBeenCalledWith(
// 'update-notification',
// expect.objectContaining({
// type: 'version-update',
// currentVersion: '1.0.0',
// previousVersion: '0.9.0',
// reason: 'version not match'
// })
// )
// })
// })
// })

View file

@ -0,0 +1,539 @@
/**
* Tests for DOM ready event handlers in createWindow function
* These handlers manage localStorage injection for installation states
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { setupMockEnvironment } from '../../../mocks/environmentMocks'
describe('createWindow - DOM Ready Event Handlers', () => {
let mockEnv: ReturnType<typeof setupMockEnvironment>
let mockWebContents: any
let mockWindow: any
beforeEach(() => {
mockEnv = setupMockEnvironment()
// Mock webContents and window
mockWebContents = {
on: vi.fn(),
once: vi.fn(),
executeJavaScript: vi.fn(),
send: vi.fn(),
loadURL: vi.fn(),
loadFile: vi.fn(),
openDevTools: vi.fn()
}
mockWindow = {
webContents: mockWebContents,
reload: vi.fn()
}
// Reset all mocks
vi.clearAllMocks()
})
afterEach(() => {
mockEnv.reset()
})
describe('Fresh Installation - Carousel State Injection', () => {
it('should inject fresh auth-storage with carousel state', () => {
// Simulate fresh installation scenario
const needsInstallation = true
// Set up DOM ready handler like createWindow does
if (needsInstallation) {
mockWebContents.on('dom-ready', () => {
const injectionScript = `
(function() {
try {
const newAuthStorage = {
state: {
token: null,
username: null,
email: null,
user_id: null,
appearance: 'light',
language: 'system',
isFirstLaunch: true,
modelType: 'cloud',
cloud_model_type: 'gpt-4.1',
initState: 'carousel',
share_token: null,
workerListData: {}
},
version: 0
};
localStorage.setItem('auth-storage', JSON.stringify(newAuthStorage));
console.log('[ELECTRON PRE-INJECT] Created fresh auth-storage with carousel state');
} catch (e) {
console.error('[ELECTRON PRE-INJECT] Failed to create storage:', e);
}
})();
`
mockWebContents.executeJavaScript(injectionScript)
})
}
// Trigger DOM ready event
const domReadyCallback = mockWebContents.on.mock.calls.find(
(call: any) => call[0] === 'dom-ready'
)?.[1]
expect(domReadyCallback).toBeDefined()
if (domReadyCallback) {
domReadyCallback()
}
// Verify JavaScript injection was called with carousel state
expect(mockWebContents.executeJavaScript).toHaveBeenCalledWith(
expect.stringContaining('initState: \'carousel\'')
)
expect(mockWebContents.executeJavaScript).toHaveBeenCalledWith(
expect.stringContaining('isFirstLaunch: true')
)
})
it('should handle JavaScript injection errors gracefully', () => {
const needsInstallation = true
// Mock executeJavaScript to reject
mockWebContents.executeJavaScript.mockRejectedValue(new Error('Injection failed'))
// Set up DOM ready handler with error handling
if (needsInstallation) {
mockWebContents.on('dom-ready', () => {
const injectionScript = `/* injection script */`
mockWebContents.executeJavaScript(injectionScript).catch((err: Error) => {
// In real code, this is logged but doesn't throw
console.error('Failed to inject script:', err)
})
})
}
// Trigger DOM ready event
const domReadyCallback = mockWebContents.on.mock.calls.find(
(call: any) => call[0] === 'dom-ready'
)?.[1]
if (domReadyCallback) {
expect(() => domReadyCallback()).not.toThrow()
}
})
it('should include all required auth-storage properties', () => {
const needsInstallation = true
if (needsInstallation) {
mockWebContents.on('dom-ready', () => {
const injectionScript = `
(function() {
try {
const newAuthStorage = {
state: {
token: null,
username: null,
email: null,
user_id: null,
appearance: 'light',
language: 'system',
isFirstLaunch: true,
modelType: 'cloud',
cloud_model_type: 'gpt-4.1',
initState: 'carousel',
share_token: null,
workerListData: {}
},
version: 0
};
localStorage.setItem('auth-storage', JSON.stringify(newAuthStorage));
} catch (e) {
console.error('Failed to create storage:', e);
}
})();
`
mockWebContents.executeJavaScript(injectionScript)
})
}
const domReadyCallback = mockWebContents.on.mock.calls.find(
(call: any) => call[0] === 'dom-ready'
)?.[1]
if (domReadyCallback) {
domReadyCallback()
}
const injectedScript = mockWebContents.executeJavaScript.mock.calls[0]?.[0]
// Verify all required properties are included
expect(injectedScript).toContain('token: null')
expect(injectedScript).toContain('username: null')
expect(injectedScript).toContain('email: null')
expect(injectedScript).toContain('user_id: null')
expect(injectedScript).toContain('appearance: \'light\'')
expect(injectedScript).toContain('language: \'system\'')
expect(injectedScript).toContain('isFirstLaunch: true')
expect(injectedScript).toContain('modelType: \'cloud\'')
expect(injectedScript).toContain('cloud_model_type: \'gpt-4.1\'')
expect(injectedScript).toContain('initState: \'carousel\'')
expect(injectedScript).toContain('share_token: null')
expect(injectedScript).toContain('workerListData: {}')
expect(injectedScript).toContain('version: 0')
})
})
describe('Completed Installation - Done State Management', () => {
it('should check and update initState to done when installation is complete', () => {
const needsInstallation = false
if (!needsInstallation) {
mockWebContents.once('dom-ready', () => {
const checkScript = `
(function() {
try {
const authStorage = localStorage.getItem('auth-storage');
if (authStorage) {
const parsed = JSON.parse(authStorage);
if (parsed.state && parsed.state.initState !== 'done') {
console.log('[ELECTRON] Updating initState from', parsed.state.initState, 'to done');
parsed.state.initState = 'done';
localStorage.setItem('auth-storage', JSON.stringify(parsed));
console.log('[ELECTRON] initState updated to done, reloading page...');
return true;
}
}
return false;
} catch (e) {
console.error('[ELECTRON] Failed to update initState:', e);
return false;
}
})();
`
mockWebContents.executeJavaScript(checkScript)
})
}
// Trigger DOM ready event
const domReadyCallback = mockWebContents.once.mock.calls.find(
(call: any) => call[0] === 'dom-ready'
)?.[1]
expect(domReadyCallback).toBeDefined()
if (domReadyCallback) {
domReadyCallback()
}
// Verify the check script was executed
expect(mockWebContents.executeJavaScript).toHaveBeenCalledWith(
expect.stringContaining('initState !== \'done\'')
)
expect(mockWebContents.executeJavaScript).toHaveBeenCalledWith(
expect.stringContaining('initState = \'done\'')
)
})
it('should trigger window reload when initState needs updating', async () => {
const needsInstallation = false
// Mock executeJavaScript to return true (indicating reload needed)
mockWebContents.executeJavaScript.mockResolvedValue(true)
if (!needsInstallation) {
mockWebContents.once('dom-ready', () => {
mockWebContents.executeJavaScript(`/* check script */`).then((needsReload: boolean) => {
if (needsReload) {
mockWindow.reload()
}
})
})
}
// Trigger DOM ready event
const domReadyCallback = mockWebContents.once.mock.calls.find(
(call: any) => call[0] === 'dom-ready'
)?.[1]
if (domReadyCallback) {
domReadyCallback()
}
// Wait for async operations
await new Promise(resolve => setTimeout(resolve, 10))
expect(mockWindow.reload).toHaveBeenCalled()
})
it('should not reload when initState is already done', async () => {
const needsInstallation = false
// Mock executeJavaScript to return false (no reload needed)
mockWebContents.executeJavaScript.mockResolvedValue(false)
if (!needsInstallation) {
mockWebContents.once('dom-ready', () => {
mockWebContents.executeJavaScript(`/* check script */`).then((needsReload: boolean) => {
if (needsReload) {
mockWindow.reload()
}
})
})
}
const domReadyCallback = mockWebContents.once.mock.calls.find(
(call: any) => call[0] === 'dom-ready'
)?.[1]
if (domReadyCallback) {
domReadyCallback()
}
// Wait for async operations
await new Promise(resolve => setTimeout(resolve, 10))
expect(mockWindow.reload).not.toHaveBeenCalled()
})
it('should handle localStorage parsing errors gracefully', async () => {
const needsInstallation = false
// Mock executeJavaScript to simulate parsing error (return false)
mockWebContents.executeJavaScript.mockResolvedValue(false)
if (!needsInstallation) {
mockWebContents.once('dom-ready', () => {
const checkScript = `
(function() {
try {
const authStorage = localStorage.getItem('auth-storage');
if (authStorage) {
const parsed = JSON.parse(authStorage); // This could throw
// ... rest of logic
}
return false;
} catch (e) {
console.error('[ELECTRON] Failed to update initState:', e);
return false; // Error case returns false
}
})();
`
mockWebContents.executeJavaScript(checkScript)
})
}
const domReadyCallback = mockWebContents.once.mock.calls.find(
(call: any) => call[0] === 'dom-ready'
)?.[1]
if (domReadyCallback) {
expect(() => domReadyCallback()).not.toThrow()
}
await new Promise(resolve => setTimeout(resolve, 10))
// Should not reload on error
expect(mockWindow.reload).not.toHaveBeenCalled()
})
})
describe('Event Handler Setup Differences', () => {
it('should use "on" event for fresh installation (can trigger multiple times)', () => {
const needsInstallation = true
// Simulate the logic from createWindow
if (needsInstallation) {
mockWebContents.on('dom-ready', () => {
// Fresh installation handler
})
}
// Verify 'on' was used instead of 'once'
expect(mockWebContents.on).toHaveBeenCalledWith('dom-ready', expect.any(Function))
expect(mockWebContents.once).not.toHaveBeenCalled()
})
it('should use "once" event for completed installation (single trigger)', () => {
const needsInstallation = false
// Simulate the logic from createWindow
if (!needsInstallation) {
mockWebContents.once('dom-ready', () => {
// Completed installation handler
})
}
// Verify 'once' was used instead of 'on'
expect(mockWebContents.once).toHaveBeenCalledWith('dom-ready', expect.any(Function))
expect(mockWebContents.on).not.toHaveBeenCalled()
})
})
describe('JavaScript Execution Content Validation', () => {
it('should create properly structured auth-storage JSON for fresh installation', () => {
const needsInstallation = true
if (needsInstallation) {
mockWebContents.on('dom-ready', () => {
const script = `
(function() {
try {
const newAuthStorage = {
state: {
token: null,
username: null,
email: null,
user_id: null,
appearance: 'light',
language: 'system',
isFirstLaunch: true,
modelType: 'cloud',
cloud_model_type: 'gpt-4.1',
initState: 'carousel',
share_token: null,
workerListData: {}
},
version: 0
};
localStorage.setItem('auth-storage', JSON.stringify(newAuthStorage));
console.log('[ELECTRON PRE-INJECT] Created fresh auth-storage with carousel state');
} catch (e) {
console.error('[ELECTRON PRE-INJECT] Failed to create storage:', e);
}
})();
`
mockWebContents.executeJavaScript(script)
})
}
const domReadyCallback = mockWebContents.on.mock.calls[0]?.[1]
if (domReadyCallback) {
domReadyCallback()
}
const executedScript = mockWebContents.executeJavaScript.mock.calls[0]?.[0]
// Verify the script is wrapped in IIFE
expect(executedScript).toMatch(/^\s*\(\s*function\s*\(\s*\)\s*\{/)
expect(executedScript).toMatch(/\}\s*\)\s*\(\s*\)\s*;?\s*$/)
// Verify it has try-catch error handling
expect(executedScript).toContain('try {')
expect(executedScript).toContain('} catch (e) {')
// Verify it sets localStorage
expect(executedScript).toContain('localStorage.setItem(\'auth-storage\'')
expect(executedScript).toContain('JSON.stringify(newAuthStorage)')
})
it('should check localStorage properly for completed installation', () => {
const needsInstallation = false
if (!needsInstallation) {
mockWebContents.once('dom-ready', () => {
const script = `
(function() {
try {
const authStorage = localStorage.getItem('auth-storage');
if (authStorage) {
const parsed = JSON.parse(authStorage);
if (parsed.state && parsed.state.initState !== 'done') {
parsed.state.initState = 'done';
localStorage.setItem('auth-storage', JSON.stringify(parsed));
return true;
}
}
return false;
} catch (e) {
console.error('[ELECTRON] Failed to update initState:', e);
return false;
}
})();
`
mockWebContents.executeJavaScript(script)
})
}
const domReadyCallback = mockWebContents.once.mock.calls[0]?.[1]
if (domReadyCallback) {
domReadyCallback()
}
const executedScript = mockWebContents.executeJavaScript.mock.calls[0]?.[0]
// Verify it gets localStorage
expect(executedScript).toContain('localStorage.getItem(\'auth-storage\')')
// Verify it parses JSON
expect(executedScript).toContain('JSON.parse(authStorage)')
// Verify it checks initState
expect(executedScript).toContain('initState !== \'done\'')
// Verify it updates initState
expect(executedScript).toContain('initState = \'done\'')
// Verify it returns boolean
expect(executedScript).toContain('return true')
expect(executedScript).toContain('return false')
})
})
describe('Console Logging in Injected Scripts', () => {
it('should include proper console logging for fresh installation', () => {
const needsInstallation = true
if (needsInstallation) {
mockWebContents.on('dom-ready', () => {
const script = `
console.log('[ELECTRON PRE-INJECT] Created fresh auth-storage with carousel state');
console.error('[ELECTRON PRE-INJECT] Failed to create storage:', e);
`
mockWebContents.executeJavaScript(script)
})
}
const domReadyCallback = mockWebContents.on.mock.calls[0]?.[1]
if (domReadyCallback) {
domReadyCallback()
}
const executedScript = mockWebContents.executeJavaScript.mock.calls[0]?.[0]
// Verify console logging is included
expect(executedScript).toContain('[ELECTRON PRE-INJECT]')
expect(executedScript).toContain('console.log')
expect(executedScript).toContain('console.error')
})
it('should include proper console logging for completed installation', () => {
const needsInstallation = false
if (!needsInstallation) {
mockWebContents.once('dom-ready', () => {
const script = `
console.log('[ELECTRON] Updating initState from', parsed.state.initState, 'to done');
console.log('[ELECTRON] initState updated to done, reloading page...');
console.error('[ELECTRON] Failed to update initState:', e);
`
mockWebContents.executeJavaScript(script)
})
}
const domReadyCallback = mockWebContents.once.mock.calls[0]?.[1]
if (domReadyCallback) {
domReadyCallback()
}
const executedScript = mockWebContents.executeJavaScript.mock.calls[0]?.[0]
// Verify console logging is included
expect(executedScript).toContain('[ELECTRON]')
expect(executedScript).toContain('console.log')
expect(executedScript).toContain('console.error')
})
})
})

View file

@ -0,0 +1,357 @@
/**
* Focused tests for the complex installation state detection logic in createWindow
* This tests the decision matrix for when installation is needed vs when it's not
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { setupMockEnvironment } from '../../../mocks/environmentMocks'
describe('createWindow - Installation State Detection Logic', () => {
let mockEnv: ReturnType<typeof setupMockEnvironment>
let installationStateChecker: (mockState: any) => Promise<boolean>
beforeEach(() => {
mockEnv = setupMockEnvironment()
// Extract the installation decision logic for focused testing
installationStateChecker = async (mockState) => {
const currentVersion = mockState.app.currentVersion
const versionExists = mockState.filesystem.versionFileExists
const savedVersion = versionExists ? mockState.filesystem.versionFileContent : ''
const uvExists = mockState.filesystem.binariesExist['uv'] || false
const bunExists = mockState.filesystem.binariesExist['bun'] || false
const installationCompleted = mockState.filesystem.installedLockExists
return !versionExists ||
savedVersion !== currentVersion ||
!uvExists ||
!bunExists ||
!installationCompleted
}
})
afterEach(() => {
mockEnv.reset()
})
describe('Version File Scenarios', () => {
it('should require installation when version file does not exist', async () => {
mockEnv.mockState.filesystem.versionFileExists = false
const needsInstallation = await installationStateChecker(mockEnv.mockState)
expect(needsInstallation).toBe(true)
})
it('should require installation when saved version differs from current version', async () => {
mockEnv.mockState.filesystem.versionFileExists = true
mockEnv.mockState.filesystem.versionFileContent = '0.9.0'
mockEnv.mockState.app.currentVersion = '1.0.0'
const needsInstallation = await installationStateChecker(mockEnv.mockState)
expect(needsInstallation).toBe(true)
})
it('should not require installation when versions match', async () => {
mockEnv.mockState.filesystem.versionFileExists = true
mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
mockEnv.mockState.app.currentVersion = '1.0.0'
mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': true }
mockEnv.mockState.filesystem.installedLockExists = true
const needsInstallation = await installationStateChecker(mockEnv.mockState)
expect(needsInstallation).toBe(false)
})
it('should handle version file with whitespace correctly', async () => {
mockEnv.mockState.filesystem.versionFileExists = true
mockEnv.mockState.filesystem.versionFileContent = ' 1.0.0 \n'
mockEnv.mockState.app.currentVersion = '1.0.0'
mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': true }
mockEnv.mockState.filesystem.installedLockExists = true
// In the real code, version content is trimmed
const trimmedVersion = mockEnv.mockState.filesystem.versionFileContent.trim()
const needsInstallation = trimmedVersion !== mockEnv.mockState.app.currentVersion ||
!mockEnv.mockState.filesystem.binariesExist['uv'] ||
!mockEnv.mockState.filesystem.binariesExist['bun'] ||
!mockEnv.mockState.filesystem.installedLockExists
expect(needsInstallation).toBe(false)
})
})
describe('Binary Existence Scenarios', () => {
it('should require installation when uv binary is missing', async () => {
mockEnv.mockState.filesystem.versionFileExists = true
mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
mockEnv.mockState.app.currentVersion = '1.0.0'
mockEnv.mockState.filesystem.binariesExist = { 'uv': false, 'bun': true }
mockEnv.mockState.filesystem.installedLockExists = true
const needsInstallation = await installationStateChecker(mockEnv.mockState)
expect(needsInstallation).toBe(true)
})
it('should require installation when bun binary is missing', async () => {
mockEnv.mockState.filesystem.versionFileExists = true
mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
mockEnv.mockState.app.currentVersion = '1.0.0'
mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': false }
mockEnv.mockState.filesystem.installedLockExists = true
const needsInstallation = await installationStateChecker(mockEnv.mockState)
expect(needsInstallation).toBe(true)
})
it('should require installation when both binaries are missing', async () => {
mockEnv.mockState.filesystem.versionFileExists = true
mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
mockEnv.mockState.app.currentVersion = '1.0.0'
mockEnv.mockState.filesystem.binariesExist = { 'uv': false, 'bun': false }
mockEnv.mockState.filesystem.installedLockExists = true
const needsInstallation = await installationStateChecker(mockEnv.mockState)
expect(needsInstallation).toBe(true)
})
})
describe('Installation Lock File Scenarios', () => {
it('should require installation when lock file is missing', async () => {
mockEnv.mockState.filesystem.versionFileExists = true
mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
mockEnv.mockState.app.currentVersion = '1.0.0'
mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': true }
mockEnv.mockState.filesystem.installedLockExists = false
const needsInstallation = await installationStateChecker(mockEnv.mockState)
expect(needsInstallation).toBe(true)
})
it('should not require installation when lock file exists', async () => {
mockEnv.mockState.filesystem.versionFileExists = true
mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
mockEnv.mockState.app.currentVersion = '1.0.0'
mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': true }
mockEnv.mockState.filesystem.installedLockExists = true
const needsInstallation = await installationStateChecker(mockEnv.mockState)
expect(needsInstallation).toBe(false)
})
})
describe('Combined Failure Scenarios', () => {
it('should require installation when multiple conditions fail', async () => {
mockEnv.mockState.filesystem.versionFileExists = false
mockEnv.mockState.filesystem.binariesExist = { 'uv': false, 'bun': false }
mockEnv.mockState.filesystem.installedLockExists = false
const needsInstallation = await installationStateChecker(mockEnv.mockState)
expect(needsInstallation).toBe(true)
})
it('should require installation when version mismatch AND missing binaries', async () => {
mockEnv.mockState.filesystem.versionFileExists = true
mockEnv.mockState.filesystem.versionFileContent = '0.9.0'
mockEnv.mockState.app.currentVersion = '1.0.0'
mockEnv.mockState.filesystem.binariesExist = { 'uv': false, 'bun': true }
mockEnv.mockState.filesystem.installedLockExists = true
const needsInstallation = await installationStateChecker(mockEnv.mockState)
expect(needsInstallation).toBe(true)
})
it('should require installation when only lock file is present', async () => {
mockEnv.mockState.filesystem.versionFileExists = false
mockEnv.mockState.filesystem.binariesExist = { 'uv': false, 'bun': false }
mockEnv.mockState.filesystem.installedLockExists = true
const needsInstallation = await installationStateChecker(mockEnv.mockState)
expect(needsInstallation).toBe(true)
})
})
describe('Edge Cases and Boundaries', () => {
it('should handle empty version strings', async () => {
mockEnv.mockState.filesystem.versionFileExists = true
mockEnv.mockState.filesystem.versionFileContent = ''
mockEnv.mockState.app.currentVersion = '1.0.0'
mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': true }
mockEnv.mockState.filesystem.installedLockExists = true
const needsInstallation = await installationStateChecker(mockEnv.mockState)
expect(needsInstallation).toBe(true)
})
it('should handle version with special characters', async () => {
mockEnv.mockState.filesystem.versionFileExists = true
mockEnv.mockState.filesystem.versionFileContent = '1.0.0-beta.1'
mockEnv.mockState.app.currentVersion = '1.0.0-beta.1'
mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': true }
mockEnv.mockState.filesystem.installedLockExists = true
const needsInstallation = await installationStateChecker(mockEnv.mockState)
expect(needsInstallation).toBe(false)
})
it('should handle null or undefined binary states', async () => {
mockEnv.mockState.filesystem.versionFileExists = true
mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
mockEnv.mockState.app.currentVersion = '1.0.0'
mockEnv.mockState.filesystem.binariesExist = {}
mockEnv.mockState.filesystem.installedLockExists = true
const needsInstallation = await installationStateChecker(mockEnv.mockState)
expect(needsInstallation).toBe(true)
})
})
describe('Platform-Specific Binary Detection', () => {
it('should check for .exe extension on Windows', async () => {
mockEnv.scenarios.missingEigentDirectories()
mockEnv.mockState.system.platform = 'win32'
// Test that binary detection considers .exe files on Windows
expect(mockEnv.processUtilsMock.getBinaryName('uv')).resolves.toBe('uv.exe')
})
it('should not add .exe extension on macOS', async () => {
mockEnv.scenarios.macOSEnvironment()
expect(mockEnv.processUtilsMock.getBinaryName('uv')).resolves.toBe('uv')
})
it('should not add .exe extension on Linux', async () => {
mockEnv.scenarios.linuxEnvironment()
expect(mockEnv.processUtilsMock.getBinaryName('uv')).resolves.toBe('uv')
})
})
describe('Decision Matrix Verification', () => {
// This test verifies the complete decision matrix
const testCases = [
{
name: 'All conditions met - no installation needed',
setup: {
versionFileExists: true,
versionFileContent: '1.0.0',
currentVersion: '1.0.0',
uvExists: true,
bunExists: true,
installedLockExists: true
},
expectedNeedsInstallation: false
},
{
name: 'Missing version file',
setup: {
versionFileExists: false,
versionFileContent: '',
currentVersion: '1.0.0',
uvExists: true,
bunExists: true,
installedLockExists: true
},
expectedNeedsInstallation: true
},
{
name: 'Version mismatch',
setup: {
versionFileExists: true,
versionFileContent: '0.9.0',
currentVersion: '1.0.0',
uvExists: true,
bunExists: true,
installedLockExists: true
},
expectedNeedsInstallation: true
},
{
name: 'Missing uv binary',
setup: {
versionFileExists: true,
versionFileContent: '1.0.0',
currentVersion: '1.0.0',
uvExists: false,
bunExists: true,
installedLockExists: true
},
expectedNeedsInstallation: true
},
{
name: 'Missing bun binary',
setup: {
versionFileExists: true,
versionFileContent: '1.0.0',
currentVersion: '1.0.0',
uvExists: true,
bunExists: false,
installedLockExists: true
},
expectedNeedsInstallation: true
},
{
name: 'Missing installation lock',
setup: {
versionFileExists: true,
versionFileContent: '1.0.0',
currentVersion: '1.0.0',
uvExists: true,
bunExists: true,
installedLockExists: false
},
expectedNeedsInstallation: true
}
]
testCases.forEach(({ name, setup, expectedNeedsInstallation }) => {
it(`should correctly handle: ${name}`, async () => {
// Set up the mock state according to the test case
mockEnv.mockState.filesystem.versionFileExists = setup.versionFileExists
mockEnv.mockState.filesystem.versionFileContent = setup.versionFileContent
mockEnv.mockState.app.currentVersion = setup.currentVersion
mockEnv.mockState.filesystem.binariesExist = {
'uv': setup.uvExists,
'bun': setup.bunExists
}
mockEnv.mockState.filesystem.installedLockExists = setup.installedLockExists
const needsInstallation = await installationStateChecker(mockEnv.mockState)
expect(needsInstallation).toBe(expectedNeedsInstallation)
})
})
})
describe('Logging Verification', () => {
it('should log installation check results', async () => {
// This test ensures that the installation decision logic provides proper logging
const mockState = mockEnv.mockState
const logData = {
needsInstallation: await installationStateChecker(mockState),
versionExists: mockState.filesystem.versionFileExists,
versionMatch: mockState.filesystem.versionFileContent === mockState.app.currentVersion,
uvExists: mockState.filesystem.binariesExist['uv'] || false,
bunExists: mockState.filesystem.binariesExist['bun'] || false,
installationCompleted: mockState.filesystem.installedLockExists
}
// Verify that all required data for logging is available
expect(logData).toHaveProperty('needsInstallation')
expect(logData).toHaveProperty('versionExists')
expect(logData).toHaveProperty('versionMatch')
expect(logData).toHaveProperty('uvExists')
expect(logData).toHaveProperty('bunExists')
expect(logData).toHaveProperty('installationCompleted')
// Verify the logic is consistent
const expectedNeedsInstallation = !logData.versionExists ||
!logData.versionMatch ||
!logData.uvExists ||
!logData.bunExists ||
!logData.installationCompleted
expect(logData.needsInstallation).toBe(expectedNeedsInstallation)
})
})
})

View file

@ -0,0 +1,199 @@
/**
* Simple demonstration test for the new process utilities mocking
* This shows how to test the functions from process.ts with our enhanced mocks
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import {
setupMockEnvironment,
createFileSystemMock,
createProcessMock,
createElectronAppMock,
createOsMock,
createPathMock,
createLogMock,
createProcessUtilsMock
} from '../../../mocks/environmentMocks'
// Set up vi.mock calls at the top level to avoid hoisting issues
vi.mock('fs', () => createFileSystemMock())
vi.mock('child_process', () => createProcessMock())
vi.mock('os', () => ({ default: createOsMock() }))
vi.mock('path', () => ({ default: createPathMock() }))
vi.mock('electron', () => ({
app: createElectronAppMock(),
BrowserWindow: vi.fn()
}))
vi.mock('electron-log', () => ({ default: createLogMock() }))
vi.mock('../../../../electron/main/utils/process', () => createProcessUtilsMock())
describe('Process Utils Mocking Demo', () => {
let mockEnv: ReturnType<typeof setupMockEnvironment>
beforeEach(() => {
mockEnv = setupMockEnvironment()
})
afterEach(() => {
mockEnv.reset()
})
describe('Binary Path Functions', () => {
it('should return correct binary paths based on platform', async () => {
// Test Windows binary naming
mockEnv.mockState.system.platform = 'win32'
const uvBinaryName = await mockEnv.processUtilsMock.getBinaryName('uv')
expect(uvBinaryName).toBe('uv.exe')
const uvBinaryPath = await mockEnv.processUtilsMock.getBinaryPath('uv')
expect(uvBinaryPath).toContain('.eigent/bin')
expect(uvBinaryPath).toContain('uv.exe')
})
it('should return correct binary paths for macOS', async () => {
mockEnv.scenarios.macOSEnvironment()
const uvBinaryName = await mockEnv.processUtilsMock.getBinaryName('uv')
expect(uvBinaryName).toBe('uv')
const uvBinaryPath = await mockEnv.processUtilsMock.getBinaryPath('uv')
expect(uvBinaryPath).toContain('.eigent/bin')
expect(uvBinaryPath).toContain('/uv')
expect(uvBinaryPath).not.toContain('.exe')
})
})
describe('Directory Path Functions', () => {
it('should return correct backend path for development mode', () => {
mockEnv.mockState.app.isPackaged = false
const backendPath = mockEnv.processUtilsMock.getBackendPath()
expect(backendPath).toContain('/backend')
expect(backendPath).not.toContain('resources')
})
it('should return correct backend path for packaged app', () => {
mockEnv.scenarios.packagedApp()
const backendPath = mockEnv.processUtilsMock.getBackendPath()
expect(backendPath).toContain('backend')
// In packaged mode, it should use resources path
expect(mockEnv.mockState.app.isPackaged).toBe(true)
})
it('should return correct cache paths', () => {
const cachePath = mockEnv.processUtilsMock.getCachePath('models')
expect(cachePath).toContain('.eigent/cache/models')
})
it('should return correct venv paths', () => {
const venvPath = mockEnv.processUtilsMock.getVenvPath('1.0.0')
expect(venvPath).toContain('.eigent/venvs/backend-1.0.0')
})
})
describe('Binary Existence Checking', () => {
it('should correctly check binary existence', async () => {
// Set up scenario where uv exists but bun doesn't
mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': false }
const uvExists = await mockEnv.processUtilsMock.isBinaryExists('uv')
const bunExists = await mockEnv.processUtilsMock.isBinaryExists('bun')
expect(uvExists).toBe(true)
expect(bunExists).toBe(false)
})
})
describe('Old Venv Cleanup', () => {
it('should cleanup old venvs correctly', async () => {
// Set up scenario with multiple old venvs
mockEnv.scenarios.multipleOldVenvs('1.0.0')
const initialOldVenvs = [...mockEnv.mockState.filesystem.oldVenvsExist]
expect(initialOldVenvs).toContain('backend-0.9.0')
expect(initialOldVenvs).toContain('backend-0.9.5')
expect(initialOldVenvs).toContain('backend-1.0.1-beta')
// Run cleanup
await mockEnv.processUtilsMock.cleanupOldVenvs('1.0.0')
// Should keep current version but remove others
const remainingVenvs = mockEnv.mockState.filesystem.oldVenvsExist
console.log(remainingVenvs);
expect(remainingVenvs).toContain('backend-1.0.0')
expect(remainingVenvs).not.toContain('backend-0.9.0')
expect(remainingVenvs).not.toContain('backend-0.9.5')
expect(remainingVenvs).not.toContain('backend-1.0.1-beta')
})
})
describe('Installation Decision Matrix', () => {
it('should correctly determine when installation is needed', () => {
// Test the decision logic that createWindow uses
const checkInstallationNeeded = (mockState: any) => {
const currentVersion = mockState.app.currentVersion
const versionExists = mockState.filesystem.versionFileExists
const savedVersion = versionExists ? mockState.filesystem.versionFileContent : ''
const uvExists = mockState.filesystem.binariesExist['uv'] || false
const bunExists = mockState.filesystem.binariesExist['bun'] || false
const installationCompleted = mockState.filesystem.installedLockExists
return !versionExists ||
savedVersion !== currentVersion ||
!uvExists ||
!bunExists ||
!installationCompleted
}
// Test fresh install scenario
mockEnv.scenarios.freshInstall()
expect(checkInstallationNeeded(mockEnv.mockState)).toBe(true)
// Test version update scenario
mockEnv.scenarios.versionUpdate('0.9.0', '1.0.0')
expect(checkInstallationNeeded(mockEnv.mockState)).toBe(true)
// Test all good scenario
mockEnv.mockState.filesystem.versionFileExists = true
mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
mockEnv.mockState.app.currentVersion = '1.0.0'
mockEnv.mockState.filesystem.binariesExist = { 'uv': true, 'bun': true }
mockEnv.mockState.filesystem.installedLockExists = true
expect(checkInstallationNeeded(mockEnv.mockState)).toBe(false)
})
})
describe('File System Operations', () => {
it('should handle directory creation correctly', () => {
// Test .eigent directory creation
mockEnv.scenarios.missingEigentDirectories()
expect(mockEnv.mockState.filesystem.eigentDirExists).toBe(false)
expect(mockEnv.mockState.filesystem.eigentBinDirExists).toBe(false)
// Simulate directory creation
mockEnv.fsMock.mkdirSync('/mock/home/.eigent', { recursive: true })
mockEnv.fsMock.mkdirSync('/mock/home/.eigent/bin', { recursive: true })
expect(mockEnv.mockState.filesystem.eigentDirExists).toBe(true)
expect(mockEnv.mockState.filesystem.eigentBinDirExists).toBe(true)
})
it('should handle file operations correctly', () => {
// Test version file operations
expect(mockEnv.mockState.filesystem.versionFileExists).toBe(true)
// Write new version
mockEnv.fsMock.writeFileSync('/path/to/version.txt', '2.0.0')
expect(mockEnv.mockState.filesystem.versionFileContent).toBe('2.0.0')
// Delete version file
mockEnv.fsMock.unlinkSync('/path/to/version.txt')
expect(mockEnv.mockState.filesystem.versionFileExists).toBe(false)
})
})
})

View file

@ -0,0 +1,407 @@
/**
* Tests for window event setup and lifecycle management in createWindow function
* Covers dev tools shortcuts, external link handling, before close handling,
* auto-update integration, webview manager, and file reader initialization
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { setupMockEnvironment } from '../../../mocks/environmentMocks'
describe('createWindow - Window Event Setup and Lifecycle', () => {
let mockEnv: ReturnType<typeof setupMockEnvironment>
let mockWebContents: any
let mockWindow: any
let mockFileReader: any
let mockWebViewManager: any
let mockUpdate: any
let mockMenu: any
beforeEach(() => {
mockEnv = setupMockEnvironment()
// Mock webContents
mockWebContents = {
on: vi.fn(),
once: vi.fn(),
executeJavaScript: vi.fn(),
send: vi.fn(),
loadURL: vi.fn(),
loadFile: vi.fn(),
openDevTools: vi.fn(),
toggleDevTools: vi.fn()
}
// Mock window
mockWindow = {
webContents: mockWebContents,
reload: vi.fn()
}
// Mock FileReader class
mockFileReader = vi.fn()
// Mock WebViewManager class
mockWebViewManager = vi.fn().mockImplementation(() => ({
createWebview: vi.fn()
}))
// Mock update function
mockUpdate = vi.fn()
// Mock Menu
mockMenu = {
setApplicationMenu: vi.fn()
}
// Reset all mocks
vi.clearAllMocks()
})
afterEach(() => {
mockEnv.reset()
})
describe('FileReader and WebViewManager Initialization', () => {
it.skip('should create 8 webviews with correct IDs', () => {
const webViewManager = new mockWebViewManager(mockWindow)
const instance = mockWebViewManager.mock.instances[0]
// Simulate the loop that creates webviews
for (let i = 1; i <= 8; i++) {
instance.createWebview(i === 1 ? undefined : i.toString())
}
expect(instance.createWebview).toHaveBeenCalledTimes(8)
expect(instance.createWebview).toHaveBeenNthCalledWith(1, undefined)
expect(instance.createWebview).toHaveBeenNthCalledWith(2, '2')
expect(instance.createWebview).toHaveBeenNthCalledWith(3, '3')
expect(instance.createWebview).toHaveBeenNthCalledWith(4, '4')
expect(instance.createWebview).toHaveBeenNthCalledWith(5, '5')
expect(instance.createWebview).toHaveBeenNthCalledWith(6, '6')
expect(instance.createWebview).toHaveBeenNthCalledWith(7, '7')
expect(instance.createWebview).toHaveBeenNthCalledWith(8, '8')
})
})
describe('Window Event Listeners Setup', () => {
it('should disable application menu', () => {
// Simulate setupWindowEventListeners
mockMenu.setApplicationMenu(null)
expect(mockMenu.setApplicationMenu).toHaveBeenCalledWith(null)
})
it('should set up application menu only once', () => {
// Simulate multiple calls to setupWindowEventListeners
mockMenu.setApplicationMenu(null)
mockMenu.setApplicationMenu(null)
expect(mockMenu.setApplicationMenu).toHaveBeenCalledTimes(2)
expect(mockMenu.setApplicationMenu).toHaveBeenCalledWith(null)
})
})
describe('DevTools Shortcuts Setup', () => {
it('should set up before-input-event listener for dev tools shortcuts', () => {
// Simulate setupDevToolsShortcuts
mockWebContents.on('before-input-event', expect.any(Function))
expect(mockWebContents.on).toHaveBeenCalledWith('before-input-event', expect.any(Function))
})
it('should handle F12 key to toggle dev tools', () => {
let beforeInputCallback: any
mockWebContents.on.mockImplementation((event: string, callback: any) => {
if (event === 'before-input-event') {
beforeInputCallback = callback
}
})
// Simulate setupDevToolsShortcuts
mockWebContents.on('before-input-event', (event: any, input: any) => {
if (input.key === 'F12' && input.type === 'keyDown') {
mockWebContents.toggleDevTools()
}
})
// Trigger F12 key
if (beforeInputCallback) {
const mockEvent = { preventDefault: vi.fn() }
const mockInput = { key: 'F12', type: 'keyDown' }
beforeInputCallback(mockEvent, mockInput)
}
expect(mockWebContents.toggleDevTools).toHaveBeenCalled()
})
it('should handle Ctrl+Shift+I to toggle dev tools on Windows/Linux', () => {
let beforeInputCallback: any
mockWebContents.on.mockImplementation((event: string, callback: any) => {
if (event === 'before-input-event') {
beforeInputCallback = callback
}
})
// Simulate setupDevToolsShortcuts
mockWebContents.on('before-input-event', (event: any, input: any) => {
if (input.control && input.shift && input.key.toLowerCase() === 'i' && input.type === 'keyDown') {
mockWebContents.toggleDevTools()
}
})
// Trigger Ctrl+Shift+I
if (beforeInputCallback) {
const mockEvent = { preventDefault: vi.fn() }
const mockInput = {
control: true,
shift: true,
key: 'I',
type: 'keyDown'
}
beforeInputCallback(mockEvent, mockInput)
}
expect(mockWebContents.toggleDevTools).toHaveBeenCalled()
})
it('should handle Cmd+Shift+I to toggle dev tools on Mac', () => {
let beforeInputCallback: any
mockWebContents.on.mockImplementation((event: string, callback: any) => {
if (event === 'before-input-event') {
beforeInputCallback = callback
}
})
// Simulate setupDevToolsShortcuts
mockWebContents.on('before-input-event', (event: any, input: any) => {
if (input.meta && input.shift && input.key.toLowerCase() === 'i' && input.type === 'keyDown') {
mockWebContents.toggleDevTools()
}
})
// Trigger Cmd+Shift+I
if (beforeInputCallback) {
const mockEvent = { preventDefault: vi.fn() }
const mockInput = {
meta: true,
shift: true,
key: 'I',
type: 'keyDown'
}
beforeInputCallback(mockEvent, mockInput)
}
expect(mockWebContents.toggleDevTools).toHaveBeenCalled()
})
it('should not trigger dev tools on key up events', () => {
let beforeInputCallback: any
mockWebContents.on.mockImplementation((event: string, callback: any) => {
if (event === 'before-input-event') {
beforeInputCallback = callback
}
})
// Simulate setupDevToolsShortcuts
mockWebContents.on('before-input-event', (event: any, input: any) => {
if (input.key === 'F12' && input.type === 'keyDown') {
mockWebContents.toggleDevTools()
}
})
// Trigger F12 key up (should not toggle)
if (beforeInputCallback) {
const mockEvent = { preventDefault: vi.fn() }
const mockInput = { key: 'F12', type: 'keyUp' }
beforeInputCallback(mockEvent, mockInput)
}
expect(mockWebContents.toggleDevTools).not.toHaveBeenCalled()
})
it('should not trigger dev tools on wrong key combinations', () => {
let beforeInputCallback: any
mockWebContents.on.mockImplementation((event: string, callback: any) => {
if (event === 'before-input-event') {
beforeInputCallback = callback
}
})
// Simulate setupDevToolsShortcuts
mockWebContents.on('before-input-event', (event: any, input: any) => {
if (input.control && input.shift && input.key.toLowerCase() === 'i' && input.type === 'keyDown') {
mockWebContents.toggleDevTools()
}
})
// Trigger wrong combination (Ctrl+I without Shift)
if (beforeInputCallback) {
const mockEvent = { preventDefault: vi.fn() }
const mockInput = {
control: true,
shift: false,
key: 'I',
type: 'keyDown'
}
beforeInputCallback(mockEvent, mockInput)
}
expect(mockWebContents.toggleDevTools).not.toHaveBeenCalled()
})
})
describe('Auto-Update Integration', () => {
it('should call update function with window reference', () => {
// Simulate auto-update setup
mockUpdate(mockWindow)
expect(mockUpdate).toHaveBeenCalledWith(mockWindow)
})
it('should call update function only once', () => {
// Simulate auto-update setup
mockUpdate(mockWindow)
expect(mockUpdate).toHaveBeenCalledTimes(1)
})
})
describe('Event Handler Organization', () => {
it('should set up event handlers in correct order', () => {
const eventSetupOrder: string[] = []
// Mock all the setup functions to track order
const setupWindowEventListeners = () => {
eventSetupOrder.push('windowEventListeners')
mockMenu.setApplicationMenu(null)
}
const setupDevToolsShortcuts = () => {
eventSetupOrder.push('devToolsShortcuts')
mockWebContents.on('before-input-event', vi.fn())
}
const setupExternalLinkHandling = () => {
eventSetupOrder.push('externalLinkHandling')
}
const handleBeforeClose = () => {
eventSetupOrder.push('beforeClose')
}
// Simulate the order in createWindow
setupWindowEventListeners()
setupDevToolsShortcuts()
setupExternalLinkHandling()
handleBeforeClose()
expect(eventSetupOrder).toEqual([
'windowEventListeners',
'devToolsShortcuts',
'externalLinkHandling',
'beforeClose'
])
})
})
describe('Window State Management', () => {
it('should handle window ready state correctly', async () => {
let didFinishLoadCallback: (() => void) | undefined
// Mock the did-finish-load event listener
mockWebContents.once.mockImplementation((event: string, callback: () => void) => {
if (event === 'did-finish-load') {
didFinishLoadCallback = callback
}
})
// Simulate waiting for window ready
const windowReadyPromise = new Promise<void>(resolve => {
mockWebContents.once('did-finish-load', () => {
resolve()
})
})
// Trigger the event
if (didFinishLoadCallback) {
didFinishLoadCallback()
}
// Should resolve without throwing
await expect(windowReadyPromise).resolves.toBeUndefined()
})
it('should log appropriate messages during window setup', () => {
// In a real test, you would verify that appropriate log messages are called
// This ensures the window setup process is properly logged
const mockLog = {
info: vi.fn(),
error: vi.fn()
}
// Simulate logging calls that would happen during window setup
mockLog.info('Window content loaded, starting dependency check immediately...')
mockLog.info('.eigent directory structure ensured')
expect(mockLog.info).toHaveBeenCalledWith(
'Window content loaded, starting dependency check immediately...'
)
expect(mockLog.info).toHaveBeenCalledWith(
'.eigent directory structure ensured'
)
})
})
describe('Integration Points', () => {
it('should properly coordinate between file reader and webview manager', () => {
const fileReader = new mockFileReader(mockWindow)
const webViewManager = new mockWebViewManager(mockWindow)
// Both should be initialized with the same window
expect(mockFileReader).toHaveBeenCalledWith(mockWindow)
expect(mockWebViewManager).toHaveBeenCalledWith(mockWindow)
})
it('should handle window initialization errors gracefully', () => {
// Mock FileReader to throw during initialization
mockFileReader.mockImplementation(() => {
throw new Error('FileReader initialization failed')
})
// Should handle gracefully in real implementation
expect(() => {
try {
new mockFileReader(mockWindow)
} catch (error) {
// Log error but don't stop execution
console.error('FileReader initialization error:', error)
}
}).not.toThrow()
})
})
describe('Memory Management', () => {
it('should properly clean up event listeners when window is destroyed', () => {
// In a real scenario, you would test that event listeners are removed
// when the window is closed to prevent memory leaks
const mockRemoveListener = vi.fn()
mockWebContents.removeListener = mockRemoveListener
// Simulate cleanup
const cleanup = () => {
mockWebContents.removeListener('before-input-event', vi.fn())
mockWebContents.removeListener('dom-ready', vi.fn())
}
cleanup()
expect(mockRemoveListener).toHaveBeenCalledTimes(2)
})
})
})

View file

@ -0,0 +1,382 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import React from 'react'
import { createTestEnvironment, waitForInstallationState } from '../../mocks/testUtils'
import { useInstallationStore } from '../../../src/store/installationStore'
/**
* Example test file demonstrating how to use the test environment
* This shows all the main scenarios you wanted to test
*/
describe('Installation Flow Examples', () => {
let testEnv: ReturnType<typeof createTestEnvironment>
beforeEach(() => {
testEnv = createTestEnvironment()
useInstallationStore.getState().reset()
})
afterEach(() => {
testEnv.reset()
})
describe('Main Test Scenarios', () => {
it('should handle when .venv is removed', async () => {
// Set up scenario
testEnv.scenarios.venvRemoved()
// Verify scenario is set up correctly
expect(testEnv.inspect.verifyScenario('venvRemoved')).toBe(true)
expect(testEnv.inspect.getInstallationState().venvExists).toBe(false)
// Trigger installation
const result = await testEnv.electronAPI.checkAndInstallDepsOnUpdate()
// Should trigger installation since .venv is missing
expect(result.success).toBe(true)
console.log(result);
expect(result.message).toContain('Dependencies installed successfully')
})
it('should handle when version file is different', async () => {
// Set up version update scenario
testEnv.scenarios.versionUpdate('0.9.0', '1.0.0')
// Verify scenario
expect(testEnv.inspect.verifyScenario('versionUpdate')).toBe(true)
// Trigger installation
const result = await testEnv.electronAPI.checkAndInstallDepsOnUpdate()
// Should install due to version mismatch
expect(result.success).toBe(true)
expect(result.message).toContain('Dependencies installed successfully after update')
})
it('should handle when uvicorn starts installing deps after page is loaded', async () => {
// Set up uvicorn dependency installation scenario
testEnv.scenarios.uvicornDepsInstall()
const { result } = renderHook(() => useInstallationStore())
// Set up event listeners manually for this test
const startInstallation = result.current.startInstallation
const addLog = result.current.addLog
const setSuccess = result.current.setSuccess
const setError = result.current.setError
// Set up the electron API event handlers to connect to the store
testEnv.electronAPI.onInstallDependenciesStart(() => {
act(() => {
startInstallation()
})
})
testEnv.electronAPI.onInstallDependenciesLog((data: { type: string; data: string }) => {
act(() => {
addLog({
type: data.type as 'stdout' | 'stderr',
data: data.data,
timestamp: new Date(),
})
})
})
testEnv.electronAPI.onInstallDependenciesComplete((data: { success: boolean; error?: string }) => {
act(() => {
if (data.success) {
setSuccess()
} else {
setError(data.error || 'Installation failed')
}
})
})
// Simulate uvicorn startup that triggers dependency detection
await act(async () => {
// Use the electron mock's simulation methods instead of calling detectInstallationLogs directly
testEnv.electronAPI.simulateInstallationStart()
// Wait a bit for state to update
await new Promise(resolve => setTimeout(resolve, 50))
})
// Should start installation
expect(result.current.state).toBe('installing')
console.log("State after startInstalling() ", result.current);
// Simulate UV sync/run command being executed
await act(async () => {
testEnv.electronAPI.simulateInstallationLog('stdout', 'Resolved 45 packages in 2.1s')
testEnv.electronAPI.simulateInstallationLog('stdout', 'Downloaded 12 packages in 1.3s')
testEnv.electronAPI.simulateInstallationLog('stdout', 'Installing packages...')
// Wait a bit for state to update
await new Promise(resolve => setTimeout(resolve, 50))
})
// Should still be installing with logs
expect(result.current.state).toBe('installing')
expect(result.current.logs.length).toBeGreaterThan(0)
// Simulate uvicorn completing successfully
await act(async () => {
testEnv.electronAPI.simulateInstallationLog('stdout', 'Uvicorn running on http://127.0.0.1:8000')
testEnv.electronAPI.simulateInstallationComplete(true)
await new Promise(resolve => setTimeout(resolve, 50))
})
// Should complete successfully
expect(result.current.state).toBe('completed')
})
})
describe('All Installation UI States', () => {
it('should test idle state', () => {
const { result } = renderHook(() => useInstallationStore())
expect(result.current.state).toBe('idle')
expect(result.current.progress).toBe(20)
expect(result.current.logs).toEqual([])
expect(result.current.error).toBeUndefined()
expect(result.current.isVisible).toBe(false)
})
it('should test installing state', async () => {
const { result } = renderHook(() => useInstallationStore())
act(() => {
result.current.startInstallation()
})
expect(result.current.state).toBe('installing')
expect(result.current.isVisible).toBe(true)
expect(result.current.progress).toBe(20)
})
it('should test error state', async () => {
const { result } = renderHook(() => useInstallationStore())
act(() => {
result.current.startInstallation()
})
act(() => {
result.current.setError('Installation failed')
})
expect(result.current.state).toBe('error')
expect(result.current.error).toBe('Installation failed')
expect(result.current.logs).toHaveLength(1)
expect(result.current.logs[0].type).toBe('stderr')
})
it('should test completed state', async () => {
const { result } = renderHook(() => useInstallationStore())
act(() => {
result.current.startInstallation()
})
act(() => {
result.current.setSuccess()
})
expect(result.current.state).toBe('completed')
expect(result.current.progress).toBe(100)
})
it('should test retry after error', async () => {
const { result } = renderHook(() => useInstallationStore())
// Start and fail installation
act(() => {
result.current.startInstallation()
result.current.setError('Installation failed')
})
expect(result.current.state).toBe('error')
// Retry installation
act(() => {
result.current.retryInstallation()
})
expect(result.current.state).toBe('installing')
expect(result.current.error).toBeUndefined()
expect(result.current.logs).toEqual([])
})
})
describe('Complete Installation Flows', () => {
it('should handle fresh installation flow', async () => {
testEnv.scenarios.freshInstall()
const { result } = renderHook(() => useInstallationStore())
// Start installation
await act(async () => {
await result.current.performInstallation()
})
// Should complete successfully
await waitForInstallationState(() => result.current, 'completed', 1000)
expect(result.current.state).toBe('completed')
})
it('should handle installation with simulation', async () => {
const { result } = renderHook(() => useInstallationStore())
// Start installation manually
act(() => {
result.current.startInstallation()
})
testEnv.electronAPI.onInstallDependenciesStart(() => {
act(() => {
result.current.startInstallation()
})
})
testEnv.electronAPI.onInstallDependenciesLog((data: { type: string; data: string }) => {
act(() => {
result.current.addLog({
type: data.type as 'stdout' | 'stderr',
data: data.data,
timestamp: new Date(),
})
})
})
testEnv.electronAPI.onInstallDependenciesComplete((data: { success: boolean; error?: string }) => {
act(() => {
if (data.success) {
result.current.setSuccess()
} else {
result.current.setError(data.error || 'Installation failed')
}
})
})
console.log("State before success installation, ", result.current)
// Simulate successful installation flow
await testEnv.simulate.successfulInstallation(50)
// Wait for completion
await waitForInstallationState(() => result.current, 'completed', 1000)
console.log("State after success installation, ", result.current)
expect(result.current.state).toBe('completed')
expect(result.current.logs.length).toBeGreaterThan(0)
})
it('should handle installation failure with retry', async () => {
const { result } = renderHook(() => useInstallationStore())
// Start installation
act(() => {
result.current.startInstallation()
})
testEnv.electronAPI.onInstallDependenciesStart(() => {
act(() => {
result.current.startInstallation()
})
})
testEnv.electronAPI.onInstallDependenciesLog((data: { type: string; data: string }) => {
act(() => {
result.current.addLog({
type: data.type as 'stdout' | 'stderr',
data: data.data,
timestamp: new Date(),
})
})
})
testEnv.electronAPI.onInstallDependenciesComplete((data: { success: boolean; error?: string }) => {
act(() => {
if (data.success) {
result.current.setSuccess()
} else {
result.current.setError(data.error || 'Installation failed')
}
})
})
// Simulate failed installation
await testEnv.simulate.failedInstallation(50, 'Network error')
// Wait for error state
await waitForInstallationState(() => result.current, 'error', 1000)
console.log("State after event listened", result.current);
expect(result.current.state).toBe('error')
expect(result.current.error).toBe('Network error')
// Fix the environment and retry
testEnv.scenarios.allGood()
act(() => {
result.current.retryInstallation()
})
// Simulate successful retry
await testEnv.simulate.successfulInstallation(50)
// Should complete successfully
await waitForInstallationState(() => result.current, 'completed', 1000)
expect(result.current.state).toBe('completed')
})
})
describe('State Inspection', () => {
it('should provide useful state inspection', () => {
testEnv.scenarios.freshInstall()
const state = testEnv.inspect.getInstallationState()
expect(state.venvExists).toBe(false)
expect(state.toolsAvailable).toBe(false)
expect(state.isInstalling).toBe(false)
expect(state.hasLockFiles).toBe(false)
})
it('should verify scenario setup', () => {
testEnv.scenarios.versionUpdate()
expect(testEnv.inspect.verifyScenario('versionUpdate')).toBe(true)
testEnv.scenarios.freshInstall()
expect(testEnv.inspect.verifyScenario('freshInstall')).toBe(true)
expect(testEnv.inspect.verifyScenario('versionUpdate')).toBe(false)
})
})
describe('Environment Changes During Tests', () => {
it('should allow changing environment state during test', async () => {
// Start with fresh install
testEnv.scenarios.freshInstall()
expect(testEnv.inspect.getInstallationState().venvExists).toBe(false)
// Simulate .venv being created
testEnv.electronAPI.mockState.venvExists = true
testEnv.mockEnv.mockState.filesystem.venvExists = true
expect(testEnv.inspect.getInstallationState().venvExists).toBe(true)
// Simulate version file being created
testEnv.electronAPI.simulateVersionChange('1.0.0')
testEnv.mockEnv.mockState.filesystem.versionFileExists = true
testEnv.mockEnv.mockState.filesystem.versionFileContent = '1.0.0'
// Now environment should be in 'all good' state
const state = testEnv.inspect.getInstallationState()
expect(state.venvExists).toBe(true)
})
})
})
// You can run this test file with:
// npm test test/unit/examples/installationFlow.test.ts

View file

@ -0,0 +1,404 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { useInstallationSetup } from '../../../src/hooks/useInstallationSetup'
import { useInstallationStore } from '../../../src/store/installationStore'
import { useAuthStore } from '../../../src/store/authStore'
import { setupElectronMocks, TestScenarios, type MockedElectronAPI } from '../../mocks/electronMocks'
// Mock the stores
vi.mock('../../../src/store/installationStore')
vi.mock('../../../src/store/authStore')
describe('useInstallationSetup Hook', () => {
let electronAPI: MockedElectronAPI
let mockInstallationStore: any
let mockAuthStore: any
beforeEach(() => {
// Set up electron mocks
const mocks = setupElectronMocks()
electronAPI = mocks.electronAPI
// Mock installation store
mockInstallationStore = {
startInstallation: vi.fn(),
addLog: vi.fn(),
setSuccess: vi.fn(),
setError: vi.fn(),
}
// Mock auth store
mockAuthStore = {
initState: 'done',
setInitState: vi.fn(),
}
// Set up mock implementations
vi.mocked(useInstallationStore).mockImplementation((selector: any) => {
if (typeof selector === 'function') {
return selector(mockInstallationStore)
}
return mockInstallationStore
})
vi.mocked(useAuthStore).mockReturnValue(mockAuthStore)
// Mock console.log to avoid noise in tests
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
})
afterEach(() => {
vi.clearAllMocks()
electronAPI.reset()
})
describe('Initial Setup', () => {
it('should check tool installation status on mount', async () => {
// Mock IPC response for tool check
electronAPI.mockState.toolInstalled = true
renderHook(() => useInstallationSetup())
await vi.waitFor(() => {
expect(window.ipcRenderer.invoke).toHaveBeenCalledWith('check-tool-installed')
})
})
it('should check backend installation status on mount', async () => {
renderHook(() => useInstallationSetup())
await vi.waitFor(() => {
expect(electronAPI.getInstallationStatus).toHaveBeenCalled()
})
})
it('should start installation if backend installation is in progress', async () => {
electronAPI.mockState.isInstalling = true
renderHook(() => useInstallationSetup())
await vi.waitFor(() => {
expect(mockInstallationStore.startInstallation).toHaveBeenCalled()
})
})
it('should set initState to carousel if tool is not installed', async () => {
// Mock tool not installed
window.ipcRenderer.invoke = vi.fn().mockResolvedValue({
success: true,
isInstalled: false
})
renderHook(() => useInstallationSetup())
await vi.waitFor(() => {
expect(mockAuthStore.setInitState).toHaveBeenCalledWith('carousel')
})
})
})
describe('Electron IPC Event Handling', () => {
it('should register all required event listeners', () => {
renderHook(() => useInstallationSetup())
expect(electronAPI.onInstallDependenciesStart).toHaveBeenCalled()
expect(electronAPI.onInstallDependenciesLog).toHaveBeenCalled()
expect(electronAPI.onInstallDependenciesComplete).toHaveBeenCalled()
})
it('should handle install-dependencies-start event', () => {
renderHook(() => useInstallationSetup())
// Get the registered callback
const startCallback = electronAPI.onInstallDependenciesStart.mock.calls[0][0]
act(() => {
startCallback()
})
expect(mockInstallationStore.startInstallation).toHaveBeenCalled()
})
it('should handle install-dependencies-log event', () => {
renderHook(() => useInstallationSetup())
// Get the registered callback
const logCallback = electronAPI.onInstallDependenciesLog.mock.calls[0][0]
const logData = { type: 'stdout', data: 'Installing packages...' }
act(() => {
logCallback(logData)
})
expect(mockInstallationStore.addLog).toHaveBeenCalledWith({
type: 'stdout',
data: 'Installing packages...',
timestamp: expect.any(Date),
})
})
it('should handle install-dependencies-complete event with success', () => {
renderHook(() => useInstallationSetup())
// Get the registered callback
const completeCallback = electronAPI.onInstallDependenciesComplete.mock.calls[0][0]
const completeData = { success: true }
act(() => {
completeCallback(completeData)
})
expect(mockInstallationStore.setSuccess).toHaveBeenCalled()
expect(mockAuthStore.setInitState).toHaveBeenCalledWith('done')
})
it('should handle install-dependencies-complete event with failure', () => {
renderHook(() => useInstallationSetup())
// Get the registered callback
const completeCallback = electronAPI.onInstallDependenciesComplete.mock.calls[0][0]
const completeData = { success: false, error: 'Installation failed' }
act(() => {
completeCallback(completeData)
})
expect(mockInstallationStore.setError).toHaveBeenCalledWith('Installation failed')
expect(mockAuthStore.setInitState).not.toHaveBeenCalledWith('done')
})
it('should handle complete event without error message', () => {
renderHook(() => useInstallationSetup())
const completeCallback = electronAPI.onInstallDependenciesComplete.mock.calls[0][0]
const completeData = { success: false }
act(() => {
completeCallback(completeData)
})
expect(mockInstallationStore.setError).toHaveBeenCalledWith('Installation failed')
})
})
describe('Event Listener Cleanup', () => {
it('should remove all event listeners on unmount', () => {
const { unmount } = renderHook(() => useInstallationSetup())
unmount()
expect(electronAPI.removeAllListeners).toHaveBeenCalledWith('install-dependencies-start')
expect(electronAPI.removeAllListeners).toHaveBeenCalledWith('install-dependencies-log')
expect(electronAPI.removeAllListeners).toHaveBeenCalledWith('install-dependencies-complete')
})
})
describe('Test Scenarios Integration', () => {
it('should handle fresh installation scenario', async () => {
TestScenarios.freshInstall(electronAPI)
// Mock tool not installed
window.ipcRenderer.invoke = vi.fn().mockResolvedValue({
success: true,
isInstalled: false
})
renderHook(() => useInstallationSetup())
await vi.waitFor(() => {
expect(mockAuthStore.setInitState).toHaveBeenCalledWith('carousel')
})
})
it('should handle version update scenario', async () => {
TestScenarios.versionUpdate(electronAPI)
renderHook(() => useInstallationSetup())
// Simulate version update detection and installation start
electronAPI.simulateInstallationStart()
await vi.waitFor(() => {
expect(mockInstallationStore.startInstallation).toHaveBeenCalled()
})
})
it('should handle venv removed scenario', async () => {
TestScenarios.venvRemoved(electronAPI)
electronAPI.mockState.isInstalling = true
renderHook(() => useInstallationSetup())
await vi.waitFor(() => {
expect(mockInstallationStore.startInstallation).toHaveBeenCalled()
})
})
it('should handle installation in progress scenario', async () => {
TestScenarios.installationInProgress(electronAPI)
renderHook(() => useInstallationSetup())
await vi.waitFor(() => {
expect(mockInstallationStore.startInstallation).toHaveBeenCalled()
})
})
it('should handle uvicorn startup with dependency installation', async () => {
TestScenarios.uvicornDepsInstall(electronAPI)
renderHook(() => useInstallationSetup())
// Simulate uvicorn detecting and installing dependencies
act(() => {
electronAPI.simulateUvicornStartup()
})
await vi.waitFor(() => {
expect(mockInstallationStore.startInstallation).toHaveBeenCalled()
})
// Should receive logs and completion
await vi.waitFor(() => {
expect(mockInstallationStore.addLog).toHaveBeenCalled()
expect(mockInstallationStore.setSuccess).toHaveBeenCalled()
})
})
})
describe('Error Handling', () => {
it('should handle tool installation check failure', async () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
window.ipcRenderer.invoke = vi.fn().mockRejectedValue(new Error('IPC failed'))
renderHook(() => useInstallationSetup())
await vi.waitFor(() => {
expect(consoleErrorSpy).toHaveBeenCalledWith(
'[useInstallationSetup] Tool installation check failed:',
expect.any(Error)
)
})
consoleErrorSpy.mockRestore()
})
it('should handle installation status check failure', async () => {
// Mock console.error to suppress expected error logs
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
electronAPI.getInstallationStatus.mockRejectedValue(new Error('Status check failed'))
renderHook(() => useInstallationSetup())
// Should not crash, should handle the error gracefully
await vi.waitFor(() => {
expect(electronAPI.getInstallationStatus).toHaveBeenCalled()
})
// Wait for error to be logged
await vi.waitFor(() => {
expect(consoleErrorSpy).toHaveBeenCalled()
})
consoleErrorSpy.mockRestore()
})
})
describe('Multiple Hook Instances', () => {
it('should handle multiple hook instances without conflicts', () => {
const { result: hook1 } = renderHook(() => useInstallationSetup())
const { result: hook2 } = renderHook(() => useInstallationSetup())
// Both hooks should register listeners
expect(electronAPI.onInstallDependenciesStart).toHaveBeenCalledTimes(2)
expect(electronAPI.onInstallDependenciesLog).toHaveBeenCalledTimes(2)
expect(electronAPI.onInstallDependenciesComplete).toHaveBeenCalledTimes(2)
})
})
describe('State Dependencies', () => {
it('should react to initState changes', async () => {
mockAuthStore.initState = 'carousel'
const { rerender } = renderHook(() => useInstallationSetup())
// Change initState to 'done'
mockAuthStore.initState = 'done'
rerender()
// Should check tool installation again
await vi.waitFor(() => {
expect(window.ipcRenderer.invoke).toHaveBeenCalledWith('check-tool-installed')
})
})
it('should not set carousel state if initState is not done', async () => {
mockAuthStore.initState = 'loading'
window.ipcRenderer.invoke = vi.fn().mockResolvedValue({
success: true,
isInstalled: false
})
renderHook(() => useInstallationSetup())
await vi.waitFor(() => {
expect(window.ipcRenderer.invoke).toHaveBeenCalledWith('check-tool-installed')
})
// Should not call setInitState because initState is not 'done'
expect(mockAuthStore.setInitState).not.toHaveBeenCalledWith('carousel')
})
})
describe('Console Logging', () => {
it('should log installation status check', async () => {
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
renderHook(() => useInstallationSetup())
await vi.waitFor(() => {
expect(consoleLogSpy).toHaveBeenCalledWith(
'[useInstallationSetup] Installation status check:',
expect.any(Object)
)
})
consoleLogSpy.mockRestore()
})
it('should log when installation listeners are registered', () => {
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
renderHook(() => useInstallationSetup())
expect(consoleLogSpy).toHaveBeenCalledWith(
'[useInstallationSetup] Installation listeners registered'
)
consoleLogSpy.mockRestore()
})
it('should log install complete events', () => {
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
renderHook(() => useInstallationSetup())
const completeCallback = electronAPI.onInstallDependenciesComplete.mock.calls[0][0]
act(() => {
completeCallback({ success: true })
})
expect(consoleLogSpy).toHaveBeenCalledWith(
'[useInstallationSetup] Install complete event received:',
{ success: true }
)
consoleLogSpy.mockRestore()
})
})
})

View file

@ -0,0 +1,420 @@
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
import { act, renderHook } from '@testing-library/react'
import { useInstallationStore, type InstallationState } from '../../../src/store/installationStore'
import { setupElectronMocks, TestScenarios, type MockedElectronAPI } from '../../mocks/electronMocks'
// Mock the authStore import since it's imported dynamically
vi.mock('../../../src/store/authStore', () => ({
useAuthStore: {
getState: () => ({
setInitState: vi.fn()
})
}
}))
describe('Installation Store', () => {
let electronAPI: MockedElectronAPI
let mockSetInitState: ReturnType<typeof vi.fn>
beforeEach(async () => {
// Set up electron mocks
const mocks = setupElectronMocks()
electronAPI = mocks.electronAPI
// Mock the authStore
const { useAuthStore } = await import('../../../src/store/authStore')
mockSetInitState = vi.fn()
useAuthStore.getState = vi.fn().mockReturnValue({
setInitState: mockSetInitState
})
// Reset the store to initial state
useInstallationStore.getState().reset()
})
afterEach(() => {
vi.clearAllMocks()
electronAPI.reset()
})
describe('Initial State', () => {
it('should have correct initial state', () => {
const { result } = renderHook(() => useInstallationStore())
expect(result.current.state).toBe('idle')
expect(result.current.progress).toBe(20)
expect(result.current.logs).toEqual([])
expect(result.current.error).toBeUndefined()
expect(result.current.isVisible).toBe(false)
})
})
describe('State Transitions', () => {
it('should transition from idle to installing when startInstallation is called', () => {
const { result } = renderHook(() => useInstallationStore())
act(() => {
result.current.startInstallation()
})
expect(result.current.state).toBe('installing')
expect(result.current.progress).toBe(20)
expect(result.current.logs).toEqual([])
expect(result.current.error).toBeUndefined()
expect(result.current.isVisible).toBe(true)
})
it('should transition to completed when setSuccess is called', () => {
const { result } = renderHook(() => useInstallationStore())
act(() => {
result.current.startInstallation()
})
act(() => {
result.current.setSuccess()
})
expect(result.current.state).toBe('completed')
expect(result.current.progress).toBe(100)
})
it('should transition to error when setError is called', () => {
const { result } = renderHook(() => useInstallationStore())
const errorMessage = 'Installation failed'
act(() => {
result.current.startInstallation()
})
act(() => {
result.current.setError(errorMessage)
})
expect(result.current.state).toBe('error')
expect(result.current.error).toBe(errorMessage)
expect(result.current.logs).toHaveLength(1)
expect(result.current.logs[0].type).toBe('stderr')
expect(result.current.logs[0].data).toBe(errorMessage)
})
it('should reset to installing state when retryInstallation is called', () => {
const { result } = renderHook(() => useInstallationStore())
// First, set error state
act(() => {
result.current.startInstallation()
})
act(() => {
result.current.setError('Some error')
})
expect(result.current.state).toBe('error')
// Then retry
act(() => {
result.current.retryInstallation()
})
expect(result.current.state).toBe('installing')
expect(result.current.logs).toEqual([])
expect(result.current.error).toBeUndefined()
expect(result.current.isVisible).toBe(true)
})
})
describe('Log Management', () => {
it('should add logs and update progress', () => {
const { result } = renderHook(() => useInstallationStore())
act(() => {
result.current.startInstallation()
})
const initialProgress = result.current.progress
act(() => {
result.current.addLog({
type: 'stdout',
data: 'Installing package...',
timestamp: new Date()
})
})
expect(result.current.logs).toHaveLength(1)
expect(result.current.logs[0].type).toBe('stdout')
expect(result.current.logs[0].data).toBe('Installing package...')
expect(result.current.progress).toBe(initialProgress + 5)
})
it('should not exceed 90% progress when adding logs', () => {
const { result } = renderHook(() => useInstallationStore())
act(() => {
result.current.startInstallation()
})
// Add many logs to test progress cap
act(() => {
for (let i = 0; i < 20; i++) {
result.current.addLog({
type: 'stdout',
data: `Log entry ${i}`,
timestamp: new Date()
})
}
})
expect(result.current.progress).toBe(90)
expect(result.current.logs).toHaveLength(20)
})
})
describe('Installation Flow Integration', () => {
it('should handle successful installation flow', async () => {
TestScenarios.versionUpdate(electronAPI)
const { result } = renderHook(() => useInstallationStore())
// Start installation
await act(async () => {
await result.current.performInstallation()
})
// Wait for the mocked installation to complete
await vi.waitFor(() => {
expect(result.current.state).toBe('completed')
}, { timeout: 1000 })
expect(electronAPI.checkAndInstallDepsOnUpdate).toHaveBeenCalled()
expect(mockSetInitState).toHaveBeenCalledWith('done')
})
it('should handle installation failure', async () => {
TestScenarios.installationError(electronAPI)
const { result } = renderHook(() => useInstallationStore())
await act(async () => {
await result.current.performInstallation()
})
// Wait for the mocked installation to fail
await vi.waitFor(() => {
expect(result.current.state).toBe('error')
}, { timeout: 1000 })
expect(result.current.error).toBe('Installation failed')
})
it('should handle fresh installation scenario', async () => {
TestScenarios.freshInstall(electronAPI)
const { result } = renderHook(() => useInstallationStore())
await act(async () => {
await result.current.performInstallation()
})
await vi.waitFor(() => {
expect(result.current.state).toBe('completed')
}, { timeout: 1000 })
expect(electronAPI.checkAndInstallDepsOnUpdate).toHaveBeenCalled()
})
})
describe('Log Export', () => {
it('should export logs successfully', async () => {
const { result } = renderHook(() => useInstallationStore())
// Mock window.location.href
const originalLocation = window.location
Object.defineProperty(window, 'location', {
value: { href: '' },
writable: true
})
// Mock alert
const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {})
await act(async () => {
await result.current.exportLog()
})
expect(electronAPI.exportLog).toHaveBeenCalled()
expect(alertSpy).toHaveBeenCalledWith('Log saved: /mock/path/to/log.txt')
expect(window.location.href).toBe('https://github.com/eigent-ai/eigent/issues/new/choose')
// Restore
Object.defineProperty(window, 'location', {
value: originalLocation,
writable: true
})
alertSpy.mockRestore()
})
it('should handle export failure', async () => {
electronAPI.exportLog.mockResolvedValue({
success: false,
error: 'Export failed'
})
const { result } = renderHook(() => useInstallationStore())
const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {})
await act(async () => {
await result.current.exportLog()
})
expect(alertSpy).toHaveBeenCalledWith('Export cancelled: Export failed')
alertSpy.mockRestore()
})
})
describe('Computed Selectors', () => {
it('useLatestLog should return the most recent log', () => {
const { result: storeResult } = renderHook(() => useInstallationStore())
const { result: latestLogResult } = renderHook(() => useInstallationStore((state: any) =>
state.logs[state.logs.length - 1]
))
expect(latestLogResult.current).toBeUndefined()
act(() => {
storeResult.current.startInstallation()
storeResult.current.addLog({
type: 'stdout',
data: 'First log',
timestamp: new Date()
})
storeResult.current.addLog({
type: 'stderr',
data: 'Latest log',
timestamp: new Date()
})
})
expect(latestLogResult.current.data).toBe('Latest log')
expect(latestLogResult.current.type).toBe('stderr')
})
it('useInstallationStatus should return correct status', () => {
const { result: storeResult } = renderHook(() => useInstallationStore())
const { result: statusResult } = renderHook(() => {
const state = useInstallationStore((state: any) => state.state)
const isVisible = useInstallationStore((state: any) => state.isVisible)
return {
isInstalling: state === 'installing',
installationState: state,
shouldShowInstallScreen: isVisible && state !== 'completed',
isInstallationComplete: state === 'completed',
canRetry: state === 'error',
}
})
// Initial state
expect(statusResult.current.isInstalling).toBe(false)
expect(statusResult.current.installationState).toBe('idle')
expect(statusResult.current.shouldShowInstallScreen).toBe(false)
expect(statusResult.current.isInstallationComplete).toBe(false)
expect(statusResult.current.canRetry).toBe(false)
// Installing state
act(() => {
storeResult.current.startInstallation()
})
expect(statusResult.current.isInstalling).toBe(true)
expect(statusResult.current.shouldShowInstallScreen).toBe(true)
expect(statusResult.current.canRetry).toBe(false)
// Error state
act(() => {
storeResult.current.setError('Some error')
})
expect(statusResult.current.isInstalling).toBe(false)
expect(statusResult.current.canRetry).toBe(true)
// Completed state
act(() => {
storeResult.current.setSuccess()
})
expect(statusResult.current.isInstallationComplete).toBe(true)
expect(statusResult.current.shouldShowInstallScreen).toBe(false)
})
})
describe('Edge Cases', () => {
it('should handle multiple rapid state changes', () => {
const { result } = renderHook(() => useInstallationStore())
act(() => {
result.current.startInstallation()
result.current.setError('Error 1')
result.current.retryInstallation()
result.current.setSuccess()
})
expect(result.current.state).toBe('completed')
expect(result.current.progress).toBe(100)
})
it('should handle visibility changes correctly', () => {
const { result } = renderHook(() => useInstallationStore())
expect(result.current.isVisible).toBe(false)
act(() => {
result.current.setVisible(true)
})
expect(result.current.isVisible).toBe(true)
act(() => {
result.current.completeSetup()
})
expect(result.current.state).toBe('completed')
expect(result.current.isVisible).toBe(false)
})
it('should handle manual progress updates', () => {
const { result } = renderHook(() => useInstallationStore())
act(() => {
result.current.updateProgress(75)
})
expect(result.current.progress).toBe(75)
})
})
describe('Installation State Sequence', () => {
it('should follow correct state sequence for normal installation', async () => {
const { result } = renderHook(() => useInstallationStore())
const states: InstallationState[] = []
// Subscribe to state changes
useInstallationStore.subscribe((state: any) => {
states.push(state.state)
})
await act(async () => {
await result.current.performInstallation()
})
await vi.waitFor(() => {
expect(result.current.state).toBe('completed')
}, { timeout: 1000 })
// Should have progressed through: idle -> installing -> completed
expect(states).toContain('installing')
expect(states).toContain('completed')
})
})
})

View file

@ -23,7 +23,6 @@ export default defineConfig({
'test/',
'dist/',
'dist-electron/',
'electron/',
'build/',
'**/*.d.ts',
'**/*.config.*',