| |
| |
| |
| |
| |
|
|
| import { describe, it, expect, vi, beforeEach } from 'vitest'; |
| import { renderWithProviders } from '../../../test-utils/render.js'; |
| import type React from 'react'; |
| import { Box, type Text } from 'ink'; |
| import { |
| RadioButtonSelect, |
| type RadioSelectItem, |
| type RadioButtonSelectProps, |
| } from './RadioButtonSelect.js'; |
| import { |
| BaseSelectionList, |
| type BaseSelectionListProps, |
| type RenderItemContext, |
| } from './BaseSelectionList.js'; |
|
|
| vi.mock('./BaseSelectionList.js', () => ({ |
| BaseSelectionList: vi.fn(() => null), |
| })); |
|
|
| vi.mock('../../semantic-colors.js', () => ({ |
| theme: { |
| text: { secondary: 'COLOR_SECONDARY' }, |
| ui: { focus: 'COLOR_FOCUS' }, |
| background: { focus: 'COLOR_FOCUS_BG' }, |
| }, |
| })); |
|
|
| const MockedBaseSelectionList = vi.mocked( |
| BaseSelectionList, |
| ) as unknown as ReturnType<typeof vi.fn>; |
|
|
| type RadioRenderItemFn = ( |
| item: RadioSelectItem<string>, |
| context: RenderItemContext, |
| ) => React.JSX.Element; |
| const extractRenderItem = (): RadioRenderItemFn => { |
| const mockCalls = MockedBaseSelectionList.mock.calls; |
|
|
| if (mockCalls.length === 0) { |
| throw new Error( |
| 'BaseSelectionList was not called. Ensure RadioButtonSelect is rendered before calling extractRenderItem.', |
| ); |
| } |
|
|
| const props = mockCalls[0][0] as BaseSelectionListProps< |
| string, |
| RadioSelectItem<string> |
| >; |
|
|
| if (typeof props.renderItem !== 'function') { |
| throw new Error('renderItem prop was not found on BaseSelectionList call.'); |
| } |
|
|
| return props.renderItem as RadioRenderItemFn; |
| }; |
|
|
| describe('RadioButtonSelect', () => { |
| const mockOnSelect = vi.fn(); |
| const mockOnHighlight = vi.fn(); |
|
|
| const ITEMS: Array<RadioSelectItem<string>> = [ |
| { label: 'Option 1', value: 'one', key: 'one' }, |
| { label: 'Option 2', value: 'two', key: 'two' }, |
| { label: 'Option 3', value: 'three', disabled: true, key: 'three' }, |
| ]; |
|
|
| const renderComponent = async ( |
| props: Partial<RadioButtonSelectProps<string>> = {}, |
| ) => { |
| const defaultProps: RadioButtonSelectProps<string> = { |
| items: ITEMS, |
| onSelect: mockOnSelect, |
| ...props, |
| }; |
| return renderWithProviders(<RadioButtonSelect {...defaultProps} />); |
| }; |
|
|
| beforeEach(() => { |
| vi.clearAllMocks(); |
| }); |
|
|
| describe('Prop forwarding to BaseSelectionList', () => { |
| it('should forward all props correctly when provided', async () => { |
| const props = { |
| items: ITEMS, |
| initialIndex: 1, |
| onSelect: mockOnSelect, |
| onHighlight: mockOnHighlight, |
| isFocused: false, |
| showScrollArrows: true, |
| maxItemsToShow: 5, |
| showNumbers: false, |
| }; |
|
|
| await renderComponent(props); |
|
|
| expect(BaseSelectionList).toHaveBeenCalledTimes(1); |
| expect(BaseSelectionList).toHaveBeenCalledWith( |
| expect.objectContaining({ |
| ...props, |
| renderItem: expect.any(Function), |
| }), |
| undefined, |
| ); |
| }); |
|
|
| it('should use default props if not provided', async () => { |
| await renderComponent({ |
| items: ITEMS, |
| onSelect: mockOnSelect, |
| }); |
|
|
| expect(BaseSelectionList).toHaveBeenCalledWith( |
| expect.objectContaining({ |
| initialIndex: 0, |
| isFocused: true, |
| showScrollArrows: false, |
| maxItemsToShow: 10, |
| showNumbers: true, |
| }), |
| undefined, |
| ); |
| }); |
| }); |
|
|
| describe('renderItem implementation', () => { |
| let renderItem: RadioRenderItemFn; |
| const mockContext: RenderItemContext = { |
| isSelected: false, |
| titleColor: 'MOCK_TITLE_COLOR', |
| numberColor: 'MOCK_NUMBER_COLOR', |
| }; |
|
|
| beforeEach(async () => { |
| await renderComponent(); |
| renderItem = extractRenderItem(); |
| }); |
|
|
| it('should render the standard label display with correct color and truncation', () => { |
| const item = ITEMS[0]; |
|
|
| const result = renderItem(item, mockContext); |
|
|
| expect(result.type).toBe(Box); |
| const props = result.props as { children: React.ReactNode }; |
| const textComponent = (props.children as React.ReactElement[])[0]; |
| const textProps = textComponent?.props as React.ComponentProps< |
| typeof Text |
| >; |
|
|
| expect(textProps?.color).toBe(mockContext.titleColor); |
| expect(textProps?.children).toBe('Option 1'); |
| expect(textProps?.wrap).toBe('truncate'); |
| }); |
|
|
| it('should render the special theme display when theme props are present', () => { |
| const themeItem: RadioSelectItem<string> = { |
| label: 'Theme A (Light)', |
| value: 'a-light', |
| themeNameDisplay: 'Theme A', |
| themeTypeDisplay: '(Light)', |
| key: 'a-light', |
| }; |
|
|
| const result = renderItem(themeItem, mockContext); |
|
|
| expect(result?.props?.color).toBe(mockContext.titleColor); |
| expect(result?.props?.wrap).toBe('truncate'); |
|
|
| const children = result?.props?.children; |
|
|
| if (!Array.isArray(children) || children.length < 3) { |
| throw new Error( |
| 'Expected children to be an array with at least 3 elements for theme display', |
| ); |
| } |
|
|
| expect(children[0]).toBe('Theme A'); |
| expect(children[1]).toBe(' '); |
|
|
| const nestedTextElement = children[2] as React.ReactElement<{ |
| color?: string; |
| children?: React.ReactNode; |
| }>; |
| expect(nestedTextElement?.props?.color).toBe('COLOR_SECONDARY'); |
| expect(nestedTextElement?.props?.children).toBe('(Light)'); |
| }); |
|
|
| it('should fall back to standard display if only one theme prop is present', () => { |
| const partialThemeItem: RadioSelectItem<string> = { |
| label: 'Incomplete Theme', |
| value: 'incomplete', |
| themeNameDisplay: 'Only Name', |
| key: 'incomplete', |
| }; |
|
|
| const result = renderItem(partialThemeItem, mockContext); |
|
|
| expect(result.type).toBe(Box); |
| const props = result.props as { children: React.ReactNode }; |
| const textComponent = (props.children as React.ReactElement[])[0]; |
| const textProps = textComponent?.props as React.ComponentProps< |
| typeof Text |
| >; |
| expect(textProps?.children).toBe('Incomplete Theme'); |
| }); |
| }); |
| }); |
|
|