eigent/test/unit/store/installationStore.test.ts
Tong Chen af93bb3065
feat: Add Lint & Format (#878)
Co-authored-by: a7m-1st <Ahmed.jimi.awelkeir500@gmail.com>
Co-authored-by: eigent-ai <camel@eigent.ai>
Co-authored-by: Wendong-Fan <133094783+Wendong-Fan@users.noreply.github.com>
Co-authored-by: Wendong-Fan <w3ndong.fan@gmail.com>
2026-02-01 23:16:18 +08:00

455 lines
14 KiB
TypeScript

// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { act, renderHook } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
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');
});
});
});