import { mount } from '@vue/test-utils' import { describe, expect, it } from 'vitest' import TimePicker from '../../components/TimePicker.vue' describe('TimePicker', () => { it('opens from keyboard disclosure shortcuts', async () => { const wrapper = mount(TimePicker) await wrapper.get('.mint-time-picker').trigger('keydown', { key: 'ArrowDown' }) expect(wrapper.find('.mint-time-picker__dropdown').exists()).toBe(true) }) it('uses shared dropdown state for Escape and outside-click close behavior', async () => { const wrapper = mount(TimePicker, { props: { modelValue: '14:30', }, attachTo: document.body, }) await wrapper.get('input').trigger('click') expect(wrapper.find('.mint-time-picker__dropdown').exists()).toBe(true) document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })) await wrapper.vm.$nextTick() expect(wrapper.find('.mint-time-picker__dropdown').exists()).toBe(false) await wrapper.get('input').trigger('click') expect(wrapper.find('.mint-time-picker__dropdown').exists()).toBe(true) document.body.dispatchEvent(new MouseEvent('click', { bubbles: true })) await wrapper.vm.$nextTick() expect(wrapper.find('.mint-time-picker__dropdown').exists()).toBe(false) wrapper.unmount() }) it('publishes the highlighted slot through aria-activedescendant', async () => { const wrapper = mount(TimePicker, { props: { modelValue: '14:30' }, attachTo: document.body, }) const input = wrapper.get('input') expect(input.attributes('aria-activedescendant')).toBeUndefined() await wrapper.get('.mint-time-picker').trigger('keydown', { key: 'ArrowDown' }) await wrapper.vm.$nextTick() const listbox = wrapper.get('.mint-time-picker__list') expect(input.attributes('aria-controls')).toBe(listbox.attributes('id')) const activeId = input.attributes('aria-activedescendant') expect(activeId).toBeDefined() expect(wrapper.find(`#${activeId}`).exists()).toBe(true) wrapper.unmount() }) it('jumps to the first and last selectable slot with Home and End', async () => { const wrapper = mount(TimePicker, { props: { min: '09:00', max: '10:00', step: 30 }, attachTo: document.body, }) const root = wrapper.get('.mint-time-picker') await root.trigger('keydown', { key: 'ArrowDown' }) await root.trigger('keydown', { key: 'End' }) await wrapper.vm.$nextTick() const slots = wrapper.findAll('.mint-time-picker__slot') expect(slots.at(-1)?.classes()).toContain('mint-time-picker__slot--highlighted') await root.trigger('keydown', { key: 'Home' }) await wrapper.vm.$nextTick() expect(wrapper.findAll('.mint-time-picker__slot')[0].classes()) .toContain('mint-time-picker__slot--highlighted') wrapper.unmount() }) it('returns focus to the input when Escape closes the list', async () => { const wrapper = mount(TimePicker, { attachTo: document.body }) const input = wrapper.get('input').element await wrapper.get('.mint-time-picker').trigger('keydown', { key: 'ArrowDown' }) expect(wrapper.find('.mint-time-picker__dropdown').exists()).toBe(true) await wrapper.get('.mint-time-picker').trigger('keydown', { key: 'Escape' }) await wrapper.vm.$nextTick() await wrapper.vm.$nextTick() expect(wrapper.find('.mint-time-picker__dropdown').exists()).toBe(false) expect(document.activeElement).toBe(input) wrapper.unmount() }) it('should render the dropdown on the shared popover surface when opened', async () => { const wrapper = mount(TimePicker) await wrapper.get('input').trigger('click') expect(wrapper.get('.mint-time-picker__dropdown').classes()).toContain('mint-popover') }) it('should mark the selected slot as a selected menu item when opened', async () => { const wrapper = mount(TimePicker, { props: { modelValue: '00:15' } }) await wrapper.get('input').trigger('click') expect(wrapper.findAll('.mint-time-picker__slot')[1].classes()).toContain('mint-menu-item--selected') }) it('should reserve the check column on unselected slots when opened', async () => { const wrapper = mount(TimePicker, { props: { modelValue: '00:15' } }) await wrapper.get('input').trigger('click') expect(wrapper.findAll('.mint-time-picker__slot')[0].find('.mint-menu-item__check').exists()).toBe(true) }) it('should mark a slot unavailable when disabledSlots returns a reason', async () => { const wrapper = mount(TimePicker, { props: { disabledSlots: (time: string) => time === '00:15' && 'Booked · HPLC-02' } }) await wrapper.get('input').trigger('click') expect(wrapper.findAll('.mint-time-picker__slot')[1].attributes('aria-disabled')).toBe('true') }) it('should render the reason inline when a slot is unavailable', async () => { const wrapper = mount(TimePicker, { props: { disabledSlots: (time: string) => time === '00:15' && 'Booked · HPLC-02' } }) await wrapper.get('input').trigger('click') expect(wrapper.findAll('.mint-time-picker__slot')[1].get('.mint-time-picker__slot-reason').text()).toBe('Booked · HPLC-02') }) it('should not render a reason when disabledSlots returns true', async () => { const wrapper = mount(TimePicker, { props: { disabledSlots: (time: string) => time === '00:15' } }) await wrapper.get('input').trigger('click') expect(wrapper.findAll('.mint-time-picker__slot')[1].find('.mint-time-picker__slot-reason').exists()).toBe(false) }) it('should not emit a value when an unavailable slot is clicked', async () => { const wrapper = mount(TimePicker, { props: { disabledSlots: (time: string) => time === '00:15' && 'Maintenance' } }) await wrapper.get('input').trigger('click') await wrapper.findAll('.mint-time-picker__slot')[1].trigger('click') expect(wrapper.emitted('update:modelValue')).toBeUndefined() }) it('should skip unavailable slots when moving the highlight with ArrowDown', async () => { const wrapper = mount(TimePicker, { props: { disabledSlots: (time: string) => time === '00:15' && 'Maintenance' } }) await wrapper.get('input').trigger('click') await wrapper.get('.mint-time-picker').trigger('keydown', { key: 'ArrowDown' }) expect(wrapper.findAll('.mint-time-picker__slot')[2].classes()).toContain('mint-menu-item--active') }) })