Download packages/cli/src/ui/components/views/ExtensionRegistryView.test.tsx from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 8.59 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/components/views/ExtensionRegistryView.test.tsx
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/cli/src/ui/components/views/ExtensionRegistryView.test.tsx
-
curl -L -o ExtensionRegistryView.test.tsx https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/components/views/ExtensionRegistryView.test.tsx
8.59 kB
| /** | |
| * @license | |
| * Copyright 2026 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import React from 'react'; | |
| import { renderWithProviders } from '../../../test-utils/render.js'; | |
| import { waitFor } from '../../../test-utils/async.js'; | |
| import { makeFakeConfig } from '@google/gemini-cli-core'; | |
| import { describe, it, expect, vi, beforeEach } from 'vitest'; | |
| import { ExtensionRegistryView } from './ExtensionRegistryView.js'; | |
| import { type ExtensionManager } from '../../../config/extension-manager.js'; | |
| import { useExtensionRegistry } from '../../hooks/useExtensionRegistry.js'; | |
| import { useExtensionUpdates } from '../../hooks/useExtensionUpdates.js'; | |
| import { useRegistrySearch } from '../../hooks/useRegistrySearch.js'; | |
| import { type RegistryExtension } from '../../../config/extensionRegistryClient.js'; | |
| import { type UIState } from '../../contexts/UIStateContext.js'; | |
| import { | |
| type SearchListState, | |
| type GenericListItem, | |
| } from '../shared/SearchableList.js'; | |
| import { type TextBuffer } from '../shared/text-buffer.js'; | |
| import { type UseHistoryManagerReturn } from '../../hooks/useHistoryManager.js'; | |
| import { ExtensionUpdateState } from '../../state/extensions.js'; | |
| // Mocks | |
| vi.mock('../../hooks/useExtensionRegistry.js'); | |
| vi.mock('../../hooks/useExtensionUpdates.js'); | |
| vi.mock('../../hooks/useRegistrySearch.js'); | |
| vi.mock('../../../config/extension-manager.js'); | |
| const mockExtensions: RegistryExtension[] = [ | |
| { | |
| id: 'ext1', | |
| extensionName: 'Test Extension 1', | |
| extensionDescription: 'Description 1', | |
| fullName: 'author/ext1', | |
| extensionVersion: '1.0.0', | |
| rank: 1, | |
| stars: 10, | |
| url: 'http://example.com', | |
| repoDescription: 'Repo Desc 1', | |
| avatarUrl: 'http://avatar.com', | |
| lastUpdated: '2023-01-01', | |
| hasMCP: false, | |
| hasContext: false, | |
| hasHooks: false, | |
| hasSkills: false, | |
| hasCustomCommands: false, | |
| isGoogleOwned: false, | |
| licenseKey: 'mit', | |
| }, | |
| { | |
| id: 'ext2', | |
| extensionName: 'Test Extension 2', | |
| extensionDescription: 'Description 2', | |
| fullName: 'author/ext2', | |
| extensionVersion: '2.0.0', | |
| rank: 2, | |
| stars: 20, | |
| url: 'http://example.com/2', | |
| repoDescription: 'Repo Desc 2', | |
| avatarUrl: 'http://avatar.com/2', | |
| lastUpdated: '2023-01-02', | |
| hasMCP: true, | |
| hasContext: true, | |
| hasHooks: true, | |
| hasSkills: true, | |
| hasCustomCommands: true, | |
| isGoogleOwned: true, | |
| licenseKey: 'apache-2.0', | |
| }, | |
| ]; | |
| describe('ExtensionRegistryView', () => { | |
| let mockExtensionManager: ExtensionManager; | |
| let mockOnSelect: ReturnType<typeof vi.fn>; | |
| let mockOnClose: ReturnType<typeof vi.fn>; | |
| let mockSearch: ReturnType<typeof vi.fn>; | |
| beforeEach(() => { | |
| vi.clearAllMocks(); | |
| mockExtensionManager = { | |
| getExtensions: vi.fn().mockReturnValue([]), | |
| } as unknown as ExtensionManager; | |
| mockOnSelect = vi.fn(); | |
| mockOnClose = vi.fn(); | |
| mockSearch = vi.fn(); | |
| vi.mocked(useExtensionRegistry).mockReturnValue({ | |
| extensions: mockExtensions, | |
| loading: false, | |
| error: null, | |
| search: mockSearch, | |
| }); | |
| vi.mocked(useExtensionUpdates).mockReturnValue({ | |
| extensionsUpdateState: new Map(), | |
| dispatchExtensionStateUpdate: vi.fn(), | |
| } as unknown as ReturnType<typeof useExtensionUpdates>); | |
| // Mock useRegistrySearch implementation | |
| vi.mocked(useRegistrySearch).mockImplementation( | |
| (props: { items: GenericListItem[]; onSearch?: (q: string) => void }) => | |
| ({ | |
| filteredItems: props.items, // Pass through items | |
| searchBuffer: { | |
| text: '', | |
| cursorOffset: 0, | |
| viewport: { width: 10, height: 1 }, | |
| visualCursor: [0, 0] as [number, number], | |
| viewportVisualLines: [{ text: '', visualRowIndex: 0 }], | |
| visualScrollRow: 0, | |
| lines: [''], | |
| cursor: [0, 0] as [number, number], | |
| selectionAnchor: undefined, | |
| } as unknown as TextBuffer, | |
| searchQuery: '', | |
| setSearchQuery: vi.fn(), | |
| maxLabelWidth: 10, | |
| }) as unknown as SearchListState<GenericListItem>, | |
| ); | |
| }); | |
| const renderView = async () => | |
| renderWithProviders( | |
| <ExtensionRegistryView | |
| extensionManager={mockExtensionManager} | |
| onSelect={mockOnSelect} | |
| onClose={mockOnClose} | |
| />, | |
| { | |
| config: makeFakeConfig(), | |
| uiState: { | |
| staticExtraHeight: 5, | |
| terminalHeight: 40, | |
| historyManager: { | |
| addItem: vi.fn(), | |
| } as unknown as UseHistoryManagerReturn, | |
| } as Partial<UIState>, | |
| }, | |
| ); | |
| it('should render extensions', async () => { | |
| const { lastFrame } = await renderView(); | |
| await waitFor(() => { | |
| expect(lastFrame()).toContain('Test Extension 1'); | |
| expect(lastFrame()).toContain('Test Extension 2'); | |
| }); | |
| }); | |
| it('should use useRegistrySearch hook', async () => { | |
| await renderView(); | |
| expect(useRegistrySearch).toHaveBeenCalled(); | |
| }); | |
| it('should call search function when typing', async () => { | |
| // Mock useRegistrySearch to trigger onSearch | |
| vi.mocked(useRegistrySearch).mockImplementation( | |
| (props: { | |
| items: GenericListItem[]; | |
| onSearch?: (q: string) => void; | |
| }): SearchListState<GenericListItem> => { | |
| const { onSearch } = props; | |
| // Simulate typing | |
| React.useEffect(() => { | |
| if (onSearch) { | |
| onSearch('test query'); | |
| } | |
| }, [onSearch]); | |
| return { | |
| filteredItems: props.items, | |
| searchBuffer: { | |
| text: 'test query', | |
| cursorOffset: 10, | |
| viewport: { width: 10, height: 1 }, | |
| visualCursor: [0, 10] as [number, number], | |
| viewportVisualLines: [{ text: 'test query', visualRowIndex: 0 }], | |
| visualScrollRow: 0, | |
| lines: ['test query'], | |
| cursor: [0, 10] as [number, number], | |
| selectionAnchor: undefined, | |
| } as unknown as TextBuffer, | |
| searchQuery: 'test query', | |
| setSearchQuery: vi.fn(), | |
| maxLabelWidth: 10, | |
| } as unknown as SearchListState<GenericListItem>; | |
| }, | |
| ); | |
| await renderView(); | |
| await waitFor(() => { | |
| expect(useRegistrySearch).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| onSearch: mockSearch, | |
| }), | |
| ); | |
| }); | |
| }); | |
| it('should call onSelect when extension is selected and Enter is pressed in details', async () => { | |
| const { stdin, lastFrame } = await renderView(); | |
| // Select the first extension in the list (Enter opens details) | |
| await React.act(async () => { | |
| stdin.write('\r'); | |
| }); | |
| // Verify we are in details view | |
| await waitFor(() => { | |
| expect(lastFrame()).toContain('author/ext1'); | |
| expect(lastFrame()).toContain('[Enter] Install'); | |
| }); | |
| // Ensure onSelect hasn't been called yet | |
| expect(mockOnSelect).not.toHaveBeenCalled(); | |
| // Press Enter again in the details view to trigger install | |
| await React.act(async () => { | |
| stdin.write('\r'); | |
| }); | |
| await waitFor(() => { | |
| expect(mockOnSelect).toHaveBeenCalledWith( | |
| mockExtensions[0], | |
| expect.any(Function), | |
| ); | |
| }); | |
| }); | |
| it('should show [Update available] and hide [Installed] when update is available', async () => { | |
| mockExtensionManager.getExtensions = vi | |
| .fn() | |
| .mockReturnValue([{ name: 'Test Extension 1' }]); | |
| vi.mocked(useExtensionUpdates).mockReturnValue({ | |
| extensionsUpdateState: new Map([ | |
| ['Test Extension 1', ExtensionUpdateState.UPDATE_AVAILABLE], | |
| ]), | |
| dispatchExtensionStateUpdate: vi.fn(), | |
| } as unknown as ReturnType<typeof useExtensionUpdates>); | |
| const { lastFrame } = await renderView(); | |
| await waitFor(() => { | |
| expect(lastFrame()).toContain('[Update available]'); | |
| expect(lastFrame()).not.toContain('[Installed]'); | |
| }); | |
| }); | |
| it('should show [Updating...] and hide [Installed] when update is in progress', async () => { | |
| mockExtensionManager.getExtensions = vi | |
| .fn() | |
| .mockReturnValue([{ name: 'Test Extension 1' }]); | |
| vi.mocked(useExtensionUpdates).mockReturnValue({ | |
| extensionsUpdateState: new Map([ | |
| ['Test Extension 1', ExtensionUpdateState.UPDATING], | |
| ]), | |
| dispatchExtensionStateUpdate: vi.fn(), | |
| } as unknown as ReturnType<typeof useExtensionUpdates>); | |
| const { lastFrame } = await renderView(); | |
| await waitFor(() => { | |
| expect(lastFrame()).toContain('[Updating...]'); | |
| expect(lastFrame()).not.toContain('[Installed]'); | |
| }); | |
| }); | |
| }); | |