/**
* @vitest-environment happy-dom
*/
import { describe, it, expect, vi } from 'vitest';
import { render } from 'ink-testing-library';
import React from 'react';
import { Select } from '../../../components/common/Select.js';
// Helper to wait for async updates
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
interface TestItem {
label: string;
value: string;
}
describe('Select', () => {
const mockItems: TestItem[] = [
{ label: 'Option 1', value: 'opt1' },
{ label: 'Option 2', value: 'opt2' },
{ label: 'Option 3', value: 'opt3' },
{ label: 'Option 4', value: 'opt4' },
{ label: 'Option 5', value: 'opt5' },
];
describe('Rendering', () => {
it('should render all items', () => {
const onSelect = vi.fn();
const { lastFrame } = render();
expect(lastFrame()).toContain('Option 1');
expect(lastFrame()).toContain('Option 2');
expect(lastFrame()).toContain('Option 3');
});
it('should highlight first item by default', () => {
const onSelect = vi.fn();
const { lastFrame } = render();
// Cyan color code indicates selected item
expect(lastFrame()).toContain('›');
});
it('should highlight item at initialIndex', () => {
const onSelect = vi.fn();
const { lastFrame } = render(
);
const output = lastFrame();
// Should have exactly one selected indicator
const selectedCount = (output.match(/›/g) || []).length;
expect(selectedCount).toBe(1);
});
it('should render with empty items array', () => {
const onSelect = vi.fn();
const { lastFrame } = render();
expect(lastFrame()).toBeDefined();
});
it('should respect limit prop for scrolling', () => {
const onSelect = vi.fn();
const { lastFrame } = render();
const output = lastFrame();
// Should only show 3 items when limit is 3
expect(output).toContain('Option 1');
expect(output).toContain('Option 2');
expect(output).toContain('Option 3');
// Option 4 and 5 should not be visible initially
expect(output).not.toContain('Option 4');
expect(output).not.toContain('Option 5');
});
});
describe('Navigation - No Looping (Critical Feature)', () => {
it('should implement boundary checks to prevent looping', () => {
// Unit test: verify the logic used in implementation
// Math.max(0, current - 1) - prevents going below 0
// Math.min(items.length - 1, current + 1) - prevents going above max
const onSelect = vi.fn();
const { lastFrame } = render();
// Verify component renders (implementation uses Math.max/min for boundaries)
expect(lastFrame()).toBeDefined();
expect(lastFrame()).toContain('Option 1');
});
it('should start at first item by default', () => {
const onSelect = vi.fn();
const { lastFrame } = render();
const output = lastFrame();
// First line should have the selection indicator
const lines = output.split('\n').filter((l) => l.trim());
expect(lines[0]).toContain('›');
expect(lines[0]).toContain('Option 1');
});
it('should respect initialIndex without looping', () => {
const onSelect = vi.fn();
const { lastFrame } = render(
);
// Should start at last item (index 4)
const output = lastFrame();
expect(output).toContain('Option 5');
expect(output).toContain('›');
});
it('should handle initialIndex at 0', () => {
const onSelect = vi.fn();
const { lastFrame } = render(
);
const output = lastFrame();
const lines = output.split('\n').filter((l) => l.trim());
expect(lines[0]).toContain('Option 1');
});
});
describe('Navigation - Input Handling', () => {
it('should use useInput hook for keyboard handling', () => {
// Verify component accepts keyboard input by checking it renders properly
const onSelect = vi.fn();
const { stdin } = render();
// Component should handle input without errors
expect(() => stdin.write('\u001B[B')).not.toThrow();
expect(() => stdin.write('\u001B[A')).not.toThrow();
expect(() => stdin.write('j')).not.toThrow();
expect(() => stdin.write('k')).not.toThrow();
});
it('should support vim-style navigation keys (j/k)', () => {
const onSelect = vi.fn();
const { stdin } = render();
// Should accept j and k keys
expect(() => stdin.write('j')).not.toThrow();
expect(() => stdin.write('k')).not.toThrow();
});
it('should support arrow keys', () => {
const onSelect = vi.fn();
const { stdin } = render();
// Should accept arrow keys
expect(() => stdin.write('\u001B[A')).not.toThrow(); // Up
expect(() => stdin.write('\u001B[B')).not.toThrow(); // Down
});
});
describe('Selection', () => {
it('should call onSelect when Enter is pressed', () => {
const onSelect = vi.fn();
const { stdin } = render();
stdin.write('\r'); // Enter key
// Should be called at least once
expect(onSelect).toHaveBeenCalled();
});
it('should pass selected item to onSelect callback', () => {
const onSelect = vi.fn();
render();
// onSelect should be configured to receive item objects
// Actual keyboard testing is limited by ink-testing-library
expect(onSelect).toBeInstanceOf(Function);
});
it('should handle Enter key without errors', () => {
const onSelect = vi.fn();
const { stdin } = render();
expect(() => stdin.write('\r')).not.toThrow();
});
});
describe('Scrolling with limit', () => {
it('should implement offset-based scrolling logic', () => {
// Verify limit prop is accepted and used for slicing
const onSelect = vi.fn();
const { lastFrame } = render();
const output = lastFrame();
// Should show limited items initially
expect(output).toContain('Option 1');
expect(output).toContain('Option 2');
expect(output).toContain('Option 3');
});
it('should handle limit smaller than items length', () => {
const onSelect = vi.fn();
const { lastFrame } = render();
const output = lastFrame();
const lines = output.split('\n').filter((l) => l.trim());
// Should only show 2 items
expect(lines.length).toBeLessThanOrEqual(2);
});
it('should handle limit larger than items length', () => {
const onSelect = vi.fn();
const { lastFrame } = render();
// Should show all items without error
const output = lastFrame();
expect(output).toContain('Option 1');
expect(output).toContain('Option 5');
});
});
describe('Key propagation (Critical Feature)', () => {
it('should not interfere with other keys like q', () => {
const onSelect = vi.fn();
const { stdin } = render();
// Press q key (should be ignored by Select and propagate to parent)
stdin.write('q');
// onSelect should not be called
expect(onSelect).not.toHaveBeenCalled();
});
it('should not interfere with other keys like m, n, c', () => {
const onSelect = vi.fn();
const { stdin } = render();
stdin.write('m');
stdin.write('n');
stdin.write('c');
// None of these should trigger selection
expect(onSelect).not.toHaveBeenCalled();
});
});
});