mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-28 14:04:41 -07:00
Co-authored-by: hoteye <hoteye@users.noreply.github.com> Co-authored-by: cornmander <shikhman@google.com>
This commit is contained in:
@@ -43,7 +43,7 @@ export const TOGGLE_TYPES: ReadonlySet<SettingsType | undefined> = new Set([
|
|||||||
'enum',
|
'enum',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
interface SettingEnumOption {
|
export interface SettingEnumOption {
|
||||||
value: string | number;
|
value: string | number;
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||||
|
import { EnumSelector } from './EnumSelector.js';
|
||||||
|
import type { SettingEnumOption } from '../../../config/settingsSchema.js';
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
|
const LANGUAGE_OPTIONS: readonly SettingEnumOption[] = [
|
||||||
|
{ label: 'English', value: 'en' },
|
||||||
|
{ label: '中文 (简体)', value: 'zh' },
|
||||||
|
{ label: 'Español', value: 'es' },
|
||||||
|
{ label: 'Français', value: 'fr' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const NUMERIC_OPTIONS: readonly SettingEnumOption[] = [
|
||||||
|
{ label: 'Low', value: 1 },
|
||||||
|
{ label: 'Medium', value: 2 },
|
||||||
|
{ label: 'High', value: 3 },
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('<EnumSelector />', () => {
|
||||||
|
it('renders with string options and matches snapshot', () => {
|
||||||
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<EnumSelector
|
||||||
|
options={LANGUAGE_OPTIONS}
|
||||||
|
currentValue="en"
|
||||||
|
isActive={true}
|
||||||
|
onValueChange={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders with numeric options and matches snapshot', () => {
|
||||||
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<EnumSelector
|
||||||
|
options={NUMERIC_OPTIONS}
|
||||||
|
currentValue={2}
|
||||||
|
isActive={true}
|
||||||
|
onValueChange={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders inactive state and matches snapshot', () => {
|
||||||
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<EnumSelector
|
||||||
|
options={LANGUAGE_OPTIONS}
|
||||||
|
currentValue="zh"
|
||||||
|
isActive={false}
|
||||||
|
onValueChange={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders with single option and matches snapshot', () => {
|
||||||
|
const singleOption: readonly SettingEnumOption[] = [
|
||||||
|
{ label: 'Only Option', value: 'only' },
|
||||||
|
];
|
||||||
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<EnumSelector
|
||||||
|
options={singleOption}
|
||||||
|
currentValue="only"
|
||||||
|
isActive={true}
|
||||||
|
onValueChange={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders nothing when no options are provided', () => {
|
||||||
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<EnumSelector
|
||||||
|
options={[]}
|
||||||
|
currentValue=""
|
||||||
|
isActive={true}
|
||||||
|
onValueChange={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(lastFrame()).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles currentValue not found in options', () => {
|
||||||
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<EnumSelector
|
||||||
|
options={LANGUAGE_OPTIONS}
|
||||||
|
currentValue="invalid"
|
||||||
|
isActive={true}
|
||||||
|
onValueChange={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
// Should default to first option
|
||||||
|
expect(lastFrame()).toContain('English');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates when currentValue changes externally', () => {
|
||||||
|
const { rerender, lastFrame } = renderWithProviders(
|
||||||
|
<EnumSelector
|
||||||
|
options={LANGUAGE_OPTIONS}
|
||||||
|
currentValue="en"
|
||||||
|
isActive={true}
|
||||||
|
onValueChange={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(lastFrame()).toContain('English');
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<EnumSelector
|
||||||
|
options={LANGUAGE_OPTIONS}
|
||||||
|
currentValue="zh"
|
||||||
|
isActive={true}
|
||||||
|
onValueChange={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(lastFrame()).toContain('中文 (简体)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows navigation arrows when multiple options available', () => {
|
||||||
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<EnumSelector
|
||||||
|
options={LANGUAGE_OPTIONS}
|
||||||
|
currentValue="en"
|
||||||
|
isActive={true}
|
||||||
|
onValueChange={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(lastFrame()).toContain('←');
|
||||||
|
expect(lastFrame()).toContain('→');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides navigation arrows when single option available', () => {
|
||||||
|
const singleOption: readonly SettingEnumOption[] = [
|
||||||
|
{ label: 'Only Option', value: 'only' },
|
||||||
|
];
|
||||||
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<EnumSelector
|
||||||
|
options={singleOption}
|
||||||
|
currentValue="only"
|
||||||
|
isActive={true}
|
||||||
|
onValueChange={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(lastFrame()).not.toContain('←');
|
||||||
|
expect(lastFrame()).not.toContain('→');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import type React from 'react';
|
||||||
|
import { Box, Text } from 'ink';
|
||||||
|
import { Colors } from '../../colors.js';
|
||||||
|
import type { SettingEnumOption } from '../../../config/settingsSchema.js';
|
||||||
|
|
||||||
|
interface EnumSelectorProps {
|
||||||
|
options: readonly SettingEnumOption[];
|
||||||
|
currentValue: string | number;
|
||||||
|
isActive: boolean;
|
||||||
|
onValueChange: (value: string | number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A left-right scrolling selector for enum values
|
||||||
|
*/
|
||||||
|
export function EnumSelector({
|
||||||
|
options,
|
||||||
|
currentValue,
|
||||||
|
isActive,
|
||||||
|
onValueChange: _onValueChange,
|
||||||
|
}: EnumSelectorProps): React.JSX.Element {
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(() => {
|
||||||
|
// Guard against empty options array
|
||||||
|
if (!options || options.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const index = options.findIndex((option) => option.value === currentValue);
|
||||||
|
return index >= 0 ? index : 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update index when currentValue changes externally
|
||||||
|
useEffect(() => {
|
||||||
|
// Guard against empty options array
|
||||||
|
if (!options || options.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const index = options.findIndex((option) => option.value === currentValue);
|
||||||
|
// Always update index, defaulting to 0 if value not found
|
||||||
|
setCurrentIndex(index >= 0 ? index : 0);
|
||||||
|
}, [currentValue, options]);
|
||||||
|
|
||||||
|
// Guard against empty options array
|
||||||
|
if (!options || options.length === 0) {
|
||||||
|
return <Box />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Left/right navigation is handled by parent component
|
||||||
|
// This component is purely for display
|
||||||
|
// onValueChange is kept for interface compatibility but not used internally
|
||||||
|
|
||||||
|
const currentOption = options[currentIndex] || options[0];
|
||||||
|
const canScrollLeft = options.length > 1;
|
||||||
|
const canScrollRight = options.length > 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="row" alignItems="center">
|
||||||
|
<Text
|
||||||
|
color={isActive && canScrollLeft ? Colors.AccentGreen : Colors.Gray}
|
||||||
|
>
|
||||||
|
{canScrollLeft ? '←' : ' '}
|
||||||
|
</Text>
|
||||||
|
<Text> </Text>
|
||||||
|
<Text
|
||||||
|
color={isActive ? Colors.AccentGreen : Colors.Foreground}
|
||||||
|
bold={isActive}
|
||||||
|
>
|
||||||
|
{currentOption.label}
|
||||||
|
</Text>
|
||||||
|
<Text> </Text>
|
||||||
|
<Text
|
||||||
|
color={isActive && canScrollRight ? Colors.AccentGreen : Colors.Gray}
|
||||||
|
>
|
||||||
|
{canScrollRight ? '→' : ' '}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export the interface for external use
|
||||||
|
export type { EnumSelectorProps };
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`<EnumSelector /> > renders inactive state and matches snapshot 1`] = `"← 中文 (简体) →"`;
|
||||||
|
|
||||||
|
exports[`<EnumSelector /> > renders with numeric options and matches snapshot 1`] = `"← Medium →"`;
|
||||||
|
|
||||||
|
exports[`<EnumSelector /> > renders with single option and matches snapshot 1`] = `" Only Option"`;
|
||||||
|
|
||||||
|
exports[`<EnumSelector /> > renders with string options and matches snapshot 1`] = `"← English →"`;
|
||||||
Reference in New Issue
Block a user