@oslokommune/punkt-elements
Version:
Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo
313 lines (261 loc) • 12.3 kB
text/typescript
import '@testing-library/jest-dom'
import { fireEvent } from '@testing-library/dom'
import { axe, toHaveNoViolations } from 'jest-axe'
import { vi } from 'vitest'
import { createElementTest, BaseTestConfig } from '../../tests/test-framework'
import { CustomElementFor } from '../../tests/component-registry'
import './searchinput'
expect.extend(toHaveNoViolations)
interface SearchInputTestConfig extends BaseTestConfig {
id?: string
name?: string
appearance?: 'local' | 'local-with-button' | 'global'
label?: string
placeholder?: string
disabled?: boolean
fullwidth?: boolean
action?: string
method?: 'get' | 'post' | 'dialog'
value?: string
}
const createSearchInputTest = async (config: SearchInputTestConfig = {}) => {
const { container, element } = await createElementTest<
CustomElementFor<'pkt-searchinput'>,
SearchInputTestConfig
>('pkt-searchinput', {
id: 'test-searchinput',
...config,
})
return {
container,
searchinput: element,
}
}
afterEach(() => {
document.body.innerHTML = ''
})
describe('PktSearchInput', () => {
describe('wrapper', () => {
test('renders form when action exists', async () => {
const { searchinput } = await createSearchInputTest({ action: '/search', method: 'get' })
const wrapper = searchinput.querySelector('.pkt-searchinput')
expect(wrapper?.tagName.toLowerCase()).toBe('form')
expect(wrapper).toHaveAttribute('role', 'search')
expect(wrapper).toHaveAttribute('action', '/search')
expect(wrapper).toHaveAttribute('method', 'get')
})
test('renders div when action does not exist', async () => {
const { searchinput } = await createSearchInputTest()
const wrapper = searchinput.querySelector('.pkt-searchinput')
expect(wrapper?.tagName.toLowerCase()).toBe('div')
expect(wrapper).toHaveAttribute('role', 'search')
expect(searchinput.querySelector('form')).toBeNull()
})
})
describe('label', () => {
test('renders label when label exists', async () => {
const { searchinput } = await createSearchInputTest({ label: 'Test Label' })
await searchinput.updateComplete
const label = searchinput.querySelector('label.pkt-inputwrapper__label')
expect(label).toBeInTheDocument()
expect(label?.textContent).toContain('Test Label')
expect(label).toHaveAttribute('for', 'test-searchinput')
})
test('does not render label when label is absent', async () => {
const { searchinput } = await createSearchInputTest()
await searchinput.updateComplete
expect(searchinput.querySelector('label')).toBeNull()
})
})
describe('input attributes', () => {
test('input has expected type, name, id, placeholder, disabled', async () => {
const { searchinput } = await createSearchInputTest({
id: 'my-search',
name: 'q',
placeholder: 'Finn…',
disabled: true,
})
await searchinput.updateComplete
const input = searchinput.querySelector('input') as HTMLInputElement
expect(input.type).toBe('search')
expect(input.id).toBe('my-search')
expect(input.name).toBe('q')
expect(input.placeholder).toBe('Finn…')
expect(input.disabled).toBe(true)
})
test('name defaults to id when name is not set', async () => {
const { searchinput } = await createSearchInputTest({ id: 'search-field' })
await searchinput.updateComplete
expect((searchinput.querySelector('input') as HTMLInputElement).name).toBe('search-field')
})
test('placeholder defaults to Søk…', async () => {
const { searchinput } = await createSearchInputTest()
await searchinput.updateComplete
expect((searchinput.querySelector('input') as HTMLInputElement).placeholder).toBe('Søk…')
})
})
describe('appearance and fullwidth classes', () => {
test('applies appearance modifier and fullwidth on wrapper', async () => {
const { searchinput } = await createSearchInputTest({
appearance: 'global',
fullwidth: true,
})
await searchinput.updateComplete
const wrapper = searchinput.querySelector('.pkt-searchinput')
expect(wrapper?.classList.contains('pkt-searchinput--global')).toBe(true)
expect(wrapper?.classList.contains('pkt-searchinput--fullwidth')).toBe(true)
})
test('local uses pkt-input__container; local-with-button uses pkt-searchinput__field', async () => {
const { searchinput: localEl } = await createSearchInputTest({ appearance: 'local' })
await localEl.updateComplete
expect(localEl.querySelector('.pkt-input__container')).toBeTruthy()
expect(localEl.querySelector('.pkt-searchinput__field')).toBeNull()
const { searchinput: lwb } = await createSearchInputTest({
appearance: 'local-with-button',
})
await lwb.updateComplete
expect(lwb.querySelector('.pkt-searchinput__field')).toBeTruthy()
expect(lwb.querySelector('.pkt-input__container')).toBeNull()
})
})
describe('pkt-search', () => {
test('fires on Enter with detail { value }', async () => {
const { searchinput } = await createSearchInputTest()
const searchSpy = vi.fn()
searchinput.addEventListener('pkt-search', searchSpy)
const input = searchinput.querySelector('input') as HTMLInputElement
fireEvent.input(input, { target: { value: 'oslo' } })
fireEvent.keyDown(input, { key: 'Enter' })
expect(searchSpy).toHaveBeenCalledTimes(1)
expect(searchSpy.mock.calls[0][0].detail).toEqual({ value: 'oslo' })
})
test('fires on search button click with detail { value }', async () => {
const { searchinput } = await createSearchInputTest({ value: 'kommune' })
await searchinput.updateComplete
const searchSpy = vi.fn()
searchinput.addEventListener('pkt-search', searchSpy)
const button = searchinput.querySelector('.pkt-searchinput__button') as HTMLButtonElement
fireEvent.click(button)
expect(searchSpy).toHaveBeenCalledTimes(1)
expect(searchSpy.mock.calls[0][0].detail).toEqual({ value: 'kommune' })
})
})
describe('search submit button classes', () => {
test('local: tertiary, icon-only, pkt-input-icon', async () => {
const { searchinput } = await createSearchInputTest({ appearance: 'local' })
await searchinput.updateComplete
const button = searchinput.querySelector('.pkt-searchinput__button') as HTMLButtonElement
expect(button.tagName.toLowerCase()).toBe('button')
expect(button.classList.contains('pkt-input-icon')).toBe(true)
expect(button.classList.contains('pkt-btn--tertiary')).toBe(true)
expect(button.classList.contains('pkt-btn--icon-only')).toBe(true)
expect(button.classList.contains('pkt-btn--medium')).toBe(true)
})
test('global: primary, yellow', async () => {
const { searchinput } = await createSearchInputTest({ appearance: 'global' })
await searchinput.updateComplete
const button = searchinput.querySelector('.pkt-searchinput__button') as HTMLButtonElement
expect(button.classList.contains('pkt-btn--primary')).toBe(true)
expect(button.classList.contains('pkt-btn--yellow')).toBe(true)
expect(button.classList.contains('pkt-input-icon')).toBe(false)
})
})
describe('suggestions', () => {
test('renders from suggestions property without mutating the array', async () => {
const suggestions = [
{ title: 'A', text: 'tekst', href: 'https://oslo.kommune.no' },
{ title: 'B', text: 'tekst 2' },
]
const snapshot = JSON.stringify(suggestions)
const { searchinput } = await createSearchInputTest()
searchinput.suggestions = suggestions
await searchinput.updateComplete
expect(searchinput.querySelectorAll('.pkt-searchinput__suggestion')).toHaveLength(2)
expect(JSON.stringify(suggestions)).toBe(snapshot)
})
test('suggestion with href renders as link', async () => {
const { searchinput } = await createSearchInputTest()
searchinput.suggestions = [{ title: 'Lenke', href: 'https://oslo.kommune.no' }]
await searchinput.updateComplete
const link = searchinput.querySelector('a.pkt-searchinput__suggestion')
expect(link).toBeTruthy()
expect(link?.getAttribute('href')).toBe('https://oslo.kommune.no')
})
test('suggestion without href renders as button type="button"', async () => {
const { searchinput } = await createSearchInputTest()
searchinput.suggestions = [{ title: 'Knapp', text: 'beskrivelse' }]
await searchinput.updateComplete
const btn = searchinput.querySelector('button.pkt-searchinput__suggestion')
expect(btn).toBeTruthy()
expect(btn?.getAttribute('type')).toBe('button')
})
test('nonInteractive suggestion renders as div and does not emit pkt-suggestion-click', async () => {
const suggestion = { text: 'Ingen resultater', nonInteractive: true }
const { searchinput } = await createSearchInputTest()
searchinput.suggestions = [suggestion]
await searchinput.updateComplete
expect(searchinput.querySelector('div.pkt-searchinput__suggestion')).toBeTruthy()
expect(searchinput.querySelector('button.pkt-searchinput__suggestion')).toBeNull()
const spy = vi.fn()
searchinput.addEventListener('pkt-suggestion-click', spy)
const div = searchinput.querySelector('div.pkt-searchinput__suggestion') as HTMLDivElement
fireEvent.click(div)
expect(spy).not.toHaveBeenCalled()
})
test('pkt-suggestion-click fires with detail { index, suggestion } on link click', async () => {
const suggestion = { title: 'T', href: 'https://oslo.kommune.no' }
const { searchinput } = await createSearchInputTest()
searchinput.suggestions = [suggestion]
await searchinput.updateComplete
const spy = vi.fn()
searchinput.addEventListener('pkt-suggestion-click', spy)
searchinput.addEventListener('pkt-suggestion-click', (e: Event) => e.preventDefault())
const link = searchinput.querySelector('a.pkt-searchinput__suggestion') as HTMLAnchorElement
fireEvent.click(link)
expect(spy).toHaveBeenCalledTimes(1)
const evt = spy.mock.calls[0][0] as CustomEvent
expect(evt.detail).toEqual({ index: 0, suggestion })
expect(evt.cancelable).toBe(true)
})
test('pkt-suggestion-click fires with detail { index, suggestion } on button click', async () => {
const suggestion = { title: 'X', text: 'y' }
const { searchinput } = await createSearchInputTest()
searchinput.suggestions = [suggestion]
await searchinput.updateComplete
const spy = vi.fn()
searchinput.addEventListener('pkt-suggestion-click', spy)
const btn = searchinput.querySelector('button.pkt-searchinput__suggestion') as HTMLButtonElement
fireEvent.click(btn)
expect(spy).toHaveBeenCalledTimes(1)
expect((spy.mock.calls[0][0] as CustomEvent).detail).toEqual({
index: 0,
suggestion,
})
})
test('canceling pkt-suggestion-click causes preventDefault on the anchor MouseEvent', async () => {
const { searchinput } = await createSearchInputTest()
searchinput.suggestions = [{ title: 'Resultat', href: 'https://oslo.kommune.no' }]
await searchinput.updateComplete
searchinput.addEventListener('pkt-suggestion-click', (e: Event) => e.preventDefault())
const link = searchinput.querySelector('a.pkt-searchinput__suggestion') as HTMLAnchorElement
let seen: MouseEvent | undefined
link.addEventListener('click', (e: MouseEvent) => {
seen = e
})
fireEvent.click(link)
expect(seen).toBeDefined()
expect(seen!.defaultPrevented).toBe(true)
})
})
describe('accessibility', () => {
test('has no axe violations with label and suggestions', async () => {
const { searchinput } = await createSearchInputTest({
label: 'Søk etter noe',
})
searchinput.suggestions = [{ title: 'Resultat', text: 'Eksempeltekst' }]
await searchinput.updateComplete
const results = await axe(searchinput)
expect(results).toHaveNoViolations()
})
})
})