mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-18 01:51:20 -07:00
332 lines
8.8 KiB
TypeScript
332 lines
8.8 KiB
TypeScript
|
|
/**
|
||
|
|
* @license
|
||
|
|
* Copyright 2025 Google LLC
|
||
|
|
* SPDX-License-Identifier: Apache-2.0
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||
|
|
import { render, cleanup } from 'ink-testing-library';
|
||
|
|
import { AppContainer } from './AppContainer.js';
|
||
|
|
import { type Config, makeFakeConfig } from '@google/gemini-cli-core';
|
||
|
|
import type { LoadedSettings } from '../config/settings.js';
|
||
|
|
import type { InitializationResult } from '../core/initializer.js';
|
||
|
|
|
||
|
|
// Mock App component to isolate AppContainer testing
|
||
|
|
vi.mock('./App.js', () => ({
|
||
|
|
App: () => 'App Component',
|
||
|
|
}));
|
||
|
|
|
||
|
|
// Mock all the hooks and utilities
|
||
|
|
vi.mock('./hooks/useHistory.js');
|
||
|
|
vi.mock('./hooks/useThemeCommand.js');
|
||
|
|
vi.mock('./hooks/useAuthCommand.js');
|
||
|
|
vi.mock('./hooks/useEditorSettings.js');
|
||
|
|
vi.mock('./hooks/useSettingsCommand.js');
|
||
|
|
vi.mock('./hooks/useSlashCommandProcessor.js');
|
||
|
|
vi.mock('./hooks/useConsoleMessages.js');
|
||
|
|
vi.mock('./hooks/useTerminalSize.js', () => ({
|
||
|
|
useTerminalSize: vi.fn(() => ({ columns: 80, rows: 24 })),
|
||
|
|
}));
|
||
|
|
vi.mock('./hooks/useGeminiStream.js');
|
||
|
|
vi.mock('./hooks/useVim.js');
|
||
|
|
vi.mock('./hooks/useFocus.js');
|
||
|
|
vi.mock('./hooks/useBracketedPaste.js');
|
||
|
|
vi.mock('./hooks/useKeypress.js');
|
||
|
|
vi.mock('./hooks/useLoadingIndicator.js');
|
||
|
|
vi.mock('./hooks/useFolderTrust.js');
|
||
|
|
vi.mock('./hooks/useMessageQueue.js');
|
||
|
|
vi.mock('./hooks/useAutoAcceptIndicator.js');
|
||
|
|
vi.mock('./hooks/useWorkspaceMigration.js');
|
||
|
|
vi.mock('./hooks/useGitBranchName.js');
|
||
|
|
vi.mock('./contexts/VimModeContext.js');
|
||
|
|
vi.mock('./contexts/SessionContext.js');
|
||
|
|
vi.mock('./hooks/useTextBuffer.js');
|
||
|
|
vi.mock('./hooks/useLogger.js');
|
||
|
|
|
||
|
|
// Mock external utilities
|
||
|
|
vi.mock('../utils/events.js');
|
||
|
|
vi.mock('../utils/handleAutoUpdate.js');
|
||
|
|
vi.mock('./utils/ConsolePatcher.js');
|
||
|
|
vi.mock('../utils/cleanup.js');
|
||
|
|
|
||
|
|
describe('AppContainer State Management', () => {
|
||
|
|
let mockConfig: Config;
|
||
|
|
let mockSettings: LoadedSettings;
|
||
|
|
let mockInitResult: InitializationResult;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
vi.clearAllMocks();
|
||
|
|
|
||
|
|
// Mock Config
|
||
|
|
mockConfig = makeFakeConfig();
|
||
|
|
|
||
|
|
// Mock LoadedSettings
|
||
|
|
mockSettings = {
|
||
|
|
merged: {
|
||
|
|
hideBanner: false,
|
||
|
|
hideFooter: false,
|
||
|
|
hideTips: false,
|
||
|
|
showMemoryUsage: false,
|
||
|
|
theme: 'default',
|
||
|
|
},
|
||
|
|
} as unknown as LoadedSettings;
|
||
|
|
|
||
|
|
// Mock InitializationResult
|
||
|
|
mockInitResult = {
|
||
|
|
themeError: null,
|
||
|
|
authError: null,
|
||
|
|
shouldOpenAuthDialog: false,
|
||
|
|
geminiMdFileCount: 0,
|
||
|
|
} as InitializationResult;
|
||
|
|
});
|
||
|
|
|
||
|
|
afterEach(() => {
|
||
|
|
cleanup();
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Basic Rendering', () => {
|
||
|
|
it('renders without crashing with minimal props', () => {
|
||
|
|
expect(() => {
|
||
|
|
render(
|
||
|
|
<AppContainer
|
||
|
|
config={mockConfig}
|
||
|
|
settings={mockSettings}
|
||
|
|
version="1.0.0"
|
||
|
|
initializationResult={mockInitResult}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
}).not.toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('renders with startup warnings', () => {
|
||
|
|
const startupWarnings = ['Warning 1', 'Warning 2'];
|
||
|
|
|
||
|
|
expect(() => {
|
||
|
|
render(
|
||
|
|
<AppContainer
|
||
|
|
config={mockConfig}
|
||
|
|
settings={mockSettings}
|
||
|
|
startupWarnings={startupWarnings}
|
||
|
|
version="1.0.0"
|
||
|
|
initializationResult={mockInitResult}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
}).not.toThrow();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('State Initialization', () => {
|
||
|
|
it('initializes with theme error from initialization result', () => {
|
||
|
|
const initResultWithError = {
|
||
|
|
...mockInitResult,
|
||
|
|
themeError: 'Failed to load theme',
|
||
|
|
};
|
||
|
|
|
||
|
|
expect(() => {
|
||
|
|
render(
|
||
|
|
<AppContainer
|
||
|
|
config={mockConfig}
|
||
|
|
settings={mockSettings}
|
||
|
|
version="1.0.0"
|
||
|
|
initializationResult={initResultWithError}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
}).not.toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles debug mode state', () => {
|
||
|
|
const debugConfig = makeFakeConfig();
|
||
|
|
vi.spyOn(debugConfig, 'getDebugMode').mockReturnValue(true);
|
||
|
|
|
||
|
|
expect(() => {
|
||
|
|
render(
|
||
|
|
<AppContainer
|
||
|
|
config={debugConfig}
|
||
|
|
settings={mockSettings}
|
||
|
|
version="1.0.0"
|
||
|
|
initializationResult={mockInitResult}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
}).not.toThrow();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Context Providers', () => {
|
||
|
|
it('provides AppContext with correct values', () => {
|
||
|
|
const { unmount } = render(
|
||
|
|
<AppContainer
|
||
|
|
config={mockConfig}
|
||
|
|
settings={mockSettings}
|
||
|
|
version="2.0.0"
|
||
|
|
initializationResult={mockInitResult}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
|
||
|
|
// Should render and unmount cleanly
|
||
|
|
expect(() => unmount()).not.toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('provides UIStateContext with state management', () => {
|
||
|
|
expect(() => {
|
||
|
|
render(
|
||
|
|
<AppContainer
|
||
|
|
config={mockConfig}
|
||
|
|
settings={mockSettings}
|
||
|
|
version="1.0.0"
|
||
|
|
initializationResult={mockInitResult}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
}).not.toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('provides UIActionsContext with action handlers', () => {
|
||
|
|
expect(() => {
|
||
|
|
render(
|
||
|
|
<AppContainer
|
||
|
|
config={mockConfig}
|
||
|
|
settings={mockSettings}
|
||
|
|
version="1.0.0"
|
||
|
|
initializationResult={mockInitResult}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
}).not.toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('provides ConfigContext with config object', () => {
|
||
|
|
expect(() => {
|
||
|
|
render(
|
||
|
|
<AppContainer
|
||
|
|
config={mockConfig}
|
||
|
|
settings={mockSettings}
|
||
|
|
version="1.0.0"
|
||
|
|
initializationResult={mockInitResult}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
}).not.toThrow();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Settings Integration', () => {
|
||
|
|
it('handles settings with all display options disabled', () => {
|
||
|
|
const settingsAllHidden = {
|
||
|
|
merged: {
|
||
|
|
hideBanner: true,
|
||
|
|
hideFooter: true,
|
||
|
|
hideTips: true,
|
||
|
|
showMemoryUsage: false,
|
||
|
|
},
|
||
|
|
} as unknown as LoadedSettings;
|
||
|
|
|
||
|
|
expect(() => {
|
||
|
|
render(
|
||
|
|
<AppContainer
|
||
|
|
config={mockConfig}
|
||
|
|
settings={settingsAllHidden}
|
||
|
|
version="1.0.0"
|
||
|
|
initializationResult={mockInitResult}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
}).not.toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles settings with memory usage enabled', () => {
|
||
|
|
const settingsWithMemory = {
|
||
|
|
merged: {
|
||
|
|
hideBanner: false,
|
||
|
|
hideFooter: false,
|
||
|
|
hideTips: false,
|
||
|
|
showMemoryUsage: true,
|
||
|
|
},
|
||
|
|
} as unknown as LoadedSettings;
|
||
|
|
|
||
|
|
expect(() => {
|
||
|
|
render(
|
||
|
|
<AppContainer
|
||
|
|
config={mockConfig}
|
||
|
|
settings={settingsWithMemory}
|
||
|
|
version="1.0.0"
|
||
|
|
initializationResult={mockInitResult}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
}).not.toThrow();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Version Handling', () => {
|
||
|
|
it('handles different version formats', () => {
|
||
|
|
const versions = ['1.0.0', '2.1.3-beta', '3.0.0-nightly'];
|
||
|
|
|
||
|
|
versions.forEach((version) => {
|
||
|
|
expect(() => {
|
||
|
|
render(
|
||
|
|
<AppContainer
|
||
|
|
config={mockConfig}
|
||
|
|
settings={mockSettings}
|
||
|
|
version={version}
|
||
|
|
initializationResult={mockInitResult}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
}).not.toThrow();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Error Handling', () => {
|
||
|
|
it('handles config methods that might throw', () => {
|
||
|
|
const errorConfig = makeFakeConfig();
|
||
|
|
vi.spyOn(errorConfig, 'getModel').mockImplementation(() => {
|
||
|
|
throw new Error('Config error');
|
||
|
|
});
|
||
|
|
|
||
|
|
// Should still render without crashing - errors should be handled internally
|
||
|
|
expect(() => {
|
||
|
|
render(
|
||
|
|
<AppContainer
|
||
|
|
config={errorConfig}
|
||
|
|
settings={mockSettings}
|
||
|
|
version="1.0.0"
|
||
|
|
initializationResult={mockInitResult}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
}).not.toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles undefined settings gracefully', () => {
|
||
|
|
const undefinedSettings = {
|
||
|
|
merged: {},
|
||
|
|
} as LoadedSettings;
|
||
|
|
|
||
|
|
expect(() => {
|
||
|
|
render(
|
||
|
|
<AppContainer
|
||
|
|
config={mockConfig}
|
||
|
|
settings={undefinedSettings}
|
||
|
|
version="1.0.0"
|
||
|
|
initializationResult={mockInitResult}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
}).not.toThrow();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Provider Hierarchy', () => {
|
||
|
|
it('establishes correct provider nesting order', () => {
|
||
|
|
// This tests that all the context providers are properly nested
|
||
|
|
// and that the component tree can be built without circular dependencies
|
||
|
|
const { unmount } = render(
|
||
|
|
<AppContainer
|
||
|
|
config={mockConfig}
|
||
|
|
settings={mockSettings}
|
||
|
|
version="1.0.0"
|
||
|
|
initializationResult={mockInitResult}
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(() => unmount()).not.toThrow();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// TODO: Add comprehensive integration test once all hook mocks are complete
|
||
|
|
// For now, the 14 passing unit tests provide good coverage of AppContainer functionality
|