From afc0b61868dea1f379a71ba4d3de6dc67fb02d79 Mon Sep 17 00:00:00 2001 From: Evan Sutherland Date: Tue, 15 Aug 2023 22:44:44 -0500 Subject: [PATCH 1/7] add shim support for vue 3.3 --- src/types/maybe.ts | 1 + src/utilities/vue.ts | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 src/utilities/vue.ts diff --git a/src/types/maybe.ts b/src/types/maybe.ts index a18a2ed8..3825ec5e 100644 --- a/src/types/maybe.ts +++ b/src/types/maybe.ts @@ -3,4 +3,5 @@ import { Ref, UnwrapRef } from 'vue' export type MaybePromise = T | Promise export type MaybeRef = T | Ref export type MaybeUnwrapRef = T | UnwrapRef +export type MaybeRefOrGetter = MaybeRef | (() => T) export type MaybeArray = T | T[] \ No newline at end of file diff --git a/src/utilities/vue.ts b/src/utilities/vue.ts new file mode 100644 index 00000000..48a70699 --- /dev/null +++ b/src/utilities/vue.ts @@ -0,0 +1,8 @@ +import { unref } from 'vue' +import { MaybeRefOrGetter } from '@/types/maybe' +import { isFunction } from '@/utilities/functions' + +// temp shim for Vue 3.3^ function +export function toValue(source: MaybeRefOrGetter): T { + return isFunction(source) ? source() : unref(source) +} \ No newline at end of file From ffaa0dbd2f8b5f5055a08aa9a9dcdc7484168ab7 Mon Sep 17 00:00:00 2001 From: Evan Sutherland Date: Tue, 15 Aug 2023 22:46:26 -0500 Subject: [PATCH 2/7] added new useEventListener composition --- README.md | 6 ++- src/useEventListener/README.md | 30 +++++++++++++++ src/useEventListener/index.ts | 1 + src/useEventListener/useEventListener.ts | 49 ++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/useEventListener/README.md create mode 100644 src/useEventListener/index.ts create mode 100644 src/useEventListener/useEventListener.ts diff --git a/README.md b/README.md index 7fcaa498..f9981ce7 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,22 @@ # vue-compositions + A collection of reusable vue compositions ## Installation -``` + +```bash npm i --save @prefecthq/vue-compositions ``` ## Compositions + - [useBoolean](https://github.com/prefecthq/vue-compositions/tree/main/src/useBoolean) - [useChildrenAreWrapped](https://github.com/prefecthq/vue-compositions/tree/main/src/useChildrenAreWrapped) - [useComputedStyle](https://github.com/prefecthq/vue-compositions/tree/main/src/useComputedStyle) - [useDebouncedRef](https://github.com/prefecthq/vue-compositions/tree/main/src/useDebouncedRef) - [useElementRect](https://github.com/prefecthq/vue-compositions/tree/main/src/useElementRect) - [useElementWidth](https://github.com/prefecthq/vue-compositions/tree/main/src/useElementWidth) +- [useEventListener](https://github.com/prefecthq/vue-compositions/tree/main/src/useEventListener) - [useGlobalEventListener](https://github.com/prefecthq/vue-compositions/tree/main/src/useGlobalEventListener) - [useIntersectionObserver](https://github.com/prefecthq/vue-compositions/tree/main/src/useIntersectionObserver) - [useIsSame](https://github.com/prefecthq/vue-compositions/tree/main/src/useIsSame) diff --git a/src/useEventListener/README.md b/src/useEventListener/README.md new file mode 100644 index 00000000..24577a72 --- /dev/null +++ b/src/useEventListener/README.md @@ -0,0 +1,30 @@ +# useEventListener + +The `useEventListener` composition can be used to automatically handle setup and teardown of event listeners on the document or HTMElement scope. The options argument extends browser AddEventListenerOptions with `immediate` boolean, which defaults to `true` but when passed in as `false`, will prevent the composition from adding the listener automatically. + +## Example + +```typescript +import { useEventListener } from '@prefecthq/vue-compositions' + +function handleEvent(event: MouseEvent) { + // Respond to event +} +const element = ref() +useEventListener(element, 'keyup', handleEvent) +``` + +## Arguments + +| Name | Type | Default | +|-----------|-----------------------------------------------------------|-----------| +| type | `K (K extends keyof DocumentEventMap)` | None | +| callback | `(this: Document, event: DocumentEventMap[K]) => unknown` | None | +| options | `AddEventListenerOptions & { immediate: boolean }` | None | + +## Returns + +| Name | Type | Description | +|--------|-------------|---------------------------------------------------| +| add | () => void | Manually attach the event listener (has no effect if the event listener already exists) | +| remove | () => void | Manually detach the event listener | diff --git a/src/useEventListener/index.ts b/src/useEventListener/index.ts new file mode 100644 index 00000000..d5d98860 --- /dev/null +++ b/src/useEventListener/index.ts @@ -0,0 +1 @@ +export * from './useEventListener' \ No newline at end of file diff --git a/src/useEventListener/useEventListener.ts b/src/useEventListener/useEventListener.ts new file mode 100644 index 00000000..3be749be --- /dev/null +++ b/src/useEventListener/useEventListener.ts @@ -0,0 +1,49 @@ +import { getCurrentScope, onScopeDispose, watch } from 'vue' +import { MaybeRefOrGetter } from '@/types/maybe' +import { toValue } from '@/utilities/vue' + +export type UseEventListener = { + add: () => void, + remove: () => void, +} + +export type UseEventListenerOptions = AddEventListenerOptions & { + immediate?: boolean, +} + +const defaultOptions: UseEventListenerOptions = { + immediate: true, +} + +export function useEventListener(target: MaybeRefOrGetter, key: K, callback: (this: Document, event: DocumentEventMap[K]) => unknown, options?: UseEventListenerOptions): UseEventListener +export function useEventListener(target: MaybeRefOrGetter, key: K, callback: (this: HTMLElement, event: HTMLElementEventMap[K]) => unknown, options?: UseEventListenerOptions): UseEventListener +// eslint-disable-next-line max-params +export function useEventListener(target: MaybeRefOrGetter, key: K, callback: (this: Node, event: Event) => unknown, options: UseEventListenerOptions = {}): UseEventListener { + const { immediate, ...listenerOptions } = { ...defaultOptions, ...options } + + function add(): void { + toValue(target)?.addEventListener(key, callback, listenerOptions) + } + + function remove(): void { + toValue(target)?.removeEventListener(key, callback, listenerOptions) + } + + if (getCurrentScope()) { + onScopeDispose(() => remove()) + } + + if (immediate) { + add() + } + + watch(() => toValue(target), () => { + remove() + add() + }) + + return { + add, + remove, + } +} \ No newline at end of file From 4ce3b2313ad6b12fa323549dc510df0096193317 Mon Sep 17 00:00:00 2001 From: Evan Sutherland Date: Tue, 15 Aug 2023 22:46:51 -0500 Subject: [PATCH 3/7] using useEventListener to drive functionality of useGlobalEventListener --- src/useGlobalEventListener/README.md | 8 ++++++-- .../useGlobalEventListener.ts | 18 +++--------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/useGlobalEventListener/README.md b/src/useGlobalEventListener/README.md index f804bc60..23ef0b36 100644 --- a/src/useGlobalEventListener/README.md +++ b/src/useGlobalEventListener/README.md @@ -1,17 +1,20 @@ # useGlobalEventListener + The `useGlobalEventListener` composition can be used to automatically handle setup and teardown of event listeners on the document scope. It takes the same arguments as the global `addEventListener` method ## Example + ```typescript import { useGlobalEventListener } from '@prefecthq/vue-compositions' -function handleEvent(event: Event) { +function handleEvent(event: MouseEvent) { // Respond to event } useGlobalEventListener('keyup', handleEvent) ``` ## Arguments + | Name | Type | Default | |-----------|-----------------------------------------------------------|-----------| | type | `K (K extends keyof DocumentEventMap)` | None | @@ -19,7 +22,8 @@ useGlobalEventListener('keyup', handleEvent) | options | `AddEventListenerOptions` | None | ## Returns + | Name | Type | Description | |--------|-------------|---------------------------------------------------| | add | () => void | Manually attach the event listener (has no effect if the event listener already exists) | -| remove | () => void | Manually detach the event listener | \ No newline at end of file +| remove | () => void | Manually detach the event listener | diff --git a/src/useGlobalEventListener/useGlobalEventListener.ts b/src/useGlobalEventListener/useGlobalEventListener.ts index f8f39e6c..004f87e6 100644 --- a/src/useGlobalEventListener/useGlobalEventListener.ts +++ b/src/useGlobalEventListener/useGlobalEventListener.ts @@ -1,4 +1,4 @@ -import { tryOnScopeDispose } from '@/utilities/tryOnScopeDispose' +import { useEventListener } from '@/useEventListener' type UseGlobalEventListener = { add: () => void, @@ -31,20 +31,8 @@ type UseGlobalEventListener = { export function useGlobalEventListener( type: K, callback: (this: Document, event: DocumentEventMap[K]) => unknown, - options?: boolean | AddEventListenerOptions, + options?: AddEventListenerOptions, ): UseGlobalEventListener { - - const add = (): void => { - document.addEventListener(type, callback, options) - } - - const remove = (): void => { - document.removeEventListener(type, callback, options) - } - - add() - tryOnScopeDispose(remove) - - return { add, remove } + return useEventListener(document, type, callback, options) } From 78b144ff12fc4913f1e4acf3e67a97ef33b93f1a Mon Sep 17 00:00:00 2001 From: Evan Sutherland Date: Sat, 19 Aug 2023 08:17:11 -0500 Subject: [PATCH 4/7] code review suggestions --- src/useEventListener/README.md | 11 +++++--- src/useEventListener/useEventListener.ts | 33 +++++++++++++----------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/useEventListener/README.md b/src/useEventListener/README.md index 24577a72..240d074a 100644 --- a/src/useEventListener/README.md +++ b/src/useEventListener/README.md @@ -2,6 +2,10 @@ The `useEventListener` composition can be used to automatically handle setup and teardown of event listeners on the document or HTMElement scope. The options argument extends browser AddEventListenerOptions with `immediate` boolean, which defaults to `true` but when passed in as `false`, will prevent the composition from adding the listener automatically. +The composition will return two methods `add` and `remove`. Calling add will trigger `addEventListener` on the target. Calling `remove` will trigger `removeEventListener` on the target. + +The composition uses a watcher to remove and re-add the eventListener automatically when the `target` changes. Note this will NOT execute if `options.immediate` is `false`, or if `remove` is called. + ## Example ```typescript @@ -18,13 +22,14 @@ useEventListener(element, 'keyup', handleEvent) | Name | Type | Default | |-----------|-----------------------------------------------------------|-----------| +| target | `MaybeRefOrGetter` | None | | type | `K (K extends keyof DocumentEventMap)` | None | -| callback | `(this: Document, event: DocumentEventMap[K]) => unknown` | None | -| options | `AddEventListenerOptions & { immediate: boolean }` | None | +| callback | `(this: Document \| HTMLElement, event: DocumentEventMap[K] \| HTMLElementEventMap[K]) => unknown` | None | +| options | `AddEventListenerOptions & { immediate: boolean }` | { immediate: true } | ## Returns | Name | Type | Description | |--------|-------------|---------------------------------------------------| | add | () => void | Manually attach the event listener (has no effect if the event listener already exists) | -| remove | () => void | Manually detach the event listener | +| remove | () => void | Manually detach the event listener, prevent watch from automatically reattaching on target change. | diff --git a/src/useEventListener/useEventListener.ts b/src/useEventListener/useEventListener.ts index 3be749be..8a40c257 100644 --- a/src/useEventListener/useEventListener.ts +++ b/src/useEventListener/useEventListener.ts @@ -1,5 +1,6 @@ -import { getCurrentScope, onScopeDispose, watch } from 'vue' +import { ref, watch } from 'vue' import { MaybeRefOrGetter } from '@/types/maybe' +import { tryOnScopeDispose } from '@/utilities/tryOnScopeDispose' import { toValue } from '@/utilities/vue' export type UseEventListener = { @@ -20,30 +21,32 @@ export function useEventListener(target: Ma // eslint-disable-next-line max-params export function useEventListener(target: MaybeRefOrGetter, key: K, callback: (this: Node, event: Event) => unknown, options: UseEventListenerOptions = {}): UseEventListener { const { immediate, ...listenerOptions } = { ...defaultOptions, ...options } + const manualMode = ref(!immediate) - function add(): void { + function addEventListener(): void { toValue(target)?.addEventListener(key, callback, listenerOptions) } - function remove(): void { + function removeEventListener(): void { toValue(target)?.removeEventListener(key, callback, listenerOptions) } - if (getCurrentScope()) { - onScopeDispose(() => remove()) - } - - if (immediate) { - add() - } + tryOnScopeDispose(removeEventListener) watch(() => toValue(target), () => { - remove() - add() - }) + if (!manualMode.value) { + removeEventListener() + addEventListener() + } + }, { immediate: true }) return { - add, - remove, + add: () => { + addEventListener() + }, + remove: () => { + manualMode.value = true + removeEventListener() + }, } } \ No newline at end of file From 829d5405c82fb8f993f1ce2822d127af854589f3 Mon Sep 17 00:00:00 2001 From: Evan Sutherland Date: Sat, 19 Aug 2023 08:17:19 -0500 Subject: [PATCH 5/7] added unit tests for useEventListener --- src/useEventListener/useEventListener.spec.ts | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 src/useEventListener/useEventListener.spec.ts diff --git a/src/useEventListener/useEventListener.spec.ts b/src/useEventListener/useEventListener.spec.ts new file mode 100644 index 00000000..e3b31025 --- /dev/null +++ b/src/useEventListener/useEventListener.spec.ts @@ -0,0 +1,158 @@ +import { fireEvent, render } from '@testing-library/vue' +import { vi, describe, it, test, expect, afterEach } from 'vitest' +import { ref } from 'vue' +import { useEventListener } from '@/useEventListener/useEventListener' +import { timeout } from '@/utilities/tests' +import * as utils from '@/utilities/vue' + +describe('useEventListener', () => { + + afterEach(() => { + vi.restoreAllMocks() + }) + + test.each([ + undefined, + null, + ])('given falsy target never adds event listener', (initialValue) => { + const callback = vi.fn() + const target = ref(initialValue) + vi.spyOn(utils, 'toValue').mockReturnValue(initialValue) + + useEventListener(target, 'click', callback) + + if (target.value) { + const addEventListenerMock = vi.spyOn(target.value, 'addEventListener') + expect(addEventListenerMock).not.toBeCalled() + } + + expect(callback).not.toBeCalled() + }) + + it('adds event listener', () => { + const target = ref() + const element = document.createElement('p') + vi.spyOn(utils, 'toValue').mockReturnValue(element) + const addEventListenerMock = vi.spyOn(element, 'addEventListener') + + useEventListener(target, 'click', vi.fn()) + + expect(addEventListenerMock).toHaveBeenCalledOnce() + }) + + it('given immediate false wont automatically add event listener', () => { + const target = ref() + const element = document.createElement('p') + vi.spyOn(utils, 'toValue').mockReturnValue(element) + const addEventListenerMock = vi.spyOn(element, 'addEventListener') + + useEventListener(target, 'click', vi.fn(), { immediate: false }) + + expect(addEventListenerMock).not.toHaveBeenCalled() + }) + + it('add is called always adds listener', () => { + const target = ref() + const element = document.createElement('p') + vi.spyOn(utils, 'toValue').mockReturnValue(element) + const addEventListenerMock = vi.spyOn(element, 'addEventListener') + + const { add } = useEventListener(target, 'click', vi.fn(), { immediate: false }) + add() + + expect(addEventListenerMock).toHaveBeenCalledOnce() + }) + + it('remove is called always removes listener', () => { + const target = ref() + const element = document.createElement('p') + vi.spyOn(utils, 'toValue').mockReturnValue(element) + const addEventListenerMock = vi.spyOn(element, 'removeEventListener') + + const { remove } = useEventListener(target, 'click', vi.fn(), { immediate: false }) + remove() + + expect(addEventListenerMock).toHaveBeenCalledOnce() + }) + + it('triggers callback on event', () => { + const callback = vi.fn() + const target = ref() + const element = document.createElement('p') + vi.spyOn(utils, 'toValue').mockReturnValue(element) + + useEventListener(target, 'click', callback) + + fireEvent.click(element) + + expect(callback).toHaveBeenCalledOnce() + }) + + it('on scope dispose removes listener', () => { + const target = ref() + + const { unmount } = render({ + setup: () => { + useEventListener(target, 'click', vi.fn(), { immediate: false }) + }, + }) + + const element = document.createElement('p') + vi.spyOn(utils, 'toValue').mockReturnValue(element) + const addEventListenerMock = vi.spyOn(element, 'removeEventListener') + + unmount() + + expect(addEventListenerMock).toHaveBeenCalled() + }) + + it('changing target automatically reattaches event listener', async () => { + const target = ref() + const originalElement = document.createElement('p') + const updatedElement = document.createElement('div') + + const currentElement = ref(originalElement) + vi.spyOn(utils, 'toValue').mockImplementation(() => currentElement.value) + + const originalAddEventListenerMock = vi.spyOn(originalElement, 'addEventListener') + const originalRemoveEventListenerMock = vi.spyOn(originalElement, 'removeEventListener') + const updatedAddEventListenerMock = vi.spyOn(updatedElement, 'addEventListener') + + useEventListener(target, 'click', vi.fn()) + + currentElement.value = updatedElement + + // because reattaching would happen in watch + await timeout() + + expect(originalAddEventListenerMock).toHaveBeenCalledOnce() + expect(originalRemoveEventListenerMock).toHaveBeenCalledOnce() + expect(updatedAddEventListenerMock).toHaveBeenCalledOnce() + }) + + it('changing target wont reattach if remove was called', async () => { + const target = ref() + const originalElement = document.createElement('p') + const updatedElement = document.createElement('div') + + const currentElement = ref(originalElement) + vi.spyOn(utils, 'toValue').mockImplementation(() => currentElement.value) + + const originalAddEventListenerMock = vi.spyOn(originalElement, 'addEventListener') + const updatedAddEventListenerMock = vi.spyOn(updatedElement, 'addEventListener') + + const { remove } = useEventListener(target, 'click', vi.fn()) + + remove() + + currentElement.value = updatedElement + + // because reattaching would happen in watch + await timeout() + + expect(originalAddEventListenerMock).toHaveBeenCalledOnce() + expect(updatedAddEventListenerMock).not.toHaveBeenCalled() + }) + +}) + From 000ce708db982facb62d1feb575ab00720f11381 Mon Sep 17 00:00:00 2001 From: Evan Sutherland Date: Sat, 19 Aug 2023 15:20:27 -0500 Subject: [PATCH 6/7] code review suggestions --- src/useEventListener/useEventListener.spec.ts | 51 ++++++------------- 1 file changed, 15 insertions(+), 36 deletions(-) diff --git a/src/useEventListener/useEventListener.spec.ts b/src/useEventListener/useEventListener.spec.ts index e3b31025..4888e464 100644 --- a/src/useEventListener/useEventListener.spec.ts +++ b/src/useEventListener/useEventListener.spec.ts @@ -17,22 +17,15 @@ describe('useEventListener', () => { ])('given falsy target never adds event listener', (initialValue) => { const callback = vi.fn() const target = ref(initialValue) - vi.spyOn(utils, 'toValue').mockReturnValue(initialValue) useEventListener(target, 'click', callback) - if (target.value) { - const addEventListenerMock = vi.spyOn(target.value, 'addEventListener') - expect(addEventListenerMock).not.toBeCalled() - } - expect(callback).not.toBeCalled() }) it('adds event listener', () => { - const target = ref() const element = document.createElement('p') - vi.spyOn(utils, 'toValue').mockReturnValue(element) + const target = ref(element) const addEventListenerMock = vi.spyOn(element, 'addEventListener') useEventListener(target, 'click', vi.fn()) @@ -41,9 +34,8 @@ describe('useEventListener', () => { }) it('given immediate false wont automatically add event listener', () => { - const target = ref() const element = document.createElement('p') - vi.spyOn(utils, 'toValue').mockReturnValue(element) + const target = ref(element) const addEventListenerMock = vi.spyOn(element, 'addEventListener') useEventListener(target, 'click', vi.fn(), { immediate: false }) @@ -52,9 +44,8 @@ describe('useEventListener', () => { }) it('add is called always adds listener', () => { - const target = ref() const element = document.createElement('p') - vi.spyOn(utils, 'toValue').mockReturnValue(element) + const target = ref(element) const addEventListenerMock = vi.spyOn(element, 'addEventListener') const { add } = useEventListener(target, 'click', vi.fn(), { immediate: false }) @@ -64,22 +55,20 @@ describe('useEventListener', () => { }) it('remove is called always removes listener', () => { - const target = ref() const element = document.createElement('p') - vi.spyOn(utils, 'toValue').mockReturnValue(element) - const addEventListenerMock = vi.spyOn(element, 'removeEventListener') + const target = ref(element) + const removeEventListenerMock = vi.spyOn(element, 'removeEventListener') const { remove } = useEventListener(target, 'click', vi.fn(), { immediate: false }) remove() - expect(addEventListenerMock).toHaveBeenCalledOnce() + expect(removeEventListenerMock).toHaveBeenCalledOnce() }) it('triggers callback on event', () => { const callback = vi.fn() - const target = ref() const element = document.createElement('p') - vi.spyOn(utils, 'toValue').mockReturnValue(element) + const target = ref(element) useEventListener(target, 'click', callback) @@ -89,7 +78,9 @@ describe('useEventListener', () => { }) it('on scope dispose removes listener', () => { - const target = ref() + const element = document.createElement('p') + const target = ref(element) + const addEventListenerMock = vi.spyOn(element, 'removeEventListener') const { unmount } = render({ setup: () => { @@ -97,47 +88,35 @@ describe('useEventListener', () => { }, }) - const element = document.createElement('p') - vi.spyOn(utils, 'toValue').mockReturnValue(element) - const addEventListenerMock = vi.spyOn(element, 'removeEventListener') - unmount() expect(addEventListenerMock).toHaveBeenCalled() }) it('changing target automatically reattaches event listener', async () => { - const target = ref() const originalElement = document.createElement('p') - const updatedElement = document.createElement('div') - - const currentElement = ref(originalElement) - vi.spyOn(utils, 'toValue').mockImplementation(() => currentElement.value) + const target = ref(originalElement) + const updatedElement = document.createElement('p') - const originalAddEventListenerMock = vi.spyOn(originalElement, 'addEventListener') const originalRemoveEventListenerMock = vi.spyOn(originalElement, 'removeEventListener') const updatedAddEventListenerMock = vi.spyOn(updatedElement, 'addEventListener') useEventListener(target, 'click', vi.fn()) - currentElement.value = updatedElement + target.value = updatedElement // because reattaching would happen in watch await timeout() - expect(originalAddEventListenerMock).toHaveBeenCalledOnce() expect(originalRemoveEventListenerMock).toHaveBeenCalledOnce() expect(updatedAddEventListenerMock).toHaveBeenCalledOnce() }) it('changing target wont reattach if remove was called', async () => { - const target = ref() const originalElement = document.createElement('p') + const target = ref(originalElement) const updatedElement = document.createElement('div') - const currentElement = ref(originalElement) - vi.spyOn(utils, 'toValue').mockImplementation(() => currentElement.value) - const originalAddEventListenerMock = vi.spyOn(originalElement, 'addEventListener') const updatedAddEventListenerMock = vi.spyOn(updatedElement, 'addEventListener') @@ -145,7 +124,7 @@ describe('useEventListener', () => { remove() - currentElement.value = updatedElement + target.value = updatedElement // because reattaching would happen in watch await timeout() From e543f20fbff73788d42f460f882f0ce4dc128403 Mon Sep 17 00:00:00 2001 From: Evan Sutherland Date: Sat, 19 Aug 2023 15:20:55 -0500 Subject: [PATCH 7/7] code review suggestions --- src/useEventListener/useEventListener.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/useEventListener/useEventListener.spec.ts b/src/useEventListener/useEventListener.spec.ts index 4888e464..c1657921 100644 --- a/src/useEventListener/useEventListener.spec.ts +++ b/src/useEventListener/useEventListener.spec.ts @@ -3,7 +3,6 @@ import { vi, describe, it, test, expect, afterEach } from 'vitest' import { ref } from 'vue' import { useEventListener } from '@/useEventListener/useEventListener' import { timeout } from '@/utilities/tests' -import * as utils from '@/utilities/vue' describe('useEventListener', () => {