@oslokommune/punkt-elements
Version:
Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo
699 lines (591 loc) • 24.8 kB
text/typescript
import { describe, it, expect, vi } from 'vitest'
import {
valueUtils,
inputTypeUtils,
formUtils,
calendarUtils,
eventUtils,
cssUtils,
dateProcessingUtils,
keyboardUtils,
} from './datepicker-utils'
import { createRef } from 'lit/directives/ref.js'
const mockIsIOS = vi.hoisted(() => vi.fn(() => false))
vi.mock('shared-utils/device-utils', () => ({
isIOS: mockIsIOS,
}))
describe('datepicker-utils', () => {
describe('valueUtils', () => {
describe('validateRangeOrder', () => {
it('should return true for incomplete ranges', () => {
expect(valueUtils.validateRangeOrder([])).toBe(true)
expect(valueUtils.validateRangeOrder(['2024-01-01'])).toBe(true)
})
it('should return true for valid date ranges', () => {
expect(valueUtils.validateRangeOrder(['2024-01-01', '2024-01-31'])).toBe(true)
expect(valueUtils.validateRangeOrder(['2024-01-01', '2024-01-01'])).toBe(true)
})
it('should return false for invalid date ranges', () => {
expect(valueUtils.validateRangeOrder(['2024-01-31', '2024-01-01'])).toBe(false)
})
})
describe('sortDates', () => {
it('should sort dates chronologically', () => {
const dates = ['2024-03-15', '2024-01-10', '2024-02-20']
const sorted = valueUtils.sortDates(dates)
expect(sorted).toEqual(['2024-01-10', '2024-02-20', '2024-03-15'])
})
it('should handle empty array', () => {
expect(valueUtils.sortDates([])).toEqual([])
})
})
describe('filterSelectableDates', () => {
it('should filter dates outside min/max range', () => {
const dates = ['2024-01-01', '2024-01-15', '2024-01-31']
const filtered = valueUtils.filterSelectableDates(dates, '2024-01-10', '2024-01-20')
expect(filtered).toEqual(['2024-01-15'])
})
it('should filter excluded dates', () => {
const dates = ['2024-01-01', '2024-01-02', '2024-01-03']
const filtered = valueUtils.filterSelectableDates(
dates,
null,
null,
['2024-01-02'],
undefined,
)
expect(filtered).toEqual(['2024-01-01', '2024-01-03'])
})
it('should filter excluded weekdays', () => {
const dates = ['2024-01-01', '2024-01-06', '2024-01-07'] // Mon, Sat, Sun
const filtered = valueUtils.filterSelectableDates(dates, null, null, undefined, ['0', '6']) // Exclude weekends
expect(filtered).toEqual(['2024-01-01'])
})
})
})
describe('inputTypeUtils', () => {
describe('getInputType', () => {
it('should return "text" for iOS devices', () => {
mockIsIOS.mockReturnValue(true)
const type = inputTypeUtils.getInputType()
expect(type).toBe('text')
})
it('should return "date" for non-iOS devices', () => {
mockIsIOS.mockReturnValue(false)
const type = inputTypeUtils.getInputType()
expect(type).toBe('date')
})
})
})
describe('formUtils', () => {
describe('submitForm', () => {
it('should call requestSubmit on the form if available', () => {
const mockForm = { requestSubmit: vi.fn() }
const element = {
internals: { form: mockForm },
} as any
formUtils.submitForm(element)
expect(mockForm.requestSubmit).toHaveBeenCalledOnce()
})
it('should not throw if form is not available', () => {
const element = {} as any
expect(() => formUtils.submitForm(element)).not.toThrow()
})
})
describe('submitFormOrFallback', () => {
it('should submit form if internals has form', () => {
const mockForm = { requestSubmit: vi.fn() }
const fallback = vi.fn()
const internals = { form: mockForm }
formUtils.submitFormOrFallback(internals, fallback)
expect(mockForm.requestSubmit).toHaveBeenCalledOnce()
expect(fallback).not.toHaveBeenCalled()
})
it('should call fallback if no form available', () => {
const fallback = vi.fn()
formUtils.submitFormOrFallback(null, fallback)
expect(fallback).toHaveBeenCalledOnce()
})
it('should call fallback if internals has no form', () => {
const fallback = vi.fn()
const internals = {}
formUtils.submitFormOrFallback(internals, fallback)
expect(fallback).toHaveBeenCalledOnce()
})
})
describe('validateDateInput', () => {
it('should return early if input has no value', () => {
const input = { value: '' } as HTMLInputElement
const internals = { setValidity: vi.fn() }
formUtils.validateDateInput(input, internals)
expect(internals.setValidity).not.toHaveBeenCalled()
})
it('should set rangeUnderflow validity when value is below minimum', () => {
const input = { value: '2024-01-01' } as HTMLInputElement
const internals = { setValidity: vi.fn() }
const strings = { forms: { messages: { rangeUnderflow: 'Too early' } } }
formUtils.validateDateInput(input, internals, '2024-01-10', null, strings)
expect(internals.setValidity).toHaveBeenCalledWith(
{ rangeUnderflow: true },
'Too early',
input,
)
})
it('should set rangeOverflow validity when value is above maximum', () => {
const input = { value: '2024-01-31' } as HTMLInputElement
const internals = { setValidity: vi.fn() }
const strings = { forms: { messages: { rangeOverflow: 'Too late' } } }
formUtils.validateDateInput(input, internals, null, '2024-01-20', strings)
expect(internals.setValidity).toHaveBeenCalledWith(
{ rangeOverflow: true },
'Too late',
input,
)
})
it('should use default messages when strings not provided', () => {
const input = { value: '2024-01-01' } as HTMLInputElement
const internals = { setValidity: vi.fn() }
formUtils.validateDateInput(input, internals, '2024-01-10', null)
expect(internals.setValidity).toHaveBeenCalledWith(
{ rangeUnderflow: true },
'Value is below minimum',
input,
)
})
})
})
describe('calendarUtils', () => {
describe('addToSelected', () => {
it('should return early if target has no value', () => {
const event = { target: { value: '' } } as any
const calendarRef = createRef()
calendarUtils.addToSelected(event, calendarRef as any)
// Should not throw
})
it('should clear input value after processing', () => {
const mockCalendar = { handleDateSelect: vi.fn() }
const target = { value: '2024-01-15' }
const event = { target } as any
const calendarRef = { value: mockCalendar } as any
calendarUtils.addToSelected(event, calendarRef)
expect(target.value).toBe('')
})
it('should call handleDateSelect with valid date', () => {
const mockCalendar = { handleDateSelect: vi.fn() }
const target = { value: '2024-01-15' }
const event = { target } as any
const calendarRef = { value: mockCalendar } as any
calendarUtils.addToSelected(event, calendarRef)
expect(mockCalendar.handleDateSelect).toHaveBeenCalled()
})
it('should respect min/max constraints', () => {
const mockCalendar = { handleDateSelect: vi.fn() }
const target = { value: '2024-01-05' }
const event = { target } as any
const calendarRef = { value: mockCalendar } as any
calendarUtils.addToSelected(event, calendarRef, '2024-01-10', '2024-01-20')
expect(mockCalendar.handleDateSelect).not.toHaveBeenCalled()
expect(target.value).toBe('')
})
})
describe('handleCalendarPosition', () => {
it('should return early if refs are not available', () => {
const popupRef = createRef()
const inputRef = createRef()
expect(() =>
calendarUtils.handleCalendarPosition(popupRef as any, inputRef as any),
).not.toThrow()
})
it('should position calendar below input by default', () => {
const mockPopup = { style: { top: '' }, getBoundingClientRect: () => ({ height: 300 }) }
const mockInput = {
getBoundingClientRect: () => ({ height: 40, top: 100 }),
parentElement: null,
}
const popupRef = { value: mockPopup } as any
const inputRef = { value: mockInput } as any
calendarUtils.handleCalendarPosition(popupRef, inputRef)
expect(mockPopup.style.top).toBe('100%')
})
it('should position calendar above input if not enough space below', () => {
const mockPopup = { style: { top: '' }, getBoundingClientRect: () => ({ height: 400 }) }
const mockInput = {
getBoundingClientRect: () => ({ height: 40, top: window.innerHeight - 100 }),
parentElement: null,
}
const popupRef = { value: mockPopup } as any
const inputRef = { value: mockInput } as any
calendarUtils.handleCalendarPosition(popupRef, inputRef)
expect(mockPopup.style.top).toContain('calc(100%')
expect(mockPopup.style.top).toContain('px')
})
it('should account for counter when hasCounter is true', () => {
const mockPopup = { style: { top: '' }, getBoundingClientRect: () => ({ height: 300 }) }
const mockInput = {
getBoundingClientRect: () => ({ height: 40, top: 100 }),
parentElement: null,
}
const popupRef = { value: mockPopup } as any
const inputRef = { value: mockInput } as any
calendarUtils.handleCalendarPosition(popupRef, inputRef, true)
expect(mockPopup.style.top).toBe('calc(100% - 30px)')
})
})
})
describe('eventUtils', () => {
describe('createDocumentClickListener', () => {
it('should return a function', () => {
const listener = eventUtils.createDocumentClickListener(
createRef() as any,
null,
createRef() as any,
() => true,
vi.fn(),
vi.fn(),
)
expect(typeof listener).toBe('function')
})
it('should call onBlur and hideCalendar when clicking outside', () => {
const onBlur = vi.fn()
const hideCalendar = vi.fn()
const inputRef = { value: { contains: () => false } } as any
const btnRef = { value: { contains: () => false } } as any
const listener = eventUtils.createDocumentClickListener(
inputRef,
null,
btnRef,
() => true,
onBlur,
hideCalendar,
)
const event = {
target: document.createElement('div'),
} as any
listener(event)
expect(onBlur).toHaveBeenCalledOnce()
expect(hideCalendar).toHaveBeenCalledOnce()
})
it('should not call handlers when clicking inside input', () => {
const onBlur = vi.fn()
const hideCalendar = vi.fn()
const inputRef = { value: { contains: () => true } } as any
const btnRef = { value: { contains: () => false } } as any
const listener = eventUtils.createDocumentClickListener(
inputRef,
null,
btnRef,
() => true,
onBlur,
hideCalendar,
)
const event = {
target: document.createElement('div'),
} as any
listener(event)
expect(onBlur).not.toHaveBeenCalled()
expect(hideCalendar).not.toHaveBeenCalled()
})
})
describe('createDocumentKeydownListener', () => {
it('should return a function', () => {
const listener = eventUtils.createDocumentKeydownListener(() => true, vi.fn())
expect(typeof listener).toBe('function')
})
it('should call hideCalendar on Escape key', () => {
const hideCalendar = vi.fn()
const listener = eventUtils.createDocumentKeydownListener(() => true, hideCalendar)
const event = { key: 'Escape' } as KeyboardEvent
listener(event)
expect(hideCalendar).toHaveBeenCalledOnce()
})
it('should not call hideCalendar if calendar is not open', () => {
const hideCalendar = vi.fn()
const listener = eventUtils.createDocumentKeydownListener(() => false, hideCalendar)
const event = { key: 'Escape' } as KeyboardEvent
listener(event)
expect(hideCalendar).not.toHaveBeenCalled()
})
it('should not call hideCalendar on other keys', () => {
const hideCalendar = vi.fn()
const listener = eventUtils.createDocumentKeydownListener(() => true, hideCalendar)
const event = { key: 'Enter' } as KeyboardEvent
listener(event)
expect(hideCalendar).not.toHaveBeenCalled()
})
})
describe('handleFocusOut', () => {
it('should call onBlur and hideCalendar when focus leaves element', () => {
const onBlur = vi.fn()
const hideCalendar = vi.fn()
const element = { contains: () => false } as any
const event = { target: document.createElement('div') } as any
eventUtils.handleFocusOut(event, element, onBlur, hideCalendar)
expect(onBlur).toHaveBeenCalledOnce()
expect(hideCalendar).toHaveBeenCalledOnce()
})
it('should not call handlers when focus stays within element', () => {
const onBlur = vi.fn()
const hideCalendar = vi.fn()
const element = { contains: () => true } as any
const event = { target: document.createElement('div') } as any
eventUtils.handleFocusOut(event, element, onBlur, hideCalendar)
expect(onBlur).not.toHaveBeenCalled()
expect(hideCalendar).not.toHaveBeenCalled()
})
})
})
describe('cssUtils', () => {
describe('getInputClasses', () => {
it('should return base classes', () => {
const classes = cssUtils.getInputClasses(false, false, false, false)
expect(classes['pkt-input']).toBe(true)
expect(classes['pkt-datepicker__input']).toBe(true)
})
it('should include fullwidth class when fullwidth is true', () => {
const classes = cssUtils.getInputClasses(true, false, false, false)
expect(classes['pkt-input--fullwidth']).toBe(true)
})
it('should include hasrangelabels class when showRangeLabels is true', () => {
const classes = cssUtils.getInputClasses(false, true, false, false)
expect(classes['pkt-datepicker--hasrangelabels']).toBe(true)
})
it('should include multiple class when multiple is true', () => {
const classes = cssUtils.getInputClasses(false, false, true, false)
expect(classes['pkt-datepicker--multiple']).toBe(true)
})
it('should include range class when range is true', () => {
const classes = cssUtils.getInputClasses(false, false, false, true)
expect(classes['pkt-datepicker--range']).toBe(true)
})
it('should include ios-readonly-hack when readonly is false and inputType is text', () => {
const classes = cssUtils.getInputClasses(false, false, false, false, false, 'text')
expect(classes['ios-readonly-hack']).toBe(true)
})
})
describe('getButtonClasses', () => {
it('should return button classes', () => {
const classes = cssUtils.getButtonClasses()
expect(classes['pkt-input-icon']).toBe(true)
expect(classes['pkt-btn']).toBe(true)
expect(classes['pkt-btn--icon-only']).toBe(true)
expect(classes['pkt-btn--tertiary']).toBe(true)
expect(classes['pkt-datepicker__calendar-button']).toBe(true)
})
})
describe('getRangeLabelClasses', () => {
it('should return correct classes when showRangeLabels is true', () => {
const classes = cssUtils.getRangeLabelClasses(true)
expect(classes['pkt-input-prefix']).toBe(true)
expect(classes['pkt-hide']).toBe(false)
})
it('should return correct classes when showRangeLabels is false', () => {
const classes = cssUtils.getRangeLabelClasses(false)
expect(classes['pkt-input-prefix']).toBe(false)
expect(classes['pkt-hide']).toBe(true)
})
})
})
describe('dateProcessingUtils', () => {
describe('processDateSelection', () => {
it('should return first date for single selection', () => {
const result = dateProcessingUtils.processDateSelection(['2024-01-15'], false, false)
expect(result).toBe('2024-01-15')
})
it('should return empty string when no dates for single selection', () => {
const result = dateProcessingUtils.processDateSelection([], false, false)
expect(result).toBe('')
})
it('should return comma-separated dates for multiple selection', () => {
const result = dateProcessingUtils.processDateSelection(
['2024-01-15', '2024-01-20'],
true,
false,
)
expect(result).toBe('2024-01-15,2024-01-20')
})
it('should return comma-separated dates for range selection', () => {
const result = dateProcessingUtils.processDateSelection(
['2024-01-15', '2024-01-20'],
false,
true,
)
expect(result).toBe('2024-01-15,2024-01-20')
})
})
describe('updateInputValues', () => {
it('should return early if inputRef has no value', () => {
const inputRef = { value: null } as any
expect(() =>
dateProcessingUtils.updateInputValues(inputRef, null, [], false, false, vi.fn()),
).not.toThrow()
})
it('should update both inputs for range', () => {
const input = { value: '' } as any
const inputTo = { value: '' } as any
const inputRef = { value: input } as any
const inputRefTo = { value: inputTo } as any
const manageValidity = vi.fn()
dateProcessingUtils.updateInputValues(
inputRef,
inputRefTo,
['2024-01-15', '2024-01-20'],
true,
false,
manageValidity,
)
expect(input.value).toBe('2024-01-15')
expect(inputTo.value).toBe('2024-01-20')
expect(manageValidity).toHaveBeenCalledTimes(2)
})
it('should update single input for non-multiple, non-range', () => {
const input = { value: '' } as any
const inputRef = { value: input } as any
const manageValidity = vi.fn()
dateProcessingUtils.updateInputValues(
inputRef,
null,
['2024-01-15'],
false,
false,
manageValidity,
)
expect(input.value).toBe('2024-01-15')
expect(manageValidity).toHaveBeenCalledOnce()
})
it('should not update input for multiple selection', () => {
const input = { value: 'initial' } as any
const inputRef = { value: input } as any
const manageValidity = vi.fn()
dateProcessingUtils.updateInputValues(
inputRef,
null,
['2024-01-15'],
false,
true,
manageValidity,
)
expect(input.value).toBe('initial')
expect(manageValidity).not.toHaveBeenCalled()
})
})
describe('processRangeBlur', () => {
it('should call manageValidity and handleDateSelect when target has value', () => {
const target = { value: '2024-01-15' } as any
const event = { target } as any
const mockCalendar = { handleDateSelect: vi.fn() }
const calendarRef = { value: mockCalendar } as any
const clearInputValue = vi.fn()
const manageValidity = vi.fn()
dateProcessingUtils.processRangeBlur(
event,
['2024-01-10', '2024-01-20'],
calendarRef,
clearInputValue,
manageValidity,
)
expect(manageValidity).toHaveBeenCalledWith(target)
expect(clearInputValue).not.toHaveBeenCalled()
expect(mockCalendar.handleDateSelect).toHaveBeenCalled()
})
it('should clear input value when target is empty but values[0] exists', () => {
const target = { value: '' } as any
const event = { target } as any
const mockCalendar = { handleDateSelect: vi.fn() }
const calendarRef = { value: mockCalendar } as any
const clearInputValue = vi.fn()
const manageValidity = vi.fn()
dateProcessingUtils.processRangeBlur(
event,
['2024-01-10'],
calendarRef,
clearInputValue,
manageValidity,
)
expect(clearInputValue).toHaveBeenCalled()
expect(manageValidity).not.toHaveBeenCalled()
expect(mockCalendar.handleDateSelect).not.toHaveBeenCalled()
})
})
})
describe('keyboardUtils', () => {
describe('handleInputKeydown', () => {
it('should call toggleCalendar on Space key', () => {
const toggleCalendar = vi.fn()
const event = { key: ' ', preventDefault: vi.fn() } as any
keyboardUtils.handleInputKeydown(event, toggleCalendar)
expect(event.preventDefault).toHaveBeenCalled()
expect(toggleCalendar).toHaveBeenCalledWith(event)
})
it('should call submitForm on Enter key when provided', () => {
const toggleCalendar = vi.fn()
const submitForm = vi.fn()
const event = { key: 'Enter', preventDefault: vi.fn() } as any
keyboardUtils.handleInputKeydown(event, toggleCalendar, submitForm)
expect(event.preventDefault).toHaveBeenCalled()
expect(submitForm).toHaveBeenCalled()
expect(toggleCalendar).not.toHaveBeenCalled()
})
it('should call focusNextInput on Enter when submitForm not provided', () => {
const toggleCalendar = vi.fn()
const focusNextInput = vi.fn()
const event = { key: 'Enter', preventDefault: vi.fn() } as any
keyboardUtils.handleInputKeydown(event, toggleCalendar, undefined, focusNextInput)
expect(event.preventDefault).toHaveBeenCalled()
expect(focusNextInput).toHaveBeenCalled()
})
it('should call blurInput on Enter when neither submitForm nor focusNextInput provided', () => {
const toggleCalendar = vi.fn()
const blurInput = vi.fn()
const event = { key: 'Enter', preventDefault: vi.fn() } as any
keyboardUtils.handleInputKeydown(event, toggleCalendar, undefined, undefined, blurInput)
expect(event.preventDefault).toHaveBeenCalled()
expect(blurInput).toHaveBeenCalled()
})
it('should call commaHandler on comma key when provided', () => {
const toggleCalendar = vi.fn()
const commaHandler = vi.fn()
const event = { key: ',', preventDefault: vi.fn() } as any
keyboardUtils.handleInputKeydown(
event,
toggleCalendar,
undefined,
undefined,
undefined,
commaHandler,
)
expect(event.preventDefault).toHaveBeenCalled()
expect(commaHandler).toHaveBeenCalledWith(event)
})
it('should call blurInput on comma key when commaHandler not provided', () => {
const toggleCalendar = vi.fn()
const blurInput = vi.fn()
const event = { key: ',', preventDefault: vi.fn() } as any
keyboardUtils.handleInputKeydown(event, toggleCalendar, undefined, undefined, blurInput)
expect(event.preventDefault).toHaveBeenCalled()
expect(blurInput).toHaveBeenCalled()
})
})
describe('handleButtonKeydown', () => {
it('should call toggleCalendar on Enter key', () => {
const toggleCalendar = vi.fn()
const event = { key: 'Enter', preventDefault: vi.fn() } as any
keyboardUtils.handleButtonKeydown(event, toggleCalendar)
expect(event.preventDefault).toHaveBeenCalled()
expect(toggleCalendar).toHaveBeenCalledWith(event)
})
it('should call toggleCalendar on Space key', () => {
const toggleCalendar = vi.fn()
const event = { key: ' ', preventDefault: vi.fn() } as any
keyboardUtils.handleButtonKeydown(event, toggleCalendar)
expect(event.preventDefault).toHaveBeenCalled()
expect(toggleCalendar).toHaveBeenCalledWith(event)
})
it('should not call toggleCalendar on other keys', () => {
const toggleCalendar = vi.fn()
const event = { key: 'a', preventDefault: vi.fn() } as any
keyboardUtils.handleButtonKeydown(event, toggleCalendar)
expect(toggleCalendar).not.toHaveBeenCalled()
})
})
})
})