diff --git a/js/src/util/time.js b/js/src/util/time.js index 758f7c889..080babe15 100644 --- a/js/src/util/time.js +++ b/js/src/util/time.js @@ -189,5 +189,5 @@ export const isAmPm = locale => */ export const isValidTime = time => { const d = new Date(`1970-01-01 ${time}`) - return d instanceof Date && d.getTime() + return d instanceof Date && !Number.isNaN(d.getTime()) } diff --git a/js/tests/unit/range-slider.spec.js b/js/tests/unit/range-slider.spec.js index 0af785f50..89f0a38f9 100644 --- a/js/tests/unit/range-slider.spec.js +++ b/js/tests/unit/range-slider.spec.js @@ -2,7 +2,7 @@ import EventHandler from '../../src/dom/event-handler.js' import RangeSlider from '../../src/range-slider.js' import { - clearFixture, createEvent, getFixture, jQueryMock + clearFixture, getFixture, jQueryMock } from '../helpers/fixture.js' describe('RangeSlider', () => { diff --git a/js/tests/unit/rating.spec.js b/js/tests/unit/rating.spec.js new file mode 100644 index 000000000..5b3a96074 --- /dev/null +++ b/js/tests/unit/rating.spec.js @@ -0,0 +1,363 @@ +/* eslint-env jasmine */ + +import Rating from '../../src/rating.js' +import { + getFixture, clearFixture, createEvent, jQueryMock +} from '../helpers/fixture.js' + +describe('Rating', () => { + let fixtureEl + + beforeAll(() => { + fixtureEl = getFixture() + }) + + afterEach(() => { + clearFixture() + }) + + describe('Default', () => { + it('should return plugin default config', () => { + expect(Rating.Default).toEqual(jasmine.any(Object)) + }) + }) + + describe('DefaultType', () => { + it('should return plugin default type config', () => { + expect(Rating.DefaultType).toEqual(jasmine.any(Object)) + }) + }) + + describe('NAME', () => { + it('should return plugin NAME', () => { + expect(Rating.NAME).toEqual('rating') + }) + }) + + describe('constructor', () => { + it('should create a Rating instance with default config if no config is provided', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const rating = new Rating(div) + + expect(rating).toBeInstanceOf(Rating) + expect(rating._config).toBeDefined() + expect(rating._element).toEqual(div) + }) + + it('should allow overriding default config', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const rating = new Rating(div, { + itemCount: 3, + value: 2, + readOnly: true + }) + + expect(rating._config.itemCount).toEqual(3) + expect(rating._currentValue).toEqual(2) + expect(rating._config.readOnly).toBeTrue() + }) + + it('should apply the "disabled" class when config.disabled = true', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + // for instance: disabled is set to true + const rating = new Rating(div, { disabled: true }) + expect(rating._element.classList).toContain('disabled') + }) + + it('should create the correct number of rating items', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + new Rating(div, { itemCount: 5 }) // eslint-disable-line no-new + + expect(div.querySelectorAll('.rating-item')).toHaveSize(5) + }) + + it('should set the initial checked input if "value" is provided', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + new Rating(div, { value: 2 }) // eslint-disable-line no-new + + const checkedInputs = div.querySelectorAll('.rating-item-input:checked') + expect(checkedInputs).toHaveSize(1) + expect(checkedInputs[0].value).toEqual('2') + }) + }) + + describe('update', () => { + it('should update config and re-render the rating UI', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + const rating = new Rating(div, { itemCount: 3, value: 1 }) + + const previousHTML = div.innerHTML + rating.update({ itemCount: 5, value: 3 }) + + expect(div.innerHTML).not.toEqual(previousHTML) + expect(div.querySelectorAll('.rating-item')).toHaveSize(5) + const checkedInput = div.querySelector('.rating-item-input:checked') + expect(checkedInput.value).toEqual('3') + }) + }) + + describe('reset', () => { + it('should reset the rating to the new given value', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + const rating = new Rating(div, { itemCount: 5, value: 4 }) + + rating.reset(2) + const checkedInput = div.querySelector('.rating-item-input:checked') + expect(checkedInput.value).toEqual('2') + }) + + it('should reset the rating to null if no argument is provided', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + const rating = new Rating(div, { value: 3 }) + + rating.reset() + const checkedInput = div.querySelector('.rating-item-input:checked') + expect(checkedInput).toBeNull() + }) + + it('should emit a "change.coreui.rating" event on reset', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + const rating = new Rating(div, { value: 3 }) + const listener = jasmine.createSpy('listener') + + div.addEventListener('change.coreui.rating', listener) + rating.reset() + expect(listener).toHaveBeenCalled() + }) + }) + + describe('events', () => { + it('should emit "change.coreui.rating" when a rating input is changed', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + new Rating(div, { itemCount: 3 }) // eslint-disable-line no-new + + div.addEventListener('change.coreui.rating', event => { + expect(event.value).toBe('2') + resolve() + }) + + // Simulate clicking the second radio + const input = div.querySelectorAll('.rating-item-label')[1] + input.click() + }) + }) + + it('should clear the rating if "allowClear" is true and the same value is clicked again', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + // eslint-disable-next-line no-new + new Rating(div, { + value: 2, + allowClear: true, + itemCount: 5 + }) + + // Listen for a new change event + div.addEventListener('change.coreui.rating', event => { + expect(event.value).toBeNull() + resolve() + }) + + const input2 = div.querySelectorAll('.rating-item-input')[1] // value="2" + input2.click() + }) + }) + + it('should emit "hover.coreui.rating" on mouseenter with the hovered value', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + new Rating(div, { itemCount: 3 }) // eslint-disable-line no-new + + const label = div.querySelectorAll('.rating-item-label')[1] + div.addEventListener('hover.coreui.rating', event => { + expect(event.value).toBe('2') + resolve() + }) + + const mouseover = createEvent('mouseover') + label.dispatchEvent(mouseover) + }) + }) + + it('should remove "active" class from all labels when mouse leaves, unless an input is checked', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + new Rating(div, { itemCount: 3, value: 2 }) // eslint-disable-line no-new + + const label = div.querySelectorAll('.rating-item-label')[2] + // first hover: + const mouseenter = createEvent('mouseenter') + label.dispatchEvent(mouseenter) + // all previous labels (0,1,2) active + + const mouseleave = createEvent('mouseleave') + label.dispatchEvent(mouseleave) + + // Because the rating has a checked input for value="2", items 0 & 1 should remain active + const activeLabels = div.querySelectorAll('.rating-item-label.active') + expect(activeLabels).toHaveSize(2) + }) + + it('should remove all "active" classes if no input is checked and mouse leaves', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + new Rating(div, { itemCount: 3, value: null }) // eslint-disable-line no-new + + const label = div.querySelectorAll('.rating-item-label')[1] + const mouseover = createEvent('mouseover') + label.dispatchEvent(mouseover) + + let activeLabels = div.querySelectorAll('.rating-item-label.active') + expect(activeLabels).toHaveSize(2) // items[0] and items[1] + + const mouseout = createEvent('mouseout') + label.dispatchEvent(mouseout) + + activeLabels = div.querySelectorAll('.rating-item-label.active') + expect(activeLabels).toHaveSize(0) + }) + }) + + describe('readonly & disabled', () => { + it('should not change or hover if readOnly is true', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + new Rating(div, { itemCount: 3, readOnly: true }) // eslint-disable-line no-new + + // Attempt to click on an input + const inputs = div.querySelectorAll('.rating-item-input') + inputs[1].click() + + const checkedInput = div.querySelector('.rating-item-input:checked') + expect(checkedInput).toBeNull() // Did not change + + // Attempt to trigger mouseenter + const label = div.querySelectorAll('.rating-item-label')[1] + const mouseenter = createEvent('mouseenter') + label.dispatchEvent(mouseenter) + + const activeLabels = div.querySelectorAll('.rating-item-label.active') + expect(activeLabels).toHaveSize(0) + }) + + it('should not change or hover if disabled is true', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + new Rating(div, { itemCount: 3, disabled: true }) // eslint-disable-line no-new + + // Attempt to click on an input + const inputs = div.querySelectorAll('.rating-item-input') + inputs[1].click() + + const checkedInput = div.querySelector('.rating-item-input:checked') + expect(checkedInput).toBeNull() // Did not change + }) + }) + + describe('data-api', () => { + it('should create rating elements on window load event', () => { + fixtureEl.innerHTML = ` +
+ ` + const ratingEl = fixtureEl.querySelector('#myRating') + + // Manually trigger the load event + const loadEvent = createEvent('load') + window.dispatchEvent(loadEvent) + + const ratingInstance = Rating.getInstance(ratingEl) + expect(ratingInstance).not.toBeNull() + expect(ratingInstance._config.itemCount).toEqual(4) + expect(ratingInstance._currentValue).toEqual(2) // from data attribute + }) + }) + + describe('jQueryInterface', () => { + it('should create a rating via jQueryInterface', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + + jQueryMock.fn.rating = Rating.jQueryInterface + jQueryMock.elements = [div] + jQueryMock.fn.rating.call(jQueryMock) + + expect(Rating.getInstance(div)).not.toBeNull() + }) + + it('should throw error on undefined method', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + jQueryMock.fn.rating = Rating.jQueryInterface + jQueryMock.elements = [div] + + expect(() => { + jQueryMock.fn.rating.call(jQueryMock, 'noMethod') + }).toThrowError(TypeError, 'No method named "noMethod"') + }) + }) + + describe('getInstance', () => { + it('should return rating instance', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + const rating = new Rating(div) + + expect(Rating.getInstance(div)).toEqual(rating) + expect(Rating.getInstance(div)).toBeInstanceOf(Rating) + }) + + it('should return null when there is no rating instance', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + + expect(Rating.getInstance(div)).toBeNull() + }) + }) + + describe('getOrCreateInstance', () => { + it('should return rating instance', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + const rating = new Rating(div) + + expect(Rating.getOrCreateInstance(div)).toEqual(rating) + expect(Rating.getInstance(div)).toEqual(Rating.getOrCreateInstance(div, {})) + expect(Rating.getOrCreateInstance(div)).toBeInstanceOf(Rating) + }) + + it('should return new instance when there is no rating instance', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + + expect(Rating.getInstance(div)).toBeNull() + expect(Rating.getOrCreateInstance(div)).toBeInstanceOf(Rating) + }) + + it('should return the same instance when exists, ignoring new configuration', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + const rating = new Rating(div, { itemCount: 3 }) + + const rating2 = Rating.getOrCreateInstance(div, { itemCount: 5 }) + expect(rating2).toEqual(rating) + // config should still show itemCount as 3 + expect(rating2._config.itemCount).toEqual(3) + }) + }) +}) diff --git a/js/tests/unit/time-picker.spec.js b/js/tests/unit/time-picker.spec.js new file mode 100644 index 000000000..63d069eab --- /dev/null +++ b/js/tests/unit/time-picker.spec.js @@ -0,0 +1,425 @@ +/* eslint-env jasmine */ + +import TimePicker from '../../src/time-picker.js' +import { + getFixture, clearFixture, createEvent, jQueryMock +} from '../helpers/fixture.js' + +describe('TimePicker', () => { + let fixtureEl + + beforeAll(() => { + fixtureEl = getFixture() + }) + + afterEach(() => { + clearFixture() + }) + + describe('Default', () => { + it('should return plugin default config', () => { + expect(TimePicker.Default).toEqual(jasmine.any(Object)) + }) + }) + + describe('constructor', () => { + it('should create a time picker instance with default config if no config is provided', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const tp = new TimePicker(div) + + expect(tp).toBeInstanceOf(TimePicker) + expect(tp._config).toBeDefined() + expect(tp._element).toEqual(div) + }) + + it('should set initial time if provided in config', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const timeString = '10:15:30' + const tp = new TimePicker(div, { + time: timeString + }) + + // _date is set internally, check that it was interpreted as 10:15:30 + expect(tp._date).toBeInstanceOf(Date) + expect(tp._date.getHours()).toBe(10) + expect(tp._date.getMinutes()).toBe(15) + expect(tp._date.getSeconds()).toBe(30) + }) + + it('should create input group and dropdown by default (type=dropdown)', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('#myTimePicker') + new TimePicker(div) // eslint-disable-line no-new + + // Check the structure + const inputGroup = div.querySelector('.time-picker-input-group') + expect(inputGroup).not.toBeNull() + + const dropdown = div.querySelector('.time-picker-dropdown') + expect(dropdown).not.toBeNull() + }) + + it('should set size class if config.size is given', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + new TimePicker(div, { size: 'lg' }) // eslint-disable-line no-new + + expect(div.classList.contains('time-picker-lg')).toBeTrue() + }) + + it('should add "disabled" class if config.disabled is true', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + new TimePicker(div, { disabled: true }) // eslint-disable-line no-new + + expect(div.classList.contains('disabled')).toBeTrue() + }) + + it('should add "is-valid" class if config.valid is true', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + new TimePicker(div, { valid: true }) // eslint-disable-line no-new + + expect(div.classList.contains('is-valid')).toBeTrue() + }) + + it('should add "is-invalid" class if config.invalid is true', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + new TimePicker(div, { invalid: true }) // eslint-disable-line no-new + + expect(div.classList.contains('is-invalid')).toBeTrue() + }) + }) + + describe('toggle', () => { + it('should show when toggled if not shown, and hide if shown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + const tp = new TimePicker(div) + + // Initially hidden + expect(div.classList.contains('show')).toBeFalse() + + // Listen for show event + div.addEventListener('shown.coreui.time-picker', () => { + expect(div.classList.contains('show')).toBeTrue() + + // Toggle again to hide + tp.toggle() + }) + + // Listen for hide event + div.addEventListener('hidden.coreui.time-picker', () => { + expect(div.classList.contains('show')).toBeFalse() + resolve() + }) + + // Start toggling + tp.toggle() + }) + }) + }) + + describe('show', () => { + it('should do nothing if already shown', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + const tp = new TimePicker(div) + + // Show it once + tp.show() + // Show it again + tp.show() + + // Should still be shown exactly once + expect(div.classList.contains('show')).toBeTrue() + }) + + it('should set aria-expanded="true" when shown', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + const tp = new TimePicker(div) + + tp.show() + expect(div.getAttribute('aria-expanded')).toBe('true') + }) + + it('should emit show.coreui.time-picker and shown.coreui.time-picker events', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + new TimePicker(div) // eslint-disable-line no-new + + let showEventTriggered = false + + div.addEventListener('show.coreui.time-picker', () => { + showEventTriggered = true + }) + + div.addEventListener('shown.coreui.time-picker', () => { + expect(showEventTriggered).toBeTrue() + expect(div.classList.contains('show')).toBeTrue() + resolve() + }) + + TimePicker.getInstance(div).show() + }) + }) + }) + + describe('hide', () => { + it('should emit hide.coreui.time-picker and hidden.coreui.time-picker events', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + new TimePicker(div) // eslint-disable-line no-new + + let hideEventTriggered = false + + div.addEventListener('hide.coreui.time-picker', () => { + hideEventTriggered = true + }) + + div.addEventListener('hidden.coreui.time-picker', () => { + expect(hideEventTriggered).toBeTrue() + expect(div.classList.contains('show')).toBeFalse() + resolve() + }) + + // Show first, then hide + TimePicker.getInstance(div).show() + TimePicker.getInstance(div).hide() + }) + }) + }) + + describe('cancel', () => { + it('should revert to _initialDate and emit timeChange', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + const timeString = '11:30:00' + const tp = new TimePicker(div, { time: timeString }) + + tp.show() // sets _initialDate = new Date('1970-01-01 11:30:00') + // Modify the time to something else + tp._date = new Date('1970-01-01T13:00:00') + + div.addEventListener('timeChange.coreui.time-picker', event => { + // Should revert to 11:30:00 + expect(event.date.getHours()).toBe(11) + expect(event.date.getMinutes()).toBe(30) + resolve() + }) + + tp.cancel() + }) + }) + }) + + describe('clear', () => { + it('should set _date to null, clear the input value, emit timeChange', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + const tp = new TimePicker(div, { time: '10:00:00' }) + + div.addEventListener('timeChange.coreui.time-picker', event => { + expect(event.date).toBeNull() + expect(tp._date).toBeNull() + expect(tp._input.value).toBe('') + resolve() + }) + + tp.clear() + }) + }) + }) + + describe('reset', () => { + it('should revert to the config.time, re-render, emit timeChange', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + const tp = new TimePicker(div, { time: '10:00:00' }) + + // Change it to something else + tp._date = new Date('1970-01-01T15:00:00') + expect(tp._date).not.toContain('10:') + + div.addEventListener('timeChange.coreui.time-picker', event => { + // Should revert to '10:00:00' + expect(event.date).toBeInstanceOf(Date) + expect(event.date.getHours()).toBe(10) + resolve() + }) + + tp.reset() + }) + }) + }) + + describe('update', () => { + it('should update config, date, ampm, then re-render time picker selection', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + const tp = new TimePicker(div, { time: '08:00:00' }) + + const oldDate = tp._date + tp.update({ time: '09:30:00' }) + + expect(tp._date).not.toEqual(oldDate) + expect(tp._date.getHours()).toBe(9) + expect(tp._date.getMinutes()).toBe(30) + }) + }) + + describe('dispose', () => { + it('should dispose the TimePicker instance', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + const tp = new TimePicker(div) + + const spy = spyOn(tp, 'dispose').and.callThrough() + + tp.dispose() + expect(spy).toHaveBeenCalled() + expect(TimePicker.getInstance(div)).toBeNull() + }) + }) + + describe('Events', () => { + it('should emit timeChange event when an item is clicked in the roll variant', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + const tp = new TimePicker(div, { + time: '01:00:00', + variant: 'roll' + }) + + // Show so that body is rendered + tp.show() + + // Wait for next tick + setTimeout(() => { + div.addEventListener('timeChange.coreui.time-picker', event => { + expect(event.date.getHours()).toBe(2) + resolve() + }) + + // Find the "2" hours cell in the roll + const hourCells = div.querySelectorAll('[data-coreui-hours]') + // hourCells likely includes "1", "2", "3", etc. + const hourCell2 = Array.from(hourCells).find(cell => + cell.getAttribute('data-coreui-hours') === '2' + ) + hourCell2.click() + }, 10) + }) + }) + }) + + describe('data-api', () => { + it('should initialize timePickers on load event', () => { + fixtureEl.innerHTML = ` +
+ ` + const tpEl = fixtureEl.querySelector('#tp1') + + // Trigger data-api load + const loadEvent = createEvent('load') + window.dispatchEvent(loadEvent) + + const tp = TimePicker.getInstance(tpEl) + expect(tp).not.toBeNull() + expect(tp._date.getHours()).toBe(9) + }) + }) + + describe('jQueryInterface', () => { + it('should create a time picker via jQueryInterface', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + + jQueryMock.fn.timePicker = TimePicker.jQueryInterface + jQueryMock.elements = [div] + jQueryMock.fn.timePicker.call(jQueryMock) + + expect(TimePicker.getInstance(div)).not.toBeNull() + }) + + it('should throw error on undefined method', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + + jQueryMock.fn.timePicker = TimePicker.jQueryInterface + jQueryMock.elements = [div] + + expect(() => { + jQueryMock.fn.timePicker.call(jQueryMock, 'noMethod') + }).toThrowError(TypeError, 'No method named "noMethod"') + }) + }) + + describe('getInstance', () => { + it('should return timePicker instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const tp = new TimePicker(div) + + expect(TimePicker.getInstance(div)).toEqual(tp) + expect(TimePicker.getInstance(div)).toBeInstanceOf(TimePicker) + }) + + it('should return null when there is no timePicker instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + expect(TimePicker.getInstance(div)).toBeNull() + }) + }) + + describe('getOrCreateInstance', () => { + it('should return the existing instance if it exists', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const tp = new TimePicker(div) + expect(TimePicker.getOrCreateInstance(div)).toBe(tp) + }) + + it('should create a new instance if it does not exist', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + expect(TimePicker.getInstance(div)).toBeNull() + + const tp = TimePicker.getOrCreateInstance(div, { + time: '12:45:00' + }) + expect(tp).toBeInstanceOf(TimePicker) + expect(tp._date.getHours()).toBe(12) + expect(tp._date.getMinutes()).toBe(45) + }) + + it('should ignore new config if instance already exists', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const tp = new TimePicker(div, { time: '01:00:00' }) + const tp2 = TimePicker.getOrCreateInstance(div, { time: '05:00:00' }) + + expect(tp2).toBe(tp) + // The new time should not override the old config + expect(tp._date.getHours()).toBe(1) + }) + }) +}) diff --git a/js/tests/unit/util/calendar.spec.js b/js/tests/unit/util/calendar.spec.js new file mode 100644 index 000000000..c1d515178 --- /dev/null +++ b/js/tests/unit/util/calendar.spec.js @@ -0,0 +1,541 @@ +/* eslint-env jasmine */ + +import { + convertIsoWeekToDate, + convertToDateObject, + createGroupsInArray, + getCalendarDate, + getDateBySelectionType, + getFirstAvailableDateInRange, + getMonthsNames, + getSelectableDates, + getYears, + getMonthDetails, + getWeekNumber, + isDateDisabled, + isDateInRange, + isDateSelected, + isDisableDateInRange, + isMonthDisabled, + isMonthSelected, + isMonthInRange, + isSameDateAs, + isToday, + isYearDisabled, + isYearSelected, + isYearInRange, + removeTimeFromDate +} from '../../../src/util/calendar.js' + +describe('Calendar Utilities', () => { + describe('convertIsoWeekToDate', () => { + it('should convert a valid ISO week string to the corresponding Monday', () => { + const result = convertIsoWeekToDate('2023W05') + // 2023-W05 starts on Monday, 2023-01-30 + expect(result).toBeInstanceOf(Date) + expect(result.getFullYear()).toBe(2023) + expect(result.getMonth()).toBe(0) // January + expect(result.getDate()).toBe(30) + }) + + it('should handle lowercase "w" in ISO string', () => { + const result = convertIsoWeekToDate('2023w10') + // 2023-W10 starts on Monday, 2023-03-06 + expect(result).toBeInstanceOf(Date) + expect(result.getFullYear()).toBe(2023) + expect(result.getMonth()).toBe(2) // March + expect(result.getDate()).toBe(6) + }) + + it('should return an Invalid Date if the input string is malformed', () => { + const result = convertIsoWeekToDate('abcdW01') + // Some browsers might interpret "NaN" year. Let's check if it's not a valid date + expect(Number.isNaN(result.getTime())).toBeTrue() + }) + }) + + describe('convertToDateObject', () => { + it('should return null if date is null', () => { + const result = convertToDateObject(null, 'day') + expect(result).toBeNull() + }) + + it('should return the same Date object if date is already a Date', () => { + const originalDate = new Date(2023, 0, 1) + const result = convertToDateObject(originalDate, 'day') + expect(result).toEqual(originalDate) + }) + + it('should parse a string date for "day" selectionType', () => { + const result = convertToDateObject('2023-02-15', 'day') + expect(result).toBeInstanceOf(Date) + expect(result.getFullYear()).toBe(2023) + expect(result.getMonth()).toBe(1) // February + expect(result.getDate()).toBe(15) + }) + + it('should call convertIsoWeekToDate for "week" selectionType', () => { + const result = convertToDateObject('2023W12', 'week') + // Check if we got a Monday in the 12th ISO week + // 2023-W12 starts on Monday, 2023-03-20 + expect(result).toBeInstanceOf(Date) + expect(result.getFullYear()).toBe(2023) + expect(result.getMonth()).toBe(2) + expect(result.getDate()).toBe(20) + }) + + it('should adjust timezone offset for "month" selectionType', () => { + // Simulate a date string + const result = convertToDateObject('2023-06-01', 'month') + // We only check it doesn't produce an invalid date + expect(result).toBeInstanceOf(Date) + }) + + it('should parse the string for "year" selectionType', () => { + const result = convertToDateObject('2025-08-15', 'year') + expect(result).toBeInstanceOf(Date) + expect(result.getFullYear()).toBe(2025) + // We only check that it's a valid date + }) + }) + + describe('createGroupsInArray', () => { + it('should create groups of arrays', () => { + const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9] + const result = createGroupsInArray(arr, 3) + // We have 9 items, grouping into 3 => roughly: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + expect(result).toHaveSize(3) + expect(result[0]).toEqual([1, 2, 3]) + expect(result[1]).toEqual([4, 5, 6]) + expect(result[2]).toEqual([7, 8, 9]) + }) + + it('should handle an empty array', () => { + const result = createGroupsInArray([], 2) + expect(result).toEqual([[], []]) + }) + }) + + describe('getCalendarDate', () => { + const baseDate = new Date(2023, 0, 15) // Jan 15, 2023 + + it('should add month if view is "days"', () => { + const result = getCalendarDate(baseDate, 1, 'days') + expect(result.getFullYear()).toBe(2023) + expect(result.getMonth()).toBe(1) // February + expect(result.getDate()).toBe(1) + }) + + it('should add year if view is "months"', () => { + const result = getCalendarDate(baseDate, 2, 'months') + expect(result.getFullYear()).toBe(2025) + expect(result.getMonth()).toBe(0) + expect(result.getDate()).toBe(1) + }) + + it('should add 12 * order years if view is "years"', () => { + const result = getCalendarDate(baseDate, -1, 'years') + expect(result.getFullYear()).toBe(2023 - 12) + expect(result.getMonth()).toBe(0) + expect(result.getDate()).toBe(1) + }) + + it('should return the original date if order is 0', () => { + const result = getCalendarDate(baseDate, 0, 'days') + expect(result).toBe(baseDate) + }) + }) + + describe('getDateBySelectionType', () => { + it('should return null if date is null', () => { + expect(getDateBySelectionType(null, 'day')).toBeNull() + }) + + it('should return an ISO week string if selectionType is "week"', () => { + // 2023-03-13 is Monday of ISO week 11 + const date = new Date(2023, 2, 13) + const result = getDateBySelectionType(date, 'week') + expect(result).toBe('2023W11') + }) + + it('should return "YYYY-MM" if selectionType is "month"', () => { + const date = new Date(2023, 5, 10) // 2023-06-10 + const result = getDateBySelectionType(date, 'month') + expect(result).toBe('2023-06') + }) + + it('should return "YYYY" if selectionType is "year"', () => { + const date = new Date(2030, 0, 1) + const result = getDateBySelectionType(date, 'year') + expect(result).toBe('2030') + }) + + it('should return the Date object if selectionType is "day"', () => { + const date = new Date(2025, 10, 20) + const result = getDateBySelectionType(date, 'day') + expect(result).toBe(date) + }) + }) + + describe('getFirstAvailableDateInRange', () => { + it('should return the start date if there are no disabled dates', () => { + const start = new Date(2023, 0, 1) + const end = new Date(2023, 0, 5) + const result = getFirstAvailableDateInRange(start, end, null, null, undefined) + expect(result).toEqual(start) + }) + + it('should return the first not-disabled date in the range', () => { + const start = new Date(2023, 0, 1) + const end = new Date(2023, 0, 5) + const disabledDates = [ + new Date(2023, 0, 1), + new Date(2023, 0, 2) + ] + const result = getFirstAvailableDateInRange(start, end, null, null, disabledDates) + // The first available is Jan 3 + expect(result).toEqual(new Date(2023, 0, 3)) + }) + + it('should return null if all dates in the range are disabled', () => { + const start = new Date(2023, 0, 1) + const end = new Date(2023, 0, 2) + const disabledDates = [ + new Date(2023, 0, 1), + new Date(2023, 0, 2) + ] + const result = getFirstAvailableDateInRange(start, end, null, null, disabledDates) + expect(result).toBeNull() + }) + }) + + describe('getMonthsNames', () => { + it('should return an array of 12 month names (short)', () => { + const result = getMonthsNames('en-US', 'short') + expect(result).toHaveSize(12) + expect(result[0]).toBeDefined() + }) + + it('should return an array of 12 month names (long)', () => { + const result = getMonthsNames('en-US', 'long') + expect(result).toHaveSize(12) + expect(result[0]).toBeDefined() + }) + }) + + describe('getSelectableDates', () => { + it('should return all elements that match the default selector inside an element', () => { + const container = document.createElement('tr') + container.innerHTML = ` + + + + ` + const result = getSelectableDates(container) + expect(result).toHaveSize(2) + }) + + it('should allow a custom selector', () => { + const container = document.createElement('div') + container.innerHTML = ` +
+
+ ` + const result = getSelectableDates(container, '.date.selectable') + expect(result).toHaveSize(2) + }) + }) + + describe('getYears', () => { + it('should generate years around a given center year', () => { + const result = getYears(2020, 2) + // range=2 => 4 total => 2020-2 => 2018, 2019, 2020, 2021 + expect(result).toEqual([2018, 2019, 2020, 2021]) + }) + }) + + describe('getMonthDetails', () => { + it('should return an array of weeks with day objects', () => { + const result = getMonthDetails(2023, 0, 1) // January 2023, firstDayOfWeek=1 (Monday) + // Typically 6 weeks for a 42-day calendar layout + expect(result).toBeInstanceOf(Array) + expect(result.length).toBeGreaterThan(0) + // Each element in the array is { weekNumber, days: [ ... ] } + expect(result[0].days).toBeInstanceOf(Array) + // Each day is { date: ..., month: 'previous'|'current'|'next' } + }) + }) + + describe('getWeekNumber', () => { + it('should return correct ISO week number', () => { + // 2023-01-01 is a Sunday => last week of 2022 in ISO + const date = new Date(2023, 0, 1) + const weekNum = getWeekNumber(date) + // The ISO week for Sunday 2023-01-01 is typically 52 or 52/53 from the previous year + // We'll just ensure it's not 1 + expect(weekNum).not.toBe(1) + }) + + it('should properly handle mid-year dates', () => { + // Monday 2023-06-05 => ISO week 23 + const date = new Date(2023, 5, 5) + const weekNum = getWeekNumber(date) + expect(weekNum).toBe(23) + }) + }) + + describe('isDateDisabled', () => { + it('should return true if date < min', () => { + const date = new Date(2023, 0, 1) + const min = new Date(2023, 0, 2) + expect(isDateDisabled(date, min, null, undefined)).toBeTrue() + }) + + it('should return true if date > max', () => { + const date = new Date(2023, 0, 10) + const max = new Date(2023, 0, 5) + expect(isDateDisabled(date, null, max, undefined)).toBeTrue() + }) + + it('should return false if within min/max and no disabledDates provided', () => { + const date = new Date(2023, 0, 5) + const min = new Date(2023, 0, 1) + const max = new Date(2023, 0, 10) + expect(isDateDisabled(date, min, max, undefined)).toBeFalse() + }) + + it('should return true if disabledDates is a function returning true', () => { + const date = new Date(2023, 0, 5) + const fn = d => d.getDate() === 5 + expect(isDateDisabled(date, null, null, fn)).toBeTrue() + }) + + it('should return true if disabledDates is a single Date matching the date', () => { + const date = new Date(2023, 1, 1) + const disabled = new Date(2023, 1, 1) + expect(isDateDisabled(date, null, null, disabled)).toBeTrue() + }) + + it('should return true if date is in a disabled range', () => { + const date = new Date(2023, 2, 5) + const disabled = [[new Date(2023, 2, 1), new Date(2023, 2, 10)]] + expect(isDateDisabled(date, null, null, disabled)).toBeTrue() + }) + }) + + describe('isDateInRange', () => { + it('should return true if date is between start and end', () => { + const date = new Date(2023, 0, 5) + const start = new Date(2023, 0, 1) + const end = new Date(2023, 0, 10) + expect(isDateInRange(date, start, end)).toBeTrue() + }) + + it('should return false if date is outside start/end', () => { + const date = new Date(2023, 0, 15) + const start = new Date(2023, 0, 1) + const end = new Date(2023, 0, 10) + expect(isDateInRange(date, start, end)).toBeFalse() + }) + }) + + describe('isDateSelected', () => { + it('should be true if date equals start', () => { + const date = new Date(2023, 0, 5) + const start = new Date(2023, 0, 5) + expect(isDateSelected(date, start, null)).toBeTrue() + }) + + it('should be true if date equals end', () => { + const date = new Date(2023, 0, 5) + const end = new Date(2023, 0, 5) + expect(isDateSelected(date, null, end)).toBeTrue() + }) + + it('should be false otherwise', () => { + const date = new Date(2023, 0, 5) + const start = new Date(2023, 0, 1) + const end = new Date(2023, 0, 2) + expect(isDateSelected(date, start, end)).toBeFalse() + }) + }) + + describe('isDisableDateInRange', () => { + it('should return false if range does not contain a disabled date', () => { + const start = new Date(2023, 0, 1) + const end = new Date(2023, 0, 3) + const disabledDates = [new Date(2023, 0, 5)] + expect(isDisableDateInRange(start, end, disabledDates)).toBeFalse() + }) + + it('should return true if range contains a disabled date', () => { + const start = new Date(2023, 0, 1) + const end = new Date(2023, 0, 5) + const disabledDates = [new Date(2023, 0, 3)] + expect(isDisableDateInRange(start, end, disabledDates)).toBeTrue() + }) + }) + + describe('isMonthDisabled', () => { + it('should return true if month < min', () => { + const date = new Date(2023, 0, 1) + const min = new Date(2023, 1, 1) // Feb + expect(isMonthDisabled(date, min, null, undefined)).toBeTrue() + }) + + it('should return true if month > max', () => { + const date = new Date(2023, 5, 1) + const max = new Date(2023, 3, 1) // April + expect(isMonthDisabled(date, null, max, undefined)).toBeTrue() + }) + + it('should return false if no disabledDates and within min/max', () => { + const date = new Date(2023, 2, 1) + const min = new Date(2023, 0, 1) + const max = new Date(2023, 5, 1) + expect(isMonthDisabled(date, min, max, undefined)).toBeFalse() + }) + }) + + describe('isMonthSelected', () => { + it('should return true if date matches start month', () => { + const date = new Date(2023, 2, 10) // March + const start = new Date(2023, 2, 1) // March + expect(isMonthSelected(date, start, null)).toBeTrue() + }) + + it('should return true if date matches end month', () => { + const date = new Date(2023, 2, 10) // March + const end = new Date(2023, 2, 12) // March + expect(isMonthSelected(date, null, end)).toBeTrue() + }) + + it('should return false otherwise', () => { + const date = new Date(2023, 3, 1) + const start = new Date(2023, 2, 1) + const end = new Date(2023, 2, 10) + expect(isMonthSelected(date, start, end)).toBeFalse() + }) + }) + + describe('isMonthInRange', () => { + it('should return true if month is in range of start and end', () => { + const date = new Date(2023, 2, 10) + const start = new Date(2023, 1, 1) + const end = new Date(2023, 3, 1) + expect(isMonthInRange(date, start, end)).toBeTrue() + }) + + it('should return false if month is out of range', () => { + const date = new Date(2023, 5, 10) + const start = new Date(2023, 1, 1) + const end = new Date(2023, 3, 1) + expect(isMonthInRange(date, start, end)).toBeFalse() + }) + }) + + describe('isSameDateAs', () => { + it('should return true if both dates have same day, month, year', () => { + const d1 = new Date(2023, 0, 1) + const d2 = new Date(2023, 0, 1) + expect(isSameDateAs(d1, d2)).toBeTrue() + }) + + it('should return false otherwise', () => { + const d1 = new Date(2023, 0, 1) + const d2 = new Date(2023, 0, 2) + expect(isSameDateAs(d1, d2)).toBeFalse() + }) + + it('should return true if both are null', () => { + expect(isSameDateAs(null, null)).toBeTrue() + }) + }) + + describe('isToday', () => { + it('should return true if date is today', () => { + const today = new Date() + expect(isToday(today)).toBeTrue() + }) + + it('should return false if not today', () => { + const notToday = new Date(2000, 0, 1) + expect(isToday(notToday)).toBeFalse() + }) + }) + + describe('isYearDisabled', () => { + it('should return true if year < minYear', () => { + const date = new Date(2022, 0, 1) + const min = new Date(2023, 0, 1) + expect(isYearDisabled(date, min, null, undefined)).toBeTrue() + }) + + it('should return true if year > maxYear', () => { + const date = new Date(2025, 0, 1) + const max = new Date(2024, 0, 1) + expect(isYearDisabled(date, null, max, undefined)).toBeTrue() + }) + + it('should return false if year in range', () => { + const date = new Date(2023, 5, 1) + const min = new Date(2023, 0, 1) + const max = new Date(2023, 11, 31) + expect(isYearDisabled(date, min, max, undefined)).toBeFalse() + }) + }) + + describe('isYearSelected', () => { + it('should return true if date year matches start year', () => { + const date = new Date(2023, 0, 1) + const start = new Date(2023, 5, 1) + expect(isYearSelected(date, start, null)).toBeTrue() + }) + + it('should return true if date year matches end year', () => { + const date = new Date(2023, 0, 1) + const end = new Date(2023, 10, 1) + expect(isYearSelected(date, null, end)).toBeTrue() + }) + + it('should return false otherwise', () => { + const date = new Date(2023, 0, 1) + const start = new Date(2022, 0, 1) + const end = new Date(2024, 0, 1) + // Even though 2023 is between 2022 and 2024, it must match exactly + expect(isYearSelected(date, start, end)).toBeFalse() + }) + }) + + describe('isYearInRange', () => { + it('should return true if date year is between start year and end year', () => { + const date = new Date(2023, 5, 1) + const start = new Date(2022, 0, 1) + const end = new Date(2024, 0, 1) + expect(isYearInRange(date, start, end)).toBeTrue() + }) + + it('should return false if date year is out of range', () => { + const date = new Date(2025, 5, 1) + const start = new Date(2022, 0, 1) + const end = new Date(2024, 0, 1) + expect(isYearInRange(date, start, end)).toBeFalse() + }) + }) + + describe('removeTimeFromDate', () => { + it('should return a new Date object with hours, minutes, seconds, ms set to 0', () => { + const original = new Date(2023, 0, 10, 12, 30, 45, 999) + const cleared = removeTimeFromDate(original) + + expect(cleared).not.toBe(original) // should be a distinct object + expect(cleared.getFullYear()).toBe(2023) + expect(cleared.getMonth()).toBe(0) + expect(cleared.getDate()).toBe(10) + expect(cleared.getHours()).toBe(0) + expect(cleared.getMinutes()).toBe(0) + expect(cleared.getSeconds()).toBe(0) + expect(cleared.getMilliseconds()).toBe(0) + }) + }) +}) diff --git a/js/tests/unit/util/time.spec.js b/js/tests/unit/util/time.spec.js new file mode 100644 index 000000000..27da8ac08 --- /dev/null +++ b/js/tests/unit/util/time.spec.js @@ -0,0 +1,231 @@ +/* eslint-env jasmine */ + +import { + convert12hTo24h, + convert24hTo12h, + convertTimeToDate, + getAmPm, + formatTimePartials, + getLocalizedTimePartials, + getSelectedHour, + getSelectedMinutes, + getSelectedSeconds, + isAmPm, + isValidTime +} from '../../../src/util/time.js' + +describe('Time Utilities', () => { + describe('convert12hTo24h', () => { + it('should convert 12 AM to 0 (midnight)', () => { + expect(convert12hTo24h('am', 12)).toBe(0) + }) + + it('should not change hours for AM except 12 AM', () => { + expect(convert12hTo24h('am', 1)).toBe(1) + expect(convert12hTo24h('am', 6)).toBe(6) + expect(convert12hTo24h('am', 11)).toBe(11) + }) + + it('should convert 12 PM to 12', () => { + expect(convert12hTo24h('pm', 12)).toBe(12) + }) + + it('should add 12 to hours for PM except 12', () => { + expect(convert12hTo24h('pm', 1)).toBe(13) + expect(convert12hTo24h('pm', 6)).toBe(18) + expect(convert12hTo24h('pm', 11)).toBe(23) + }) + }) + + describe('convert24hTo12h', () => { + it('should convert 0 to 12 (midnight)', () => { + expect(convert24hTo12h(0)).toBe(12) + }) + + it('should return same hours modulo 12 for typical times', () => { + expect(convert24hTo12h(1)).toBe(1) + expect(convert24hTo12h(12)).toBe(12) + expect(convert24hTo12h(13)).toBe(1) + expect(convert24hTo12h(23)).toBe(11) + }) + }) + + describe('convertTimeToDate', () => { + it('should return null for falsy values', () => { + expect(convertTimeToDate(null)).toBeNull() + expect(convertTimeToDate(undefined)).toBeNull() + }) + + it('should return the same Date if input is already a Date', () => { + const date = new Date('1970-01-01T05:00:00') + const result = convertTimeToDate(date) + expect(result).toBe(date) + }) + + it('should parse string times into a Date object for 1970-01-01', () => { + const result = convertTimeToDate('02:30:00') + expect(result).toBeInstanceOf(Date) + // We can't guarantee the time zone, but let's check hours & minutes + expect(result.getHours()).toBe(2) + expect(result.getMinutes()).toBe(30) + }) + }) + + describe('getAmPm', () => { + it('should return "am" for morning times', () => { + const date = new Date('2023-01-01T08:00:00') // 8:00 AM + // Use 'en-US' for example + expect(getAmPm(date, 'en-US')).toBe('am') + }) + + it('should return "pm" for afternoon/evening times', () => { + const date = new Date('2023-01-01T15:00:00') // 3:00 PM + expect(getAmPm(date, 'en-US')).toBe('pm') + }) + + it('should default to hours >= 12 => pm, <12 => am if "AM"/"PM" is not found in locale time string', () => { + // E.g., a locale that might not include the "AM"/"PM" substring + const dateMorning = new Date('2023-01-01T03:00:00') + const dateAfternoon = new Date('2023-01-01T13:00:00') + // We'll pretend 'en-GB' doesn't include "AM"/"PM" (some do, but let's test logic anyway) + expect(getAmPm(dateMorning, 'en-GB')).toBe('am') + expect(getAmPm(dateAfternoon, 'en-GB')).toBe('pm') + }) + }) + + describe('formatTimePartials', () => { + it('should return an array of formatted objects', () => { + const values = [0, 1, 2] + const result = formatTimePartials(values, 'en-US', 'hour') + expect(result).toHaveSize(3) + + // Each item is { value, label: ... } + expect(result[0].value).toBe(0) + expect(result[0].label).toBeTruthy() // "12" in 12-hour or "0" in 24-hour for hour partial + }) + + it('should respect the partial type (hour, minute, second)', () => { + const hours = [0, 1, 23] + const formattedHours = formatTimePartials(hours, 'en-US', 'hour') + // The 'label' should reflect the hours part from the formatted date/time + expect(formattedHours[0].value).toBe(0) + expect(formattedHours[1].value).toBe(1) + expect(formattedHours[2].value).toBe(23) + }) + }) + + describe('getLocalizedTimePartials', () => { + it('should generate the correct hours array for 12-hour format', () => { + const { listOfHours, hour12 } = getLocalizedTimePartials('en-US', true) + expect(hour12).toBeTrue() + // By default, we expect 1..12 + expect(listOfHours).toHaveSize(12) + expect(listOfHours[0].value).toBe(1) + expect(listOfHours[11].value).toBe(12) + }) + + it('should generate the correct hours array for 24-hour format', () => { + const { listOfHours, hour12 } = getLocalizedTimePartials('en-US', false) + expect(hour12).toBeFalse() + // By default, we expect 0..23 + expect(listOfHours).toHaveSize(24) + expect(listOfHours[0].value).toBe(0) + expect(listOfHours[23].value).toBe(23) + }) + + it('should filter hours if a function is passed', () => { + // For example, only even hours in 24-hour format + const hoursFn = hour => hour % 2 === 0 + const { listOfHours } = getLocalizedTimePartials('en-US', false, hoursFn) + expect(listOfHours.length).toBe(12) // 0,2,4,...,22 + }) + + it('should return a custom hours array if an array is passed', () => { + const { listOfHours } = getLocalizedTimePartials('en-US', false, [0, 6, 12, 18]) + expect(listOfHours).toHaveSize(4) + expect(listOfHours.map(({ value }) => value)).toEqual([0, 6, 12, 18]) + }) + + it('should produce minutes and seconds arrays too', () => { + const { listOfMinutes, listOfSeconds } = getLocalizedTimePartials('en-US', false) + expect(listOfMinutes).toHaveSize(60) + expect(listOfSeconds).toHaveSize(60) + }) + }) + + describe('getSelectedHour', () => { + it('should return empty string if date is null', () => { + expect(getSelectedHour(null, 'en-US')).toBe('') + }) + + it('should return 12-hour format if ampm=true', () => { + const date = new Date('1970-01-01T15:00:00') // 3 PM in 24-hour + // ampm=true => convert24hTo12h => 3 + expect(getSelectedHour(date, 'en-US', true)).toBe(3) + }) + + it('should return 24-hour format if ampm=false', () => { + const date = new Date('1970-01-01T15:00:00') + expect(getSelectedHour(date, 'en-US', false)).toBe(15) + }) + + it('should detect automatically if locale uses am/pm and ampm = "auto"', () => { + const date = new Date('1970-01-01T15:00:00') + // For "en-US", isAmPm => true + expect(getSelectedHour(date, 'en-US', 'auto')).toBe(3) + }) + }) + + describe('getSelectedMinutes', () => { + it('should return empty string if date is null', () => { + expect(getSelectedMinutes(null)).toBe('') + }) + + it('should return the minutes', () => { + const date = new Date('1970-01-01T05:42:00') + expect(getSelectedMinutes(date)).toBe(42) + }) + }) + + describe('getSelectedSeconds', () => { + it('should return empty string if date is null', () => { + expect(getSelectedSeconds(null)).toBe('') + }) + + it('should return the seconds', () => { + const date = new Date('1970-01-01T05:42:37') + expect(getSelectedSeconds(date)).toBe(37) + }) + }) + + describe('isAmPm', () => { + it('should return true if locale uses AM/PM', () => { + // "en-US" typically uses AM/PM + expect(isAmPm('en-US')).toBeTrue() + }) + + it('should return false if locale does not typically use AM/PM', () => { + // "en-GB" sometimes uses 24h, but let's test: + // Might not guarantee real-world correctness, but we test logic + // If it doesn't contain 'AM' or 'PM' in the date string, returns false + // This test can be environment-dependent, but let's keep it + const result = isAmPm('en-GB') + // Usually "en-GB" might or might not show 12 or 24 hour format + // We'll just check the logic—this could be false in many environment setups + expect(typeof result).toBe('boolean') + }) + }) + + describe('isValidTime', () => { + it('should return true for a valid time string', () => { + expect(isValidTime('02:30:00')).toBeTrue() + expect(isValidTime('14:59:59')).toBeTrue() + }) + + it('should return false for invalid strings', () => { + expect(isValidTime('abc')).toBeFalse() + expect(isValidTime('25:00:00')).toBeFalse() // 25 is not a valid hour + expect(isValidTime('-01:00:00')).toBeFalse() + }) + }) +})