@oslokommune/punkt-elements
Version:
Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo
365 lines (310 loc) • 11.2 kB
text/typescript
import '@testing-library/jest-dom'
import { axe, toHaveNoViolations } from 'jest-axe'
import { fireEvent } from '@testing-library/dom'
import { createElementTest, BaseTestConfig } from '../../tests/test-framework'
import { CustomElementFor } from '../../tests/component-registry'
import { type IPktListbox } from './listbox'
import './listbox'
export interface ListboxTestConfig extends Partial<IPktListbox>, BaseTestConfig {
label?: string
id?: string
}
// Properties that must be set via JS because their attribute names are kebab-case
const jsOnlyProps = [
'options',
'isOpen',
'includeSearch',
'isMultiSelect',
'allowUserInput',
'maxIsReached',
'customUserInput',
'searchPlaceholder',
'searchValue',
'maxLength',
'userMessage',
] as const
// Use shared framework
export const createListboxTest = async (config: ListboxTestConfig = {}) => {
const htmlConfig: Record<string, unknown> = {}
const jsConfig: Record<string, unknown> = {}
for (const [key, value] of Object.entries(config)) {
if ((jsOnlyProps as readonly string[]).includes(key)) {
jsConfig[key] = value
} else {
htmlConfig[key] = value
}
}
const { container, element } = await createElementTest<
CustomElementFor<'pkt-listbox'>,
BaseTestConfig & Record<string, unknown>
>('pkt-listbox', htmlConfig)
// Set JS-only properties directly
for (const [key, value] of Object.entries(jsConfig)) {
;(element as any)[key] = value
}
await element.updateComplete
return {
container,
listbox: element,
}
}
expect.extend(toHaveNoViolations)
// Cleanup after each test
afterEach(() => {
document.body.innerHTML = ''
})
describe('PktListbox', () => {
describe('Rendering and basic functionality', () => {
test('renders without errors', async () => {
const { listbox } = await createListboxTest()
expect(listbox).toBeInTheDocument()
expect(listbox).toBeTruthy()
})
test('renders with options', async () => {
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
]
const { listbox } = await createListboxTest({
label: 'Test Listbox',
options,
})
const listboxElement = listbox.querySelector('.pkt-listbox')
expect(listboxElement).toBeInTheDocument()
const optionElements = listbox.querySelectorAll('.pkt-listbox__option')
expect(optionElements).toHaveLength(2)
})
})
describe('Properties and attributes', () => {
test('applies default properties correctly', async () => {
const { listbox } = await createListboxTest()
expect(listbox.isOpen).toBe(false)
expect(listbox.disabled).toBe(false)
expect(listbox.includeSearch).toBe(false)
expect(listbox.isMultiSelect).toBe(false)
expect(listbox.allowUserInput).toBe(false)
expect(listbox.maxLength).toBe(0)
})
test('sets properties correctly', async () => {
const { listbox } = await createListboxTest({
isOpen: true,
disabled: true,
includeSearch: true,
isMultiSelect: true,
})
expect(listbox.isOpen).toBe(true)
expect(listbox.disabled).toBe(true)
expect(listbox.includeSearch).toBe(true)
expect(listbox.isMultiSelect).toBe(true)
})
})
describe('Option handling', () => {
test('renders single select options correctly', async () => {
const options = [
{ value: 'option1', label: 'Option 1', selected: true },
{ value: 'option2', label: 'Option 2' },
]
const { listbox } = await createListboxTest({ options })
const selectedOption = listbox.querySelector('.pkt-listbox__option--selected')
expect(selectedOption).toBeInTheDocument()
const checkIcon = listbox.querySelector('pkt-icon[name="check-big"]')
expect(checkIcon).toBeInTheDocument()
})
test('renders multi-select options with checkboxes', async () => {
const options = [
{ value: 'option1', label: 'Option 1', selected: true },
{ value: 'option2', label: 'Option 2' },
]
const { listbox } = await createListboxTest({
isMultiSelect: true,
options,
})
const checkboxes = listbox.querySelectorAll('input[type="checkbox"]')
expect(checkboxes).toHaveLength(2)
expect(checkboxes[0]).toBeChecked()
expect(checkboxes[1]).not.toBeChecked()
})
test('handles option click', async () => {
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
]
const { listbox } = await createListboxTest({ options })
// Listen for the option-toggle event
let toggledValue: string | null = null
listbox.addEventListener('option-toggle', (e: any) => {
toggledValue = e.detail
})
const optionElement = listbox.querySelector('.pkt-listbox__option')
fireEvent.click(optionElement!)
await listbox.updateComplete
expect(toggledValue).toBe('option1')
})
})
describe('Search functionality', () => {
test('renders search when includeSearch is true', async () => {
const { listbox } = await createListboxTest({ includeSearch: true })
const searchInput = listbox.querySelector('[role="searchbox"]')
expect(searchInput).toBeInTheDocument()
})
test('filters options based on search', async () => {
const options = [
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
]
const { listbox } = await createListboxTest({
includeSearch: true,
options,
})
// Set search value and trigger filtering
listbox.searchValue = 'app'
listbox.filterOptions()
await listbox.updateComplete
// Should filter to only show Apple - check filtered options
const visibleOptions = listbox.querySelectorAll('.pkt-listbox__option')
expect(visibleOptions).toHaveLength(1)
expect(visibleOptions[0].textContent?.trim()).toContain('Apple')
})
})
describe('User input functionality', () => {
test('renders new option banner when allowUserInput is true', async () => {
const { listbox } = await createListboxTest({
allowUserInput: true,
customUserInput: 'New',
})
const newOptionBanner = listbox.querySelector('.pkt-listbox__banner--new-option')
expect(newOptionBanner).toBeInTheDocument()
expect(newOptionBanner?.getAttribute('data-value')).toBe('New')
// Check that the text contains the basic structure
expect(newOptionBanner?.textContent).toMatch(/Legg til.*New/)
})
})
describe('Maximum selection', () => {
test('shows maximum reached banner', async () => {
const options = [
{ value: 'option1', label: 'Option 1', selected: true },
{ value: 'option2', label: 'Option 2', selected: true },
]
const { listbox } = await createListboxTest({
isMultiSelect: true,
maxLength: 3,
options,
})
const banner = listbox.querySelector('.pkt-listbox__banner--maximum-reached')
expect(banner).toBeInTheDocument()
expect(banner?.textContent).toContain('2 av maks 3')
})
})
describe('Multi-select functionality', () => {
test('sets aria-multiselectable to true when isMultiSelect is true', async () => {
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
]
const { listbox } = await createListboxTest({
isMultiSelect: true,
options,
})
const listboxElement = listbox.querySelector('[role="listbox"]')
expect(listboxElement?.getAttribute('aria-multiselectable')).toBe('true')
})
test('allows multiple options to be selected', async () => {
const options = [
{ value: 'option1', label: 'Option 1', selected: true },
{ value: 'option2', label: 'Option 2', selected: true },
{ value: 'option3', label: 'Option 3', selected: false },
]
const { listbox } = await createListboxTest({
isMultiSelect: true,
options,
})
const checkboxes = listbox.querySelectorAll('input[type="checkbox"]')
expect(checkboxes[0]).toBeChecked()
expect(checkboxes[1]).toBeChecked()
expect(checkboxes[2]).not.toBeChecked()
})
test('dispatches select-all event when Cmd+A is pressed', async () => {
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' },
]
const { listbox } = await createListboxTest({
isMultiSelect: true,
options,
})
let selectAllCalled = false
listbox.addEventListener('select-all', () => {
selectAllCalled = true
})
const firstOption = listbox.querySelector('.pkt-listbox__option')
firstOption?.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'a',
metaKey: true,
bubbles: true,
}),
)
await listbox.updateComplete
expect(selectAllCalled).toBe(true)
})
test('dispatches select-all event when Ctrl+A is pressed', async () => {
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
]
const { listbox } = await createListboxTest({
isMultiSelect: true,
options,
})
let selectAllCalled = false
listbox.addEventListener('select-all', () => {
selectAllCalled = true
})
const firstOption = listbox.querySelector('.pkt-listbox__option')
firstOption?.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'a',
ctrlKey: true,
bubbles: true,
}),
)
await listbox.updateComplete
expect(selectAllCalled).toBe(true)
})
test('toggles individual options in multi-select mode', async () => {
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
]
const { listbox } = await createListboxTest({
isMultiSelect: true,
options,
})
const toggledValues: string[] = []
listbox.addEventListener('option-toggle', (e: any) => {
toggledValues.push(e.detail)
})
const optionElements = listbox.querySelectorAll('.pkt-listbox__option')
fireEvent.click(optionElements[0])
fireEvent.click(optionElements[1])
await listbox.updateComplete
expect(toggledValues).toEqual(['option1', 'option2'])
})
})
describe('Accessibility', () => {
test('basic listbox is accessible', async () => {
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
]
const { container } = await createListboxTest({
label: 'Accessible Listbox',
options,
})
await new Promise((resolve) => setTimeout(resolve, 100))
const results = await axe(container)
expect(results).toHaveNoViolations()
})
})
})