2025-07-17 15:51:42 -07:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
2025-08-15 10:54:00 -07:00
|
|
|
import { renderWithProviders } from '../../../test-utils/render.js';
|
2025-08-14 16:48:54 -07:00
|
|
|
import { waitFor } from '@testing-library/react';
|
2025-07-17 15:51:42 -07:00
|
|
|
import {
|
|
|
|
|
RadioButtonSelect,
|
|
|
|
|
type RadioSelectItem,
|
|
|
|
|
} from './RadioButtonSelect.js';
|
2025-08-14 16:48:54 -07:00
|
|
|
import { describe, it, expect, vi } from 'vitest';
|
2025-07-17 15:51:42 -07:00
|
|
|
|
|
|
|
|
const ITEMS: Array<RadioSelectItem<string>> = [
|
|
|
|
|
{ label: 'Option 1', value: 'one' },
|
|
|
|
|
{ label: 'Option 2', value: 'two' },
|
|
|
|
|
{ label: 'Option 3', value: 'three', disabled: true },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
describe('<RadioButtonSelect />', () => {
|
|
|
|
|
it('renders a list of items and matches snapshot', () => {
|
2025-08-15 10:54:00 -07:00
|
|
|
const { lastFrame } = renderWithProviders(
|
2025-07-17 15:51:42 -07:00
|
|
|
<RadioButtonSelect items={ITEMS} onSelect={() => {}} isFocused={true} />,
|
|
|
|
|
);
|
|
|
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders with the second item selected and matches snapshot', () => {
|
2025-08-15 10:54:00 -07:00
|
|
|
const { lastFrame } = renderWithProviders(
|
2025-08-14 16:48:54 -07:00
|
|
|
<RadioButtonSelect items={ITEMS} initialIndex={1} onSelect={() => {}} />,
|
2025-07-17 15:51:42 -07:00
|
|
|
);
|
|
|
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders with numbers hidden and matches snapshot', () => {
|
2025-08-15 10:54:00 -07:00
|
|
|
const { lastFrame } = renderWithProviders(
|
2025-07-17 15:51:42 -07:00
|
|
|
<RadioButtonSelect
|
|
|
|
|
items={ITEMS}
|
|
|
|
|
onSelect={() => {}}
|
|
|
|
|
showNumbers={false}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders with scroll arrows and matches snapshot', () => {
|
|
|
|
|
const manyItems = Array.from({ length: 20 }, (_, i) => ({
|
|
|
|
|
label: `Item ${i + 1}`,
|
|
|
|
|
value: `item-${i + 1}`,
|
|
|
|
|
}));
|
2025-08-15 10:54:00 -07:00
|
|
|
const { lastFrame } = renderWithProviders(
|
2025-07-17 15:51:42 -07:00
|
|
|
<RadioButtonSelect
|
|
|
|
|
items={manyItems}
|
|
|
|
|
onSelect={() => {}}
|
|
|
|
|
showScrollArrows={true}
|
|
|
|
|
maxItemsToShow={5}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders with special theme display and matches snapshot', () => {
|
|
|
|
|
const themeItems: Array<RadioSelectItem<string>> = [
|
|
|
|
|
{
|
|
|
|
|
label: 'Theme A (Light)',
|
|
|
|
|
value: 'a-light',
|
|
|
|
|
themeNameDisplay: 'Theme A',
|
|
|
|
|
themeTypeDisplay: '(Light)',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Theme B (Dark)',
|
|
|
|
|
value: 'b-dark',
|
|
|
|
|
themeNameDisplay: 'Theme B',
|
|
|
|
|
themeTypeDisplay: '(Dark)',
|
|
|
|
|
},
|
|
|
|
|
];
|
2025-08-15 10:54:00 -07:00
|
|
|
const { lastFrame } = renderWithProviders(
|
2025-08-14 16:48:54 -07:00
|
|
|
<RadioButtonSelect items={themeItems} onSelect={() => {}} />,
|
2025-07-17 15:51:42 -07:00
|
|
|
);
|
|
|
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders a list with >10 items and matches snapshot', () => {
|
|
|
|
|
const manyItems = Array.from({ length: 12 }, (_, i) => ({
|
|
|
|
|
label: `Item ${i + 1}`,
|
|
|
|
|
value: `item-${i + 1}`,
|
|
|
|
|
}));
|
2025-08-15 10:54:00 -07:00
|
|
|
const { lastFrame } = renderWithProviders(
|
2025-08-14 16:48:54 -07:00
|
|
|
<RadioButtonSelect items={manyItems} onSelect={() => {}} />,
|
2025-07-17 15:51:42 -07:00
|
|
|
);
|
|
|
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders nothing when no items are provided', () => {
|
2025-08-15 10:54:00 -07:00
|
|
|
const { lastFrame } = renderWithProviders(
|
2025-07-17 15:51:42 -07:00
|
|
|
<RadioButtonSelect items={[]} onSelect={() => {}} isFocused={true} />,
|
|
|
|
|
);
|
|
|
|
|
expect(lastFrame()).toBe('');
|
|
|
|
|
});
|
|
|
|
|
});
|
2025-08-14 16:48:54 -07:00
|
|
|
|
|
|
|
|
describe('keyboard navigation', () => {
|
|
|
|
|
it('should call onSelect when "enter" is pressed', () => {
|
|
|
|
|
const onSelect = vi.fn();
|
2025-08-15 10:54:00 -07:00
|
|
|
const { stdin } = renderWithProviders(
|
2025-08-14 16:48:54 -07:00
|
|
|
<RadioButtonSelect items={ITEMS} onSelect={onSelect} />,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
stdin.write('\r');
|
|
|
|
|
|
|
|
|
|
expect(onSelect).toHaveBeenCalledWith('one');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('when isFocused is false', () => {
|
|
|
|
|
it('should not handle any keyboard input', () => {
|
|
|
|
|
const onSelect = vi.fn();
|
2025-08-15 10:54:00 -07:00
|
|
|
const { stdin } = renderWithProviders(
|
2025-08-14 16:48:54 -07:00
|
|
|
<RadioButtonSelect
|
|
|
|
|
items={ITEMS}
|
|
|
|
|
onSelect={onSelect}
|
|
|
|
|
isFocused={false}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
stdin.write('\u001B[B'); // Down arrow
|
|
|
|
|
stdin.write('\u001B[A'); // Up arrow
|
|
|
|
|
stdin.write('\r'); // Enter
|
|
|
|
|
|
|
|
|
|
expect(onSelect).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe.each([
|
|
|
|
|
{ description: 'when isFocused is true', isFocused: true },
|
|
|
|
|
{ description: 'when isFocused is omitted', isFocused: undefined },
|
|
|
|
|
])('$description', ({ isFocused }) => {
|
|
|
|
|
it('should navigate down with arrow key and select with enter', async () => {
|
|
|
|
|
const onSelect = vi.fn();
|
2025-08-15 10:54:00 -07:00
|
|
|
const { stdin, lastFrame } = renderWithProviders(
|
2025-08-14 16:48:54 -07:00
|
|
|
<RadioButtonSelect
|
|
|
|
|
items={ITEMS}
|
|
|
|
|
onSelect={onSelect}
|
|
|
|
|
isFocused={isFocused}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
stdin.write('\u001B[B'); // Down arrow
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(lastFrame()).toContain('● 2. Option 2');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
stdin.write('\r');
|
|
|
|
|
|
|
|
|
|
expect(onSelect).toHaveBeenCalledWith('two');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should navigate up with arrow key and select with enter', async () => {
|
|
|
|
|
const onSelect = vi.fn();
|
2025-08-15 10:54:00 -07:00
|
|
|
const { stdin, lastFrame } = renderWithProviders(
|
2025-08-14 16:48:54 -07:00
|
|
|
<RadioButtonSelect
|
|
|
|
|
items={ITEMS}
|
|
|
|
|
onSelect={onSelect}
|
|
|
|
|
initialIndex={1}
|
|
|
|
|
isFocused={isFocused}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
stdin.write('\u001B[A'); // Up arrow
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(lastFrame()).toContain('● 1. Option 1');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
stdin.write('\r');
|
|
|
|
|
|
|
|
|
|
expect(onSelect).toHaveBeenCalledWith('one');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|