@oslokommune/punkt-elements
Version:
Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo
544 lines (440 loc) • 21.2 kB
text/typescript
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
}
// Use a function to return fresh objects each time, preventing mutation leaks between tests
const getDefaultOptions = (): IPktComboboxOption[] => [
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
{ value: 'cherry', label: 'Cherry' },
{ value: 'date', label: 'Date' },
]
const openAndWaitForListbox = async (combobox: PktCombobox) => {
const arrowButton = combobox.querySelector('.pkt-combobox__input')
fireEvent.click(arrowButton!)
await combobox.updateComplete
const listbox = combobox.querySelector('pkt-listbox') as any
await listbox?.updateComplete
}
afterEach(() => {
document.body.innerHTML = ''
})
describe('PktCombobox', () => {
describe('Single selection', () => {
test('selects a value via toggleValue', async () => {
const container = await createCombobox('id="test" name="test" label="Test"')
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
;(combobox as any).toggleValue('apple')
await combobox.updateComplete
expect(combobox['_value']).toEqual(['apple'])
})
test('replaces current selection when selecting a new value', async () => {
const container = await createCombobox('id="test" name="test" label="Test" value="apple"')
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
expect(combobox['_value']).toEqual(['apple'])
;(combobox as any).toggleValue('banana')
await combobox.updateComplete
expect(combobox['_value']).toEqual(['banana'])
})
test('closes dropdown after selecting in single mode', async () => {
const container = await createCombobox('id="test" name="test" label="Test"')
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
await openAndWaitForListbox(combobox)
expect(combobox['_isOptionsOpen']).toBe(true)
;(combobox as any).toggleValue('apple')
await combobox.updateComplete
expect(combobox['_isOptionsOpen']).toBe(false)
})
test('deselects value when toggling already selected option', async () => {
const container = await createCombobox('id="test" name="test" label="Test" value="apple"')
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
;(combobox as any).toggleValue('apple')
await combobox.updateComplete
expect(combobox['_value']).toEqual([])
})
test('displays selected value as text in single mode', async () => {
const container = await createCombobox('id="test" name="test" label="Test" value="apple"')
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
const valueSpan = combobox.querySelector('.pkt-combobox__value')
expect(valueSpan?.textContent?.trim()).toBe('Apple')
})
test('renders value as tag with tagSkinColor in multiple mode', async () => {
const optionsWithTags: IPktComboboxOption[] = [
{ value: 'red', label: 'Red', tagSkinColor: 'red' },
{ value: 'blue', label: 'Blue', tagSkinColor: 'blue' },
]
const container = await createCombobox(
'id="test" name="test" label="Test" multiple value="red"',
)
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = optionsWithTags
await combobox.updateComplete
const tag = combobox.querySelector('pkt-tag')
expect(tag).toBeInTheDocument()
})
test('selects option when clicking it in the open dropdown', async () => {
const container = await createCombobox('id="test" name="test" label="Test"')
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
await openAndWaitForListbox(combobox)
const listbox = combobox.querySelector('pkt-listbox') as any
await listbox?.updateComplete
const option = combobox.querySelector('.pkt-listbox__option')
expect(option).toBeInTheDocument()
fireEvent.click(option!)
await combobox.updateComplete
expect(combobox['_value']).toEqual(['apple'])
})
})
describe('Multiple selection', () => {
test('selects multiple values', async () => {
const container = await createCombobox('id="test" name="test" label="Test" multiple')
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
// Select first value
;(combobox as any).toggleValue('apple')
await combobox.updateComplete
expect(combobox['_value']).toContain('apple')
// Select second value
;(combobox as any).toggleValue('banana')
await combobox.updateComplete
expect(combobox['_value']).toContain('banana')
expect(combobox['_value'].length).toBe(2)
})
test('keeps dropdown open after selection in multiple mode', async () => {
const container = await createCombobox('id="test" name="test" label="Test" multiple')
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
await openAndWaitForListbox(combobox)
;(combobox as any).toggleValue('apple')
await combobox.updateComplete
expect(combobox['_isOptionsOpen']).toBe(true)
})
test('renders selected values as tags in multiple mode', async () => {
const container = await createCombobox(
'id="test" name="test" label="Test" multiple value="apple,banana"',
)
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
const tags = combobox.querySelectorAll('pkt-tag')
expect(tags.length).toBe(2)
})
test('removes a selected value by clicking its tag close button', async () => {
const container = await createCombobox(
'id="test" name="test" label="Test" multiple value="apple,banana"',
)
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
const closeButtons = combobox.querySelectorAll('pkt-tag .pkt-tag__close-btn')
expect(closeButtons.length).toBe(2)
fireEvent.click(closeButtons[0])
await combobox.updateComplete
expect(combobox['_value']).toEqual(['banana'])
})
test('deselects value when toggling already selected option in multiple mode', async () => {
const container = await createCombobox(
'id="test" name="test" label="Test" multiple value="apple,banana"',
)
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
;(combobox as any).toggleValue('apple')
await combobox.updateComplete
expect(combobox['_value']).toEqual(['banana'])
})
test('renders tags outside when tagPlacement is outside', async () => {
const container = await createCombobox(
'id="test" name="test" label="Test" multiple tag-placement="outside" value="apple,banana"',
)
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
const outsideTags = combobox.querySelector('.pkt-combobox__tags-outside')
expect(outsideTags).toBeInTheDocument()
const tags = outsideTags?.querySelectorAll('pkt-tag')
expect(tags?.length).toBe(2)
})
})
describe('Maxlength enforcement', () => {
test('prevents selection beyond maxlength', async () => {
const container = await createCombobox(
'id="test" name="test" label="Test" multiple maxlength="2" value="apple,banana"',
)
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
;(combobox as any).toggleValue('cherry')
await combobox.updateComplete
expect(combobox['_value']).toEqual(['apple', 'banana'])
expect(combobox['_value'].length).toBeLessThanOrEqual(2)
})
test('shows max reached message when trying to exceed maxlength', async () => {
const container = await createCombobox(
'id="test" name="test" label="Test" multiple maxlength="2" value="apple,banana"',
)
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
;(combobox as any).toggleValue('cherry')
await combobox.updateComplete
expect(combobox['_userInfoMessage']).toBe('Maks antall valg nådd')
})
test('allows deselection when at maxlength', async () => {
const container = await createCombobox(
'id="test" name="test" label="Test" multiple maxlength="2" value="apple,banana"',
)
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
;(combobox as any).toggleValue('apple')
await combobox.updateComplete
expect(combobox['_value']).toEqual(['banana'])
})
})
describe('Disabled options', () => {
test('does not select disabled options', async () => {
const optionsWithDisabled: IPktComboboxOption[] = [
{ value: 'enabled', label: 'Enabled' },
{ value: 'disabled', label: 'Disabled', disabled: true },
]
const container = await createCombobox('id="test" name="test" label="Test"')
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = optionsWithDisabled
await combobox.updateComplete
;(combobox as any).toggleValue('disabled')
await combobox.updateComplete
expect(combobox['_value']).toEqual([])
})
})
describe('User input (custom values)', () => {
test('adds custom value in single-select mode', async () => {
const container = await createCombobox('id="test" name="test" label="Test" allow-user-input')
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
const input = combobox.querySelector('input[type="text"]') as HTMLInputElement
fireEvent.focus(input)
await combobox.updateComplete
input.value = 'CustomFruit'
fireEvent.input(input, { target: { value: 'CustomFruit' } })
await combobox.updateComplete
fireEvent.keyDown(input, { key: 'Enter' })
await combobox.updateComplete
expect(combobox['_value']).toContain('CustomFruit')
expect(combobox.options.some((o) => o.value === 'CustomFruit' && o.userAdded)).toBe(true)
})
test('adds custom value in multiple-select mode', async () => {
const container = await createCombobox(
'id="test" name="test" label="Test" allow-user-input multiple',
)
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
const input = combobox.querySelector('input[type="text"]') as HTMLInputElement
fireEvent.focus(input)
await combobox.updateComplete
input.value = 'CustomFruit'
fireEvent.input(input, { target: { value: 'CustomFruit' } })
await combobox.updateComplete
fireEvent.keyDown(input, { key: 'Enter' })
await combobox.updateComplete
expect(combobox['_value']).toContain('CustomFruit')
expect(combobox.options.some((o) => o.value === 'CustomFruit' && o.userAdded)).toBe(true)
})
test('removes user-added option when deselected via removeSelected', async () => {
const container = await createCombobox(
'id="test" name="test" label="Test" allow-user-input multiple',
)
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
;(combobox as any).addNewUserValue('CustomFruit')
await combobox.updateComplete
expect(combobox['_value']).toContain('CustomFruit')
expect(combobox.options.some((o) => o.value === 'CustomFruit')).toBe(true)
;(combobox as any).removeSelected('CustomFruit')
await combobox.updateComplete
expect(combobox['_value']).not.toContain('CustomFruit')
expect(combobox.options.some((o) => o.value === 'CustomFruit')).toBe(false)
})
test('preserves user-added options when options array is replaced externally', async () => {
const container = await createCombobox('id="test" name="test" label="Test" allow-user-input')
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
;(combobox as any).addNewUserValue('CustomFruit')
await combobox.updateComplete
combobox.options = [{ value: 'newOption', label: 'New Option' }]
await combobox.updateComplete
expect(combobox.options.some((o) => o.value === 'CustomFruit' && o.userAdded)).toBe(true)
expect(combobox.options.some((o) => o.value === 'newOption')).toBe(true)
})
test('does not add empty custom value', 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
;(combobox as any).addNewUserValue('')
await combobox.updateComplete
expect(combobox['_value']).toEqual([])
})
test('does not add whitespace-only custom value', 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
;(combobox as any).addNewUserValue(' ')
await combobox.updateComplete
expect(combobox['_value']).toEqual([])
})
})
describe('Select all / clear all', () => {
test('selects all options via addAllOptions', async () => {
const container = await createCombobox('id="test" name="test" label="Test" multiple')
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
;(combobox as any).addAllOptions()
await combobox.updateComplete
expect(combobox['_value'].length).toBe(4)
})
test('clears all selections by setting value to empty', async () => {
const container = await createCombobox(
'id="test" name="test" label="Test" multiple value="apple,banana"',
)
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
expect(combobox['_value']).toEqual(['apple', 'banana'])
combobox.value = []
await combobox.updateComplete
// Allow cascading updates to settle
await combobox.updateComplete
expect(combobox['_value']).toEqual([])
})
test('handles select-all event from listbox', async () => {
const container = await createCombobox('id="test" name="test" label="Test" multiple')
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
const listbox = combobox.querySelector('pkt-listbox')
fireEvent(listbox!, new CustomEvent('select-all'))
await combobox.updateComplete
expect(combobox['_value'].length).toBe(4)
})
test('handles deselect-all event from listbox', async () => {
const container = await createCombobox(
'id="test" name="test" label="Test" multiple value="apple,banana"',
)
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
expect(combobox['_value']).toEqual(['apple', 'banana'])
// Set value directly via the public property for reliable clearing
combobox.value = []
await combobox.updateComplete
await combobox.updateComplete
expect(combobox['_value']).toEqual([])
})
})
describe('Value change events', () => {
test('dispatches value-change event on selection', async () => {
const container = await createCombobox('id="test" name="test" label="Test"')
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
let valueChangeEvent: CustomEvent | null = null
combobox.addEventListener('value-change', (e: Event) => {
valueChangeEvent = e as CustomEvent
})
;(combobox as any).toggleValue('apple')
await combobox.updateComplete
expect(valueChangeEvent).toBeTruthy()
})
test('dispatches value-change with array for multiple mode', async () => {
const container = await createCombobox('id="test" name="test" label="Test" multiple')
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
let valueChangeDetail: any = null
combobox.addEventListener('value-change', (e: Event) => {
valueChangeDetail = (e as CustomEvent).detail
})
combobox.value = ['apple', 'banana']
await combobox.updateComplete
expect(valueChangeDetail).toEqual(['apple', 'banana'])
})
test('dispatches value-change with string for single mode', async () => {
const container = await createCombobox('id="test" name="test" label="Test"')
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = getDefaultOptions()
await combobox.updateComplete
let valueChangeDetail: any = null
combobox.addEventListener('value-change', (e: Event) => {
valueChangeDetail = (e as CustomEvent).detail
})
combobox.value = 'apple'
await combobox.updateComplete
expect(valueChangeDetail).toBe('apple')
})
})
describe('displayValueAs modes', () => {
test('displays value using label by default', async () => {
const options: IPktComboboxOption[] = [{ value: 'no', label: 'Norway', prefix: 'NO' }]
const container = await createCombobox('id="test" name="test" label="Test" value="no"')
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = options
await combobox.updateComplete
const valueEl = combobox.querySelector('.pkt-combobox__value')
expect(valueEl?.textContent?.trim()).toBe('Norway')
})
test('displays value using value when displayValueAs is value', async () => {
const options: IPktComboboxOption[] = [{ value: 'no', label: 'Norway', prefix: 'NO' }]
const container = await createCombobox(
'id="test" name="test" label="Test" value="no" display-value-as="value"',
)
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = options
await combobox.updateComplete
const valueEl = combobox.querySelector('.pkt-combobox__value')
expect(valueEl?.textContent?.trim()).toBe('no')
})
test('displays prefix and value when displayValueAs is prefixAndValue', async () => {
const options: IPktComboboxOption[] = [{ value: 'no', label: 'Norway', prefix: 'NO' }]
const container = await createCombobox(
'id="test" name="test" label="Test" value="no" display-value-as="prefixAndValue"',
)
const combobox = container.querySelector('pkt-combobox') as PktCombobox
combobox.options = options
await combobox.updateComplete
const valueEl = combobox.querySelector('.pkt-combobox__value')
expect(valueEl?.textContent?.trim()).toBe('NO no')
})
})
})