import '@testing-library/jest-dom' import { fireEvent, render, screen } from '@testing-library/react' import type { IPktComboboxOption } from 'shared-types/combobox' import { PktCombobox } from './Combobox' import type { IPktCombobox } from './types' const comboboxId = 'test-combobox' const label = 'Test Combobox' const getDefaultOptions = (): IPktComboboxOption[] => [ { value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }, { value: 'cherry', label: 'Cherry', disabled: true }, { value: 'date', label: 'Date' }, ] const createComboboxTest = (props: Partial = {}) => { const defaultProps: IPktCombobox = { label, id: comboboxId, ...props, } return render() } describe('PktCombobox', () => { describe('Rendering and basic functionality', () => { test('renders without errors', () => { const { container } = createComboboxTest() const combobox = container.querySelector('.pkt-combobox') expect(combobox).toBeInTheDocument() }) test('renders with correct structure', () => { const { container } = createComboboxTest() const wrapper = container.querySelector('.pkt-inputwrapper') const inputDiv = container.querySelector('.pkt-combobox__input') const arrowButton = container.querySelector('.pkt-combobox__input') const listbox = container.querySelector('.pkt-listbox') expect(wrapper).toBeInTheDocument() expect(inputDiv).toBeInTheDocument() expect(arrowButton).toBeInTheDocument() expect(listbox).toBeInTheDocument() }) test('renders select-only combobox with correct ARIA attributes', () => { const { container } = createComboboxTest() const comboboxInput = container.querySelector('.pkt-combobox__input') expect(comboboxInput?.getAttribute('id')).toBe(`${comboboxId}-combobox`) expect(comboboxInput?.getAttribute('aria-expanded')).toBe('false') expect(comboboxInput?.getAttribute('aria-controls')).toBe(`${comboboxId}-listbox`) expect(comboboxInput?.getAttribute('aria-haspopup')).toBe('listbox') expect(comboboxInput?.getAttribute('aria-labelledby')).toBe(`${comboboxId}-combobox-label`) expect(comboboxInput?.getAttribute('role')).toBe('combobox') }) }) describe('Properties and attributes', () => { test('handles multiple property correctly', () => { const { container } = createComboboxTest({ multiple: true, defaultValue: ['apple'], options: getDefaultOptions(), }) const tags = container.querySelectorAll('.pkt-tag') expect(tags.length).toBeGreaterThan(0) }) test('handles allowUserInput property correctly', () => { const { container } = createComboboxTest({ allowUserInput: true }) const textInput = container.querySelector('input[type="text"][role="combobox"]') expect(textInput).toBeInTheDocument() expect(textInput?.getAttribute('role')).toBe('combobox') }) test('handles typeahead property correctly', () => { const { container } = createComboboxTest({ typeahead: true }) const textInput = container.querySelector('input[type="text"]') expect(textInput).toBeInTheDocument() expect(textInput?.getAttribute('aria-autocomplete')).toBe('both') }) test('handles disabled property correctly', () => { const { container } = createComboboxTest({ disabled: true }) const comboboxInput = container.querySelector('.pkt-combobox__input') expect(comboboxInput).toHaveClass('pkt-combobox__input--disabled') expect(comboboxInput?.getAttribute('tabindex')).toBe('-1') }) test('handles fullwidth property correctly', () => { const { container } = createComboboxTest({ fullwidth: true }) const inputDiv = container.querySelector('.pkt-combobox__input') expect(inputDiv).toHaveClass('pkt-combobox__input--fullwidth') }) test('renders with custom className', () => { const { container } = createComboboxTest({ className: 'my-custom-class' }) const outerDiv = container.querySelector('.pkt-combobox-component') expect(outerDiv).toHaveClass('my-custom-class') }) }) describe('Options handling', () => { test('handles options provided via options prop', () => { const { container } = createComboboxTest({ options: getDefaultOptions(), }) const optionElements = container.querySelectorAll('.pkt-listbox__option') expect(optionElements.length).toBe(4) expect(optionElements[0].getAttribute('data-value')).toBe('apple') expect(optionElements[1].getAttribute('data-value')).toBe('banana') }) test('handles options provided via children', () => { const { container } = render( , ) const optionElements = container.querySelectorAll('.pkt-listbox__option') expect(optionElements.length).toBe(2) expect(optionElements[0].getAttribute('data-value')).toBe('value1') expect(optionElements[1].getAttribute('data-value')).toBe('value2') }) test('handles defaultOptions property', () => { const defaultOptions: IPktComboboxOption[] = [ { value: 'default1', label: 'Default 1' }, { value: 'default2', label: 'Default 2' }, ] const { container } = createComboboxTest({ defaultOptions }) const optionElements = container.querySelectorAll('.pkt-listbox__option') expect(optionElements.length).toBe(2) expect(optionElements[0].getAttribute('data-value')).toBe('default1') }) test('handles disabled options', () => { const { container } = createComboboxTest({ options: getDefaultOptions(), }) const disabledOption = container.querySelector('[data-value="cherry"]') expect(disabledOption?.getAttribute('data-disabled')).toBe('true') }) }) describe('Value handling', () => { test('handles single value correctly', () => { const { container } = createComboboxTest({ defaultValue: 'apple', options: getDefaultOptions(), }) const formInput = container.querySelector('input.pkt-visually-hidden') as HTMLInputElement expect(formInput.value).toBe('apple') }) test('handles multiple values correctly', () => { const { container } = createComboboxTest({ defaultValue: ['apple', 'banana'], multiple: true, options: getDefaultOptions(), }) const formInput = container.querySelector('input.pkt-visually-hidden') as HTMLInputElement expect(formInput.value).toBe('apple,banana') }) test('handles controlled value correctly', () => { const { container } = createComboboxTest({ value: 'apple', options: getDefaultOptions(), }) const formInput = container.querySelector('input.pkt-visually-hidden') as HTMLInputElement expect(formInput.value).toBe('apple') }) }) describe('Placeholder functionality', () => { test('shows placeholder when no value selected', () => { const { container } = createComboboxTest({ placeholder: 'Select an option' }) const placeholder = container.querySelector('.pkt-combobox__placeholder') expect(placeholder).toBeInTheDocument() expect(placeholder?.textContent).toBe('Select an option') }) test('hides placeholder when value is selected', () => { const { container } = createComboboxTest({ placeholder: 'Select an option', defaultValue: 'apple', options: getDefaultOptions(), }) const placeholder = container.querySelector('.pkt-combobox__placeholder') expect(placeholder).not.toBeInTheDocument() }) test('shows placeholder in multiple mode with outside tag placement', () => { const { container } = createComboboxTest({ placeholder: 'Select options', multiple: true, tagPlacement: 'outside', }) const placeholder = container.querySelector('.pkt-combobox__placeholder') expect(placeholder).toBeInTheDocument() expect(placeholder?.textContent).toBe('Select options') }) }) describe('Tag placement functionality', () => { test('renders tags inside input by default in multiple mode', () => { const { container } = createComboboxTest({ multiple: true, defaultValue: ['apple'], options: getDefaultOptions(), }) const outsideTags = container.querySelector('.pkt-combobox__tags-outside') expect(outsideTags).not.toBeInTheDocument() const inlineTags = container.querySelectorAll('.pkt-combobox__input .pkt-tag') expect(inlineTags.length).toBeGreaterThan(0) }) test('renders tags outside input when tagPlacement is outside', () => { const { container } = createComboboxTest({ multiple: true, tagPlacement: 'outside', defaultValue: ['apple', 'banana'], options: getDefaultOptions(), }) const outsideTags = container.querySelector('.pkt-combobox__tags-outside') expect(outsideTags).toBeInTheDocument() const tags = outsideTags?.querySelectorAll('.pkt-tag') expect(tags?.length).toBe(2) }) }) describe('Input field functionality', () => { test('renders hidden input when not allowUserInput or typeahead', () => { const { container } = createComboboxTest({ name: 'test-name' }) const hiddenInput = container.querySelector('input[type="hidden"]') const textInput = container.querySelector('.pkt-combobox__input input[type="text"]') expect(hiddenInput).toBeInTheDocument() expect(textInput).not.toBeInTheDocument() expect(hiddenInput?.getAttribute('id')).toBe(`${comboboxId}-input`) expect(hiddenInput?.getAttribute('name')).toBe('test-name-input') }) test('renders text input when allowUserInput is true', () => { const { container } = createComboboxTest({ allowUserInput: true, name: 'test-name', }) const textInput = container.querySelector('input[type="text"][role="combobox"]') const hiddenInput = container.querySelector('input[type="hidden"]') expect(textInput).toBeInTheDocument() expect(hiddenInput).not.toBeInTheDocument() expect(textInput?.getAttribute('id')).toBe(`${comboboxId}-input`) expect(textInput?.getAttribute('name')).toBe('test-name-input') expect(textInput?.getAttribute('aria-controls')).toBe(`${comboboxId}-listbox`) }) test('renders text input when typeahead is true', () => { const { container } = createComboboxTest({ typeahead: true }) const textInput = container.querySelector('input[type="text"]') expect(textInput).toBeInTheDocument() expect(textInput?.getAttribute('aria-autocomplete')).toBe('both') }) test('sets correct aria-autocomplete for allowUserInput', () => { const { container } = createComboboxTest({ allowUserInput: true }) const textInput = container.querySelector('input[type="text"]') expect(textInput?.getAttribute('aria-autocomplete')).toBe('list') }) }) describe('Dropdown functionality', () => { test('opens dropdown when arrow button is clicked', () => { const { container } = createComboboxTest() const arrowButton = container.querySelector('.pkt-combobox__input') const inputDiv = container.querySelector('.pkt-combobox__input') expect(arrowButton?.getAttribute('aria-expanded')).toBe('false') expect(inputDiv).not.toHaveClass('pkt-combobox__input--open') fireEvent.click(arrowButton!) expect(arrowButton?.getAttribute('aria-expanded')).toBe('true') expect(inputDiv).toHaveClass('pkt-combobox__input--open') }) test('toggles dropdown state with multiple clicks', () => { const { container } = createComboboxTest() const arrowButton = container.querySelector('.pkt-combobox__input') fireEvent.click(arrowButton!) expect(arrowButton?.getAttribute('aria-expanded')).toBe('true') fireEvent.click(arrowButton!) expect(arrowButton?.getAttribute('aria-expanded')).toBe('false') }) test('does not open when disabled', () => { const { container } = createComboboxTest({ disabled: true }) const arrowButton = container.querySelector('.pkt-combobox__input') fireEvent.click(arrowButton!) expect(arrowButton?.getAttribute('aria-expanded')).toBe('false') }) }) describe('Search functionality', () => { test('renders search input when includeSearch is true', () => { const { container } = createComboboxTest({ includeSearch: true }) const searchInput = container.querySelector('.pkt-listbox__search input') expect(searchInput).toBeInTheDocument() expect(searchInput?.getAttribute('role')).toBe('searchbox') }) test('sets search placeholder correctly', () => { const { container } = createComboboxTest({ includeSearch: true, searchPlaceholder: 'Search items...', }) const searchInput = container.querySelector('.pkt-listbox__search input') as HTMLInputElement expect(searchInput.placeholder).toBe('Search items...') }) }) describe('Max length functionality', () => { test('shows counter when maxlength is set in multiple mode', () => { const { container } = createComboboxTest({ multiple: true, maxlength: 5, defaultValue: ['apple', 'banana'], options: getDefaultOptions(), }) const counter = container.querySelector('.pkt-input__counter') expect(counter).toBeInTheDocument() }) }) describe('Error handling', () => { test('applies error styling when hasError is true', () => { const { container } = createComboboxTest({ hasError: true }) const inputDiv = container.querySelector('.pkt-combobox__input') expect(inputDiv).toHaveClass('pkt-combobox__input--error') }) test('renders error message text', () => { createComboboxTest({ hasError: true, errorMessage: 'Test error', }) expect(screen.getByText('Test error')).toBeInTheDocument() }) }) describe('Form integration', () => { test('uses id for form input name when no name specified', () => { const { container } = createComboboxTest() const formInput = container.querySelector('input.pkt-visually-hidden') as HTMLInputElement expect(formInput).toHaveAttribute('name', comboboxId) }) test('uses custom name for form input when specified', () => { const { container } = createComboboxTest({ name: 'myField', defaultValue: 'apple', options: getDefaultOptions(), }) const formInput = container.querySelector('input.pkt-visually-hidden') as HTMLInputElement expect(formInput).toHaveAttribute('name', 'myField') expect(formInput.value).toBe('apple') }) test('renders hidden form input', () => { const { container } = createComboboxTest() const formInput = container.querySelector('input.pkt-visually-hidden') expect(formInput).toBeInTheDocument() expect(formInput).toHaveClass('pkt-visually-hidden') expect(formInput?.getAttribute('tabindex')).toBe('-1') }) }) describe('InputWrapper integration', () => { test('renders label text', () => { createComboboxTest({ label: 'Custom Label' }) expect(screen.getByText('Custom Label')).toBeInTheDocument() }) test('renders helptext', () => { createComboboxTest({ helptext: 'Help text' }) expect(screen.getByText('Help text')).toBeInTheDocument() }) test('renders optional tag', () => { const { container } = createComboboxTest({ optionalTag: true, optionalText: 'Valgfritt', }) expect(container.textContent).toContain('Valgfritt') }) test('renders required tag', () => { const { container } = createComboboxTest({ requiredTag: true, requiredText: 'Må fylles ut', }) expect(container.textContent).toContain('Må fylles ut') }) test('renders label as screen-reader-only when useWrapper is false', () => { const { container } = createComboboxTest({ useWrapper: false }) const labelEl = container.querySelector('label') expect(labelEl).toBeInTheDocument() expect(labelEl).toHaveClass('pkt-sr-only') }) }) })