UNPKG

@oslokommune/punkt-elements

Version:

Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo

437 lines (340 loc) 16.6 kB
import '@testing-library/jest-dom' import { fireEvent } from '@testing-library/dom' import './combobox' import { PktCombobox } from './combobox' import type { IPktComboboxOption } from './combobox' const waitForCustomElements = async () => { await customElements.whenDefined('pkt-combobox') } const createCombobox = async (comboboxProps = '') => { const container = document.createElement('div') container.innerHTML = ` <pkt-combobox ${comboboxProps}></pkt-combobox> ` document.body.appendChild(container) await waitForCustomElements() return container } const defaultOptions: IPktComboboxOption[] = [ { value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }, { value: 'cherry', label: 'Cherry' }, { value: 'date', label: 'Date' }, ] afterEach(() => { document.body.innerHTML = '' }) describe('PktCombobox', () => { describe('Keyboard navigation', () => { test('opens dropdown with Enter on arrow button', async () => { const container = await createCombobox('id="test" name="test" label="Test"') const combobox = container.querySelector('pkt-combobox') as PktCombobox await combobox.updateComplete const arrowButton = combobox.querySelector('.pkt-combobox__input') fireEvent.keyDown(arrowButton!, { key: 'Enter' }) await combobox.updateComplete expect(combobox['_isOptionsOpen']).toBe(true) }) test('opens dropdown with Space on arrow button', async () => { const container = await createCombobox('id="test" name="test" label="Test"') const combobox = container.querySelector('pkt-combobox') as PktCombobox await combobox.updateComplete const arrowButton = combobox.querySelector('.pkt-combobox__input') fireEvent.keyDown(arrowButton!, { key: ' ' }) await combobox.updateComplete expect(combobox['_isOptionsOpen']).toBe(true) }) test('opens dropdown with ArrowDown on arrow button', async () => { const container = await createCombobox('id="test" name="test" label="Test"') const combobox = container.querySelector('pkt-combobox') as PktCombobox await combobox.updateComplete const arrowButton = combobox.querySelector('.pkt-combobox__input') fireEvent.keyDown(arrowButton!, { key: 'ArrowDown' }) await combobox.updateComplete expect(combobox['_isOptionsOpen']).toBe(true) }) test('toggles dropdown closed with Enter on arrow button', async () => { const container = await createCombobox('id="test" name="test" label="Test"') const combobox = container.querySelector('pkt-combobox') as PktCombobox await combobox.updateComplete const arrowButton = combobox.querySelector('.pkt-combobox__input') // Open fireEvent.keyDown(arrowButton!, { key: 'Enter' }) await combobox.updateComplete expect(combobox['_isOptionsOpen']).toBe(true) // Close fireEvent.keyDown(arrowButton!, { key: 'Enter' }) await combobox.updateComplete expect(combobox['_isOptionsOpen']).toBe(false) }) test('does not toggle on non-toggle keys', async () => { const container = await createCombobox('id="test" name="test" label="Test"') const combobox = container.querySelector('pkt-combobox') as PktCombobox await combobox.updateComplete const arrowButton = combobox.querySelector('.pkt-combobox__input') fireEvent.keyDown(arrowButton!, { key: 'Escape' }) await combobox.updateComplete expect(combobox['_isOptionsOpen']).toBe(false) }) test('submits value with Enter in text input', async () => { const container = await createCombobox('id="test" name="test" label="Test" allow-user-input') const combobox = container.querySelector('pkt-combobox') as PktCombobox combobox.options = [...defaultOptions] await combobox.updateComplete const input = combobox.querySelector('input[type="text"]') as HTMLInputElement fireEvent.focus(input) await combobox.updateComplete input.value = 'apple' fireEvent.keyDown(input, { key: 'Enter' }) await combobox.updateComplete expect(combobox['_value']).toContain('apple') }) test('closes dropdown with Escape in text input', async () => { const container = await createCombobox('id="test" name="test" label="Test" allow-user-input') const combobox = container.querySelector('pkt-combobox') as PktCombobox combobox.options = [...defaultOptions] await combobox.updateComplete const input = combobox.querySelector('input[type="text"]') as HTMLInputElement fireEvent.focus(input) await combobox.updateComplete expect(combobox['_isOptionsOpen']).toBe(true) fireEvent.keyDown(input, { key: 'Escape' }) await combobox.updateComplete expect(combobox['_isOptionsOpen']).toBe(false) }) test('does not open dropdown when disabled', async () => { const container = await createCombobox('id="test" name="test" label="Test" disabled') const combobox = container.querySelector('pkt-combobox') as PktCombobox await combobox.updateComplete const arrowButton = combobox.querySelector('.pkt-combobox__input') fireEvent.keyDown(arrowButton!, { key: 'Enter' }) await combobox.updateComplete expect(combobox['_isOptionsOpen']).toBe(false) }) }) describe('Focus handling', () => { test('opens dropdown on input focus', async () => { const container = await createCombobox('id="test" name="test" label="Test" allow-user-input') const combobox = container.querySelector('pkt-combobox') as PktCombobox combobox.options = [...defaultOptions] await combobox.updateComplete const input = combobox.querySelector('input[type="text"]') as HTMLInputElement fireEvent.focus(input) await combobox.updateComplete expect(combobox['_isOptionsOpen']).toBe(true) expect(combobox['_inputFocus']).toBe(true) }) test('populates input with current value on focus in single-select', async () => { const container = await createCombobox( 'id="test" name="test" label="Test" allow-user-input value="apple"', ) const combobox = container.querySelector('pkt-combobox') as PktCombobox combobox.options = [...defaultOptions] await combobox.updateComplete const input = combobox.querySelector('input[type="text"]') as HTMLInputElement fireEvent.focus(input) await combobox.updateComplete expect(input.value).toBe('Apple') }) test('handles blur correctly', async () => { const container = await createCombobox('id="test" name="test" label="Test" allow-user-input') const combobox = container.querySelector('pkt-combobox') as PktCombobox await combobox.updateComplete const input = combobox.querySelector('input[type="text"]') as HTMLInputElement fireEvent.focus(input) await combobox.updateComplete fireEvent.blur(input) await combobox.updateComplete expect(combobox['_inputFocus']).toBe(false) }) test('opens dropdown on input container click', async () => { const container = await createCombobox('id="test" name="test" label="Test"') const combobox = container.querySelector('pkt-combobox') as PktCombobox await combobox.updateComplete const inputDiv = combobox.querySelector('.pkt-combobox__input') fireEvent.click(inputDiv!) await combobox.updateComplete expect(combobox['_isOptionsOpen']).toBe(true) }) test('does not open when disabled and input container is clicked', async () => { const container = await createCombobox('id="test" name="test" label="Test" disabled') const combobox = container.querySelector('pkt-combobox') as PktCombobox await combobox.updateComplete const inputDiv = combobox.querySelector('.pkt-combobox__input') fireEvent.click(inputDiv!) await combobox.updateComplete expect(combobox['_isOptionsOpen']).toBe(false) }) }) describe('Focus-out behavior', () => { test('closes dropdown when clicking outside combobox', async () => { const container = await createCombobox('id="test" name="test" label="Test"') const combobox = container.querySelector('pkt-combobox') as PktCombobox await combobox.updateComplete const arrowButton = combobox.querySelector('.pkt-combobox__input') fireEvent.click(arrowButton!) await combobox.updateComplete expect(combobox['_isOptionsOpen']).toBe(true) fireEvent.click(document.body) await combobox.updateComplete expect(combobox['_isOptionsOpen']).toBe(false) }) test('selects matching option on focus-out when allowUserInput is off', async () => { const container = await createCombobox('id="test" name="test" label="Test" typeahead') const combobox = container.querySelector('pkt-combobox') as PktCombobox combobox.options = [...defaultOptions] await combobox.updateComplete const input = combobox.querySelector('input[type="text"]') as HTMLInputElement fireEvent.focus(input) await combobox.updateComplete input.value = 'Apple' combobox['_isOptionsOpen'] = true await combobox.updateComplete ;(combobox as any).closeAndProcessInput() await combobox.updateComplete expect(combobox['_value']).toContain('apple') }) test('adds custom value on focus-out when allowUserInput is on', async () => { const container = await createCombobox('id="test" name="test" label="Test" allow-user-input') const combobox = container.querySelector('pkt-combobox') as PktCombobox combobox.options = [...defaultOptions] await combobox.updateComplete const input = combobox.querySelector('input[type="text"]') as HTMLInputElement fireEvent.focus(input) await combobox.updateComplete input.value = 'NewFruit' combobox['_isOptionsOpen'] = true await combobox.updateComplete ;(combobox as any).closeAndProcessInput() await combobox.updateComplete expect(combobox['_value']).toContain('NewFruit') }) }) describe('Search and filtering', () => { test('filters options when typing in typeahead mode', async () => { const container = await createCombobox('id="test" name="test" label="Test" typeahead') const combobox = container.querySelector('pkt-combobox') as PktCombobox combobox.options = [...defaultOptions] await combobox.updateComplete const input = combobox.querySelector('input[type="text"]') as HTMLInputElement fireEvent.focus(input) await combobox.updateComplete // Type to filter input.value = 'app' fireEvent.input(input, { target: { value: 'app' } }) await combobox.updateComplete const listbox = combobox.querySelector('pkt-listbox') as any await listbox?.updateComplete // Internal _options should be filtered const filteredCount = combobox['_options'].length expect(filteredCount).toBeLessThan(defaultOptions.length) }) test('shows no-match message when search has no results and allowUserInput is off', async () => { const container = await createCombobox('id="test" name="test" label="Test" typeahead') const combobox = container.querySelector('pkt-combobox') as PktCombobox combobox.options = [...defaultOptions] await combobox.updateComplete const input = combobox.querySelector('input[type="text"]') as HTMLInputElement fireEvent.focus(input) await combobox.updateComplete input.value = 'zzzzz' fireEvent.input(input, { target: { value: 'zzzzz' } }) await combobox.updateComplete const listbox = combobox.querySelector('pkt-listbox') as any await listbox?.updateComplete const visibleOptions = combobox.querySelectorAll('.pkt-listbox__option') expect(visibleOptions.length).toBe(0) }) test('shows add-value banner when search has no exact match and allowUserInput is on', async () => { const container = await createCombobox('id="test" name="test" label="Test" allow-user-input') const combobox = container.querySelector('pkt-combobox') as PktCombobox combobox.options = [...defaultOptions] await combobox.updateComplete const input = combobox.querySelector('input[type="text"]') as HTMLInputElement fireEvent.focus(input) await combobox.updateComplete input.value = 'NewFruit' fireEvent.input(input, { target: { value: 'NewFruit' } }) await combobox.updateComplete const listbox = combobox.querySelector('pkt-listbox') as any await listbox?.updateComplete const addBanner = combobox.querySelector('.pkt-listbox__banner--new-option') expect(addBanner).toBeInTheDocument() }) test('dispatches search event on internal search state change', async () => { const container = await createCombobox('id="test" name="test" label="Test" include-search') const combobox = container.querySelector('pkt-combobox') as PktCombobox await combobox.updateComplete let searchEventDetail: string | null = null combobox.addEventListener('search', (e: Event) => { searchEventDetail = (e as CustomEvent).detail }) combobox['_search'] = 'test query' await combobox.updateComplete expect(searchEventDetail).toBe('test query') }) test('resets search when option is selected via toggleValue', async () => { const container = await createCombobox('id="test" name="test" label="Test" typeahead') const combobox = container.querySelector('pkt-combobox') as PktCombobox combobox.options = [...defaultOptions] await combobox.updateComplete // Set some search state combobox['_search'] = 'app' await combobox.updateComplete // Select an option ;(combobox as any).toggleValue('apple') await combobox.updateComplete expect(combobox['_search']).toBe('') }) }) describe('Listbox search (includeSearch)', () => { test('passes includeSearch to listbox', async () => { const container = await createCombobox('id="test" name="test" label="Test" include-search') const combobox = container.querySelector('pkt-combobox') as PktCombobox await combobox.updateComplete const listbox = combobox.querySelector('pkt-listbox') expect(listbox?.hasAttribute('include-search')).toBe(true) }) test('passes searchPlaceholder to listbox', async () => { const container = await createCombobox( 'id="test" name="test" label="Test" include-search search-placeholder="Søk her..."', ) const combobox = container.querySelector('pkt-combobox') as PktCombobox await combobox.updateComplete const listbox = combobox.querySelector('pkt-listbox') as any expect(listbox?.searchPlaceholder).toBe('Søk her...') }) test('updates search state on listbox search-change event', async () => { const container = await createCombobox('id="test" name="test" label="Test" include-search') const combobox = container.querySelector('pkt-combobox') as PktCombobox combobox.options = [...defaultOptions] await combobox.updateComplete // Open dropdown first const arrowButton = combobox.querySelector('.pkt-combobox__input') fireEvent.click(arrowButton!) await combobox.updateComplete // Simulate search input in the listbox search field const listbox = combobox.querySelector('pkt-listbox') as any await listbox?.updateComplete const searchInput = combobox.querySelector('[role="searchbox"]') as HTMLInputElement if (searchInput) { fireEvent.input(searchInput, { target: { value: 'app' } }) await combobox.updateComplete } expect(combobox['_search']).toBe('app') }) }) describe('Disconnected callback cleanup', () => { test('cleans up body click handler on disconnect', async () => { const container = await createCombobox('id="test" name="test" label="Test"') const combobox = container.querySelector('pkt-combobox') as PktCombobox await combobox.updateComplete const arrowButton = combobox.querySelector('.pkt-combobox__input') fireEvent.click(arrowButton!) await combobox.updateComplete expect(combobox['_isOptionsOpen']).toBe(true) combobox.remove() expect(() => fireEvent.click(document.body)).not.toThrow() }) }) })