UNPKG

@oslokommune/punkt-elements

Version:

Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo

365 lines (310 loc) 11.2 kB
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() }) }) })