UNPKG

@oslokommune/punkt-elements

Version:

Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo

699 lines (591 loc) 24.8 kB
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() }) }) }) })