summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/hooks/useInputHistory.test.ts
blob: 34af31b2a98ba32035408a806f1f06bf9b471057 (plain)
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
// packages/cli/src/ui/hooks/useInputHistory.test.ts
import { renderHook, act } from '@testing-library/react';
import { useInput } from 'ink';
import { vi, describe, test, expect, beforeEach, Mock } from 'vitest';
import { useInputHistory } from './useInputHistory.js';

// Mock the useInput hook from Ink
vi.mock('ink', async (importOriginal) => {
  const originalInk = await importOriginal<typeof import('ink')>();
  return {
    ...originalInk, // Keep other exports
    useInput: vi.fn(), // Mock useInput
  };
});

// Helper type for the mocked useInput callback
type UseInputCallback = (input: string, key: any) => void;

describe('useInputHistory Hook', () => {
  let mockUseInputCallback: UseInputCallback | undefined;
  const mockUserMessages = ['msg1', 'msg2', 'msg3']; // Sample history

  beforeEach(() => {
    // Reset the mock before each test and capture the callback
    (useInput as Mock).mockImplementation((callback, options) => {
      // Only store the callback if the hook is active in the test
      if (options?.isActive !== false) {
        mockUseInputCallback = callback;
      } else {
        mockUseInputCallback = undefined;
      }
    });
  });

  // Helper function to simulate key press by invoking the captured callback
  const simulateKeyPress = (key: object, input: string = '') => {
    act(() => {
      if (mockUseInputCallback) {
        mockUseInputCallback(input, key);
      } else {
        // Optionally throw an error if trying to press key when inactive
        // console.warn('Simulated key press while useInput was inactive');
      }
    });
  };

  test('should initialize with empty query', () => {
    const { result } = renderHook(() =>
      useInputHistory({ userMessages: [], isActive: true }),
    );
    expect(result.current.query).toBe('');
  });

  test('up arrow should do nothing if history is empty', () => {
    const { result } = renderHook(() =>
      useInputHistory({ userMessages: [], isActive: true }),
    );
    simulateKeyPress({ upArrow: true });
    expect(result.current.query).toBe('');
  });

  test('up arrow should recall the last message', () => {
    const { result } = renderHook(() =>
      useInputHistory({ userMessages: mockUserMessages, isActive: true }),
    );
    simulateKeyPress({ upArrow: true });
    expect(result.current.query).toBe('msg3'); // Last message
  });

  test('repeated up arrows should navigate history', () => {
    const { result } = renderHook(() =>
      useInputHistory({ userMessages: mockUserMessages, isActive: true }),
    );
    simulateKeyPress({ upArrow: true }); // -> msg3
    simulateKeyPress({ upArrow: true }); // -> msg2
    expect(result.current.query).toBe('msg2');
    simulateKeyPress({ upArrow: true }); // -> msg1
    expect(result.current.query).toBe('msg1');
    simulateKeyPress({ upArrow: true }); // -> stays on msg1
    expect(result.current.query).toBe('msg1');
  });

  test('down arrow should navigate history forward', () => {
    const { result } = renderHook(() =>
      useInputHistory({ userMessages: mockUserMessages, isActive: true }),
    );
    simulateKeyPress({ upArrow: true }); // -> msg3
    simulateKeyPress({ upArrow: true }); // -> msg2
    simulateKeyPress({ upArrow: true }); // -> msg1
    expect(result.current.query).toBe('msg1');

    simulateKeyPress({ downArrow: true }); // -> msg2
    expect(result.current.query).toBe('msg2');
    simulateKeyPress({ downArrow: true }); // -> msg3
    expect(result.current.query).toBe('msg3');
  });

  test('down arrow should restore original query', () => {
    const { result } = renderHook(() =>
      useInputHistory({ userMessages: mockUserMessages, isActive: true }),
    );

    // Set initial query
    act(() => {
      result.current.setQuery('original typing');
    });
    expect(result.current.query).toBe('original typing');

    simulateKeyPress({ upArrow: true }); // -> msg3
    expect(result.current.query).toBe('msg3');

    simulateKeyPress({ downArrow: true }); // -> original typing
    expect(result.current.query).toBe('original typing');

    // Pressing down again should do nothing
    simulateKeyPress({ downArrow: true });
    expect(result.current.query).toBe('original typing');
  });

  test('typing should reset navigation', () => {
    const { result } = renderHook(() =>
      useInputHistory({ userMessages: mockUserMessages, isActive: true }),
    );

    simulateKeyPress({ upArrow: true }); // -> msg3
    expect(result.current.query).toBe('msg3');

    // Simulate typing 'x' (Note: we manually call setQuery here, as useInput is mocked)
    act(() => {
      result.current.setQuery(result.current.query + 'x');
    });
    // Also simulate the input event that would trigger the reset
    simulateKeyPress({}, 'x');
    expect(result.current.query).toBe('msg3x');

    simulateKeyPress({ upArrow: true }); // Should restart navigation -> msg3
    expect(result.current.query).toBe('msg3');
  });

  test('calling resetHistoryNav should clear navigation state', () => {
    const { result } = renderHook(() =>
      useInputHistory({ userMessages: mockUserMessages, isActive: true }),
    );

    // Set initial query and navigate
    act(() => {
      result.current.setQuery('original');
    });
    simulateKeyPress({ upArrow: true }); // -> msg3
    expect(result.current.query).toBe('msg3');

    // Reset
    act(() => {
      result.current.resetHistoryNav();
    });

    // Press down - should restore original query ('original') because nav was reset
    // However, our current resetHistoryNav also clears originalQueryBeforeNav.
    // Let's test that down does nothing because historyIndex is -1
    simulateKeyPress({ downArrow: true });
    expect(result.current.query).toBe('msg3'); // Stays msg3 because downArrow doesn't run when index is -1

    // Press up - should start nav again from the top
    simulateKeyPress({ upArrow: true });
    expect(result.current.query).toBe('msg3');
  });

  test('should not trigger callback if isActive is false', () => {
    renderHook(() =>
      useInputHistory({ userMessages: mockUserMessages, isActive: false }),
    );
    // mockUseInputCallback should be undefined because isActive was false
    expect(mockUseInputCallback).toBeUndefined();
    // Attempting to simulate should not throw error (or check internal state if possible)
    expect(() => simulateKeyPress({ upArrow: true })).not.toThrow();
  });
});