Files
gemini-cli/packages/cli/src/ui/hooks/useReverseSearchCompletion.test.tsx
T
2026-03-20 16:16:14 +00:00

309 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { act } from 'react';
import { renderHookWithProviders } from '../../test-utils/render.js';
import { useReverseSearchCompletion } from './useReverseSearchCompletion.js';
import { useTextBuffer } from '../components/shared/text-buffer.js';
describe('useReverseSearchCompletion', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
function useTextBufferForTest(text: string) {
return useTextBuffer({
initialText: text,
initialCursorOffset: text.length,
viewport: { width: 80, height: 20 },
onChange: () => {},
});
}
describe('Core Hook Behavior', () => {
describe('State Management', () => {
it('should initialize with default state', async () => {
const mockShellHistory = ['echo hello'];
const { result } = await renderHookWithProviders(() =>
useReverseSearchCompletion(
useTextBufferForTest(''),
mockShellHistory,
false,
),
);
expect(result.current.suggestions).toEqual([]);
expect(result.current.activeSuggestionIndex).toBe(-1);
expect(result.current.visibleStartIndex).toBe(0);
expect(result.current.showSuggestions).toBe(false);
expect(result.current.isLoadingSuggestions).toBe(false);
});
it('should reset state when reverseSearchActive becomes false', async () => {
const mockShellHistory = ['echo hello'];
const { result, rerender } = await renderHookWithProviders(
({ text, active }) => {
const textBuffer = useTextBufferForTest(text);
return useReverseSearchCompletion(
textBuffer,
mockShellHistory,
active,
);
},
{ initialProps: { text: 'echo', active: true } },
);
// Simulate reverseSearchActive becoming false
rerender({ text: 'echo', active: false });
expect(result.current.suggestions).toEqual([]);
expect(result.current.activeSuggestionIndex).toBe(-1);
expect(result.current.visibleStartIndex).toBe(0);
expect(result.current.showSuggestions).toBe(false);
});
describe('Navigation', () => {
it('should handle navigateUp with no suggestions', async () => {
const mockShellHistory = ['echo hello'];
const { result } = await renderHookWithProviders(() =>
useReverseSearchCompletion(
useTextBufferForTest('grep'),
mockShellHistory,
true,
),
);
act(() => {
result.current.navigateUp();
});
expect(result.current.activeSuggestionIndex).toBe(-1);
});
it('should handle navigateDown with no suggestions', async () => {
const mockShellHistory = ['echo hello'];
const { result } = await renderHookWithProviders(() =>
useReverseSearchCompletion(
useTextBufferForTest('grep'),
mockShellHistory,
true,
),
);
act(() => {
result.current.navigateDown();
});
expect(result.current.activeSuggestionIndex).toBe(-1);
});
it('should navigate up through suggestions with wrap-around', async () => {
const mockShellHistory = [
'ls -l',
'ls -la',
'cd /some/path',
'git status',
'echo "Hello, World!"',
'echo Hi',
];
const { result } = await renderHookWithProviders(() =>
useReverseSearchCompletion(
useTextBufferForTest('echo'),
mockShellHistory,
true,
),
);
expect(result.current.suggestions.length).toBe(2);
expect(result.current.activeSuggestionIndex).toBe(0);
act(() => {
result.current.navigateUp();
});
expect(result.current.activeSuggestionIndex).toBe(1);
});
it('should navigate down through suggestions with wrap-around', async () => {
const mockShellHistory = [
'ls -l',
'ls -la',
'cd /some/path',
'git status',
'echo "Hello, World!"',
'echo Hi',
];
const { result } = await renderHookWithProviders(() =>
useReverseSearchCompletion(
useTextBufferForTest('ls'),
mockShellHistory,
true,
),
);
expect(result.current.suggestions.length).toBe(2);
expect(result.current.activeSuggestionIndex).toBe(0);
act(() => {
result.current.navigateDown();
});
expect(result.current.activeSuggestionIndex).toBe(1);
});
it('should handle navigation with multiple suggestions', async () => {
const mockShellHistory = [
'ls -l',
'ls -la',
'cd /some/path/l',
'git status',
'echo "Hello, World!"',
'echo "Hi all"',
];
const { result } = await renderHookWithProviders(() =>
useReverseSearchCompletion(
useTextBufferForTest('l'),
mockShellHistory,
true,
),
);
expect(result.current.suggestions.length).toBe(5);
expect(result.current.activeSuggestionIndex).toBe(0);
act(() => {
result.current.navigateDown();
});
expect(result.current.activeSuggestionIndex).toBe(1);
act(() => {
result.current.navigateDown();
});
expect(result.current.activeSuggestionIndex).toBe(2);
act(() => {
result.current.navigateUp();
});
expect(result.current.activeSuggestionIndex).toBe(1);
act(() => {
result.current.navigateUp();
});
expect(result.current.activeSuggestionIndex).toBe(0);
act(() => {
result.current.navigateUp();
});
expect(result.current.activeSuggestionIndex).toBe(4);
});
it('should handle navigation with large suggestion lists and scrolling', async () => {
const largeMockCommands = Array.from(
{ length: 15 },
(_, i) => `echo ${i}`,
);
const { result } = await renderHookWithProviders(() =>
useReverseSearchCompletion(
useTextBufferForTest('echo'),
largeMockCommands,
true,
),
);
expect(result.current.suggestions.length).toBe(15);
expect(result.current.activeSuggestionIndex).toBe(0);
expect(result.current.visibleStartIndex).toBe(0);
act(() => {
result.current.navigateUp();
});
expect(result.current.activeSuggestionIndex).toBe(14);
expect(result.current.visibleStartIndex).toBe(Math.max(0, 15 - 8));
});
});
});
});
describe('Filtering', () => {
it('filters history by buffer.text and sets showSuggestions', async () => {
const history = ['foo', 'barfoo', 'baz'];
const { result } = await renderHookWithProviders(() =>
useReverseSearchCompletion(useTextBufferForTest('foo'), history, true),
);
// should only return the two entries containing "foo"
expect(result.current.suggestions.map((s) => s.value)).toEqual([
'foo',
'barfoo',
]);
expect(result.current.showSuggestions).toBe(true);
});
it('hides suggestions when there are no matches', async () => {
const history = ['alpha', 'beta'];
const { result } = await renderHookWithProviders(() =>
useReverseSearchCompletion(useTextBufferForTest('γ'), history, true),
);
expect(result.current.suggestions).toEqual([]);
expect(result.current.showSuggestions).toBe(false);
});
it('does not cause state updates or timeouts when reverseSearchActive is false', async () => {
const history = ['alpha', 'beta'];
const { result, rerender } = await renderHookWithProviders(
({ active }) => {
const textBuffer = useTextBufferForTest('');
return {
search: useReverseSearchCompletion(textBuffer, history, active),
buffer: textBuffer,
};
},
{ initialProps: { active: false } },
);
// Typing while inactive should NOT update debounced search state.
// We change text to 'a', advance time past the debounce period (100ms),
// and ensure suggestions remain empty because the debounced value never changed.
act(() => {
result.current.buffer.setText('a');
});
act(() => {
vi.advanceTimersByTime(200);
});
expect(result.current.search.suggestions).toEqual([]);
// Activating reverse search and setting text to 'alpha' forces useDebouncedValue
// to receive 'alpha', scheduling the timeout.
act(() => {
result.current.buffer.setText('alpha');
});
rerender({ active: true });
act(() => {
vi.advanceTimersByTime(200);
});
expect(result.current.search.suggestions.map((s) => s.value)).toEqual([
'alpha',
]);
});
});
});