Improve test coverage for cli/src/ui/components (#13598)

This commit is contained in:
Megha Bansal
2025-11-22 08:17:29 +05:30
committed by GitHub
parent bdf80ea7c0
commit e205a468d9
48 changed files with 2897 additions and 51 deletions

View File

@@ -0,0 +1,57 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { AboutBox } from './AboutBox.js';
import { describe, it, expect, vi } from 'vitest';
// Mock GIT_COMMIT_INFO
vi.mock('../../generated/git-commit.js', () => ({
GIT_COMMIT_INFO: 'mock-commit-hash',
}));
describe('AboutBox', () => {
const defaultProps = {
cliVersion: '1.0.0',
osVersion: 'macOS',
sandboxEnv: 'default',
modelVersion: 'gemini-pro',
selectedAuthType: 'oauth',
gcpProject: '',
ideClient: '',
};
it('renders with required props', () => {
const { lastFrame } = render(<AboutBox {...defaultProps} />);
const output = lastFrame();
expect(output).toContain('About Gemini CLI');
expect(output).toContain('1.0.0');
expect(output).toContain('mock-commit-hash');
expect(output).toContain('gemini-pro');
expect(output).toContain('default');
expect(output).toContain('macOS');
expect(output).toContain('OAuth');
});
it.each([
['userEmail', 'test@example.com', 'User Email'],
['gcpProject', 'my-project', 'GCP Project'],
['ideClient', 'vscode', 'IDE Client'],
])('renders optional prop %s', (prop, value, label) => {
const props = { ...defaultProps, [prop]: value };
const { lastFrame } = render(<AboutBox {...props} />);
const output = lastFrame();
expect(output).toContain(label);
expect(output).toContain(value);
});
it('renders Auth Method correctly when not oauth', () => {
const props = { ...defaultProps, selectedAuthType: 'api-key' };
const { lastFrame } = render(<AboutBox {...props} />);
const output = lastFrame();
expect(output).toContain('api-key');
});
});

View File

@@ -0,0 +1,39 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { AutoAcceptIndicator } from './AutoAcceptIndicator.js';
import { describe, it, expect } from 'vitest';
import { ApprovalMode } from '@google/gemini-cli-core';
describe('AutoAcceptIndicator', () => {
it('renders correctly for AUTO_EDIT mode', () => {
const { lastFrame } = render(
<AutoAcceptIndicator approvalMode={ApprovalMode.AUTO_EDIT} />,
);
const output = lastFrame();
expect(output).toContain('accepting edits');
expect(output).toContain('(shift + tab to toggle)');
});
it('renders correctly for YOLO mode', () => {
const { lastFrame } = render(
<AutoAcceptIndicator approvalMode={ApprovalMode.YOLO} />,
);
const output = lastFrame();
expect(output).toContain('YOLO mode');
expect(output).toContain('(ctrl + y to toggle)');
});
it('renders nothing for DEFAULT mode', () => {
const { lastFrame } = render(
<AutoAcceptIndicator approvalMode={ApprovalMode.DEFAULT} />,
);
const output = lastFrame();
expect(output).not.toContain('accepting edits');
expect(output).not.toContain('YOLO mode');
});
});

View File

@@ -0,0 +1,29 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { Banner } from './Banner.js';
import { describe, it, expect } from 'vitest';
describe('Banner', () => {
it.each([
['warning mode', true, 'Warning Message'],
['info mode', false, 'Info Message'],
])('renders in %s', (_, isWarning, text) => {
const { lastFrame } = render(
<Banner bannerText={text} isWarning={isWarning} width={80} />,
);
expect(lastFrame()).toMatchSnapshot();
});
it('handles newlines in text', () => {
const text = 'Line 1\\nLine 2';
const { lastFrame } = render(
<Banner bannerText={text} isWarning={false} width={80} />,
);
expect(lastFrame()).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,117 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { act } from 'react';
import { render } from '../../test-utils/render.js';
import { ConfigInitDisplay } from './ConfigInitDisplay.js';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { AppEvent } from '../../utils/events.js';
import { MCPServerStatus, type McpClient } from '@google/gemini-cli-core';
import { Text } from 'ink';
// Mock GeminiSpinner
vi.mock('./GeminiRespondingSpinner.js', () => ({
GeminiSpinner: () => <Text>Spinner</Text>,
}));
// Mock appEvents
const { mockOn, mockOff, mockEmit } = vi.hoisted(() => ({
mockOn: vi.fn(),
mockOff: vi.fn(),
mockEmit: vi.fn(),
}));
vi.mock('../../utils/events.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../utils/events.js')>();
return {
...actual,
appEvents: {
on: mockOn,
off: mockOff,
emit: mockEmit,
},
};
});
describe('ConfigInitDisplay', () => {
beforeEach(() => {
mockOn.mockClear();
mockOff.mockClear();
mockEmit.mockClear();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('renders initial state', () => {
const { lastFrame } = render(<ConfigInitDisplay />);
expect(lastFrame()).toMatchSnapshot();
});
it('updates message on McpClientUpdate event', async () => {
let listener: ((clients?: Map<string, McpClient>) => void) | undefined;
mockOn.mockImplementation((event, fn) => {
if (event === AppEvent.McpClientUpdate) {
listener = fn;
}
});
const { lastFrame } = render(<ConfigInitDisplay />);
// Wait for listener to be registered
await vi.waitFor(() => {
if (!listener) throw new Error('Listener not registered yet');
});
const mockClient1 = {
getStatus: () => MCPServerStatus.CONNECTED,
} as McpClient;
const mockClient2 = {
getStatus: () => MCPServerStatus.CONNECTING,
} as McpClient;
const clients = new Map<string, McpClient>([
['server1', mockClient1],
['server2', mockClient2],
]);
// Trigger the listener manually since we mocked the event emitter
act(() => {
listener!(clients);
});
// Wait for the UI to update
await vi.waitFor(() => {
expect(lastFrame()).toMatchSnapshot();
});
});
it('handles empty clients map', async () => {
let listener: ((clients?: Map<string, McpClient>) => void) | undefined;
mockOn.mockImplementation((event, fn) => {
if (event === AppEvent.McpClientUpdate) {
listener = fn;
}
});
const { lastFrame } = render(<ConfigInitDisplay />);
await vi.waitFor(() => {
if (!listener) throw new Error('Listener not registered yet');
});
if (listener) {
const safeListener = listener;
act(() => {
safeListener(new Map());
});
}
await vi.waitFor(() => {
expect(lastFrame()).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,27 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
import { describe, it, expect } from 'vitest';
describe('ConsoleSummaryDisplay', () => {
it('renders nothing when errorCount is 0', () => {
const { lastFrame } = render(<ConsoleSummaryDisplay errorCount={0} />);
expect(lastFrame()).toBe('');
});
it.each([
[1, '1 error'],
[5, '5 errors'],
])('renders correct message for %i errors', (count, expectedText) => {
const { lastFrame } = render(<ConsoleSummaryDisplay errorCount={count} />);
const output = lastFrame();
expect(output).toContain(expectedText);
expect(output).toContain('✖');
expect(output).toContain('(F12 for details)');
});
});

View File

@@ -0,0 +1,61 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
import { describe, it, expect, vi } from 'vitest';
vi.mock('@google/gemini-cli-core', () => ({
tokenLimit: () => 10000,
}));
vi.mock('../../config/settings.js', () => ({
DEFAULT_MODEL_CONFIGS: {},
LoadedSettings: class {
constructor() {
// this.merged = {};
}
},
}));
describe('ContextUsageDisplay', () => {
it('renders correct percentage left', () => {
const { lastFrame } = render(
<ContextUsageDisplay
promptTokenCount={5000}
model="gemini-pro"
terminalWidth={120}
/>,
);
const output = lastFrame();
expect(output).toContain('50% context left');
});
it('renders short label when terminal width is small', () => {
const { lastFrame } = render(
<ContextUsageDisplay
promptTokenCount={2000}
model="gemini-pro"
terminalWidth={80}
/>,
);
const output = lastFrame();
expect(output).toContain('80%');
expect(output).not.toContain('context left');
});
it('renders 0% when full', () => {
const { lastFrame } = render(
<ContextUsageDisplay
promptTokenCount={10000}
model="gemini-pro"
terminalWidth={120}
/>,
);
const output = lastFrame();
expect(output).toContain('0% context left');
});
});

View File

@@ -0,0 +1,37 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { CopyModeWarning } from './CopyModeWarning.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useUIState, type UIState } from '../contexts/UIStateContext.js';
vi.mock('../contexts/UIStateContext.js');
describe('CopyModeWarning', () => {
const mockUseUIState = vi.mocked(useUIState);
beforeEach(() => {
vi.clearAllMocks();
});
it('renders nothing when copy mode is disabled', () => {
mockUseUIState.mockReturnValue({
copyModeEnabled: false,
} as unknown as UIState);
const { lastFrame } = render(<CopyModeWarning />);
expect(lastFrame()).toBe('');
});
it('renders warning when copy mode is enabled', () => {
mockUseUIState.mockReturnValue({
copyModeEnabled: true,
} as unknown as UIState);
const { lastFrame } = render(<CopyModeWarning />);
expect(lastFrame()).toContain('In Copy Mode');
expect(lastFrame()).toContain('Press any key to exit');
});
});

View File

@@ -8,12 +8,19 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { appEvents, AppEvent } from '../../utils/events.js';
import {
profiler,
DebugProfiler,
ACTION_TIMESTAMP_CAPACITY,
FRAME_TIMESTAMP_CAPACITY,
} from './DebugProfiler.js';
import { render } from '../../test-utils/render.js';
import { useUIState, type UIState } from '../contexts/UIStateContext.js';
import { FixedDeque } from 'mnemonist';
import { debugState } from '../debug.js';
vi.mock('../contexts/UIStateContext.js', () => ({
useUIState: vi.fn(),
}));
describe('DebugProfiler', () => {
beforeEach(() => {
vi.useFakeTimers();
@@ -214,3 +221,49 @@ describe('DebugProfiler', () => {
expect(profiler.totalIdleFrames).toBe(0);
});
});
describe('DebugProfiler Component', () => {
beforeEach(() => {
// Reset the mock implementation before each test
vi.mocked(useUIState).mockReturnValue({
showDebugProfiler: false,
constrainHeight: false,
} as unknown as UIState);
// Mock process.stdin and stdout
// We need to be careful not to break the test runner's own output
// So we might want to skip mocking them if they are not strictly needed for the simple render test
// or mock them safely.
// For now, let's assume the component uses them in useEffect.
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should return null when showDebugProfiler is false', () => {
vi.mocked(useUIState).mockReturnValue({
showDebugProfiler: false,
constrainHeight: false,
} as unknown as UIState);
const { lastFrame } = render(<DebugProfiler />);
expect(lastFrame()).toBe('');
});
it('should render stats when showDebugProfiler is true', () => {
vi.mocked(useUIState).mockReturnValue({
showDebugProfiler: true,
constrainHeight: false,
} as unknown as UIState);
profiler.numFrames = 10;
profiler.totalIdleFrames = 5;
profiler.totalFlickerFrames = 2;
const { lastFrame } = render(<DebugProfiler />);
const output = lastFrame();
expect(output).toContain('Renders: 10 (total)');
expect(output).toContain('5 (idle)');
expect(output).toContain('2 (flicker)');
});
});

View File

@@ -0,0 +1,92 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { DetailedMessagesDisplay } from './DetailedMessagesDisplay.js';
import { describe, it, expect, vi } from 'vitest';
import type { ConsoleMessageItem } from '../types.js';
import { Box } from 'ink';
import type React from 'react';
vi.mock('./shared/ScrollableList.js', () => ({
ScrollableList: ({
data,
renderItem,
}: {
data: unknown[];
renderItem: (props: { item: unknown }) => React.ReactNode;
}) => (
<Box flexDirection="column">
{data.map((item: unknown, index: number) => (
<Box key={index}>{renderItem({ item })}</Box>
))}
</Box>
),
}));
describe('DetailedMessagesDisplay', () => {
it('renders nothing when messages are empty', () => {
const { lastFrame } = render(
<DetailedMessagesDisplay
messages={[]}
maxHeight={10}
width={80}
hasFocus={false}
/>,
);
expect(lastFrame()).toBe('');
});
it('renders messages correctly', () => {
const messages: ConsoleMessageItem[] = [
{ type: 'log', content: 'Log message', count: 1 },
{ type: 'warn', content: 'Warning message', count: 1 },
{ type: 'error', content: 'Error message', count: 1 },
{ type: 'debug', content: 'Debug message', count: 1 },
];
const { lastFrame } = render(
<DetailedMessagesDisplay
messages={messages}
maxHeight={20}
width={80}
hasFocus={true}
/>,
);
const output = lastFrame();
expect(output).toContain('Debug Console');
expect(output).toContain('Log message');
expect(output).toContain('Warning message');
expect(output).toContain('Error message');
expect(output).toContain('Debug message');
// Check for icons
expect(output).toContain('');
expect(output).toContain('⚠');
expect(output).toContain('✖');
expect(output).toContain('🔍');
});
it('renders message counts', () => {
const messages: ConsoleMessageItem[] = [
{ type: 'log', content: 'Repeated message', count: 5 },
];
const { lastFrame } = render(
<DetailedMessagesDisplay
messages={messages}
maxHeight={10}
width={80}
hasFocus={false}
/>,
);
const output = lastFrame();
expect(output).toContain('Repeated message');
expect(output).toContain('(x5)');
});
});

View File

@@ -0,0 +1,180 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { renderWithProviders } from '../../test-utils/render.js';
import { DialogManager } from './DialogManager.js';
import { describe, it, expect, vi } from 'vitest';
import { Text } from 'ink';
import { type UIState } from '../contexts/UIStateContext.js';
import { type RestartReason } from '../hooks/useIdeTrustListener.js';
import { type IdeInfo } from '@google/gemini-cli-core';
import { type ShellConfirmationRequest } from '../types.js';
// Mock child components
vi.mock('../IdeIntegrationNudge.js', () => ({
IdeIntegrationNudge: () => <Text>IdeIntegrationNudge</Text>,
}));
vi.mock('./LoopDetectionConfirmation.js', () => ({
LoopDetectionConfirmation: () => <Text>LoopDetectionConfirmation</Text>,
}));
vi.mock('./FolderTrustDialog.js', () => ({
FolderTrustDialog: () => <Text>FolderTrustDialog</Text>,
}));
vi.mock('./ShellConfirmationDialog.js', () => ({
ShellConfirmationDialog: () => <Text>ShellConfirmationDialog</Text>,
}));
vi.mock('./ConsentPrompt.js', () => ({
ConsentPrompt: () => <Text>ConsentPrompt</Text>,
}));
vi.mock('./ThemeDialog.js', () => ({
ThemeDialog: () => <Text>ThemeDialog</Text>,
}));
vi.mock('./SettingsDialog.js', () => ({
SettingsDialog: () => <Text>SettingsDialog</Text>,
}));
vi.mock('../auth/AuthInProgress.js', () => ({
AuthInProgress: () => <Text>AuthInProgress</Text>,
}));
vi.mock('../auth/AuthDialog.js', () => ({
AuthDialog: () => <Text>AuthDialog</Text>,
}));
vi.mock('../auth/ApiAuthDialog.js', () => ({
ApiAuthDialog: () => <Text>ApiAuthDialog</Text>,
}));
vi.mock('./EditorSettingsDialog.js', () => ({
EditorSettingsDialog: () => <Text>EditorSettingsDialog</Text>,
}));
vi.mock('../privacy/PrivacyNotice.js', () => ({
PrivacyNotice: () => <Text>PrivacyNotice</Text>,
}));
vi.mock('./ProQuotaDialog.js', () => ({
ProQuotaDialog: () => <Text>ProQuotaDialog</Text>,
}));
vi.mock('./PermissionsModifyTrustDialog.js', () => ({
PermissionsModifyTrustDialog: () => <Text>PermissionsModifyTrustDialog</Text>,
}));
vi.mock('./ModelDialog.js', () => ({
ModelDialog: () => <Text>ModelDialog</Text>,
}));
vi.mock('./IdeTrustChangeDialog.js', () => ({
IdeTrustChangeDialog: () => <Text>IdeTrustChangeDialog</Text>,
}));
describe('DialogManager', () => {
const defaultProps = {
addItem: vi.fn(),
terminalWidth: 100,
};
const baseUiState = {
constrainHeight: false,
terminalHeight: 24,
staticExtraHeight: 0,
mainAreaWidth: 80,
confirmUpdateExtensionRequests: [],
showIdeRestartPrompt: false,
proQuotaRequest: null,
shouldShowIdePrompt: false,
isFolderTrustDialogOpen: false,
shellConfirmationRequest: null,
loopDetectionConfirmationRequest: null,
confirmationRequest: null,
isThemeDialogOpen: false,
isSettingsDialogOpen: false,
isModelDialogOpen: false,
isAuthenticating: false,
isAwaitingApiKeyInput: false,
isAuthDialogOpen: false,
isEditorDialogOpen: false,
showPrivacyNotice: false,
isPermissionsDialogOpen: false,
};
it('renders nothing by default', () => {
const { lastFrame } = renderWithProviders(
<DialogManager {...defaultProps} />,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{ uiState: baseUiState as any },
);
expect(lastFrame()).toBe('');
});
const testCases: Array<[Partial<UIState>, string]> = [
[
{
showIdeRestartPrompt: true,
ideTrustRestartReason: 'update' as RestartReason,
},
'IdeTrustChangeDialog',
],
[
{
proQuotaRequest: {
failedModel: 'a',
fallbackModel: 'b',
message: 'c',
isTerminalQuotaError: false,
resolve: vi.fn(),
},
},
'ProQuotaDialog',
],
[
{
shouldShowIdePrompt: true,
currentIDE: { name: 'vscode', version: '1.0' } as unknown as IdeInfo,
},
'IdeIntegrationNudge',
],
[{ isFolderTrustDialogOpen: true }, 'FolderTrustDialog'],
[
{
shellConfirmationRequest: {
commands: [],
onConfirm: vi.fn(),
} as unknown as ShellConfirmationRequest,
},
'ShellConfirmationDialog',
],
[
{ loopDetectionConfirmationRequest: { onComplete: vi.fn() } },
'LoopDetectionConfirmation',
],
[
{ confirmationRequest: { prompt: 'foo', onConfirm: vi.fn() } },
'ConsentPrompt',
],
[
{
confirmUpdateExtensionRequests: [{ prompt: 'foo', onConfirm: vi.fn() }],
},
'ConsentPrompt',
],
[{ isThemeDialogOpen: true }, 'ThemeDialog'],
[{ isSettingsDialogOpen: true }, 'SettingsDialog'],
[{ isModelDialogOpen: true }, 'ModelDialog'],
[{ isAuthenticating: true }, 'AuthInProgress'],
[{ isAwaitingApiKeyInput: true }, 'ApiAuthDialog'],
[{ isAuthDialogOpen: true }, 'AuthDialog'],
[{ isEditorDialogOpen: true }, 'EditorSettingsDialog'],
[{ showPrivacyNotice: true }, 'PrivacyNotice'],
[{ isPermissionsDialogOpen: true }, 'PermissionsModifyTrustDialog'],
];
it.each(testCases)(
'renders %s when state is %o',
(uiStateOverride, expectedComponent) => {
const { lastFrame } = renderWithProviders(
<DialogManager {...defaultProps} />,
{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
uiState: { ...baseUiState, ...uiStateOverride } as any,
},
);
expect(lastFrame()).toContain(expectedComponent);
},
);
});

View File

@@ -0,0 +1,167 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { EditorSettingsDialog } from './EditorSettingsDialog.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { SettingScope } from '../../config/settings.js';
import type { LoadedSettings } from '../../config/settings.js';
import { KeypressProvider } from '../contexts/KeypressContext.js';
import { act } from 'react';
import { waitFor } from '../../test-utils/async.js';
// Mock editorSettingsManager
vi.mock('../editors/editorSettingsManager.js', () => ({
editorSettingsManager: {
getAvailableEditorDisplays: () => [
{ name: 'VS Code', type: 'vscode', disabled: false },
{ name: 'Vim', type: 'vim', disabled: false },
],
},
}));
describe('EditorSettingsDialog', () => {
const mockSettings = {
forScope: (scope: string) => ({
settings: {
general: {
preferredEditor: scope === SettingScope.User ? 'vscode' : undefined,
},
},
}),
merged: {
general: {
preferredEditor: 'vscode',
},
},
} as unknown as LoadedSettings;
beforeEach(() => {
vi.clearAllMocks();
});
const renderWithProvider = (ui: React.ReactNode) =>
render(<KeypressProvider>{ui}</KeypressProvider>);
it('renders correctly', () => {
const { lastFrame } = renderWithProvider(
<EditorSettingsDialog
onSelect={vi.fn()}
settings={mockSettings}
onExit={vi.fn()}
/>,
);
expect(lastFrame()).toMatchSnapshot();
});
it('calls onSelect when an editor is selected', () => {
const onSelect = vi.fn();
const { lastFrame } = renderWithProvider(
<EditorSettingsDialog
onSelect={onSelect}
settings={mockSettings}
onExit={vi.fn()}
/>,
);
expect(lastFrame()).toContain('VS Code');
});
it('switches focus between editor and scope sections on Tab', async () => {
const { lastFrame, stdin } = renderWithProvider(
<EditorSettingsDialog
onSelect={vi.fn()}
settings={mockSettings}
onExit={vi.fn()}
/>,
);
// Initial focus on editor
expect(lastFrame()).toContain('> Select Editor');
expect(lastFrame()).not.toContain('> Apply To');
// Press Tab
await act(async () => {
stdin.write('\t');
});
// Focus should be on scope
await waitFor(() => {
const frame = lastFrame() || '';
if (!frame.includes('> Apply To')) {
console.log(
'Waiting for scope focus. Current frame:',
JSON.stringify(frame),
);
}
expect(frame).toContain('> Apply To');
});
expect(lastFrame()).toContain(' Select Editor');
// Press Tab again
await act(async () => {
stdin.write('\t');
});
// Focus should be back on editor
await waitFor(() => {
expect(lastFrame()).toContain('> Select Editor');
});
});
it('calls onExit when Escape is pressed', async () => {
const onExit = vi.fn();
const { stdin } = renderWithProvider(
<EditorSettingsDialog
onSelect={vi.fn()}
settings={mockSettings}
onExit={onExit}
/>,
);
await act(async () => {
stdin.write('\u001B'); // Escape
});
await waitFor(() => {
expect(onExit).toHaveBeenCalled();
});
});
it('shows modified message when setting exists in other scope', () => {
const settingsWithOtherScope = {
forScope: (_scope: string) => ({
settings: {
general: {
preferredEditor: 'vscode', // Both scopes have it set
},
},
}),
merged: {
general: {
preferredEditor: 'vscode',
},
},
} as unknown as LoadedSettings;
const { lastFrame } = renderWithProvider(
<EditorSettingsDialog
onSelect={vi.fn()}
settings={settingsWithOtherScope}
onExit={vi.fn()}
/>,
);
const frame = lastFrame() || '';
if (!frame.includes('(Also modified')) {
console.log(
'Modified message test failure. Frame:',
JSON.stringify(frame),
);
}
expect(frame).toContain('(Also modified');
});
});

View File

@@ -0,0 +1,60 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { ExitWarning } from './ExitWarning.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useUIState, type UIState } from '../contexts/UIStateContext.js';
vi.mock('../contexts/UIStateContext.js');
describe('ExitWarning', () => {
const mockUseUIState = vi.mocked(useUIState);
beforeEach(() => {
vi.clearAllMocks();
});
it('renders nothing by default', () => {
mockUseUIState.mockReturnValue({
dialogsVisible: false,
ctrlCPressedOnce: false,
ctrlDPressedOnce: false,
} as unknown as UIState);
const { lastFrame } = render(<ExitWarning />);
expect(lastFrame()).toBe('');
});
it('renders Ctrl+C warning when pressed once and dialogs visible', () => {
mockUseUIState.mockReturnValue({
dialogsVisible: true,
ctrlCPressedOnce: true,
ctrlDPressedOnce: false,
} as unknown as UIState);
const { lastFrame } = render(<ExitWarning />);
expect(lastFrame()).toContain('Press Ctrl+C again to exit');
});
it('renders Ctrl+D warning when pressed once and dialogs visible', () => {
mockUseUIState.mockReturnValue({
dialogsVisible: true,
ctrlCPressedOnce: false,
ctrlDPressedOnce: true,
} as unknown as UIState);
const { lastFrame } = render(<ExitWarning />);
expect(lastFrame()).toContain('Press Ctrl+D again to exit');
});
it('renders nothing if dialogs are not visible', () => {
mockUseUIState.mockReturnValue({
dialogsVisible: false,
ctrlCPressedOnce: true,
ctrlDPressedOnce: true,
} as unknown as UIState);
const { lastFrame } = render(<ExitWarning />);
expect(lastFrame()).toBe('');
});
});

View File

@@ -0,0 +1,72 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { GeminiRespondingSpinner } from './GeminiRespondingSpinner.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useStreamingContext } from '../contexts/StreamingContext.js';
import { useIsScreenReaderEnabled } from 'ink';
import { StreamingState } from '../types.js';
import {
SCREEN_READER_LOADING,
SCREEN_READER_RESPONDING,
} from '../textConstants.js';
vi.mock('../contexts/StreamingContext.js');
vi.mock('ink', async () => {
const actual = await vi.importActual('ink');
return {
...actual,
useIsScreenReaderEnabled: vi.fn(),
};
});
describe('GeminiRespondingSpinner', () => {
const mockUseStreamingContext = vi.mocked(useStreamingContext);
const mockUseIsScreenReaderEnabled = vi.mocked(useIsScreenReaderEnabled);
beforeEach(() => {
vi.clearAllMocks();
mockUseIsScreenReaderEnabled.mockReturnValue(false);
});
it('renders spinner when responding', () => {
mockUseStreamingContext.mockReturnValue(StreamingState.Responding);
const { lastFrame } = render(<GeminiRespondingSpinner />);
// Spinner output varies, but it shouldn't be empty
expect(lastFrame()).not.toBe('');
});
it('renders screen reader text when responding and screen reader enabled', () => {
mockUseStreamingContext.mockReturnValue(StreamingState.Responding);
mockUseIsScreenReaderEnabled.mockReturnValue(true);
const { lastFrame } = render(<GeminiRespondingSpinner />);
expect(lastFrame()).toContain(SCREEN_READER_RESPONDING);
});
it('renders nothing when not responding and no non-responding display', () => {
mockUseStreamingContext.mockReturnValue(StreamingState.Idle);
const { lastFrame } = render(<GeminiRespondingSpinner />);
expect(lastFrame()).toBe('');
});
it('renders non-responding display when provided', () => {
mockUseStreamingContext.mockReturnValue(StreamingState.Idle);
const { lastFrame } = render(
<GeminiRespondingSpinner nonRespondingDisplay="Waiting..." />,
);
expect(lastFrame()).toContain('Waiting...');
});
it('renders screen reader loading text when non-responding display provided and screen reader enabled', () => {
mockUseStreamingContext.mockReturnValue(StreamingState.Idle);
mockUseIsScreenReaderEnabled.mockReturnValue(true);
const { lastFrame } = render(
<GeminiRespondingSpinner nonRespondingDisplay="Waiting..." />,
);
expect(lastFrame()).toContain(SCREEN_READER_LOADING);
});
});

View File

@@ -0,0 +1,103 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { MainContent } from './MainContent.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Box, Text } from 'ink';
import type React from 'react';
// Mock dependencies
vi.mock('../contexts/AppContext.js', () => ({
useAppContext: () => ({
version: '1.0.0',
}),
}));
vi.mock('../contexts/UIStateContext.js', () => ({
useUIState: () => ({
history: [
{ id: 1, role: 'user', content: 'Hello' },
{ id: 2, role: 'model', content: 'Hi there' },
],
pendingHistoryItems: [],
mainAreaWidth: 80,
staticAreaMaxItemHeight: 20,
availableTerminalHeight: 24,
slashCommands: [],
constrainHeight: false,
isEditorDialogOpen: false,
activePtyId: undefined,
embeddedShellFocused: false,
historyRemountKey: 0,
}),
}));
vi.mock('../hooks/useAlternateBuffer.js', () => ({
useAlternateBuffer: vi.fn(),
}));
vi.mock('./HistoryItemDisplay.js', () => ({
HistoryItemDisplay: ({ item }: { item: { content: string } }) => (
<Box>
<Text>HistoryItem: {item.content}</Text>
</Box>
),
}));
vi.mock('./AppHeader.js', () => ({
AppHeader: () => <Text>AppHeader</Text>,
}));
vi.mock('./ShowMoreLines.js', () => ({
ShowMoreLines: () => <Text>ShowMoreLines</Text>,
}));
vi.mock('./shared/ScrollableList.js', () => ({
ScrollableList: ({
data,
renderItem,
}: {
data: unknown[];
renderItem: (props: { item: unknown }) => React.JSX.Element;
}) => (
<Box flexDirection="column">
<Text>ScrollableList</Text>
{data.map((item: unknown, index: number) => (
<Box key={index}>{renderItem({ item })}</Box>
))}
</Box>
),
SCROLL_TO_ITEM_END: 0,
}));
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
describe('MainContent', () => {
beforeEach(() => {
vi.mocked(useAlternateBuffer).mockReturnValue(false);
});
it('renders in normal buffer mode', () => {
const { lastFrame } = render(<MainContent />);
const output = lastFrame();
expect(output).toContain('AppHeader');
expect(output).toContain('HistoryItem: Hello');
expect(output).toContain('HistoryItem: Hi there');
});
it('renders in alternate buffer mode', () => {
vi.mocked(useAlternateBuffer).mockReturnValue(true);
const { lastFrame } = render(<MainContent />);
const output = lastFrame();
expect(output).toContain('ScrollableList');
expect(output).toContain('AppHeader');
expect(output).toContain('HistoryItem: Hello');
expect(output).toContain('HistoryItem: Hi there');
});
});

View File

@@ -0,0 +1,55 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { MemoryUsageDisplay } from './MemoryUsageDisplay.js';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import process from 'node:process';
import { act } from 'react';
describe('MemoryUsageDisplay', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.clearAllMocks();
// Mock process.memoryUsage
vi.spyOn(process, 'memoryUsage').mockReturnValue({
rss: 1024 * 1024 * 50, // 50MB
heapTotal: 0,
heapUsed: 0,
external: 0,
arrayBuffers: 0,
});
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
it('renders memory usage', () => {
const { lastFrame } = render(<MemoryUsageDisplay />);
expect(lastFrame()).toContain('50.0 MB');
});
it('updates memory usage over time', async () => {
const { lastFrame } = render(<MemoryUsageDisplay />);
expect(lastFrame()).toContain('50.0 MB');
vi.mocked(process.memoryUsage).mockReturnValue({
rss: 1024 * 1024 * 100, // 100MB
heapTotal: 0,
heapUsed: 0,
external: 0,
arrayBuffers: 0,
});
await act(async () => {
vi.advanceTimersByTime(2000);
});
expect(lastFrame()).toContain('100.0 MB');
});
});

View File

@@ -0,0 +1,183 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { Notifications } from './Notifications.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useAppContext, type AppState } from '../contexts/AppContext.js';
import { useUIState, type UIState } from '../contexts/UIStateContext.js';
import { useIsScreenReaderEnabled } from 'ink';
import * as fs from 'node:fs/promises';
import { act } from 'react';
// Mock dependencies
vi.mock('../contexts/AppContext.js');
vi.mock('../contexts/UIStateContext.js');
vi.mock('ink', async () => {
const actual = await vi.importActual('ink');
return {
...actual,
useIsScreenReaderEnabled: vi.fn(),
};
});
vi.mock('node:fs/promises', async () => {
const actual = await vi.importActual('node:fs/promises');
return {
...actual,
access: vi.fn(),
writeFile: vi.fn(),
mkdir: vi.fn().mockResolvedValue(undefined),
};
});
vi.mock('node:os', () => ({
default: {
homedir: () => '/mock/home',
},
}));
vi.mock('node:path', async () => {
const actual = await vi.importActual<typeof import('node:path')>('node:path');
return {
...actual,
default: actual.posix,
};
});
vi.mock('@google/gemini-cli-core', () => ({
GEMINI_DIR: '.gemini',
Storage: {
getGlobalTempDir: () => '/mock/temp',
},
}));
vi.mock('../../config/settings.js', () => ({
DEFAULT_MODEL_CONFIGS: {},
LoadedSettings: class {
constructor() {
// this.merged = {};
}
},
}));
describe('Notifications', () => {
const mockUseAppContext = vi.mocked(useAppContext);
const mockUseUIState = vi.mocked(useUIState);
const mockUseIsScreenReaderEnabled = vi.mocked(useIsScreenReaderEnabled);
const mockFsAccess = vi.mocked(fs.access);
const mockFsWriteFile = vi.mocked(fs.writeFile);
beforeEach(() => {
vi.clearAllMocks();
mockUseAppContext.mockReturnValue({
startupWarnings: [],
version: '1.0.0',
} as AppState);
mockUseUIState.mockReturnValue({
initError: null,
streamingState: 'idle',
updateInfo: null,
} as unknown as UIState);
mockUseIsScreenReaderEnabled.mockReturnValue(false);
});
it('renders nothing when no notifications', () => {
const { lastFrame } = render(<Notifications />);
expect(lastFrame()).toBe('');
});
it.each([[['Warning 1']], [['Warning 1', 'Warning 2']]])(
'renders startup warnings: %s',
(warnings) => {
mockUseAppContext.mockReturnValue({
startupWarnings: warnings,
version: '1.0.0',
} as AppState);
const { lastFrame } = render(<Notifications />);
const output = lastFrame();
warnings.forEach((warning) => {
expect(output).toContain(warning);
});
},
);
it('renders init error', () => {
mockUseUIState.mockReturnValue({
initError: 'Something went wrong',
streamingState: 'idle',
updateInfo: null,
} as unknown as UIState);
const { lastFrame } = render(<Notifications />);
expect(lastFrame()).toMatchSnapshot();
});
it('does not render init error when streaming', () => {
mockUseUIState.mockReturnValue({
initError: 'Something went wrong',
streamingState: 'responding',
updateInfo: null,
} as unknown as UIState);
const { lastFrame } = render(<Notifications />);
expect(lastFrame()).toBe('');
});
it('renders update notification', () => {
mockUseUIState.mockReturnValue({
initError: null,
streamingState: 'idle',
updateInfo: { message: 'Update available' },
} as unknown as UIState);
const { lastFrame } = render(<Notifications />);
expect(lastFrame()).toMatchSnapshot();
});
it('renders screen reader nudge when enabled and not seen', async () => {
mockUseIsScreenReaderEnabled.mockReturnValue(true);
let rejectAccess: (err: Error) => void;
mockFsAccess.mockImplementation(
() =>
new Promise((_, reject) => {
rejectAccess = reject;
}),
);
const { lastFrame } = render(<Notifications />);
// Trigger rejection inside act
await act(async () => {
rejectAccess(new Error('File not found'));
});
// Wait for effect to propagate
await vi.waitFor(() => {
expect(mockFsWriteFile).toHaveBeenCalled();
});
expect(lastFrame()).toMatchSnapshot();
});
it('does not render screen reader nudge when already seen', async () => {
mockUseIsScreenReaderEnabled.mockReturnValue(true);
let resolveAccess: (val: undefined) => void;
mockFsAccess.mockImplementation(
() =>
new Promise((resolve) => {
resolveAccess = resolve;
}),
);
const { lastFrame } = render(<Notifications />);
// Trigger resolution inside act
await act(async () => {
resolveAccess(undefined);
});
expect(lastFrame()).toBe('');
expect(mockFsWriteFile).not.toHaveBeenCalled();
});
});

View File

@@ -39,7 +39,7 @@ export const Notifications = () => {
>(undefined);
useEffect(() => {
const checkScreenReaderNudge = async () => {
const checkScreenReader = async () => {
try {
await fs.access(screenReaderNudgeFilePath);
setHasSeenScreenReaderNudge(true);
@@ -47,8 +47,11 @@ export const Notifications = () => {
setHasSeenScreenReaderNudge(false);
}
};
checkScreenReaderNudge();
}, []);
if (isScreenReaderEnabled) {
checkScreenReader();
}
}, [isScreenReaderEnabled]);
const showScreenReaderNudge =
isScreenReaderEnabled && hasSeenScreenReaderNudge === false;

View File

@@ -0,0 +1,54 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { QuittingDisplay } from './QuittingDisplay.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import React from 'react';
import { useUIState, type UIState } from '../contexts/UIStateContext.js';
import { useTerminalSize } from '../hooks/useTerminalSize.js';
vi.mock('../contexts/UIStateContext.js');
vi.mock('../hooks/useTerminalSize.js');
vi.mock('./HistoryItemDisplay.js', async () => {
const { Text } = await vi.importActual('ink');
return {
HistoryItemDisplay: ({ item }: { item: { content: string } }) =>
React.createElement(Text as unknown as React.FC, null, item.content),
};
});
describe('QuittingDisplay', () => {
const mockUseUIState = vi.mocked(useUIState);
const mockUseTerminalSize = vi.mocked(useTerminalSize);
beforeEach(() => {
vi.clearAllMocks();
mockUseTerminalSize.mockReturnValue({ rows: 20, columns: 80 });
});
it('renders nothing when no quitting messages', () => {
mockUseUIState.mockReturnValue({
quittingMessages: null,
} as unknown as UIState);
const { lastFrame } = render(<QuittingDisplay />);
expect(lastFrame()).toBe('');
});
it('renders quitting messages', () => {
const mockMessages = [
{ id: '1', type: 'user', content: 'Goodbye' },
{ id: '2', type: 'model', content: 'See you later' },
];
mockUseUIState.mockReturnValue({
quittingMessages: mockMessages,
constrainHeight: false,
} as unknown as UIState);
const { lastFrame } = render(<QuittingDisplay />);
expect(lastFrame()).toContain('Goodbye');
expect(lastFrame()).toContain('See you later');
});
});

View File

@@ -0,0 +1,37 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { RawMarkdownIndicator } from './RawMarkdownIndicator.js';
import { describe, it, expect, afterEach } from 'vitest';
describe('RawMarkdownIndicator', () => {
const originalPlatform = process.platform;
afterEach(() => {
Object.defineProperty(process, 'platform', {
value: originalPlatform,
});
});
it('renders correct key binding for darwin', () => {
Object.defineProperty(process, 'platform', {
value: 'darwin',
});
const { lastFrame } = render(<RawMarkdownIndicator />);
expect(lastFrame()).toContain('raw markdown mode');
expect(lastFrame()).toContain('option+m to toggle');
});
it('renders correct key binding for other platforms', () => {
Object.defineProperty(process, 'platform', {
value: 'linux',
});
const { lastFrame } = render(<RawMarkdownIndicator />);
expect(lastFrame()).toContain('raw markdown mode');
expect(lastFrame()).toContain('alt+m to toggle');
});
});

View File

@@ -157,8 +157,7 @@ describe('SessionBrowser component', () => {
/>,
);
expect(lastFrame()).toContain('No auto-saved conversations found.');
expect(lastFrame()).toContain('Press q to exit');
expect(lastFrame()).toMatchSnapshot();
});
it('renders a list of sessions and marks current session as disabled', () => {
@@ -193,11 +192,7 @@ describe('SessionBrowser component', () => {
/>,
);
const output = lastFrame();
expect(output).toContain('Chat Sessions (2 total');
expect(output).toContain('First conversation about cats');
expect(output).toContain('Second conversation about dogs');
expect(output).toContain('(current)');
expect(lastFrame()).toMatchSnapshot();
});
it('enters search mode, filters sessions, and renders match snippets', async () => {
@@ -214,6 +209,7 @@ describe('SessionBrowser component', () => {
},
],
index: 0,
lastUpdated: '2025-01-01T12:00:00Z',
});
const otherSession = createSession({
@@ -229,6 +225,7 @@ describe('SessionBrowser component', () => {
},
],
index: 1,
lastUpdated: '2025-01-01T10:00:00Z',
});
const config = createMockConfig();
@@ -259,15 +256,9 @@ describe('SessionBrowser component', () => {
}
await waitFor(() => {
const output = lastFrame();
expect(output).toContain('Chat Sessions (1 total, filtered');
expect(output).toContain('Query is here');
expect(output).not.toContain('Nothing interesting here.');
expect(output).toContain('You:');
expect(output).toContain('query');
expect(output).toContain('(+1 more)');
expect(lastFrame()).toContain('Chat Sessions (1 total, filtered');
});
expect(lastFrame()).toMatchSnapshot();
});
it('handles keyboard navigation and resumes the selected session', () => {
@@ -276,12 +267,14 @@ describe('SessionBrowser component', () => {
file: 'one',
displayName: 'First session',
index: 0,
lastUpdated: '2025-01-02T12:00:00Z',
});
const session2 = createSession({
id: 'two',
file: 'two',
displayName: 'Second session',
index: 1,
lastUpdated: '2025-01-01T12:00:00Z',
});
const config = createMockConfig();
@@ -317,6 +310,7 @@ describe('SessionBrowser component', () => {
displayName: 'Current session',
isCurrentSession: true,
index: 0,
lastUpdated: '2025-01-02T12:00:00Z',
});
const otherSession = createSession({
id: 'other',
@@ -324,6 +318,7 @@ describe('SessionBrowser component', () => {
displayName: 'Other session',
isCurrentSession: false,
index: 1,
lastUpdated: '2025-01-01T12:00:00Z',
});
const config = createMockConfig();
@@ -364,8 +359,6 @@ describe('SessionBrowser component', () => {
/>,
);
const output = lastFrame();
expect(output).toContain('Error: storage failure');
expect(output).toContain('Press q to exit');
expect(lastFrame()).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,105 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { ShellInputPrompt } from './ShellInputPrompt.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ShellExecutionService } from '@google/gemini-cli-core';
// Mock useKeypress
const mockUseKeypress = vi.fn();
vi.mock('../hooks/useKeypress.js', () => ({
useKeypress: (handler: (input: unknown) => void, options?: unknown) =>
mockUseKeypress(handler, options),
}));
// Mock ShellExecutionService
vi.mock('@google/gemini-cli-core', async () => {
const actual = await vi.importActual('@google/gemini-cli-core');
return {
...actual,
ShellExecutionService: {
writeToPty: vi.fn(),
scrollPty: vi.fn(),
},
};
});
describe('ShellInputPrompt', () => {
const mockWriteToPty = vi.mocked(ShellExecutionService.writeToPty);
const mockScrollPty = vi.mocked(ShellExecutionService.scrollPty);
beforeEach(() => {
vi.clearAllMocks();
});
it('renders nothing', () => {
const { lastFrame } = render(
<ShellInputPrompt activeShellPtyId={1} focus={true} />,
);
expect(lastFrame()).toBe('');
});
it.each([
['a', 'a'],
['b', 'b'],
])('handles keypress input: %s', (name, sequence) => {
render(<ShellInputPrompt activeShellPtyId={1} focus={true} />);
// Get the registered handler
const handler = mockUseKeypress.mock.calls[0][0];
// Simulate keypress
handler({ name, sequence, ctrl: false, shift: false, meta: false });
expect(mockWriteToPty).toHaveBeenCalledWith(1, sequence);
});
it.each([
['up', -1],
['down', 1],
])('handles scroll %s (Ctrl+Shift+%s)', (key, direction) => {
render(<ShellInputPrompt activeShellPtyId={1} focus={true} />);
const handler = mockUseKeypress.mock.calls[0][0];
handler({ name: key, ctrl: true, shift: true, meta: false });
expect(mockScrollPty).toHaveBeenCalledWith(1, direction);
});
it('does not handle input when not focused', () => {
render(<ShellInputPrompt activeShellPtyId={1} focus={false} />);
const handler = mockUseKeypress.mock.calls[0][0];
handler({
name: 'a',
sequence: 'a',
ctrl: false,
shift: false,
meta: false,
});
expect(mockWriteToPty).not.toHaveBeenCalled();
});
it('does not handle input when no active shell', () => {
render(<ShellInputPrompt activeShellPtyId={null} focus={true} />);
const handler = mockUseKeypress.mock.calls[0][0];
handler({
name: 'a',
sequence: 'a',
ctrl: false,
shift: false,
meta: false,
});
expect(mockWriteToPty).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,17 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { ShellModeIndicator } from './ShellModeIndicator.js';
import { describe, it, expect } from 'vitest';
describe('ShellModeIndicator', () => {
it('renders correctly', () => {
const { lastFrame } = render(<ShellModeIndicator />);
expect(lastFrame()).toContain('shell mode enabled');
expect(lastFrame()).toContain('esc to disable');
});
});

View File

@@ -0,0 +1,54 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { ShowMoreLines } from './ShowMoreLines.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useOverflowState } from '../contexts/OverflowContext.js';
import { useStreamingContext } from '../contexts/StreamingContext.js';
import { StreamingState } from '../types.js';
vi.mock('../contexts/OverflowContext.js');
vi.mock('../contexts/StreamingContext.js');
describe('ShowMoreLines', () => {
const mockUseOverflowState = vi.mocked(useOverflowState);
const mockUseStreamingContext = vi.mocked(useStreamingContext);
beforeEach(() => {
vi.clearAllMocks();
});
it.each([
[new Set(), StreamingState.Idle, true], // No overflow
[new Set(['1']), StreamingState.Idle, false], // Not constraining height
[new Set(['1']), StreamingState.Responding, true], // Streaming
])(
'renders nothing when: overflow=%s, streaming=%s, constrain=%s',
(overflowingIds, streamingState, constrainHeight) => {
mockUseOverflowState.mockReturnValue({ overflowingIds } as NonNullable<
ReturnType<typeof useOverflowState>
>);
mockUseStreamingContext.mockReturnValue(streamingState);
const { lastFrame } = render(
<ShowMoreLines constrainHeight={constrainHeight} />,
);
expect(lastFrame()).toBe('');
},
);
it.each([[StreamingState.Idle], [StreamingState.WaitingForConfirmation]])(
'renders message when overflowing and state is %s',
(streamingState) => {
mockUseOverflowState.mockReturnValue({
overflowingIds: new Set(['1']),
} as NonNullable<ReturnType<typeof useOverflowState>>);
mockUseStreamingContext.mockReturnValue(streamingState);
const { lastFrame } = render(<ShowMoreLines constrainHeight={true} />);
expect(lastFrame()).toContain('Press ctrl-s to show more lines');
},
);
});

View File

@@ -0,0 +1,124 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { SuggestionsDisplay } from './SuggestionsDisplay.js';
import { describe, it, expect } from 'vitest';
import { CommandKind } from '../commands/types.js';
describe('SuggestionsDisplay', () => {
const mockSuggestions = [
{ label: 'Command 1', value: 'command1', description: 'Description 1' },
{ label: 'Command 2', value: 'command2', description: 'Description 2' },
{ label: 'Command 3', value: 'command3', description: 'Description 3' },
];
it('renders loading state', () => {
const { lastFrame } = render(
<SuggestionsDisplay
suggestions={[]}
activeIndex={0}
isLoading={true}
width={80}
scrollOffset={0}
userInput=""
mode="reverse"
/>,
);
expect(lastFrame()).toMatchSnapshot();
});
it('renders nothing when empty and not loading', () => {
const { lastFrame } = render(
<SuggestionsDisplay
suggestions={[]}
activeIndex={0}
isLoading={false}
width={80}
scrollOffset={0}
userInput=""
mode="reverse"
/>,
);
expect(lastFrame()).toBe('');
});
it('renders suggestions list', () => {
const { lastFrame } = render(
<SuggestionsDisplay
suggestions={mockSuggestions}
activeIndex={0}
isLoading={false}
width={80}
scrollOffset={0}
userInput=""
mode="reverse"
/>,
);
expect(lastFrame()).toMatchSnapshot();
});
it('highlights active item', () => {
// This test relies on visual inspection or implementation details (colors)
// For now, we just ensure it renders without error and contains the item
const { lastFrame } = render(
<SuggestionsDisplay
suggestions={mockSuggestions}
activeIndex={1}
isLoading={false}
width={80}
scrollOffset={0}
userInput=""
mode="reverse"
/>,
);
expect(lastFrame()).toMatchSnapshot();
});
it('handles scrolling', () => {
const manySuggestions = Array.from({ length: 20 }, (_, i) => ({
label: `Cmd ${i}`,
value: `Cmd ${i}`,
description: `Description ${i}`,
}));
const { lastFrame } = render(
<SuggestionsDisplay
suggestions={manySuggestions}
activeIndex={10}
isLoading={false}
width={80}
scrollOffset={5}
userInput=""
mode="reverse"
/>,
);
expect(lastFrame()).toMatchSnapshot();
});
it('renders MCP tag for MCP prompts', () => {
const mcpSuggestions = [
{
label: 'MCP Tool',
value: 'mcp-tool',
commandKind: CommandKind.MCP_PROMPT,
},
];
const { lastFrame } = render(
<SuggestionsDisplay
suggestions={mcpSuggestions}
activeIndex={0}
isLoading={false}
width={80}
scrollOffset={0}
userInput=""
mode="reverse"
/>,
);
expect(lastFrame()).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,32 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { ThemedGradient } from './ThemedGradient.js';
import { describe, it, expect, vi } from 'vitest';
// Mock theme to control gradient
vi.mock('../semantic-colors.js', () => ({
theme: {
ui: {
gradient: ['red', 'blue'],
},
text: {
accent: 'cyan',
},
},
}));
describe('ThemedGradient', () => {
it('renders children', () => {
const { lastFrame } = render(<ThemedGradient>Hello</ThemedGradient>);
expect(lastFrame()).toContain('Hello');
});
// Note: Testing actual gradient application is hard with ink-testing-library
// as it often renders as plain text or ANSI codes.
// We mainly ensure it doesn't crash and renders content.
});

View File

@@ -0,0 +1,25 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { Tips } from './Tips.js';
import { describe, it, expect, vi } from 'vitest';
import type { Config } from '@google/gemini-cli-core';
describe('Tips', () => {
it.each([
[0, '3. Create GEMINI.md files'],
[5, '3. /help for more information'],
])('renders correct tips when file count is %i', (count, expectedText) => {
const config = {
getGeminiMdFileCount: vi.fn().mockReturnValue(count),
} as unknown as Config;
const { lastFrame } = render(<Tips config={config} />);
const output = lastFrame();
expect(output).toContain(expectedText);
});
});

View File

@@ -0,0 +1,18 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { UpdateNotification } from './UpdateNotification.js';
import { describe, it, expect } from 'vitest';
describe('UpdateNotification', () => {
it('renders message', () => {
const { lastFrame } = render(
<UpdateNotification message="Update available!" />,
);
expect(lastFrame()).toContain('Update available!');
});
});

View File

@@ -0,0 +1,20 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Banner > handles newlines in text 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ Line 1 │
│ Line 2 │
╰──────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`Banner > renders in info mode 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ Info Message │
╰──────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`Banner > renders in warning mode 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ Warning Message │
╰──────────────────────────────────────────────────────────────────────────────╯"
`;

View File

@@ -0,0 +1,16 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`ConfigInitDisplay > handles empty clients map 1`] = `
"
Spinner Initializing..."
`;
exports[`ConfigInitDisplay > renders initial state 1`] = `
"
Spinner Initializing..."
`;
exports[`ConfigInitDisplay > updates message on McpClientUpdate event 1`] = `
"
Spinner Connecting to MCP servers... (1/2)"
`;

View File

@@ -0,0 +1,18 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`EditorSettingsDialog > renders correctly 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ > Select Editor Editor Preference │
│ ● 1. VS Code │
│ 2. Vim These editors are currently supported. Please note │
│ that some editors cannot be used in sandbox mode. │
│ Apply To │
│ ● 1. User Settings Your preferred editor is: None. │
│ 2. Workspace Settings │
│ │
│ (Use Enter to select, Tab to change │
│ focus, Esc to close) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;

View File

@@ -0,0 +1,22 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Notifications > renders init error 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Initialization Error: Something went wrong Please check API key and configuration. │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`Notifications > renders screen reader nudge when enabled and not seen 1`] = `
"You are currently in screen reader-friendly view. To switch out, open
/mock/home/.gemini/settings.json and remove the entry for "screenReader". This will disappear on
next run."
`;
exports[`Notifications > renders update notification 1`] = `
"
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Update available │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;

View File

@@ -0,0 +1,32 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`SessionBrowser component > enters search mode, filters sessions, and renders match snippets 1`] = `
" Chat Sessions (1 total, filtered) sorted by date desc
Search: query (Esc to cancel)
Index │ Msgs │ Age │ Match
#1 │ 1 │ 10mo │ You: Query is here a… (+1 more)
▼"
`;
exports[`SessionBrowser component > renders a list of sessions and marks current session as disabled 1`] = `
" Chat Sessions (2 total) sorted by date desc
Navigate: ↑/↓ Resume: Enter Search: / Delete: x Quit: q
Sort: s Reverse: r First/Last: g/G
Index │ Msgs │ Age │ Name
#1 │ 5 │ 10mo │ Second conversation about dogs (current)
#2 │ 2 │ 10mo │ First conversation about cats
▼"
`;
exports[`SessionBrowser component > shows an error state when loading sessions fails 1`] = `
" Error: storage failure
Press q to exit"
`;
exports[`SessionBrowser component > shows empty state when no sessions exist 1`] = `
" No auto-saved conversations found.
Press q to exit"
`;

View File

@@ -0,0 +1,31 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`SuggestionsDisplay > handles scrolling 1`] = `
" ▲
Cmd 5 Description 5
Cmd 6 Description 6
Cmd 7 Description 7
Cmd 8 Description 8
Cmd 9 Description 9
Cmd 10 Description 10
Cmd 11 Description 11
Cmd 12 Description 12
(11/20)"
`;
exports[`SuggestionsDisplay > highlights active item 1`] = `
" command1 Description 1
command2 Description 2
command3 Description 3"
`;
exports[`SuggestionsDisplay > renders MCP tag for MCP prompts 1`] = `" mcp-tool [MCP]"`;
exports[`SuggestionsDisplay > renders loading state 1`] = `" Loading suggestions..."`;
exports[`SuggestionsDisplay > renders suggestions list 1`] = `
" command1 Description 1
command2 Description 2
command3 Description 3"
`;

View File

@@ -0,0 +1,26 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../../test-utils/render.js';
import { ErrorMessage } from './ErrorMessage.js';
import { describe, it, expect } from 'vitest';
describe('ErrorMessage', () => {
it('renders with the correct prefix and text', () => {
const { lastFrame } = render(<ErrorMessage text="Something went wrong" />);
const output = lastFrame();
expect(output).toMatchSnapshot();
});
it('renders multiline error messages', () => {
const message = 'Error line 1\nError line 2';
const { lastFrame } = render(<ErrorMessage text={message} />);
const output = lastFrame();
expect(output).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,35 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../../test-utils/render.js';
import { InfoMessage } from './InfoMessage.js';
import { describe, it, expect } from 'vitest';
describe('InfoMessage', () => {
it('renders with the correct default prefix and text', () => {
const { lastFrame } = render(<InfoMessage text="Just so you know" />);
const output = lastFrame();
expect(output).toMatchSnapshot();
});
it('renders with a custom icon', () => {
const { lastFrame } = render(
<InfoMessage text="Custom icon test" icon="★" />,
);
const output = lastFrame();
expect(output).toMatchSnapshot();
});
it('renders multiline info messages', () => {
const message = 'Info line 1\nInfo line 2';
const { lastFrame } = render(<InfoMessage text={message} />);
const output = lastFrame();
expect(output).toMatchSnapshot();
});
});

View File

@@ -36,7 +36,7 @@ describe('ToolConfirmationMessage', () => {
/>,
);
expect(lastFrame()).not.toContain('URLs to fetch:');
expect(lastFrame()).toMatchSnapshot();
});
it('should display urls if prompt and url are different', () => {
@@ -60,10 +60,7 @@ describe('ToolConfirmationMessage', () => {
/>,
);
expect(lastFrame()).toContain('URLs to fetch:');
expect(lastFrame()).toContain(
'- https://raw.githubusercontent.com/google/gemini-react/main/README.md',
);
expect(lastFrame()).toMatchSnapshot();
});
describe('with folder trust', () => {
@@ -124,7 +121,7 @@ describe('ToolConfirmationMessage', () => {
details: mcpConfirmationDetails,
alwaysAllowText: 'always allow',
},
])('$description', ({ details, alwaysAllowText }) => {
])('$description', ({ details }) => {
it('should show "allow always" when folder is trusted', () => {
const mockConfig = {
isTrustedFolder: () => true,
@@ -140,7 +137,7 @@ describe('ToolConfirmationMessage', () => {
/>,
);
expect(lastFrame()).toContain(alwaysAllowText);
expect(lastFrame()).toMatchSnapshot();
});
it('should NOT show "allow always" when folder is untrusted', () => {
@@ -158,7 +155,7 @@ describe('ToolConfirmationMessage', () => {
/>,
);
expect(lastFrame()).not.toContain(alwaysAllowText);
expect(lastFrame()).toMatchSnapshot();
});
});
});

View File

@@ -107,10 +107,7 @@ describe('<ToolMessage />', () => {
StreamingState.Idle,
);
const output = lastFrame();
expect(output).toContain('✓'); // Success indicator
expect(output).toContain('test-tool');
expect(output).toContain('A tool for testing');
expect(output).toContain('MockMarkdown:Test result');
expect(output).toMatchSnapshot();
});
describe('ToolStatusIndicator rendering', () => {
@@ -119,7 +116,7 @@ describe('<ToolMessage />', () => {
<ToolMessage {...baseProps} status={ToolCallStatus.Success} />,
StreamingState.Idle,
);
expect(lastFrame()).toContain('✓');
expect(lastFrame()).toMatchSnapshot();
});
it('shows o for Pending status', () => {
@@ -127,7 +124,7 @@ describe('<ToolMessage />', () => {
<ToolMessage {...baseProps} status={ToolCallStatus.Pending} />,
StreamingState.Idle,
);
expect(lastFrame()).toContain('o');
expect(lastFrame()).toMatchSnapshot();
});
it('shows ? for Confirming status', () => {
@@ -135,7 +132,7 @@ describe('<ToolMessage />', () => {
<ToolMessage {...baseProps} status={ToolCallStatus.Confirming} />,
StreamingState.Idle,
);
expect(lastFrame()).toContain('?');
expect(lastFrame()).toMatchSnapshot();
});
it('shows - for Canceled status', () => {
@@ -143,7 +140,7 @@ describe('<ToolMessage />', () => {
<ToolMessage {...baseProps} status={ToolCallStatus.Canceled} />,
StreamingState.Idle,
);
expect(lastFrame()).toContain('-');
expect(lastFrame()).toMatchSnapshot();
});
it('shows x for Error status', () => {
@@ -151,7 +148,7 @@ describe('<ToolMessage />', () => {
<ToolMessage {...baseProps} status={ToolCallStatus.Error} />,
StreamingState.Idle,
);
expect(lastFrame()).toContain('x');
expect(lastFrame()).toMatchSnapshot();
});
it('shows paused spinner for Executing status when streamingState is Idle', () => {
@@ -159,9 +156,7 @@ describe('<ToolMessage />', () => {
<ToolMessage {...baseProps} status={ToolCallStatus.Executing} />,
StreamingState.Idle,
);
expect(lastFrame()).toContain('⊷');
expect(lastFrame()).not.toContain('MockRespondingSpinner');
expect(lastFrame()).not.toContain('✓');
expect(lastFrame()).toMatchSnapshot();
});
it('shows paused spinner for Executing status when streamingState is WaitingForConfirmation', () => {
@@ -169,9 +164,7 @@ describe('<ToolMessage />', () => {
<ToolMessage {...baseProps} status={ToolCallStatus.Executing} />,
StreamingState.WaitingForConfirmation,
);
expect(lastFrame()).toContain('⊷');
expect(lastFrame()).not.toContain('MockRespondingSpinner');
expect(lastFrame()).not.toContain('✓');
expect(lastFrame()).toMatchSnapshot();
});
it('shows MockRespondingSpinner for Executing status when streamingState is Responding', () => {
@@ -179,8 +172,7 @@ describe('<ToolMessage />', () => {
<ToolMessage {...baseProps} status={ToolCallStatus.Executing} />,
StreamingState.Responding, // Simulate app still responding
);
expect(lastFrame()).toContain('MockRespondingSpinner');
expect(lastFrame()).not.toContain('✓');
expect(lastFrame()).toMatchSnapshot();
});
});
@@ -196,7 +188,7 @@ describe('<ToolMessage />', () => {
StreamingState.Idle,
);
// Check that the output contains the MockDiff content as part of the whole message
expect(lastFrame()).toMatch(/MockDiff:--- a\/file\.txt/);
expect(lastFrame()).toMatchSnapshot();
});
it('renders emphasis correctly', () => {
@@ -205,7 +197,7 @@ describe('<ToolMessage />', () => {
StreamingState.Idle,
);
// Check for trailing indicator or specific color if applicable (Colors are not easily testable here)
expect(highEmphasisFrame()).toContain('←'); // Trailing indicator for high emphasis
expect(highEmphasisFrame()).toMatchSnapshot();
const { lastFrame: lowEmphasisFrame } = renderWithContext(
<ToolMessage {...baseProps} emphasis="low" />,
@@ -214,7 +206,7 @@ describe('<ToolMessage />', () => {
// For low emphasis, the name and description might be dimmed (check for dimColor if possible)
// This is harder to assert directly in text output without color checks.
// We can at least ensure it doesn't have the high emphasis indicator.
expect(lowEmphasisFrame()).not.toContain('←');
expect(lowEmphasisFrame()).toMatchSnapshot();
});
it('renders AnsiOutputText for AnsiOutput results', () => {
@@ -236,6 +228,6 @@ describe('<ToolMessage />', () => {
<ToolMessage {...baseProps} resultDisplay={ansiResult} />,
StreamingState.Idle,
);
expect(lastFrame()).toContain('MockAnsiOutput:hello');
expect(lastFrame()).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,191 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../../test-utils/render.js';
import { ToolResultDisplay } from './ToolResultDisplay.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Box, Text } from 'ink';
import type { AnsiOutput } from '@google/gemini-cli-core';
// Mock child components to simplify testing
vi.mock('./DiffRenderer.js', () => ({
DiffRenderer: ({
diffContent,
filename,
}: {
diffContent: string;
filename: string;
}) => (
<Box>
<Text>
DiffRenderer: {filename} - {diffContent}
</Text>
</Box>
),
}));
vi.mock('../../utils/MarkdownDisplay.js', () => ({
MarkdownDisplay: ({ text }: { text: string }) => (
<Box>
<Text>MarkdownDisplay: {text}</Text>
</Box>
),
}));
vi.mock('../AnsiOutput.js', () => ({
AnsiOutputText: ({ data }: { data: unknown }) => (
<Box>
<Text>AnsiOutputText: {JSON.stringify(data)}</Text>
</Box>
),
}));
vi.mock('../shared/MaxSizedBox.js', () => ({
MaxSizedBox: ({ children }: { children: React.ReactNode }) => (
<Box>
<Text>MaxSizedBox:</Text>
{children}
</Box>
),
}));
// Mock UIStateContext
const mockUseUIState = vi.fn();
vi.mock('../../contexts/UIStateContext.js', () => ({
useUIState: () => mockUseUIState(),
}));
// Mock useAlternateBuffer
const mockUseAlternateBuffer = vi.fn();
vi.mock('../../hooks/useAlternateBuffer.js', () => ({
useAlternateBuffer: () => mockUseAlternateBuffer(),
}));
describe('ToolResultDisplay', () => {
beforeEach(() => {
vi.clearAllMocks();
mockUseUIState.mockReturnValue({ renderMarkdown: true });
mockUseAlternateBuffer.mockReturnValue(false);
});
it('renders string result as markdown by default', () => {
const { lastFrame } = render(
<ToolResultDisplay resultDisplay="Some result" terminalWidth={80} />,
);
const output = lastFrame();
expect(output).toMatchSnapshot();
});
it('renders string result as plain text when renderOutputAsMarkdown is false', () => {
const { lastFrame } = render(
<ToolResultDisplay
resultDisplay="Some result"
terminalWidth={80}
availableTerminalHeight={20}
renderOutputAsMarkdown={false}
/>,
);
const output = lastFrame();
expect(output).toMatchSnapshot();
});
it('truncates very long string results', { timeout: 20000 }, () => {
const longString = 'a'.repeat(1000005);
const { lastFrame } = render(
<ToolResultDisplay
resultDisplay={longString}
terminalWidth={80}
availableTerminalHeight={20}
/>,
);
const output = lastFrame();
expect(output).toMatchSnapshot();
});
it('renders file diff result', () => {
const diffResult = {
fileDiff: 'diff content',
fileName: 'test.ts',
};
const { lastFrame } = render(
<ToolResultDisplay
resultDisplay={diffResult}
terminalWidth={80}
availableTerminalHeight={20}
/>,
);
const output = lastFrame();
expect(output).toMatchSnapshot();
});
it('renders ANSI output result', () => {
const ansiResult = {
text: 'ansi content',
};
const { lastFrame } = render(
<ToolResultDisplay
resultDisplay={ansiResult as unknown as AnsiOutput}
terminalWidth={80}
availableTerminalHeight={20}
/>,
);
const output = lastFrame();
expect(output).toMatchSnapshot();
});
it('renders nothing for todos result', () => {
const todoResult = {
todos: [],
};
const { lastFrame } = render(
<ToolResultDisplay
resultDisplay={todoResult}
terminalWidth={80}
availableTerminalHeight={20}
/>,
);
const output = lastFrame();
expect(output).toMatchSnapshot();
});
it('falls back to plain text if availableHeight is set and not in alternate buffer', () => {
mockUseAlternateBuffer.mockReturnValue(false);
// availableHeight calculation: 20 - 1 - 5 = 14 > 3
const { lastFrame } = render(
<ToolResultDisplay
resultDisplay="Some result"
terminalWidth={80}
availableTerminalHeight={20}
renderOutputAsMarkdown={true}
/>,
);
const output = lastFrame();
// Should force renderOutputAsMarkdown to false
expect(output).toMatchSnapshot();
});
it('keeps markdown if in alternate buffer even with availableHeight', () => {
mockUseAlternateBuffer.mockReturnValue(true);
const { lastFrame } = render(
<ToolResultDisplay
resultDisplay="Some result"
terminalWidth={80}
availableTerminalHeight={20}
renderOutputAsMarkdown={true}
/>,
);
const output = lastFrame();
expect(output).toMatchSnapshot();
});
});

View File

@@ -21,7 +21,7 @@ const MIN_LINES_SHOWN = 2; // show at least this many lines
// Large threshold to ensure we don't cause performance issues for very large
// outputs that will get truncated further MaxSizedBox anyway.
const MAXIMUM_RESULT_DISPLAY_CHARACTERS = 1000000;
const MAXIMUM_RESULT_DISPLAY_CHARACTERS = 20000;
export interface ToolResultDisplayProps {
resultDisplay: string | object | undefined;

View File

@@ -0,0 +1,40 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../../test-utils/render.js';
import { UserMessage } from './UserMessage.js';
import { describe, it, expect, vi } from 'vitest';
// Mock the commandUtils to control isSlashCommand behavior
vi.mock('../../utils/commandUtils.js', () => ({
isSlashCommand: vi.fn((text: string) => text.startsWith('/')),
}));
describe('UserMessage', () => {
it('renders normal user message with correct prefix', () => {
const { lastFrame } = render(
<UserMessage text="Hello Gemini" width={80} />,
);
const output = lastFrame();
expect(output).toMatchSnapshot();
});
it('renders slash command message', () => {
const { lastFrame } = render(<UserMessage text="/help" width={80} />);
const output = lastFrame();
expect(output).toMatchSnapshot();
});
it('renders multiline user message', () => {
const message = 'Line 1\nLine 2';
const { lastFrame } = render(<UserMessage text={message} width={80} />);
const output = lastFrame();
expect(output).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,26 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../../test-utils/render.js';
import { WarningMessage } from './WarningMessage.js';
import { describe, it, expect } from 'vitest';
describe('WarningMessage', () => {
it('renders with the correct prefix and text', () => {
const { lastFrame } = render(<WarningMessage text="Watch out!" />);
const output = lastFrame();
expect(output).toMatchSnapshot();
});
it('renders multiline warning messages', () => {
const message = 'Warning line 1\nWarning line 2';
const { lastFrame } = render(<WarningMessage text={message} />);
const output = lastFrame();
expect(output).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,12 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`ErrorMessage > renders multiline error messages 1`] = `
"✕ Error line 1
Error line 2
"
`;
exports[`ErrorMessage > renders with the correct prefix and text 1`] = `
"✕ Something went wrong
"
`;

View File

@@ -0,0 +1,17 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`InfoMessage > renders multiline info messages 1`] = `
"
Info line 1
Info line 2"
`;
exports[`InfoMessage > renders with a custom icon 1`] = `
"
★Custom icon test"
`;
exports[`InfoMessage > renders with the correct default prefix and text 1`] = `
"
Just so you know"
`;

View File

@@ -0,0 +1,123 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`ToolConfirmationMessage > should display urls if prompt and url are different 1`] = `
"fetch https://github.com/google/gemini-react/blob/main/README.md
URLs to fetch:
- https://raw.githubusercontent.com/google/gemini-react/main/README.md
Do you want to proceed?
● 1. Yes, allow once
2. Yes, allow always
3. No, suggest changes (esc)
"
`;
exports[`ToolConfirmationMessage > should not display urls if prompt and url are the same 1`] = `
"https://example.com
Do you want to proceed?
● 1. Yes, allow once
2. Yes, allow always
3. No, suggest changes (esc)
"
`;
exports[`ToolConfirmationMessage > with folder trust > 'for edit confirmations' > should NOT show "allow always" when folder is untrusted 1`] = `
"╭──────────────────────╮
│ │
│ No changes detected. │
│ │
╰──────────────────────╯
Apply this change?
● 1. Yes, allow once
2. Modify with external editor
3. No, suggest changes (esc)
"
`;
exports[`ToolConfirmationMessage > with folder trust > 'for edit confirmations' > should show "allow always" when folder is trusted 1`] = `
"╭──────────────────────╮
│ │
│ No changes detected. │
│ │
╰──────────────────────╯
Apply this change?
● 1. Yes, allow once
2. Yes, allow always
3. Modify with external editor
4. No, suggest changes (esc)
"
`;
exports[`ToolConfirmationMessage > with folder trust > 'for exec confirmations' > should NOT show "allow always" when folder is untrusted 1`] = `
"echo "hello"
Allow execution of: 'echo'?
● 1. Yes, allow once
2. No, suggest changes (esc)
"
`;
exports[`ToolConfirmationMessage > with folder trust > 'for exec confirmations' > should show "allow always" when folder is trusted 1`] = `
"echo "hello"
Allow execution of: 'echo'?
● 1. Yes, allow once
2. Yes, allow always ...
3. No, suggest changes (esc)
"
`;
exports[`ToolConfirmationMessage > with folder trust > 'for info confirmations' > should NOT show "allow always" when folder is untrusted 1`] = `
"https://example.com
Do you want to proceed?
● 1. Yes, allow once
2. No, suggest changes (esc)
"
`;
exports[`ToolConfirmationMessage > with folder trust > 'for info confirmations' > should show "allow always" when folder is trusted 1`] = `
"https://example.com
Do you want to proceed?
● 1. Yes, allow once
2. Yes, allow always
3. No, suggest changes (esc)
"
`;
exports[`ToolConfirmationMessage > with folder trust > 'for mcp confirmations' > should NOT show "allow always" when folder is untrusted 1`] = `
"MCP Server: test-server
Tool: test-tool
Allow execution of MCP tool "test-tool" from server "test-server"?
● 1. Yes, allow once
2. No, suggest changes (esc)
"
`;
exports[`ToolConfirmationMessage > with folder trust > 'for mcp confirmations' > should show "allow always" when folder is trusted 1`] = `
"MCP Server: test-server
Tool: test-tool
Allow execution of MCP tool "test-tool" from server "test-server"?
● 1. Yes, allow once
2. Yes, always allow tool "test-tool" from server "test-server"
3. Yes, always allow all tools from server "test-server"
4. No, suggest changes (esc)
"
`;

View File

@@ -0,0 +1,96 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows ? for Confirming status 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ ? test-tool A tool for testing │
│ │
│ MockMarkdown:Test result │"
`;
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows - for Canceled status 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ - test-tool A tool for testing │
│ │
│ MockMarkdown:Test result │"
`;
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows MockRespondingSpinner for Executing status when streamingState is Responding 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ MockRespondingSpinnertest-tool A tool for testing │
│ │
│ MockMarkdown:Test result │"
`;
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows o for Pending status 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ o test-tool A tool for testing │
│ │
│ MockMarkdown:Test result │"
`;
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows paused spinner for Executing status when streamingState is Idle 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ ⊷ test-tool A tool for testing │
│ │
│ MockMarkdown:Test result │"
`;
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows paused spinner for Executing status when streamingState is WaitingForConfirmation 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ ⊷ test-tool A tool for testing │
│ │
│ MockMarkdown:Test result │"
`;
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows x for Error status 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ x test-tool A tool for testing │
│ │
│ MockMarkdown:Test result │"
`;
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows ✓ for Success status 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ ✓ test-tool A tool for testing │
│ │
│ MockMarkdown:Test result │"
`;
exports[`<ToolMessage /> > renders AnsiOutputText for AnsiOutput results 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ ✓ test-tool A tool for testing │
│ │
│ MockAnsiOutput:hello │"
`;
exports[`<ToolMessage /> > renders DiffRenderer for diff results 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ ✓ test-tool A tool for testing │
│ │
│ MockDiff:--- a/file.txt │
│ +++ b/file.txt │
│ @@ -1 +1 @@ │
│ -old │
│ +new │"
`;
exports[`<ToolMessage /> > renders basic tool information 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ ✓ test-tool A tool for testing │
│ │
│ MockMarkdown:Test result │"
`;
exports[`<ToolMessage /> > renders emphasis correctly 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ ✓ test-tool A tool for testing ← │
│ │
│ MockMarkdown:Test result │"
`;
exports[`<ToolMessage /> > renders emphasis correctly 2`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ ✓ test-tool A tool for testing │
│ │
│ MockMarkdown:Test result │"
`;

View File

@@ -0,0 +1,326 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`ToolResultDisplay > falls back to plain text if availableHeight is set and not in alternate buffer 1`] = `"MaxSizedBox:Some result"`;
exports[`ToolResultDisplay > keeps markdown if in alternate buffer even with availableHeight 1`] = `"MarkdownDisplay: Some result"`;
exports[`ToolResultDisplay > renders ANSI output result 1`] = `"AnsiOutputText: {"text":"ansi content"}"`;
exports[`ToolResultDisplay > renders file diff result 1`] = `"DiffRenderer: test.ts - diff content"`;
exports[`ToolResultDisplay > renders nothing for todos result 1`] = `""`;
exports[`ToolResultDisplay > renders string result as markdown by default 1`] = `"MarkdownDisplay: Some result"`;
exports[`ToolResultDisplay > renders string result as plain text when renderOutputAsMarkdown is false 1`] = `"MaxSizedBox:Some result"`;
exports[`ToolResultDisplay > truncates very long string results 1`] = `
"MaxSizedBo...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
`;

View File

@@ -0,0 +1,20 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`UserMessage > renders multiline user message 1`] = `
"
> Line 1
Line 2
"
`;
exports[`UserMessage > renders normal user message with correct prefix 1`] = `
"
> Hello Gemini
"
`;
exports[`UserMessage > renders slash command message 1`] = `
"
> /help
"
`;

View File

@@ -0,0 +1,12 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`WarningMessage > renders multiline warning messages 1`] = `
"
⚠ Warning line 1
Warning line 2"
`;
exports[`WarningMessage > renders with the correct prefix and text 1`] = `
"
⚠ Watch out!"
`;