import React from 'react';
import { describe, it, expect, vi } from 'vitest';
import { render, fireEvent, act } from '@testing-library/react';
import { PixelCombobox, type PixelComboboxOption } from '../../forms/PixelCombobox';
const fruitOptions: PixelComboboxOption[] = [
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
{ value: 'cherry', label: 'Cherry' },
];
const groupedOptions: PixelComboboxOption[] = [
{ value: 'apple', label: 'Apple', group: 'Fruits' },
{ value: 'banana', label: 'Banana', group: 'Fruits' },
{ value: 'carrot', label: 'Carrot', group: 'Veggies' },
];
describe('PixelCombobox', () => {
it('renders placeholder when no value', () => {
const { getByRole } = render(
,
);
const trigger = getByRole('combobox');
expect(trigger.textContent).toContain('Pick a fruit');
});
it('clicking trigger opens listbox', () => {
const { getByRole, queryByRole } = render(
,
);
expect(queryByRole('listbox')).toBeNull();
const trigger = getByRole('combobox');
fireEvent.click(trigger);
expect(queryByRole('listbox')).toBeTruthy();
});
it('typing filters options (case-insensitive substring)', () => {
const { getByRole, getAllByRole, queryByText } = render(
,
);
fireEvent.click(getByRole('combobox'));
const search = getByRole('searchbox') as HTMLInputElement;
fireEvent.change(search, { target: { value: 'an' } });
const opts = getAllByRole('option');
expect(opts.length).toBe(1);
expect(opts[0].textContent).toContain('Banana');
expect(queryByText('Apple')).toBeNull();
expect(queryByText('Cherry')).toBeNull();
});
it('arrow down highlights next, Enter selects, listbox closes', () => {
const onChange = vi.fn();
const { getByRole, queryByRole } = render(
,
);
const trigger = getByRole('combobox');
fireEvent.click(trigger);
const search = getByRole('searchbox');
// initial highlight = 0 (Apple). ArrowDown → Banana.
fireEvent.keyDown(search, { key: 'ArrowDown' });
fireEvent.keyDown(search, { key: 'Enter' });
expect(onChange).toHaveBeenCalledWith('banana');
expect(queryByRole('listbox')).toBeNull();
});
it('controlled mode: value prop wins', () => {
const { getByRole, rerender } = render(
,
);
expect(getByRole('combobox').textContent).toContain('Apple');
rerender(
,
);
expect(getByRole('combobox').textContent).toContain('Cherry');
});
it('hidden input with name carries selected value', () => {
const { getByRole, container } = render(
,
);
const hidden = container.querySelector('input[type="hidden"][name="fruit"]') as HTMLInputElement;
expect(hidden).toBeTruthy();
expect(hidden.value).toBe('banana');
// Now change selection
fireEvent.click(getByRole('combobox'));
const search = getByRole('searchbox');
fireEvent.keyDown(search, { key: 'ArrowDown' });
fireEvent.keyDown(search, { key: 'ArrowDown' });
fireEvent.keyDown(search, { key: 'Enter' });
const hiddenAfter = container.querySelector('input[type="hidden"][name="fruit"]') as HTMLInputElement;
expect(hiddenAfter.value).toBe('cherry');
});
it('Escape closes the listbox', () => {
const { getByRole, queryByRole } = render(
,
);
fireEvent.click(getByRole('combobox'));
expect(queryByRole('listbox')).toBeTruthy();
act(() => {
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
});
expect(queryByRole('listbox')).toBeNull();
});
it('renders grouped options with headings', () => {
const { getByRole, getByText } = render(
,
);
fireEvent.click(getByRole('combobox'));
expect(getByText('Fruits')).toBeTruthy();
expect(getByText('Veggies')).toBeTruthy();
});
it('shows emptyMessage when no options match the filter', () => {
const { getByRole, getByText } = render(
,
);
fireEvent.click(getByRole('combobox'));
const search = getByRole('searchbox') as HTMLInputElement;
fireEvent.change(search, { target: { value: 'zzzz' } });
expect(getByText('No fruits')).toBeTruthy();
});
});