File size: 3,972 Bytes
52efc7b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { act } from 'react';
import { ToolMessage } from './ToolMessage.js';
import { ShellToolMessage } from './ShellToolMessage.js';
import { StreamingState } from '../../types.js';
import { renderWithProviders } from '../../../test-utils/render.js';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
  SHELL_COMMAND_NAME,
  SHELL_FOCUS_HINT_DELAY_MS,
} from '../../constants.js';
import {
  type Config,
  type ToolResultDisplay,
  CoreToolCallStatus,
} from '@google/gemini-cli-core';

vi.mock('../GeminiRespondingSpinner.js', () => ({
  GeminiRespondingSpinner: () => null,
}));

vi.mock('./ToolResultDisplay.js', () => ({
  ToolResultDisplay: () => null,
}));

describe('Focus Hint', () => {
  const mockConfig = {
    getEnableInteractiveShell: () => true,
  } as Config;

  const baseProps = {
    callId: 'tool-123',
    name: SHELL_COMMAND_NAME,
    description: 'A tool for testing',
    resultDisplay: undefined as ToolResultDisplay | undefined,
    status: CoreToolCallStatus.Executing,
    terminalWidth: 80,
    confirmationDetails: undefined,
    emphasis: 'medium' as const,
    isFirst: true,
    borderColor: 'green',
    borderDimColor: false,
    config: mockConfig,
    ptyId: 1,
    activeShellPtyId: 1,
  };

  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.restoreAllMocks();
    vi.useRealTimers();
  });

  const testCases = [
    { Component: ToolMessage, componentName: 'ToolMessage' },
    { Component: ShellToolMessage, componentName: 'ShellToolMessage' },
  ];

  describe.each(testCases)('$componentName', ({ Component }) => {
    it('shows focus hint after delay even with NO output', async () => {
      const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
        <Component {...baseProps} resultDisplay={undefined} />,
        { uiState: { streamingState: StreamingState.Idle } },
      );

      // Initially, no focus hint
      expect(lastFrame()).toMatchSnapshot('initial-no-output');

      // Advance timers by the delay
      await act(async () => {
        vi.advanceTimersByTime(SHELL_FOCUS_HINT_DELAY_MS + 100);
      });
      await waitUntilReady();

      // Now it SHOULD contain the focus hint
      expect(lastFrame()).toMatchSnapshot('after-delay-no-output');
      expect(lastFrame()).toContain('(Tab to focus)');
      unmount();
    });

    it('shows focus hint after delay with output', async () => {
      const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
        <Component {...baseProps} resultDisplay="Some output" />,
        { uiState: { streamingState: StreamingState.Idle } },
      );

      // Initially, no focus hint
      expect(lastFrame()).toMatchSnapshot('initial-with-output');

      // Advance timers
      await act(async () => {
        vi.advanceTimersByTime(SHELL_FOCUS_HINT_DELAY_MS + 100);
      });
      await waitUntilReady();

      expect(lastFrame()).toMatchSnapshot('after-delay-with-output');
      expect(lastFrame()).toContain('(Tab to focus)');
      unmount();
    });
  });

  it('handles long descriptions by shrinking them to show the focus hint', async () => {
    const longDescription = 'A'.repeat(100);
    const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
      <ToolMessage
        {...baseProps}
        description={longDescription}
        resultDisplay="output"
      />,
      { uiState: { streamingState: StreamingState.Idle } },
    );

    await act(async () => {
      vi.advanceTimersByTime(SHELL_FOCUS_HINT_DELAY_MS + 100);
    });
    await waitUntilReady();

    // The focus hint should be visible
    expect(lastFrame()).toMatchSnapshot('long-description');
    expect(lastFrame()).toContain('(Tab to focus)');
    // The name should still be visible
    expect(lastFrame()).toContain(SHELL_COMMAND_NAME);
    unmount();
  });
});