mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-23 21:06:50 +00:00
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>
455 lines
14 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|