import { mount } from '@vue/test-utils' import { defineComponent, nextTick, useTemplateRef } from 'vue' import { afterEach, describe, expect, it, vi } from 'vitest' import { useDropdownState, type UseDropdownStateOptions, type UseDropdownStateReturn, } from '../../composables/useDropdownState' function mountHarness(options: UseDropdownStateOptions = {}) { let state: UseDropdownStateReturn | undefined const wrapper = mount(defineComponent({ setup() { state = useDropdownState(options) return state }, template: '', }), { attachTo: document.body }) if (!state) { throw new Error('Dropdown harness did not initialize.') } return { wrapper, state } } describe('useDropdownState', () => { afterEach(() => { document.body.innerHTML = '' }) it('opens, closes, and toggles with callbacks only on state changes', () => { const onOpen = vi.fn() const onClose = vi.fn() const { wrapper, state } = mountHarness({ onOpen, onClose }) state.open() state.open() expect(state.isOpen.value).toBe(true) expect(onOpen).toHaveBeenCalledTimes(1) state.toggle() state.close() expect(state.isOpen.value).toBe(false) expect(onClose).toHaveBeenCalledTimes(1) state.toggle() expect(state.isOpen.value).toBe(true) expect(onOpen).toHaveBeenCalledTimes(2) wrapper.unmount() }) it('closes on outside click but keeps inside clicks open', async () => { const { wrapper, state } = mountHarness() state.open() await nextTick() await wrapper.find('.inside').trigger('click') await nextTick() expect(state.isOpen.value).toBe(true) document.body.click() await nextTick() expect(state.isOpen.value).toBe(false) wrapper.unmount() }) it('closes on Escape by default', async () => { const { wrapper, state } = mountHarness() state.open() document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })) await nextTick() expect(state.isOpen.value).toBe(false) wrapper.unmount() }) it('returns focus to the trigger only when asked', async () => { const { wrapper, state } = mountHarness() const trigger = wrapper.get('.trigger').element const inside = wrapper.get('.inside').element state.open() inside.focus() state.close() await nextTick() expect(document.activeElement).toBe(inside) state.open() inside.focus() state.close(true) await nextTick() expect(document.activeElement).toBe(trigger) wrapper.unmount() }) it('returns focus to the trigger when Escape closes the dropdown', async () => { const { wrapper, state } = mountHarness() const trigger = wrapper.get('.trigger').element state.open() wrapper.get('.inside').element.focus() document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })) await nextTick() await nextTick() expect(state.isOpen.value).toBe(false) expect(document.activeElement).toBe(trigger) wrapper.unmount() }) it('closes on scroll and resize only when closeOnScroll is set', async () => { const plain = mountHarness() plain.state.open() window.dispatchEvent(new Event('scroll')) await nextTick() expect(plain.state.isOpen.value).toBe(true) plain.wrapper.unmount() const scrolling = mountHarness({ closeOnScroll: true }) scrolling.state.open() window.dispatchEvent(new Event('scroll')) await nextTick() expect(scrolling.state.isOpen.value).toBe(false) scrolling.state.open() window.dispatchEvent(new Event('resize')) await nextTick() expect(scrolling.state.isOpen.value).toBe(false) scrolling.wrapper.unmount() }) it('should detect outside clicks against a caller-provided root template ref', async () => { let state: UseDropdownStateReturn | undefined const wrapper = mount(defineComponent({ setup() { state = useDropdownState({ rootRef: useTemplateRef('root') }) return {} }, template: '
', }), { attachTo: document.body }) state!.open() await nextTick() document.body.click() await nextTick() expect(state!.isOpen.value).toBe(false) wrapper.unmount() }) it('can opt out of outside-click and Escape closing', async () => { const { wrapper, state } = mountHarness({ closeOnOutsideClick: false, closeOnEscape: false, }) state.open() document.body.click() document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })) await nextTick() expect(state.isOpen.value).toBe(true) wrapper.unmount() }) })