From e205a468d91d5569d0a8bd14ee65657637b152db Mon Sep 17 00:00:00 2001 From: Megha Bansal Date: Sat, 22 Nov 2025 08:17:29 +0530 Subject: [PATCH] Improve test coverage for cli/src/ui/components (#13598) --- .../cli/src/ui/components/AboutBox.test.tsx | 57 +++ .../components/AutoAcceptIndicator.test.tsx | 39 +++ .../cli/src/ui/components/Banner.test.tsx | 29 ++ .../ui/components/ConfigInitDisplay.test.tsx | 117 +++++++ .../components/ConsoleSummaryDisplay.test.tsx | 27 ++ .../components/ContextUsageDisplay.test.tsx | 61 ++++ .../ui/components/CopyModeWarning.test.tsx | 37 ++ .../src/ui/components/DebugProfiler.test.tsx | 53 +++ .../DetailedMessagesDisplay.test.tsx | 92 +++++ .../src/ui/components/DialogManager.test.tsx | 180 ++++++++++ .../components/EditorSettingsDialog.test.tsx | 167 +++++++++ .../src/ui/components/ExitWarning.test.tsx | 60 ++++ .../GeminiRespondingSpinner.test.tsx | 72 ++++ .../src/ui/components/MainContent.test.tsx | 103 ++++++ .../ui/components/MemoryUsageDisplay.test.tsx | 55 +++ .../src/ui/components/Notifications.test.tsx | 183 ++++++++++ .../cli/src/ui/components/Notifications.tsx | 9 +- .../ui/components/QuittingDisplay.test.tsx | 54 +++ .../components/RawMarkdownIndicator.test.tsx | 37 ++ .../src/ui/components/SessionBrowser.test.tsx | 29 +- .../ui/components/ShellInputPrompt.test.tsx | 105 ++++++ .../ui/components/ShellModeIndicator.test.tsx | 17 + .../src/ui/components/ShowMoreLines.test.tsx | 54 +++ .../ui/components/SuggestionsDisplay.test.tsx | 124 +++++++ .../src/ui/components/ThemedGradient.test.tsx | 32 ++ packages/cli/src/ui/components/Tips.test.tsx | 25 ++ .../ui/components/UpdateNotification.test.tsx | 18 + .../__snapshots__/Banner.test.tsx.snap | 20 ++ .../ConfigInitDisplay.test.tsx.snap | 16 + .../EditorSettingsDialog.test.tsx.snap | 18 + .../__snapshots__/Notifications.test.tsx.snap | 22 ++ .../SessionBrowser.test.tsx.snap | 32 ++ .../SuggestionsDisplay.test.tsx.snap | 31 ++ .../components/messages/ErrorMessage.test.tsx | 26 ++ .../components/messages/InfoMessage.test.tsx | 35 ++ .../messages/ToolConfirmationMessage.test.tsx | 13 +- .../components/messages/ToolMessage.test.tsx | 34 +- .../messages/ToolResultDisplay.test.tsx | 191 ++++++++++ .../components/messages/ToolResultDisplay.tsx | 2 +- .../components/messages/UserMessage.test.tsx | 40 +++ .../messages/WarningMessage.test.tsx | 26 ++ .../__snapshots__/ErrorMessage.test.tsx.snap | 12 + .../__snapshots__/InfoMessage.test.tsx.snap | 17 + .../ToolConfirmationMessage.test.tsx.snap | 123 +++++++ .../__snapshots__/ToolMessage.test.tsx.snap | 96 ++++++ .../ToolResultDisplay.test.tsx.snap | 326 ++++++++++++++++++ .../__snapshots__/UserMessage.test.tsx.snap | 20 ++ .../WarningMessage.test.tsx.snap | 12 + 48 files changed, 2897 insertions(+), 51 deletions(-) create mode 100644 packages/cli/src/ui/components/AboutBox.test.tsx create mode 100644 packages/cli/src/ui/components/AutoAcceptIndicator.test.tsx create mode 100644 packages/cli/src/ui/components/Banner.test.tsx create mode 100644 packages/cli/src/ui/components/ConfigInitDisplay.test.tsx create mode 100644 packages/cli/src/ui/components/ConsoleSummaryDisplay.test.tsx create mode 100644 packages/cli/src/ui/components/ContextUsageDisplay.test.tsx create mode 100644 packages/cli/src/ui/components/CopyModeWarning.test.tsx create mode 100644 packages/cli/src/ui/components/DetailedMessagesDisplay.test.tsx create mode 100644 packages/cli/src/ui/components/DialogManager.test.tsx create mode 100644 packages/cli/src/ui/components/EditorSettingsDialog.test.tsx create mode 100644 packages/cli/src/ui/components/ExitWarning.test.tsx create mode 100644 packages/cli/src/ui/components/GeminiRespondingSpinner.test.tsx create mode 100644 packages/cli/src/ui/components/MainContent.test.tsx create mode 100644 packages/cli/src/ui/components/MemoryUsageDisplay.test.tsx create mode 100644 packages/cli/src/ui/components/Notifications.test.tsx create mode 100644 packages/cli/src/ui/components/QuittingDisplay.test.tsx create mode 100644 packages/cli/src/ui/components/RawMarkdownIndicator.test.tsx create mode 100644 packages/cli/src/ui/components/ShellInputPrompt.test.tsx create mode 100644 packages/cli/src/ui/components/ShellModeIndicator.test.tsx create mode 100644 packages/cli/src/ui/components/ShowMoreLines.test.tsx create mode 100644 packages/cli/src/ui/components/SuggestionsDisplay.test.tsx create mode 100644 packages/cli/src/ui/components/ThemedGradient.test.tsx create mode 100644 packages/cli/src/ui/components/Tips.test.tsx create mode 100644 packages/cli/src/ui/components/UpdateNotification.test.tsx create mode 100644 packages/cli/src/ui/components/__snapshots__/Banner.test.tsx.snap create mode 100644 packages/cli/src/ui/components/__snapshots__/ConfigInitDisplay.test.tsx.snap create mode 100644 packages/cli/src/ui/components/__snapshots__/EditorSettingsDialog.test.tsx.snap create mode 100644 packages/cli/src/ui/components/__snapshots__/Notifications.test.tsx.snap create mode 100644 packages/cli/src/ui/components/__snapshots__/SessionBrowser.test.tsx.snap create mode 100644 packages/cli/src/ui/components/__snapshots__/SuggestionsDisplay.test.tsx.snap create mode 100644 packages/cli/src/ui/components/messages/ErrorMessage.test.tsx create mode 100644 packages/cli/src/ui/components/messages/InfoMessage.test.tsx create mode 100644 packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx create mode 100644 packages/cli/src/ui/components/messages/UserMessage.test.tsx create mode 100644 packages/cli/src/ui/components/messages/WarningMessage.test.tsx create mode 100644 packages/cli/src/ui/components/messages/__snapshots__/ErrorMessage.test.tsx.snap create mode 100644 packages/cli/src/ui/components/messages/__snapshots__/InfoMessage.test.tsx.snap create mode 100644 packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage.test.tsx.snap create mode 100644 packages/cli/src/ui/components/messages/__snapshots__/ToolMessage.test.tsx.snap create mode 100644 packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay.test.tsx.snap create mode 100644 packages/cli/src/ui/components/messages/__snapshots__/UserMessage.test.tsx.snap create mode 100644 packages/cli/src/ui/components/messages/__snapshots__/WarningMessage.test.tsx.snap diff --git a/packages/cli/src/ui/components/AboutBox.test.tsx b/packages/cli/src/ui/components/AboutBox.test.tsx new file mode 100644 index 0000000000..b6e5968e53 --- /dev/null +++ b/packages/cli/src/ui/components/AboutBox.test.tsx @@ -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(); + 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(); + 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(); + const output = lastFrame(); + expect(output).toContain('api-key'); + }); +}); diff --git a/packages/cli/src/ui/components/AutoAcceptIndicator.test.tsx b/packages/cli/src/ui/components/AutoAcceptIndicator.test.tsx new file mode 100644 index 0000000000..d71d49d2f1 --- /dev/null +++ b/packages/cli/src/ui/components/AutoAcceptIndicator.test.tsx @@ -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( + , + ); + const output = lastFrame(); + expect(output).toContain('accepting edits'); + expect(output).toContain('(shift + tab to toggle)'); + }); + + it('renders correctly for YOLO mode', () => { + const { lastFrame } = render( + , + ); + const output = lastFrame(); + expect(output).toContain('YOLO mode'); + expect(output).toContain('(ctrl + y to toggle)'); + }); + + it('renders nothing for DEFAULT mode', () => { + const { lastFrame } = render( + , + ); + const output = lastFrame(); + expect(output).not.toContain('accepting edits'); + expect(output).not.toContain('YOLO mode'); + }); +}); diff --git a/packages/cli/src/ui/components/Banner.test.tsx b/packages/cli/src/ui/components/Banner.test.tsx new file mode 100644 index 0000000000..ec50b25510 --- /dev/null +++ b/packages/cli/src/ui/components/Banner.test.tsx @@ -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( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); + + it('handles newlines in text', () => { + const text = 'Line 1\\nLine 2'; + const { lastFrame } = render( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); +}); diff --git a/packages/cli/src/ui/components/ConfigInitDisplay.test.tsx b/packages/cli/src/ui/components/ConfigInitDisplay.test.tsx new file mode 100644 index 0000000000..ed4a60a1de --- /dev/null +++ b/packages/cli/src/ui/components/ConfigInitDisplay.test.tsx @@ -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: () => Spinner, +})); + +// 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(); + 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(); + expect(lastFrame()).toMatchSnapshot(); + }); + + it('updates message on McpClientUpdate event', async () => { + let listener: ((clients?: Map) => void) | undefined; + mockOn.mockImplementation((event, fn) => { + if (event === AppEvent.McpClientUpdate) { + listener = fn; + } + }); + + const { lastFrame } = render(); + + // 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([ + ['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) => void) | undefined; + mockOn.mockImplementation((event, fn) => { + if (event === AppEvent.McpClientUpdate) { + listener = fn; + } + }); + + const { lastFrame } = render(); + + 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(); + }); + }); +}); diff --git a/packages/cli/src/ui/components/ConsoleSummaryDisplay.test.tsx b/packages/cli/src/ui/components/ConsoleSummaryDisplay.test.tsx new file mode 100644 index 0000000000..aacb90e1e3 --- /dev/null +++ b/packages/cli/src/ui/components/ConsoleSummaryDisplay.test.tsx @@ -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(); + expect(lastFrame()).toBe(''); + }); + + it.each([ + [1, '1 error'], + [5, '5 errors'], + ])('renders correct message for %i errors', (count, expectedText) => { + const { lastFrame } = render(); + const output = lastFrame(); + expect(output).toContain(expectedText); + expect(output).toContain('โœ–'); + expect(output).toContain('(F12 for details)'); + }); +}); diff --git a/packages/cli/src/ui/components/ContextUsageDisplay.test.tsx b/packages/cli/src/ui/components/ContextUsageDisplay.test.tsx new file mode 100644 index 0000000000..caa81ee968 --- /dev/null +++ b/packages/cli/src/ui/components/ContextUsageDisplay.test.tsx @@ -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( + , + ); + const output = lastFrame(); + expect(output).toContain('50% context left'); + }); + + it('renders short label when terminal width is small', () => { + const { lastFrame } = render( + , + ); + const output = lastFrame(); + expect(output).toContain('80%'); + expect(output).not.toContain('context left'); + }); + + it('renders 0% when full', () => { + const { lastFrame } = render( + , + ); + const output = lastFrame(); + expect(output).toContain('0% context left'); + }); +}); diff --git a/packages/cli/src/ui/components/CopyModeWarning.test.tsx b/packages/cli/src/ui/components/CopyModeWarning.test.tsx new file mode 100644 index 0000000000..63ca84369f --- /dev/null +++ b/packages/cli/src/ui/components/CopyModeWarning.test.tsx @@ -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(); + expect(lastFrame()).toBe(''); + }); + + it('renders warning when copy mode is enabled', () => { + mockUseUIState.mockReturnValue({ + copyModeEnabled: true, + } as unknown as UIState); + const { lastFrame } = render(); + expect(lastFrame()).toContain('In Copy Mode'); + expect(lastFrame()).toContain('Press any key to exit'); + }); +}); diff --git a/packages/cli/src/ui/components/DebugProfiler.test.tsx b/packages/cli/src/ui/components/DebugProfiler.test.tsx index 164b90962a..b60419be8a 100644 --- a/packages/cli/src/ui/components/DebugProfiler.test.tsx +++ b/packages/cli/src/ui/components/DebugProfiler.test.tsx @@ -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(); + 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(); + const output = lastFrame(); + + expect(output).toContain('Renders: 10 (total)'); + expect(output).toContain('5 (idle)'); + expect(output).toContain('2 (flicker)'); + }); +}); diff --git a/packages/cli/src/ui/components/DetailedMessagesDisplay.test.tsx b/packages/cli/src/ui/components/DetailedMessagesDisplay.test.tsx new file mode 100644 index 0000000000..203b2364c7 --- /dev/null +++ b/packages/cli/src/ui/components/DetailedMessagesDisplay.test.tsx @@ -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; + }) => ( + + {data.map((item: unknown, index: number) => ( + {renderItem({ item })} + ))} + + ), +})); + +describe('DetailedMessagesDisplay', () => { + it('renders nothing when messages are empty', () => { + const { lastFrame } = render( + , + ); + 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( + , + ); + 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( + , + ); + const output = lastFrame(); + + expect(output).toContain('Repeated message'); + expect(output).toContain('(x5)'); + }); +}); diff --git a/packages/cli/src/ui/components/DialogManager.test.tsx b/packages/cli/src/ui/components/DialogManager.test.tsx new file mode 100644 index 0000000000..7477cc3cf4 --- /dev/null +++ b/packages/cli/src/ui/components/DialogManager.test.tsx @@ -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: () => IdeIntegrationNudge, +})); +vi.mock('./LoopDetectionConfirmation.js', () => ({ + LoopDetectionConfirmation: () => LoopDetectionConfirmation, +})); +vi.mock('./FolderTrustDialog.js', () => ({ + FolderTrustDialog: () => FolderTrustDialog, +})); +vi.mock('./ShellConfirmationDialog.js', () => ({ + ShellConfirmationDialog: () => ShellConfirmationDialog, +})); +vi.mock('./ConsentPrompt.js', () => ({ + ConsentPrompt: () => ConsentPrompt, +})); +vi.mock('./ThemeDialog.js', () => ({ + ThemeDialog: () => ThemeDialog, +})); +vi.mock('./SettingsDialog.js', () => ({ + SettingsDialog: () => SettingsDialog, +})); +vi.mock('../auth/AuthInProgress.js', () => ({ + AuthInProgress: () => AuthInProgress, +})); +vi.mock('../auth/AuthDialog.js', () => ({ + AuthDialog: () => AuthDialog, +})); +vi.mock('../auth/ApiAuthDialog.js', () => ({ + ApiAuthDialog: () => ApiAuthDialog, +})); +vi.mock('./EditorSettingsDialog.js', () => ({ + EditorSettingsDialog: () => EditorSettingsDialog, +})); +vi.mock('../privacy/PrivacyNotice.js', () => ({ + PrivacyNotice: () => PrivacyNotice, +})); +vi.mock('./ProQuotaDialog.js', () => ({ + ProQuotaDialog: () => ProQuotaDialog, +})); +vi.mock('./PermissionsModifyTrustDialog.js', () => ({ + PermissionsModifyTrustDialog: () => PermissionsModifyTrustDialog, +})); +vi.mock('./ModelDialog.js', () => ({ + ModelDialog: () => ModelDialog, +})); +vi.mock('./IdeTrustChangeDialog.js', () => ({ + IdeTrustChangeDialog: () => IdeTrustChangeDialog, +})); + +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( + , + // eslint-disable-next-line @typescript-eslint/no-explicit-any + { uiState: baseUiState as any }, + ); + expect(lastFrame()).toBe(''); + }); + + const testCases: Array<[Partial, 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( + , + { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + uiState: { ...baseUiState, ...uiStateOverride } as any, + }, + ); + expect(lastFrame()).toContain(expectedComponent); + }, + ); +}); diff --git a/packages/cli/src/ui/components/EditorSettingsDialog.test.tsx b/packages/cli/src/ui/components/EditorSettingsDialog.test.tsx new file mode 100644 index 0000000000..56638a17ba --- /dev/null +++ b/packages/cli/src/ui/components/EditorSettingsDialog.test.tsx @@ -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({ui}); + + it('renders correctly', () => { + const { lastFrame } = renderWithProvider( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); + + it('calls onSelect when an editor is selected', () => { + const onSelect = vi.fn(); + const { lastFrame } = renderWithProvider( + , + ); + + expect(lastFrame()).toContain('VS Code'); + }); + + it('switches focus between editor and scope sections on Tab', async () => { + const { lastFrame, stdin } = renderWithProvider( + , + ); + + // 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( + , + ); + + 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( + , + ); + + const frame = lastFrame() || ''; + if (!frame.includes('(Also modified')) { + console.log( + 'Modified message test failure. Frame:', + JSON.stringify(frame), + ); + } + expect(frame).toContain('(Also modified'); + }); +}); diff --git a/packages/cli/src/ui/components/ExitWarning.test.tsx b/packages/cli/src/ui/components/ExitWarning.test.tsx new file mode 100644 index 0000000000..df91560cf0 --- /dev/null +++ b/packages/cli/src/ui/components/ExitWarning.test.tsx @@ -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(); + 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(); + 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(); + 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(); + expect(lastFrame()).toBe(''); + }); +}); diff --git a/packages/cli/src/ui/components/GeminiRespondingSpinner.test.tsx b/packages/cli/src/ui/components/GeminiRespondingSpinner.test.tsx new file mode 100644 index 0000000000..88f6c3fd13 --- /dev/null +++ b/packages/cli/src/ui/components/GeminiRespondingSpinner.test.tsx @@ -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(); + // 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(); + expect(lastFrame()).toContain(SCREEN_READER_RESPONDING); + }); + + it('renders nothing when not responding and no non-responding display', () => { + mockUseStreamingContext.mockReturnValue(StreamingState.Idle); + const { lastFrame } = render(); + expect(lastFrame()).toBe(''); + }); + + it('renders non-responding display when provided', () => { + mockUseStreamingContext.mockReturnValue(StreamingState.Idle); + const { lastFrame } = render( + , + ); + 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( + , + ); + expect(lastFrame()).toContain(SCREEN_READER_LOADING); + }); +}); diff --git a/packages/cli/src/ui/components/MainContent.test.tsx b/packages/cli/src/ui/components/MainContent.test.tsx new file mode 100644 index 0000000000..4bd823503c --- /dev/null +++ b/packages/cli/src/ui/components/MainContent.test.tsx @@ -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 } }) => ( + + HistoryItem: {item.content} + + ), +})); + +vi.mock('./AppHeader.js', () => ({ + AppHeader: () => AppHeader, +})); + +vi.mock('./ShowMoreLines.js', () => ({ + ShowMoreLines: () => ShowMoreLines, +})); + +vi.mock('./shared/ScrollableList.js', () => ({ + ScrollableList: ({ + data, + renderItem, + }: { + data: unknown[]; + renderItem: (props: { item: unknown }) => React.JSX.Element; + }) => ( + + ScrollableList + {data.map((item: unknown, index: number) => ( + {renderItem({ item })} + ))} + + ), + 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(); + 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(); + const output = lastFrame(); + + expect(output).toContain('ScrollableList'); + expect(output).toContain('AppHeader'); + expect(output).toContain('HistoryItem: Hello'); + expect(output).toContain('HistoryItem: Hi there'); + }); +}); diff --git a/packages/cli/src/ui/components/MemoryUsageDisplay.test.tsx b/packages/cli/src/ui/components/MemoryUsageDisplay.test.tsx new file mode 100644 index 0000000000..de61fe22fa --- /dev/null +++ b/packages/cli/src/ui/components/MemoryUsageDisplay.test.tsx @@ -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(); + expect(lastFrame()).toContain('50.0 MB'); + }); + + it('updates memory usage over time', async () => { + const { lastFrame } = render(); + 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'); + }); +}); diff --git a/packages/cli/src/ui/components/Notifications.test.tsx b/packages/cli/src/ui/components/Notifications.test.tsx new file mode 100644 index 0000000000..6e0c178e86 --- /dev/null +++ b/packages/cli/src/ui/components/Notifications.test.tsx @@ -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('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(); + 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(); + 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(); + 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(); + expect(lastFrame()).toBe(''); + }); + + it('renders update notification', () => { + mockUseUIState.mockReturnValue({ + initError: null, + streamingState: 'idle', + updateInfo: { message: 'Update available' }, + } as unknown as UIState); + const { lastFrame } = render(); + 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(); + + // 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(); + + // Trigger resolution inside act + await act(async () => { + resolveAccess(undefined); + }); + + expect(lastFrame()).toBe(''); + expect(mockFsWriteFile).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/cli/src/ui/components/Notifications.tsx b/packages/cli/src/ui/components/Notifications.tsx index 05e00ed479..d956afac0d 100644 --- a/packages/cli/src/ui/components/Notifications.tsx +++ b/packages/cli/src/ui/components/Notifications.tsx @@ -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; diff --git a/packages/cli/src/ui/components/QuittingDisplay.test.tsx b/packages/cli/src/ui/components/QuittingDisplay.test.tsx new file mode 100644 index 0000000000..177d469d8e --- /dev/null +++ b/packages/cli/src/ui/components/QuittingDisplay.test.tsx @@ -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(); + 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(); + expect(lastFrame()).toContain('Goodbye'); + expect(lastFrame()).toContain('See you later'); + }); +}); diff --git a/packages/cli/src/ui/components/RawMarkdownIndicator.test.tsx b/packages/cli/src/ui/components/RawMarkdownIndicator.test.tsx new file mode 100644 index 0000000000..30eaf90c25 --- /dev/null +++ b/packages/cli/src/ui/components/RawMarkdownIndicator.test.tsx @@ -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(); + 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(); + expect(lastFrame()).toContain('raw markdown mode'); + expect(lastFrame()).toContain('alt+m to toggle'); + }); +}); diff --git a/packages/cli/src/ui/components/SessionBrowser.test.tsx b/packages/cli/src/ui/components/SessionBrowser.test.tsx index e063af40aa..2a2f239d11 100644 --- a/packages/cli/src/ui/components/SessionBrowser.test.tsx +++ b/packages/cli/src/ui/components/SessionBrowser.test.tsx @@ -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(); }); }); diff --git a/packages/cli/src/ui/components/ShellInputPrompt.test.tsx b/packages/cli/src/ui/components/ShellInputPrompt.test.tsx new file mode 100644 index 0000000000..815cfcadf7 --- /dev/null +++ b/packages/cli/src/ui/components/ShellInputPrompt.test.tsx @@ -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( + , + ); + expect(lastFrame()).toBe(''); + }); + + it.each([ + ['a', 'a'], + ['b', 'b'], + ])('handles keypress input: %s', (name, sequence) => { + render(); + + // 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(); + + 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(); + + 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(); + + const handler = mockUseKeypress.mock.calls[0][0]; + + handler({ + name: 'a', + sequence: 'a', + ctrl: false, + shift: false, + meta: false, + }); + + expect(mockWriteToPty).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/cli/src/ui/components/ShellModeIndicator.test.tsx b/packages/cli/src/ui/components/ShellModeIndicator.test.tsx new file mode 100644 index 0000000000..73c2b5d03a --- /dev/null +++ b/packages/cli/src/ui/components/ShellModeIndicator.test.tsx @@ -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(); + expect(lastFrame()).toContain('shell mode enabled'); + expect(lastFrame()).toContain('esc to disable'); + }); +}); diff --git a/packages/cli/src/ui/components/ShowMoreLines.test.tsx b/packages/cli/src/ui/components/ShowMoreLines.test.tsx new file mode 100644 index 0000000000..beec038bbe --- /dev/null +++ b/packages/cli/src/ui/components/ShowMoreLines.test.tsx @@ -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 + >); + mockUseStreamingContext.mockReturnValue(streamingState); + const { lastFrame } = render( + , + ); + 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>); + mockUseStreamingContext.mockReturnValue(streamingState); + const { lastFrame } = render(); + expect(lastFrame()).toContain('Press ctrl-s to show more lines'); + }, + ); +}); diff --git a/packages/cli/src/ui/components/SuggestionsDisplay.test.tsx b/packages/cli/src/ui/components/SuggestionsDisplay.test.tsx new file mode 100644 index 0000000000..6931268a37 --- /dev/null +++ b/packages/cli/src/ui/components/SuggestionsDisplay.test.tsx @@ -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( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); + + it('renders nothing when empty and not loading', () => { + const { lastFrame } = render( + , + ); + expect(lastFrame()).toBe(''); + }); + + it('renders suggestions list', () => { + const { lastFrame } = render( + , + ); + 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( + , + ); + 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( + , + ); + 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( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); +}); diff --git a/packages/cli/src/ui/components/ThemedGradient.test.tsx b/packages/cli/src/ui/components/ThemedGradient.test.tsx new file mode 100644 index 0000000000..9ea194c9f9 --- /dev/null +++ b/packages/cli/src/ui/components/ThemedGradient.test.tsx @@ -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(Hello); + 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. +}); diff --git a/packages/cli/src/ui/components/Tips.test.tsx b/packages/cli/src/ui/components/Tips.test.tsx new file mode 100644 index 0000000000..adbedb5326 --- /dev/null +++ b/packages/cli/src/ui/components/Tips.test.tsx @@ -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(); + const output = lastFrame(); + expect(output).toContain(expectedText); + }); +}); diff --git a/packages/cli/src/ui/components/UpdateNotification.test.tsx b/packages/cli/src/ui/components/UpdateNotification.test.tsx new file mode 100644 index 0000000000..fa1632d50c --- /dev/null +++ b/packages/cli/src/ui/components/UpdateNotification.test.tsx @@ -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( + , + ); + expect(lastFrame()).toContain('Update available!'); + }); +}); diff --git a/packages/cli/src/ui/components/__snapshots__/Banner.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/Banner.test.tsx.snap new file mode 100644 index 0000000000..07b9ecf5b8 --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/Banner.test.tsx.snap @@ -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 โ”‚ +โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ" +`; diff --git a/packages/cli/src/ui/components/__snapshots__/ConfigInitDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ConfigInitDisplay.test.tsx.snap new file mode 100644 index 0000000000..9c7b7cc0d7 --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/ConfigInitDisplay.test.tsx.snap @@ -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)" +`; diff --git a/packages/cli/src/ui/components/__snapshots__/EditorSettingsDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/EditorSettingsDialog.test.tsx.snap new file mode 100644 index 0000000000..dd0719a90e --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/EditorSettingsDialog.test.tsx.snap @@ -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) โ”‚ +โ”‚ โ”‚ +โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ" +`; diff --git a/packages/cli/src/ui/components/__snapshots__/Notifications.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/Notifications.test.tsx.snap new file mode 100644 index 0000000000..32704a9313 --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/Notifications.test.tsx.snap @@ -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 โ”‚ +โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ +" +`; diff --git a/packages/cli/src/ui/components/__snapshots__/SessionBrowser.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/SessionBrowser.test.tsx.snap new file mode 100644 index 0000000000..efffa48b4e --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/SessionBrowser.test.tsx.snap @@ -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" +`; diff --git a/packages/cli/src/ui/components/__snapshots__/SuggestionsDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/SuggestionsDisplay.test.tsx.snap new file mode 100644 index 0000000000..ce1640ce25 --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/SuggestionsDisplay.test.tsx.snap @@ -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" +`; diff --git a/packages/cli/src/ui/components/messages/ErrorMessage.test.tsx b/packages/cli/src/ui/components/messages/ErrorMessage.test.tsx new file mode 100644 index 0000000000..1c605e0128 --- /dev/null +++ b/packages/cli/src/ui/components/messages/ErrorMessage.test.tsx @@ -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(); + const output = lastFrame(); + + expect(output).toMatchSnapshot(); + }); + + it('renders multiline error messages', () => { + const message = 'Error line 1\nError line 2'; + const { lastFrame } = render(); + const output = lastFrame(); + + expect(output).toMatchSnapshot(); + }); +}); diff --git a/packages/cli/src/ui/components/messages/InfoMessage.test.tsx b/packages/cli/src/ui/components/messages/InfoMessage.test.tsx new file mode 100644 index 0000000000..d5c3c78de3 --- /dev/null +++ b/packages/cli/src/ui/components/messages/InfoMessage.test.tsx @@ -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(); + const output = lastFrame(); + + expect(output).toMatchSnapshot(); + }); + + it('renders with a custom icon', () => { + const { lastFrame } = render( + , + ); + const output = lastFrame(); + + expect(output).toMatchSnapshot(); + }); + + it('renders multiline info messages', () => { + const message = 'Info line 1\nInfo line 2'; + const { lastFrame } = render(); + const output = lastFrame(); + + expect(output).toMatchSnapshot(); + }); +}); diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx index 967a301feb..ef41d5590c 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx @@ -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(); }); }); }); diff --git a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx index 3d1b641389..983bca8669 100644 --- a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx @@ -107,10 +107,7 @@ describe('', () => { 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('', () => { , StreamingState.Idle, ); - expect(lastFrame()).toContain('โœ“'); + expect(lastFrame()).toMatchSnapshot(); }); it('shows o for Pending status', () => { @@ -127,7 +124,7 @@ describe('', () => { , StreamingState.Idle, ); - expect(lastFrame()).toContain('o'); + expect(lastFrame()).toMatchSnapshot(); }); it('shows ? for Confirming status', () => { @@ -135,7 +132,7 @@ describe('', () => { , StreamingState.Idle, ); - expect(lastFrame()).toContain('?'); + expect(lastFrame()).toMatchSnapshot(); }); it('shows - for Canceled status', () => { @@ -143,7 +140,7 @@ describe('', () => { , StreamingState.Idle, ); - expect(lastFrame()).toContain('-'); + expect(lastFrame()).toMatchSnapshot(); }); it('shows x for Error status', () => { @@ -151,7 +148,7 @@ describe('', () => { , 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('', () => { , 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('', () => { , 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('', () => { , StreamingState.Responding, // Simulate app still responding ); - expect(lastFrame()).toContain('MockRespondingSpinner'); - expect(lastFrame()).not.toContain('โœ“'); + expect(lastFrame()).toMatchSnapshot(); }); }); @@ -196,7 +188,7 @@ describe('', () => { 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('', () => { 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( , @@ -214,7 +206,7 @@ describe('', () => { // 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('', () => { , StreamingState.Idle, ); - expect(lastFrame()).toContain('MockAnsiOutput:hello'); + expect(lastFrame()).toMatchSnapshot(); }); }); diff --git a/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx b/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx new file mode 100644 index 0000000000..98b0af9f40 --- /dev/null +++ b/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx @@ -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; + }) => ( + + + DiffRenderer: {filename} - {diffContent} + + + ), +})); + +vi.mock('../../utils/MarkdownDisplay.js', () => ({ + MarkdownDisplay: ({ text }: { text: string }) => ( + + MarkdownDisplay: {text} + + ), +})); + +vi.mock('../AnsiOutput.js', () => ({ + AnsiOutputText: ({ data }: { data: unknown }) => ( + + AnsiOutputText: {JSON.stringify(data)} + + ), +})); + +vi.mock('../shared/MaxSizedBox.js', () => ({ + MaxSizedBox: ({ children }: { children: React.ReactNode }) => ( + + MaxSizedBox: + {children} + + ), +})); + +// 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( + , + ); + const output = lastFrame(); + + expect(output).toMatchSnapshot(); + }); + + it('renders string result as plain text when renderOutputAsMarkdown is false', () => { + const { lastFrame } = render( + , + ); + const output = lastFrame(); + + expect(output).toMatchSnapshot(); + }); + + it('truncates very long string results', { timeout: 20000 }, () => { + const longString = 'a'.repeat(1000005); + const { lastFrame } = render( + , + ); + const output = lastFrame(); + + expect(output).toMatchSnapshot(); + }); + + it('renders file diff result', () => { + const diffResult = { + fileDiff: 'diff content', + fileName: 'test.ts', + }; + const { lastFrame } = render( + , + ); + const output = lastFrame(); + + expect(output).toMatchSnapshot(); + }); + + it('renders ANSI output result', () => { + const ansiResult = { + text: 'ansi content', + }; + const { lastFrame } = render( + , + ); + const output = lastFrame(); + + expect(output).toMatchSnapshot(); + }); + + it('renders nothing for todos result', () => { + const todoResult = { + todos: [], + }; + const { lastFrame } = render( + , + ); + 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( + , + ); + 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( + , + ); + const output = lastFrame(); + + expect(output).toMatchSnapshot(); + }); +}); diff --git a/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx b/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx index b02f616eb0..49002d50e9 100644 --- a/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx +++ b/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx @@ -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; diff --git a/packages/cli/src/ui/components/messages/UserMessage.test.tsx b/packages/cli/src/ui/components/messages/UserMessage.test.tsx new file mode 100644 index 0000000000..2f130c5469 --- /dev/null +++ b/packages/cli/src/ui/components/messages/UserMessage.test.tsx @@ -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( + , + ); + const output = lastFrame(); + + expect(output).toMatchSnapshot(); + }); + + it('renders slash command message', () => { + const { lastFrame } = render(); + const output = lastFrame(); + + expect(output).toMatchSnapshot(); + }); + + it('renders multiline user message', () => { + const message = 'Line 1\nLine 2'; + const { lastFrame } = render(); + const output = lastFrame(); + + expect(output).toMatchSnapshot(); + }); +}); diff --git a/packages/cli/src/ui/components/messages/WarningMessage.test.tsx b/packages/cli/src/ui/components/messages/WarningMessage.test.tsx new file mode 100644 index 0000000000..fcb635d624 --- /dev/null +++ b/packages/cli/src/ui/components/messages/WarningMessage.test.tsx @@ -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(); + const output = lastFrame(); + + expect(output).toMatchSnapshot(); + }); + + it('renders multiline warning messages', () => { + const message = 'Warning line 1\nWarning line 2'; + const { lastFrame } = render(); + const output = lastFrame(); + + expect(output).toMatchSnapshot(); + }); +}); diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ErrorMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ErrorMessage.test.tsx.snap new file mode 100644 index 0000000000..0f5c270ae4 --- /dev/null +++ b/packages/cli/src/ui/components/messages/__snapshots__/ErrorMessage.test.tsx.snap @@ -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 +" +`; diff --git a/packages/cli/src/ui/components/messages/__snapshots__/InfoMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/InfoMessage.test.tsx.snap new file mode 100644 index 0000000000..47b63b7681 --- /dev/null +++ b/packages/cli/src/ui/components/messages/__snapshots__/InfoMessage.test.tsx.snap @@ -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" +`; diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage.test.tsx.snap new file mode 100644 index 0000000000..95aa1fca13 --- /dev/null +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage.test.tsx.snap @@ -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) +" +`; diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolMessage.test.tsx.snap new file mode 100644 index 0000000000..fd161ce9a2 --- /dev/null +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolMessage.test.tsx.snap @@ -0,0 +1,96 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` > ToolStatusIndicator rendering > shows ? for Confirming status 1`] = ` +"โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ +โ”‚ ? test-tool A tool for testing โ”‚ +โ”‚ โ”‚ +โ”‚ MockMarkdown:Test result โ”‚" +`; + +exports[` > ToolStatusIndicator rendering > shows - for Canceled status 1`] = ` +"โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ +โ”‚ - test-tool A tool for testing โ”‚ +โ”‚ โ”‚ +โ”‚ MockMarkdown:Test result โ”‚" +`; + +exports[` > ToolStatusIndicator rendering > shows MockRespondingSpinner for Executing status when streamingState is Responding 1`] = ` +"โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ +โ”‚ MockRespondingSpinnertest-tool A tool for testing โ”‚ +โ”‚ โ”‚ +โ”‚ MockMarkdown:Test result โ”‚" +`; + +exports[` > ToolStatusIndicator rendering > shows o for Pending status 1`] = ` +"โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ +โ”‚ o test-tool A tool for testing โ”‚ +โ”‚ โ”‚ +โ”‚ MockMarkdown:Test result โ”‚" +`; + +exports[` > ToolStatusIndicator rendering > shows paused spinner for Executing status when streamingState is Idle 1`] = ` +"โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ +โ”‚ โŠท test-tool A tool for testing โ”‚ +โ”‚ โ”‚ +โ”‚ MockMarkdown:Test result โ”‚" +`; + +exports[` > ToolStatusIndicator rendering > shows paused spinner for Executing status when streamingState is WaitingForConfirmation 1`] = ` +"โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ +โ”‚ โŠท test-tool A tool for testing โ”‚ +โ”‚ โ”‚ +โ”‚ MockMarkdown:Test result โ”‚" +`; + +exports[` > ToolStatusIndicator rendering > shows x for Error status 1`] = ` +"โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ +โ”‚ x test-tool A tool for testing โ”‚ +โ”‚ โ”‚ +โ”‚ MockMarkdown:Test result โ”‚" +`; + +exports[` > ToolStatusIndicator rendering > shows โœ“ for Success status 1`] = ` +"โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ +โ”‚ โœ“ test-tool A tool for testing โ”‚ +โ”‚ โ”‚ +โ”‚ MockMarkdown:Test result โ”‚" +`; + +exports[` > renders AnsiOutputText for AnsiOutput results 1`] = ` +"โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ +โ”‚ โœ“ test-tool A tool for testing โ”‚ +โ”‚ โ”‚ +โ”‚ MockAnsiOutput:hello โ”‚" +`; + +exports[` > renders DiffRenderer for diff results 1`] = ` +"โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ +โ”‚ โœ“ test-tool A tool for testing โ”‚ +โ”‚ โ”‚ +โ”‚ MockDiff:--- a/file.txt โ”‚ +โ”‚ +++ b/file.txt โ”‚ +โ”‚ @@ -1 +1 @@ โ”‚ +โ”‚ -old โ”‚ +โ”‚ +new โ”‚" +`; + +exports[` > renders basic tool information 1`] = ` +"โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ +โ”‚ โœ“ test-tool A tool for testing โ”‚ +โ”‚ โ”‚ +โ”‚ MockMarkdown:Test result โ”‚" +`; + +exports[` > renders emphasis correctly 1`] = ` +"โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ +โ”‚ โœ“ test-tool A tool for testing โ† โ”‚ +โ”‚ โ”‚ +โ”‚ MockMarkdown:Test result โ”‚" +`; + +exports[` > renders emphasis correctly 2`] = ` +"โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ +โ”‚ โœ“ test-tool A tool for testing โ”‚ +โ”‚ โ”‚ +โ”‚ MockMarkdown:Test result โ”‚" +`; diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay.test.tsx.snap new file mode 100644 index 0000000000..2919124771 --- /dev/null +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay.test.tsx.snap @@ -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" +`; diff --git a/packages/cli/src/ui/components/messages/__snapshots__/UserMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/UserMessage.test.tsx.snap new file mode 100644 index 0000000000..1d1e950bb3 --- /dev/null +++ b/packages/cli/src/ui/components/messages/__snapshots__/UserMessage.test.tsx.snap @@ -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 +" +`; diff --git a/packages/cli/src/ui/components/messages/__snapshots__/WarningMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/WarningMessage.test.tsx.snap new file mode 100644 index 0000000000..4ebfdcf4e0 --- /dev/null +++ b/packages/cli/src/ui/components/messages/__snapshots__/WarningMessage.test.tsx.snap @@ -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!" +`;