mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-09 19:45:41 +00:00
feat(test): unit test install logic main process (#449)
This commit is contained in:
commit
02ac7dc0b1
17 changed files with 5254 additions and 19 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
423
test/README.md
Normal 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
467
test/mocks/electronMocks.ts
Normal 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
|
||||
}
|
||||
}
|
||||
687
test/mocks/environmentMocks.ts
Normal file
687
test/mocks/environmentMocks.ts
Normal 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
282
test/mocks/testUtils.ts
Normal 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)
|
||||
* })
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
609
test/unit/electron/install-deps.test.ts
Normal file
609
test/unit/electron/install-deps.test.ts
Normal 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'
|
||||
// })
|
||||
// )
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
539
test/unit/electron/main/domReadyHandlers.test.ts
Normal file
539
test/unit/electron/main/domReadyHandlers.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
357
test/unit/electron/main/installationStateLogic.test.ts
Normal file
357
test/unit/electron/main/installationStateLogic.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
199
test/unit/electron/main/processUtilsDemo.test.ts
Normal file
199
test/unit/electron/main/processUtilsDemo.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
407
test/unit/electron/main/windowLifecycle.test.ts
Normal file
407
test/unit/electron/main/windowLifecycle.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
382
test/unit/examples/installationFlow.test.ts
Normal file
382
test/unit/examples/installationFlow.test.ts
Normal 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
|
||||
404
test/unit/hooks/useInstallationSetup.test.ts
Normal file
404
test/unit/hooks/useInstallationSetup.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
420
test/unit/store/installationStore.test.ts
Normal file
420
test/unit/store/installationStore.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -23,7 +23,6 @@ export default defineConfig({
|
|||
'test/',
|
||||
'dist/',
|
||||
'dist-electron/',
|
||||
'electron/',
|
||||
'build/',
|
||||
'**/*.d.ts',
|
||||
'**/*.config.*',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue