UNPKG

@oslokommune/punkt-elements

Version:

Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo

454 lines (405 loc) 16 kB
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') }) }) })