@oslokommune/punkt-elements
Version:
Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo
454 lines (405 loc) • 16 kB
text/typescript
import '@testing-library/jest-dom'
import { axe, toHaveNoViolations } from 'jest-axe'
import { fireEvent } from '@testing-library/dom'
import { vi } from 'vitest'
import { createElementTest, BaseTestConfig } from '../../tests/test-framework'
import { CustomElementFor } from '../../tests/component-registry'
import './timepicker'
export interface TimepickerTestConfig extends BaseTestConfig {
value?: string
min?: string
max?: string
step?: number
disabled?: boolean
required?: boolean
label?: string
'hide-picker'?: boolean
'step-arrows'?: boolean
fullwidth?: boolean
hasError?: boolean
errorMessage?: string
helptext?: string
id?: string
name?: string
}
export const createTimepickerTest = async (config: TimepickerTestConfig = {}) => {
const { container, element } = await createElementTest<
CustomElementFor<'pkt-timepicker'>,
TimepickerTestConfig
>('pkt-timepicker', config)
return {
container,
timepicker: element,
}
}
expect.extend(toHaveNoViolations)
afterEach(() => {
document.body.innerHTML = ''
})
describe('PktTimepicker', () => {
describe('Rendering', () => {
test('renders without errors', async () => {
const { timepicker } = await createTimepickerTest({ label: 'Tidspunkt', id: 'test' })
expect(timepicker).toBeInTheDocument()
})
test('renders hours and minutes inputs', async () => {
const { timepicker } = await createTimepickerTest({ label: 'Tidspunkt', id: 'test' })
const hoursInput = timepicker.querySelector('#test-hours')
const minutesInput = timepicker.querySelector('#test-minutes')
expect(hoursInput).toBeInTheDocument()
expect(minutesInput).toBeInTheDocument()
})
test('renders clock button by default', async () => {
const { timepicker } = await createTimepickerTest({ label: 'Tidspunkt', id: 'test' })
const button = timepicker.querySelector('.pkt-timepicker__button')
expect(button).toBeInTheDocument()
})
test('does not render clock button when hide-picker', async () => {
const { timepicker } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test',
'hide-picker': true,
})
const button = timepicker.querySelector('.pkt-timepicker__button')
expect(button).not.toBeInTheDocument()
})
test('renders decorative icon when hide-picker', async () => {
const { timepicker } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test',
'hide-picker': true,
})
const icon = timepicker.querySelector('.pkt-timepicker__icon')
expect(icon).toBeInTheDocument()
})
test('renders prev/next buttons when step-arrows', async () => {
const { timepicker } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test',
'step-arrows': true,
})
const prev = timepicker.querySelector('.pkt-timepicker__button--prev')
const next = timepicker.querySelector('.pkt-timepicker__button--next')
expect(prev).toBeInTheDocument()
expect(next).toBeInTheDocument()
})
test('does not render popup when hide-picker', async () => {
const { timepicker } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test',
'hide-picker': true,
})
const popup = timepicker.querySelector('.pkt-timepicker-popup')
expect(popup).not.toBeInTheDocument()
})
test('does not render popup when step-arrows', async () => {
const { timepicker } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test',
'step-arrows': true,
})
const popup = timepicker.querySelector('.pkt-timepicker-popup')
expect(popup).not.toBeInTheDocument()
})
test('renders hidden popup by default (not open)', async () => {
const { timepicker } = await createTimepickerTest({ label: 'Tidspunkt', id: 'test' })
const popup = timepicker.querySelector('.pkt-timepicker-popup')
expect(popup).toBeInTheDocument()
expect(popup).toHaveAttribute('hidden')
})
})
describe('Properties', () => {
test('value sets display inputs correctly', async () => {
const { timepicker } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test',
value: '09:30',
})
const hoursInput = timepicker.querySelector<HTMLInputElement>('#test-hours')
const minutesInput = timepicker.querySelector<HTMLInputElement>('#test-minutes')
expect(hoursInput?.value).toBe('09')
expect(minutesInput?.value).toBe('30')
})
test('empty value renders empty display inputs', async () => {
const { timepicker } = await createTimepickerTest({ label: 'Tidspunkt', id: 'test' })
const hoursInput = timepicker.querySelector<HTMLInputElement>('#test-hours')
const minutesInput = timepicker.querySelector<HTMLInputElement>('#test-minutes')
expect(hoursInput?.value).toBe('')
expect(minutesInput?.value).toBe('')
})
test('disabled disables all inputs', async () => {
const { timepicker } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test',
disabled: true,
})
const hoursInput = timepicker.querySelector<HTMLInputElement>('#test-hours')
const minutesInput = timepicker.querySelector<HTMLInputElement>('#test-minutes')
expect(hoursInput).toBeDisabled()
expect(minutesInput).toBeDisabled()
})
test('disabled disables the clock button', async () => {
const { timepicker } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test',
disabled: true,
})
const button = timepicker.querySelector<HTMLButtonElement>('.pkt-timepicker__button')
expect(button).toBeDisabled()
})
test('step-arrows adds pkt-timepicker--stepper class to host', async () => {
const { timepicker } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test',
'step-arrows': true,
})
expect(timepicker.classList.contains('pkt-timepicker--stepper')).toBe(true)
})
test('fullwidth adds pkt-timepicker--fullwidth class to host', async () => {
const { timepicker } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test',
fullwidth: true,
})
expect(timepicker.classList.contains('pkt-timepicker--fullwidth')).toBe(true)
})
test('min/max/step forwarded to hidden input', async () => {
const { timepicker } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test',
min: '08:00',
max: '17:00',
step: 300,
})
const hiddenInput = timepicker.querySelector<HTMLInputElement>('#test-input')
expect(hiddenInput?.getAttribute('min')).toBe('08:00')
expect(hiddenInput?.getAttribute('max')).toBe('17:00')
expect(hiddenInput?.getAttribute('step')).toBe('300')
})
})
describe('Value management', () => {
test('external value change updates display', async () => {
const { timepicker } = await createTimepickerTest({ label: 'Tidspunkt', id: 'test' })
timepicker.value = '15:45'
await timepicker.updateComplete
const hoursInput = timepicker.querySelector<HTMLInputElement>('#test-hours')
const minutesInput = timepicker.querySelector<HTMLInputElement>('#test-minutes')
expect(hoursInput?.value).toBe('15')
expect(minutesInput?.value).toBe('45')
})
test('invalid value format clears display', async () => {
const { timepicker } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test',
value: '09:30',
})
timepicker.value = 'invalid'
await timepicker.updateComplete
const hoursInput = timepicker.querySelector<HTMLInputElement>('#test-hours')
expect(hoursInput?.value).toBe('')
})
test('hidden input reflects value', async () => {
const { timepicker } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test',
value: '09:30',
})
const hiddenInput = timepicker.querySelector<HTMLInputElement>('#test-input')
expect(hiddenInput?.value).toBe('09:30')
})
})
describe('Events', () => {
test('change fires when value changes via ArrowUp', async () => {
const { timepicker } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test',
value: '09:30',
})
const changeHandler = vi.fn()
timepicker.addEventListener('change', changeHandler)
timepicker.touched = true
const hoursInput = timepicker.querySelector<HTMLInputElement>('#test-hours')!
fireEvent.keyDown(hoursInput, { key: 'ArrowUp' })
await timepicker.updateComplete
expect(changeHandler).toHaveBeenCalled()
})
test('value-change fires with HH:MM detail', async () => {
const { timepicker } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test',
value: '09:30',
})
const valueChangeHandler = vi.fn()
timepicker.addEventListener('value-change', valueChangeHandler)
timepicker.touched = true
const hoursInput = timepicker.querySelector<HTMLInputElement>('#test-hours')!
fireEvent.keyDown(hoursInput, { key: 'ArrowUp' })
await timepicker.updateComplete
expect(valueChangeHandler).toHaveBeenCalledWith(
expect.objectContaining({ detail: '10:30' }),
)
})
})
describe('Popup', () => {
test('clock button opens popup', async () => {
const { timepicker } = await createTimepickerTest({ label: 'Tidspunkt', id: 'test' })
const button = timepicker.querySelector<HTMLButtonElement>('.pkt-timepicker__button')!
fireEvent.click(button)
await timepicker.updateComplete
const popup = timepicker.querySelector('.pkt-timepicker-popup')
expect(popup).not.toHaveAttribute('hidden')
})
test('clock button toggles popup closed', async () => {
const { timepicker } = await createTimepickerTest({ label: 'Tidspunkt', id: 'test' })
const button = timepicker.querySelector<HTMLButtonElement>('.pkt-timepicker__button')!
fireEvent.click(button)
await timepicker.updateComplete
fireEvent.click(button)
await timepicker.updateComplete
const popup = timepicker.querySelector('.pkt-timepicker-popup')
expect(popup).toHaveAttribute('hidden')
})
test('popup renders hour and minute columns', async () => {
const { timepicker } = await createTimepickerTest({ label: 'Tidspunkt', id: 'test' })
const button = timepicker.querySelector<HTMLButtonElement>('.pkt-timepicker__button')!
fireEvent.click(button)
await timepicker.updateComplete
const cols = timepicker.querySelectorAll('.pkt-timepicker-popup__col')
expect(cols).toHaveLength(2)
})
test('selecting hour option updates hours display', async () => {
const { timepicker } = await createTimepickerTest({ label: 'Tidspunkt', id: 'test' })
timepicker.touched = true
const button = timepicker.querySelector<HTMLButtonElement>('.pkt-timepicker__button')!
fireEvent.click(button)
await timepicker.updateComplete
const option = timepicker.querySelector<HTMLButtonElement>(
'[data-type="hour"][data-value="9"]',
)!
fireEvent.click(option)
await timepicker.updateComplete
const hoursInput = timepicker.querySelector<HTMLInputElement>('#test-hours')
expect(hoursInput?.value).toBe('09')
})
})
describe('Stepper', () => {
test('next button increments by step', async () => {
const { timepicker } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test',
'step-arrows': true,
value: '09:00',
})
timepicker.touched = true
const next = timepicker.querySelector<HTMLButtonElement>('.pkt-timepicker__button--next')!
fireEvent.click(next)
await timepicker.updateComplete
expect(timepicker.value).toBe('09:01')
})
test('prev button decrements by step', async () => {
const { timepicker } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test',
'step-arrows': true,
value: '09:05',
step: 300,
})
timepicker.touched = true
const prev = timepicker.querySelector<HTMLButtonElement>('.pkt-timepicker__button--prev')!
fireEvent.click(prev)
await timepicker.updateComplete
expect(timepicker.value).toBe('09:00')
})
test('next button at 59 minutes rolls over to next hour', async () => {
const { timepicker } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test',
'step-arrows': true,
value: '09:59',
})
timepicker.touched = true
const next = timepicker.querySelector<HTMLButtonElement>('.pkt-timepicker__button--next')!
fireEvent.click(next)
await timepicker.updateComplete
expect(timepicker.value).toBe('10:00')
})
})
describe('Form integration', () => {
test('required with empty value fails validation', async () => {
const { timepicker } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test',
name: 'tidspunkt',
required: true,
})
expect(timepicker.internals.validity.valid).toBe(false)
})
test('form reset clears value', async () => {
const container = document.createElement('div')
container.innerHTML = `
<form>
<pkt-timepicker id="test" label="Tidspunkt" name="tidspunkt" value="09:30"></pkt-timepicker>
</form>
`
document.body.appendChild(container)
await customElements.whenDefined('pkt-timepicker')
const timepicker = container.querySelector('pkt-timepicker') as CustomElementFor<'pkt-timepicker'>
await timepicker.updateComplete
const form = container.querySelector('form')!
form.reset()
await timepicker.updateComplete
const hoursInput = timepicker.querySelector<HTMLInputElement>('#test-hours')
expect(hoursInput?.value).toBe('')
})
})
describe('Accessibility', () => {
test('passes axe in default state', async () => {
const { container } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test-axe',
})
const results = await axe(container)
expect(results).toHaveNoViolations()
})
test('passes axe with step-arrows', async () => {
const { container } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test-axe-stepper',
'step-arrows': true,
})
const results = await axe(container)
expect(results).toHaveNoViolations()
})
test('passes axe when disabled', async () => {
const { container } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test-axe-disabled',
disabled: true,
})
const results = await axe(container)
expect(results).toHaveNoViolations()
})
test('passes axe with hasError and errorMessage', async () => {
const { container } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test-axe-error',
hasError: true,
errorMessage: 'Ugyldig tidspunkt',
})
const results = await axe(container)
expect(results).toHaveNoViolations()
})
test('hours input has correct spinbutton ARIA', async () => {
const { timepicker } = await createTimepickerTest({
label: 'Tidspunkt',
id: 'test',
value: '09:30',
})
const hoursInput = timepicker.querySelector<HTMLInputElement>('#test-hours')!
expect(hoursInput.getAttribute('role')).toBe('spinbutton')
expect(hoursInput.getAttribute('aria-valuemin')).toBe('0')
expect(hoursInput.getAttribute('aria-valuemax')).toBe('23')
expect(hoursInput.getAttribute('aria-valuenow')).toBe('09')
})
})
})