| |
| |
| |
| |
| |
|
|
| import { renderWithProviders } from '../../../test-utils/render.js'; |
| import { waitFor } from '../../../test-utils/async.js'; |
| import { describe, it, expect, vi, beforeEach } from 'vitest'; |
| import { act } from 'react'; |
| import { Text } from 'ink'; |
| import { |
| BaseSettingsDialog, |
| type BaseSettingsDialogProps, |
| type SettingsDialogItem, |
| } from './BaseSettingsDialog.js'; |
| import { SettingScope } from '../../../config/settings.js'; |
|
|
| enum TerminalKeys { |
| ENTER = '\u000D', |
| TAB = '\t', |
| UP_ARROW = '\u001B[A', |
| DOWN_ARROW = '\u001B[B', |
| LEFT_ARROW = '\u001B[D', |
| RIGHT_ARROW = '\u001B[C', |
| ESCAPE = '\u001B', |
| BACKSPACE = '\u0008', |
| CTRL_L = '\u000C', |
| } |
|
|
| const createMockItems = (count = 4): SettingsDialogItem[] => { |
| const items: SettingsDialogItem[] = [ |
| { |
| key: 'boolean-setting', |
| label: 'Boolean Setting', |
| description: 'A boolean setting for testing', |
| displayValue: 'true', |
| rawValue: true, |
| type: 'boolean', |
| }, |
| { |
| key: 'string-setting', |
| label: 'String Setting', |
| description: 'A string setting for testing', |
| displayValue: 'test-value', |
| rawValue: 'test-value', |
| type: 'string', |
| }, |
| { |
| key: 'number-setting', |
| label: 'Number Setting', |
| description: 'A number setting for testing', |
| displayValue: '42', |
| rawValue: 42, |
| type: 'number', |
| }, |
| { |
| key: 'enum-setting', |
| label: 'Enum Setting', |
| description: 'An enum setting for testing', |
| displayValue: 'option-a', |
| rawValue: 'option-a', |
| type: 'enum', |
| }, |
| ]; |
|
|
| |
| if (count > items.length) { |
| for (let i = items.length; i < count; i++) { |
| items.push({ |
| key: `extra-setting-${i}`, |
| label: `Extra Setting ${i}`, |
| displayValue: `value-${i}`, |
| type: 'string', |
| }); |
| } |
| } |
|
|
| return items.slice(0, count); |
| }; |
|
|
| describe('BaseSettingsDialog', () => { |
| let mockOnItemToggle: ReturnType<typeof vi.fn>; |
| let mockOnEditCommit: ReturnType<typeof vi.fn>; |
| let mockOnItemClear: ReturnType<typeof vi.fn>; |
| let mockOnClose: ReturnType<typeof vi.fn>; |
| let mockOnScopeChange: ReturnType<typeof vi.fn>; |
|
|
| beforeEach(() => { |
| vi.clearAllMocks(); |
| mockOnItemToggle = vi.fn(); |
| mockOnEditCommit = vi.fn(); |
| mockOnItemClear = vi.fn(); |
| mockOnClose = vi.fn(); |
| mockOnScopeChange = vi.fn(); |
| }); |
|
|
| const renderDialog = async (props: Partial<BaseSettingsDialogProps> = {}) => { |
| const defaultProps: BaseSettingsDialogProps = { |
| title: 'Test Settings', |
| items: createMockItems(), |
| selectedScope: SettingScope.User, |
| maxItemsToShow: 8, |
| onItemToggle: mockOnItemToggle, |
| onEditCommit: mockOnEditCommit, |
| onItemClear: mockOnItemClear, |
| onClose: mockOnClose, |
| ...props, |
| }; |
|
|
| const result = await renderWithProviders( |
| <BaseSettingsDialog {...defaultProps} />, |
| ); |
| await result.waitUntilReady(); |
| return result; |
| }; |
|
|
| describe('rendering', () => { |
| it('should render the dialog with title', async () => { |
| const { lastFrame, unmount } = await renderDialog(); |
| expect(lastFrame()).toContain('Test Settings'); |
| unmount(); |
| }); |
|
|
| it('should render all items', async () => { |
| const { lastFrame, unmount } = await renderDialog(); |
| const frame = lastFrame(); |
|
|
| expect(frame).toContain('Boolean Setting'); |
| expect(frame).toContain('String Setting'); |
| expect(frame).toContain('Number Setting'); |
| expect(frame).toContain('Enum Setting'); |
| unmount(); |
| }); |
|
|
| it('should render help text with Ctrl+L for reset', async () => { |
| const { lastFrame, unmount } = await renderDialog(); |
| const frame = lastFrame(); |
|
|
| expect(frame).toContain('Use Enter to select'); |
| expect(frame).toContain('Ctrl+L to reset'); |
| expect(frame).toContain('Tab to change focus'); |
| expect(frame).toContain('Esc to close'); |
| unmount(); |
| }); |
|
|
| it('should render scope selector when showScopeSelector is true', async () => { |
| const { lastFrame, unmount } = await renderDialog({ |
| showScopeSelector: true, |
| onScopeChange: mockOnScopeChange, |
| }); |
|
|
| expect(lastFrame()).toContain('Apply To'); |
| unmount(); |
| }); |
|
|
| it('should not render scope selector when showScopeSelector is false', async () => { |
| const { lastFrame, unmount } = await renderDialog({ |
| showScopeSelector: false, |
| }); |
|
|
| expect(lastFrame({ allowEmpty: true })).not.toContain('Apply To'); |
| unmount(); |
| }); |
|
|
| it('should render footer content when provided', async () => { |
| const { lastFrame, unmount } = await renderDialog({ |
| footer: { |
| content: <Text>Custom Footer</Text>, |
| height: 1, |
| }, |
| }); |
|
|
| expect(lastFrame()).toContain('Custom Footer'); |
| unmount(); |
| }); |
| }); |
|
|
| describe('keyboard navigation', () => { |
| it('should close dialog on Escape', async () => { |
| const { stdin, waitUntilReady, unmount } = await renderDialog(); |
|
|
| await act(async () => { |
| stdin.write(TerminalKeys.ESCAPE); |
| }); |
| |
| await act(async () => { |
| await waitUntilReady(); |
| }); |
|
|
| await waitFor(() => { |
| expect(mockOnClose).toHaveBeenCalled(); |
| }); |
| unmount(); |
| }); |
|
|
| it('should navigate down with arrow key', async () => { |
| const { lastFrame, stdin, waitUntilReady, unmount } = |
| await renderDialog(); |
|
|
| |
| const initialFrame = lastFrame(); |
| expect(initialFrame).toContain('Boolean Setting'); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.DOWN_ARROW); |
| }); |
| await waitUntilReady(); |
|
|
| |
| await waitFor(() => { |
| const frame = lastFrame(); |
| |
| expect(frame).toContain('String Setting'); |
| }); |
| unmount(); |
| }); |
|
|
| it('should navigate up with arrow key', async () => { |
| const { stdin, waitUntilReady, unmount } = await renderDialog(); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.DOWN_ARROW); |
| }); |
| await waitUntilReady(); |
|
|
| await act(async () => { |
| stdin.write(TerminalKeys.UP_ARROW); |
| }); |
| await waitUntilReady(); |
|
|
| |
| await waitFor(() => { |
| |
| expect(mockOnClose).not.toHaveBeenCalled(); |
| }); |
| unmount(); |
| }); |
|
|
| it('should wrap around when navigating past last item', async () => { |
| const items = createMockItems(2); |
| const { stdin, waitUntilReady, unmount } = await renderDialog({ items }); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.DOWN_ARROW); |
| }); |
| await waitUntilReady(); |
| await act(async () => { |
| stdin.write(TerminalKeys.DOWN_ARROW); |
| }); |
| await waitUntilReady(); |
|
|
| |
| await waitFor(() => { |
| expect(mockOnClose).not.toHaveBeenCalled(); |
| }); |
| unmount(); |
| }); |
|
|
| it('should wrap around when navigating before first item', async () => { |
| const { stdin, waitUntilReady, unmount } = await renderDialog(); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.UP_ARROW); |
| }); |
| await waitUntilReady(); |
|
|
| |
| await waitFor(() => { |
| expect(mockOnClose).not.toHaveBeenCalled(); |
| }); |
| unmount(); |
| }); |
|
|
| it('should switch focus with Tab when scope selector is shown', async () => { |
| const { lastFrame, stdin, waitUntilReady, unmount } = await renderDialog({ |
| showScopeSelector: true, |
| onScopeChange: mockOnScopeChange, |
| }); |
|
|
| |
| expect(lastFrame()).toContain('> Test Settings'); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.TAB); |
| }); |
| await waitUntilReady(); |
|
|
| await waitFor(() => { |
| expect(lastFrame()).toContain('> Apply To'); |
| }); |
| unmount(); |
| }); |
| }); |
|
|
| describe('scrolling and resizing list (search filtering)', () => { |
| it('should preserve focus on the active item if it remains in the filtered list', async () => { |
| const items = createMockItems(5); |
| const { rerender, stdin, lastFrame, waitUntilReady, unmount } = |
| await renderDialog({ |
| items, |
| maxItemsToShow: 5, |
| }); |
|
|
| |
| |
| await act(async () => { |
| stdin.write(TerminalKeys.DOWN_ARROW); |
| }); |
| await waitUntilReady(); |
| await act(async () => { |
| stdin.write(TerminalKeys.DOWN_ARROW); |
| }); |
| await waitUntilReady(); |
|
|
| |
| const filteredItems = [items[0], items[2], items[4]]; |
| await act(async () => { |
| rerender( |
| <BaseSettingsDialog |
| title="Test Settings" |
| items={filteredItems} |
| selectedScope={SettingScope.User} |
| maxItemsToShow={5} |
| onItemToggle={mockOnItemToggle} |
| onEditCommit={mockOnEditCommit} |
| onItemClear={mockOnItemClear} |
| onClose={mockOnClose} |
| />, |
| ); |
| }); |
| |
| await waitFor(() => { |
| const frame = lastFrame(); |
| expect(frame).toContain('Boolean Setting'); |
| expect(frame).toContain('Number Setting'); |
| expect(frame).toContain('Extra Setting 4'); |
| expect(frame).not.toContain('No matches found.'); |
| }); |
|
|
| |
| |
| await act(async () => { |
| stdin.write(TerminalKeys.ENTER); |
| }); |
| await waitUntilReady(); |
|
|
| await waitFor(() => { |
| expect(mockOnItemToggle).not.toHaveBeenCalled(); |
| }); |
| unmount(); |
| }); |
|
|
| it('should reset focus to the top if the active item is filtered out', async () => { |
| const items = createMockItems(5); |
| const { rerender, stdin, lastFrame, waitUntilReady, unmount } = |
| await renderDialog({ |
| items, |
| maxItemsToShow: 5, |
| }); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.DOWN_ARROW); |
| }); |
| await waitUntilReady(); |
| await act(async () => { |
| stdin.write(TerminalKeys.DOWN_ARROW); |
| }); |
| await waitUntilReady(); |
|
|
| |
| const filteredItems = [items[0], items[1]]; |
| await act(async () => { |
| rerender( |
| <BaseSettingsDialog |
| title="Test Settings" |
| items={filteredItems} |
| selectedScope={SettingScope.User} |
| maxItemsToShow={5} |
| onItemToggle={mockOnItemToggle} |
| onEditCommit={mockOnEditCommit} |
| onItemClear={mockOnItemClear} |
| onClose={mockOnClose} |
| />, |
| ); |
| }); |
| await waitFor(() => { |
| const frame = lastFrame(); |
| expect(frame).toContain('Boolean Setting'); |
| expect(frame).toContain('String Setting'); |
| }); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.ENTER); |
| }); |
| await waitUntilReady(); |
|
|
| await waitFor(() => { |
| expect(mockOnItemToggle).toHaveBeenCalledWith( |
| 'boolean-setting', |
| expect.anything(), |
| ); |
| }); |
| unmount(); |
| }); |
| }); |
|
|
| describe('item interactions', () => { |
| it('should call onItemToggle for boolean items on Enter', async () => { |
| const { stdin, waitUntilReady, unmount } = await renderDialog(); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.ENTER); |
| }); |
| await waitUntilReady(); |
|
|
| await waitFor(() => { |
| expect(mockOnItemToggle).toHaveBeenCalledWith( |
| 'boolean-setting', |
| expect.objectContaining({ type: 'boolean' }), |
| ); |
| }); |
| unmount(); |
| }); |
|
|
| it('should call onItemToggle for enum items on Enter', async () => { |
| const items = createMockItems(4); |
| |
| const enumItem = items.find((i) => i.type === 'enum')!; |
| const { stdin, waitUntilReady, unmount } = await renderDialog({ |
| items: [enumItem], |
| }); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.ENTER); |
| }); |
| await waitUntilReady(); |
|
|
| await waitFor(() => { |
| expect(mockOnItemToggle).toHaveBeenCalledWith( |
| 'enum-setting', |
| expect.objectContaining({ type: 'enum' }), |
| ); |
| }); |
| unmount(); |
| }); |
|
|
| it('should enter edit mode for string items on Enter', async () => { |
| const items = createMockItems(4); |
| const stringItem = items.find((i) => i.type === 'string')!; |
| const { lastFrame, stdin, waitUntilReady, unmount } = await renderDialog({ |
| items: [stringItem], |
| }); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.ENTER); |
| }); |
| await waitUntilReady(); |
|
|
| |
| await waitFor(() => { |
| const frame = lastFrame(); |
| |
| expect(frame).toContain('test-value'); |
| }); |
| unmount(); |
| }); |
|
|
| it('should enter edit mode for number items on Enter', async () => { |
| const items = createMockItems(4); |
| const numberItem = items.find((i) => i.type === 'number')!; |
| const { lastFrame, stdin, waitUntilReady, unmount } = await renderDialog({ |
| items: [numberItem], |
| }); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.ENTER); |
| }); |
| await waitUntilReady(); |
|
|
| |
| await waitFor(() => { |
| const frame = lastFrame(); |
| expect(frame).toContain('42'); |
| }); |
| unmount(); |
| }); |
|
|
| it('should call onItemClear on Ctrl+L', async () => { |
| const { stdin, waitUntilReady, unmount } = await renderDialog(); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.CTRL_L); |
| }); |
| await waitUntilReady(); |
|
|
| await waitFor(() => { |
| expect(mockOnItemClear).toHaveBeenCalledWith( |
| 'boolean-setting', |
| expect.objectContaining({ type: 'boolean' }), |
| ); |
| }); |
| unmount(); |
| }); |
| }); |
|
|
| describe('edit mode', () => { |
| it('should prioritize editValue over rawValue stringification', async () => { |
| const objectItem: SettingsDialogItem = { |
| key: 'object-setting', |
| label: 'Object Setting', |
| description: 'A complex object setting', |
| displayValue: '{"foo":"bar"}', |
| type: 'object', |
| rawValue: { foo: 'bar' }, |
| editValue: '{"foo":"bar"}', |
| }; |
| const { stdin } = await renderDialog({ |
| items: [objectItem], |
| }); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.ENTER); |
| }); |
| await act(async () => { |
| stdin.write(TerminalKeys.ENTER); |
| }); |
|
|
| await waitFor(() => { |
| expect(mockOnEditCommit).toHaveBeenCalledWith( |
| 'object-setting', |
| '{"foo":"bar"}', |
| expect.objectContaining({ type: 'object' }), |
| ); |
| }); |
| }); |
|
|
| it('should commit edit on Enter', async () => { |
| const items = createMockItems(4); |
| const stringItem = items.find((i) => i.type === 'string')!; |
| const { stdin, waitUntilReady, unmount } = await renderDialog({ |
| items: [stringItem], |
| }); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.ENTER); |
| }); |
| await waitUntilReady(); |
|
|
| |
| await act(async () => { |
| stdin.write('x'); |
| }); |
| await waitUntilReady(); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.ENTER); |
| }); |
| await waitUntilReady(); |
|
|
| await waitFor(() => { |
| expect(mockOnEditCommit).toHaveBeenCalledWith( |
| 'string-setting', |
| 'test-valuex', |
| expect.objectContaining({ type: 'string' }), |
| ); |
| }); |
| unmount(); |
| }); |
|
|
| it('should commit edit on Escape', async () => { |
| const items = createMockItems(4); |
| const stringItem = items.find((i) => i.type === 'string')!; |
| const { stdin, waitUntilReady, unmount } = await renderDialog({ |
| items: [stringItem], |
| }); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.ENTER); |
| }); |
| await waitUntilReady(); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.ESCAPE); |
| }); |
| |
| await act(async () => { |
| await waitUntilReady(); |
| }); |
|
|
| await waitFor(() => { |
| expect(mockOnEditCommit).toHaveBeenCalled(); |
| }); |
| unmount(); |
| }); |
|
|
| it('should commit edit and navigate on Down arrow', async () => { |
| const items = createMockItems(4); |
| const stringItem = items.find((i) => i.type === 'string')!; |
| const numberItem = items.find((i) => i.type === 'number')!; |
| const { stdin, waitUntilReady, unmount } = await renderDialog({ |
| items: [stringItem, numberItem], |
| }); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.ENTER); |
| }); |
| await waitUntilReady(); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.DOWN_ARROW); |
| }); |
| await waitUntilReady(); |
|
|
| await waitFor(() => { |
| expect(mockOnEditCommit).toHaveBeenCalled(); |
| }); |
| unmount(); |
| }); |
|
|
| it('should commit edit and navigate on Up arrow', async () => { |
| const items = createMockItems(4); |
| const stringItem = items.find((i) => i.type === 'string')!; |
| const numberItem = items.find((i) => i.type === 'number')!; |
| const { stdin, waitUntilReady, unmount } = await renderDialog({ |
| items: [stringItem, numberItem], |
| }); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.DOWN_ARROW); |
| }); |
| await waitUntilReady(); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.ENTER); |
| }); |
| await waitUntilReady(); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.UP_ARROW); |
| }); |
| await waitUntilReady(); |
|
|
| await waitFor(() => { |
| expect(mockOnEditCommit).toHaveBeenCalled(); |
| }); |
| unmount(); |
| }); |
|
|
| it('should allow number input for number fields', async () => { |
| const items = createMockItems(4); |
| const numberItem = items.find((i) => i.type === 'number')!; |
| const { stdin, waitUntilReady, unmount } = await renderDialog({ |
| items: [numberItem], |
| }); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.ENTER); |
| }); |
| await waitUntilReady(); |
|
|
| |
| await act(async () => { |
| stdin.write('1'); |
| }); |
| await waitUntilReady(); |
| await act(async () => { |
| stdin.write('2'); |
| }); |
| await waitUntilReady(); |
| await act(async () => { |
| stdin.write('3'); |
| }); |
| await waitUntilReady(); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.ENTER); |
| }); |
| await waitUntilReady(); |
|
|
| await waitFor(() => { |
| expect(mockOnEditCommit).toHaveBeenCalledWith( |
| 'number-setting', |
| '42123', |
| expect.objectContaining({ type: 'number' }), |
| ); |
| }); |
| unmount(); |
| }); |
|
|
| it('should support quick number entry for number fields', async () => { |
| const items = createMockItems(4); |
| const numberItem = items.find((i) => i.type === 'number')!; |
| const { stdin, waitUntilReady, unmount } = await renderDialog({ |
| items: [numberItem], |
| }); |
|
|
| |
| await act(async () => { |
| stdin.write('5'); |
| }); |
| await waitUntilReady(); |
|
|
| |
| await waitFor(async () => { |
| |
| await act(async () => { |
| stdin.write(TerminalKeys.ENTER); |
| }); |
| await waitUntilReady(); |
| }); |
|
|
| await waitFor(() => { |
| expect(mockOnEditCommit).toHaveBeenCalledWith( |
| 'number-setting', |
| '5', |
| expect.objectContaining({ type: 'number' }), |
| ); |
| }); |
| unmount(); |
| }); |
|
|
| it('should allow j and k characters to be typed in string edit fields without triggering navigation', async () => { |
| const items = createMockItems(4); |
| const stringItem = items.find((i) => i.type === 'string')!; |
| const { stdin, waitUntilReady, unmount } = await renderDialog({ |
| items: [stringItem], |
| }); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.ENTER); |
| }); |
| await waitUntilReady(); |
|
|
| |
| await act(async () => { |
| stdin.write('j'); |
| }); |
| await waitUntilReady(); |
|
|
| |
| await act(async () => { |
| stdin.write('k'); |
| }); |
| await waitUntilReady(); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.ENTER); |
| }); |
| await waitUntilReady(); |
|
|
| |
| await waitFor(() => { |
| expect(mockOnEditCommit).toHaveBeenCalledWith( |
| 'string-setting', |
| 'test-valuejk', |
| expect.objectContaining({ type: 'string' }), |
| ); |
| }); |
| unmount(); |
| }); |
| }); |
|
|
| describe('custom key handling', () => { |
| it('should call onKeyPress and respect its return value', async () => { |
| const customKeyHandler = vi.fn().mockReturnValue(true); |
| const { stdin, waitUntilReady, unmount } = await renderDialog({ |
| onKeyPress: customKeyHandler, |
| }); |
|
|
| |
| await act(async () => { |
| stdin.write('r'); |
| }); |
| await waitUntilReady(); |
|
|
| await waitFor(() => { |
| expect(customKeyHandler).toHaveBeenCalled(); |
| }); |
|
|
| |
| expect(mockOnClose).not.toHaveBeenCalled(); |
| unmount(); |
| }); |
| }); |
|
|
| describe('focus management', () => { |
| it('should keep focus on settings when scope selector is hidden', async () => { |
| const { lastFrame, stdin, waitUntilReady, unmount } = await renderDialog({ |
| showScopeSelector: false, |
| }); |
|
|
| |
| await act(async () => { |
| stdin.write(TerminalKeys.TAB); |
| }); |
| await waitUntilReady(); |
|
|
| await waitFor(() => { |
| |
| expect(lastFrame()).toContain('> Test Settings'); |
| }); |
| unmount(); |
| }); |
| }); |
|
|
| describe('responsiveness', () => { |
| it('should show the scope selector when availableHeight is sufficient (25)', async () => { |
| const { lastFrame, unmount } = await renderDialog({ |
| availableHeight: 25, |
| showScopeSelector: true, |
| }); |
|
|
| const frame = lastFrame(); |
| expect(frame).toContain('Apply To'); |
| unmount(); |
| }); |
|
|
| it('should hide the scope selector when availableHeight is small (24) to show more items', async () => { |
| const { lastFrame, unmount } = await renderDialog({ |
| availableHeight: 24, |
| showScopeSelector: true, |
| }); |
|
|
| const frame = lastFrame(); |
| expect(frame).not.toContain('Apply To'); |
| unmount(); |
| }); |
|
|
| it('should reduce the number of visible items based on height', async () => { |
| |
| const { lastFrame, unmount } = await renderDialog({ |
| availableHeight: 25, |
| items: createMockItems(10), |
| }); |
|
|
| const frame = lastFrame(); |
| |
| expect(frame).toContain('Boolean Setting'); |
| expect(frame).toContain('String Setting'); |
| |
| expect(frame).not.toContain('Number Setting'); |
| unmount(); |
| }); |
|
|
| it('should show scroll indicators when list is truncated by height', async () => { |
| const { lastFrame, unmount } = await renderDialog({ |
| availableHeight: 25, |
| items: createMockItems(10), |
| }); |
|
|
| const frame = lastFrame(); |
| |
| expect(frame).toContain('▼'); |
| expect(frame).toContain('▲'); |
| unmount(); |
| }); |
| }); |
| }); |
|
|