Files
gemini-cli/packages/cli/src/ui/components/shared/SearchableList.test.tsx

158 lines
3.8 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import React from 'react';
import { render } from '../../../test-utils/render.js';
import { waitFor } from '../../../test-utils/async.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { SearchableList, type SearchableListProps } from './SearchableList.js';
import { KeypressProvider } from '../../contexts/KeypressContext.js';
import { type GenericListItem } from '../../hooks/useFuzzyList.js';
// Mock UI State
vi.mock('../../contexts/UIStateContext.js', () => ({
useUIState: () => ({
mainAreaWidth: 100,
}),
}));
const mockItems: GenericListItem[] = [
{
key: 'item-1',
label: 'Item One',
description: 'Description for item one',
},
{
key: 'item-2',
label: 'Item Two',
description: 'Description for item two',
},
{
key: 'item-3',
label: 'Item Three',
description: 'Description for item three',
},
];
describe('SearchableList', () => {
let mockOnSelect: ReturnType<typeof vi.fn>;
let mockOnClose: ReturnType<typeof vi.fn>;
beforeEach(() => {
vi.clearAllMocks();
mockOnSelect = vi.fn();
mockOnClose = vi.fn();
});
const renderList = (
props: Partial<SearchableListProps<GenericListItem>> = {},
) => {
const defaultProps: SearchableListProps<GenericListItem> = {
title: 'Test List',
items: mockItems,
onSelect: mockOnSelect,
onClose: mockOnClose,
...props,
};
return render(
<KeypressProvider>
<SearchableList {...defaultProps} />
</KeypressProvider>,
);
};
it('should render all items initially', async () => {
const { lastFrame, waitUntilReady } = renderList();
await waitUntilReady();
const frame = lastFrame();
// Check for title
expect(frame).toContain('Test List');
// Check for items
expect(frame).toContain('Item One');
expect(frame).toContain('Item Two');
expect(frame).toContain('Item Three');
// Check for descriptions
expect(frame).toContain('Description for item one');
});
it('should filter items based on search query', async () => {
const { lastFrame, stdin } = renderList();
// Type "Two" into search
await React.act(async () => {
stdin.write('Two');
});
await waitFor(() => {
const frame = lastFrame();
expect(frame).toContain('Item Two');
expect(frame).not.toContain('Item One');
expect(frame).not.toContain('Item Three');
});
});
it('should show "No items found." when no items match', async () => {
const { lastFrame, stdin } = renderList();
// Type something that won't match
await React.act(async () => {
stdin.write('xyz123');
});
await waitFor(() => {
const frame = lastFrame();
expect(frame).toContain('No items found.');
});
});
it('should handle selection with Enter', async () => {
const { stdin } = renderList();
// Select first item (default active)
await React.act(async () => {
stdin.write('\r'); // Enter
});
await waitFor(() => {
expect(mockOnSelect).toHaveBeenCalledWith(mockItems[0]);
});
});
it('should handle navigation and selection', async () => {
const { stdin } = renderList();
// Navigate down to second item
await React.act(async () => {
stdin.write('\u001B[B'); // Down Arrow
});
// Select second item
await React.act(async () => {
stdin.write('\r'); // Enter
});
await waitFor(() => {
expect(mockOnSelect).toHaveBeenCalledWith(mockItems[1]);
});
});
it('should handle close with Esc', async () => {
const { stdin } = renderList();
await React.act(async () => {
stdin.write('\u001B'); // Esc
});
await waitFor(() => {
expect(mockOnClose).toHaveBeenCalled();
});
});
});