Files
gemini-cli/packages/cli/src/ui/components/views/ExtensionDetails.test.tsx
2026-03-31 17:05:08 +00:00

212 lines
6.1 KiB
TypeScript

/**
* @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 { describe, it, expect, vi, beforeEach } from 'vitest';
import { ExtensionDetails } from './ExtensionDetails.js';
import { type RegistryExtension } from '../../../config/extensionRegistryClient.js';
import { ExtensionUpdateState } from '../../state/extensions.js';
const mockExtension: RegistryExtension = {
id: 'ext1',
extensionName: 'Test Extension',
extensionDescription: 'A test extension description',
fullName: 'author/test-extension',
extensionVersion: '1.2.3',
rank: 1,
stars: 123,
url: 'https://github.com/author/test-extension',
repoDescription: 'Repo description',
avatarUrl: '',
lastUpdated: '2023-10-27',
hasMCP: true,
hasContext: true,
hasHooks: true,
hasSkills: true,
hasCustomCommands: true,
isGoogleOwned: true,
licenseKey: 'Apache-2.0',
};
const linkableExtension: RegistryExtension = {
...mockExtension,
url: '/local/path/to/extension',
};
describe('ExtensionDetails', () => {
let mockOnBack: ReturnType<typeof vi.fn>;
let mockOnInstall: ReturnType<typeof vi.fn>;
let mockOnLink: ReturnType<typeof vi.fn>;
beforeEach(() => {
mockOnBack = vi.fn();
mockOnInstall = vi.fn();
mockOnLink = vi.fn();
});
const renderDetails = async (
isInstalled = false,
updateState?: ExtensionUpdateState,
onUpdate = vi.fn(),
) =>
renderWithProviders(
<ExtensionDetails
extension={mockExtension}
onBack={mockOnBack}
onInstall={mockOnInstall}
onLink={mockOnLink}
isInstalled={isInstalled}
updateState={updateState}
onUpdate={onUpdate}
/>,
);
it('should render extension details correctly', async () => {
const { lastFrame } = await renderDetails();
await waitFor(() => {
expect(lastFrame()).toContain('Test Extension');
expect(lastFrame()).toContain('v1.2.3');
expect(lastFrame()).toContain('123');
expect(lastFrame()).toContain('[G]');
expect(lastFrame()).toContain('author/test-extension');
expect(lastFrame()).toContain('A test extension description');
expect(lastFrame()).toContain('MCP');
expect(lastFrame()).toContain('Context file');
expect(lastFrame()).toContain('Hooks');
expect(lastFrame()).toContain('Skills');
expect(lastFrame()).toContain('Commands');
});
});
it('should show install prompt when not installed', async () => {
const { lastFrame } = await renderDetails(false);
await waitFor(() => {
expect(lastFrame()).toContain('[Enter] Install');
expect(lastFrame()).not.toContain('Already Installed');
});
});
it('should show already installed message when installed', async () => {
const { lastFrame } = await renderDetails(true);
await waitFor(() => {
expect(lastFrame()).toContain('Already Installed');
expect(lastFrame()).not.toContain('[Enter] Install');
});
});
it('should call onBack when Escape is pressed', async () => {
const { stdin } = await renderDetails();
await React.act(async () => {
stdin.write('\x1b'); // Escape
});
await waitFor(() => {
expect(mockOnBack).toHaveBeenCalled();
});
});
it('should call onInstall when Enter is pressed and not installed', async () => {
const { stdin } = await renderDetails(false);
await React.act(async () => {
stdin.write('\r'); // Enter
});
await waitFor(() => {
expect(mockOnInstall).toHaveBeenCalled();
});
});
it('should NOT call onInstall when Enter is pressed and already installed', async () => {
vi.useFakeTimers();
const { stdin } = await renderDetails(true);
await React.act(async () => {
stdin.write('\r'); // Enter
});
// Advance timers to trigger the keypress flush
await React.act(async () => {
vi.runAllTimers();
});
expect(mockOnInstall).not.toHaveBeenCalled();
vi.useRealTimers();
});
it('should call onLink when "l" is pressed and is linkable', async () => {
const { stdin } = await renderWithProviders(
<ExtensionDetails
extension={linkableExtension}
onBack={mockOnBack}
onInstall={mockOnInstall}
onLink={mockOnLink}
isInstalled={false}
/>,
);
await React.act(async () => {
stdin.write('l');
});
await waitFor(() => {
expect(mockOnLink).toHaveBeenCalled();
});
});
it('should NOT show "Link" button for GitHub extensions', async () => {
const { lastFrame } = await renderDetails(true);
await waitFor(() => {
expect(lastFrame()).not.toContain('[L] Link');
});
});
it('should show "Link" button for local extensions', async () => {
const { lastFrame } = await renderWithProviders(
<ExtensionDetails
extension={linkableExtension}
onBack={mockOnBack}
onInstall={mockOnInstall}
onLink={mockOnLink}
isInstalled={false}
/>,
);
await waitFor(() => {
expect(lastFrame()).toContain('[L] Link');
});
});
it('should show update button when update is available', async () => {
const { lastFrame } = await renderDetails(
true,
ExtensionUpdateState.UPDATE_AVAILABLE,
);
await waitFor(() => {
expect(lastFrame()).toContain('[I] Update');
});
});
it('should call onUpdate when "i" is pressed', async () => {
const mockOnUpdate = vi.fn();
const { stdin } = await renderDetails(
true,
ExtensionUpdateState.UPDATE_AVAILABLE,
mockOnUpdate,
);
await React.act(async () => {
stdin.write('i');
});
await waitFor(() => {
expect(mockOnUpdate).toHaveBeenCalled();
});
});
it('should show [Updating...] and hide "Already Installed" when update is in progress', async () => {
const { lastFrame } = await renderDetails(
true,
ExtensionUpdateState.UPDATING,
);
await waitFor(() => {
expect(lastFrame()).toContain('[Updating...]');
expect(lastFrame()).not.toContain('Already Installed');
});
});
});