import React, { useState } from 'react'; import { describe, it, expect, vi } from 'vitest'; import { render, fireEvent } from '@testing-library/react'; import { PixelSelect } from '../../forms/PixelSelect'; const OPTIONS = [ { value: 'red', label: 'Red' }, { value: 'green', label: 'Green' }, { value: 'blue', label: 'Blue' }, ]; describe('PixelSelect — trigger & listbox', () => { it('renders a combobox trigger with placeholder, closed by default', () => { const { getByRole, queryByRole, getByText } = render( , ); const trigger = getByRole('combobox'); expect(trigger.getAttribute('aria-expanded')).toBe('false'); expect(trigger.getAttribute('aria-haspopup')).toBe('listbox'); expect(getByText('Pick a color...')).toBeTruthy(); expect(queryByRole('listbox')).toBeNull(); }); it('click opens the listbox with one option per item; selected gets aria-selected', () => { const { getByRole, getAllByRole } = render( , ); const trigger = getByRole('combobox'); fireEvent.click(trigger); expect(trigger.getAttribute('aria-expanded')).toBe('true'); expect(getByRole('listbox')).toBeTruthy(); const opts = getAllByRole('option'); expect(opts.length).toBe(3); expect(opts.map((o) => o.getAttribute('aria-selected'))).toEqual(['false', 'true', 'false']); }); it('uncontrolled: picking an option updates the trigger label, closes, fires onChange', () => { const onChange = vi.fn(); const { getByRole, queryByRole } = render( , ); const trigger = getByRole('combobox'); fireEvent.click(trigger); fireEvent.click(getByRole('option', { name: 'Blue' })); expect(onChange).toHaveBeenLastCalledWith('blue'); expect(queryByRole('listbox')).toBeNull(); expect(trigger.textContent).toContain('Blue'); }); it('controlled: value prop drives the displayed selection', () => { function Wrap() { const [v, setV] = useState('red'); return ; } const { getByRole } = render(); const trigger = getByRole('combobox'); expect(trigger.textContent).toContain('Red'); fireEvent.click(trigger); fireEvent.click(getByRole('option', { name: 'Green' })); expect(trigger.textContent).toContain('Green'); }); }); describe('PixelSelect — keyboard', () => { it('ArrowDown opens the listbox and highlights the first option', () => { const { getByRole } = render(); const trigger = getByRole('combobox'); fireEvent.keyDown(trigger, { key: 'ArrowDown' }); expect(getByRole('listbox')).toBeTruthy(); }); it('ArrowDown + Enter selects the highlighted option', () => { const onChange = vi.fn(); const { getByRole, queryByRole } = render( , ); const trigger = getByRole('combobox'); fireEvent.keyDown(trigger, { key: 'ArrowDown' }); // open, highlight idx 0 fireEvent.keyDown(trigger, { key: 'ArrowDown' }); // highlight idx 1 fireEvent.keyDown(trigger, { key: 'Enter' }); expect(onChange).toHaveBeenLastCalledWith('green'); expect(queryByRole('listbox')).toBeNull(); }); it('End jumps the highlight to the last option', () => { const onChange = vi.fn(); const { getByRole } = render(); const trigger = getByRole('combobox'); fireEvent.keyDown(trigger, { key: 'End' }); // opens + highlights last fireEvent.keyDown(trigger, { key: 'Enter' }); expect(onChange).toHaveBeenLastCalledWith('blue'); }); it('Escape closes the listbox without selecting', () => { const onChange = vi.fn(); const { getByRole, queryByRole } = render( , ); const trigger = getByRole('combobox'); fireEvent.click(trigger); expect(getByRole('listbox')).toBeTruthy(); fireEvent.keyDown(trigger, { key: 'Escape' }); expect(queryByRole('listbox')).toBeNull(); expect(onChange).not.toHaveBeenCalled(); }); }); describe('PixelSelect — states & wiring', () => { it('disabled blocks opening', () => { const { getByRole, queryByRole } = render( , ); const trigger = getByRole('combobox') as HTMLButtonElement; expect(trigger.disabled).toBe(true); expect(trigger.getAttribute('aria-disabled')).toBe('true'); fireEvent.click(trigger); expect(queryByRole('listbox')).toBeNull(); }); it('serializes the value through a hidden input when name is set', () => { const { container } = render( , ); const hidden = container.querySelector('input[type="hidden"]') as HTMLInputElement; expect(hidden).toBeTruthy(); expect(hidden.name).toBe('color'); expect(hidden.value).toBe('red'); }); it('error sets aria-invalid and renders the message; label/hint render via FieldShell', () => { const { getByRole, getByText } = render( , ); expect(getByRole('combobox').getAttribute('aria-invalid')).toBe('true'); expect(getByText('Color')).toBeTruthy(); expect(getByText('Required field')).toBeTruthy(); }); it('forwards id, aria-describedby, and ref to the trigger', () => { const ref = React.createRef(); const { getByRole } = render( , ); const trigger = getByRole('combobox'); expect(trigger.id).toBe('sel-1'); expect(trigger.getAttribute('aria-describedby')).toBe('desc-1'); expect(ref.current).toBe(trigger); }); });