Skip to content

Commit

Permalink
feat(useEventListener): accept multiple events or listeners (#2180)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
vaakian and antfu committed Nov 9, 2022
1 parent 11c1a35 commit ab0eea2
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 72 deletions.
2 changes: 1 addition & 1 deletion packages/.vitepress/plugins/markdownTransform.ts
Expand Up @@ -78,7 +78,7 @@ export async function getFunctionMarkdown(pkg: string, name: string) {
## Type Declarations
<details>
<summary op50 italic>Show Type Declarations</summary>
<summary op50 italic cursor-pointer select-none>Show Type Declarations</summary>
${code}
Expand Down
6 changes: 3 additions & 3 deletions packages/.vitepress/theme/components/TeamMember.vue
Expand Up @@ -45,17 +45,17 @@ defineProps<{
:aria-label="`Sponsor ${data.name}`"
/>
</div>
<div v-if="data.functions || data.packages" bg-gray:5 mb2 p2 rounded grid="~ cols-[20px_1fr] gap-y-2" items-start w="5/6" mxa>
<div v-if="data.functions || data.packages" bg-gray:5 mb2 p3 rounded grid="~ cols-[20px_1fr] gap-x-1 gap-y-2" items-start w="5/6" mxa>
<template v-if="data.functions">
<div op50 i-carbon:function-math title="Functions" />
<div op50 ma i-carbon:function-math title="Functions" />
<div flex="~ col gap-1" text-left text-sm w-max>
<a v-for="f of data.functions" :key="f" :href="`/${f}`" target="_blank">
<code>{{ f }}</code>
</a>
</div>
</template>
<template v-if="data.packages">
<div op50 i-carbon-cube title="Packages" />
<div op50 ma i-carbon-cube title="Packages" />
<div flex="~ col gap-1" text-left text-sm w-max>
<a v-for="f of data.packages" :key="f" href="/add-ons">
<code>@vueuse/{{ f }}</code>
Expand Down
236 changes: 191 additions & 45 deletions packages/core/useEventListener/index.test.ts
@@ -1,84 +1,230 @@
import type { Fn } from '@vueuse/shared'
import type { SpyInstance } from 'vitest'
import { noop } from '@vueuse/shared'
import { isVue2 } from 'vue-demi'
import type { Ref } from 'vue'
import { effectScope, nextTick, ref } from 'vue'
import { useEventListener } from '.'

describe('useEventListener', () => {
let target: Ref<HTMLDivElement | null>
let listener: () => any
const options = { capture: true }
let stop: Fn
let target: HTMLDivElement
let removeSpy: SpyInstance
let addSpy: SpyInstance

beforeEach(() => {
target = ref(document.createElement('div'))
listener = vi.fn()
target = document.createElement('div')
removeSpy = vitest.spyOn(target, 'removeEventListener')
addSpy = vitest.spyOn(target, 'addEventListener')
})

it('should not listen when target is invalid', async () => {
useEventListener(target, 'click', listener)
const el = target.value
target.value = null
await nextTick()
el?.dispatchEvent(new MouseEvent('click'))
await nextTick()
it('should be defined', () => {
expect(useEventListener).toBeDefined()
})

describe('given both none array', () => {
const listener = vitest.fn()
const event = 'click'

beforeEach(() => {
listener.mockReset()
stop = useEventListener(target, event, listener, options)
})

it('should add listener', () => {
expect(addSpy).toBeCalledTimes(1)
})

it('should trigger listener', () => {
expect(listener).not.toBeCalled()
target.dispatchEvent(new MouseEvent(event))
expect(listener).toBeCalledTimes(1)
})

it('should remove listener', () => {
expect(removeSpy).not.toBeCalled()

expect(listener).toHaveBeenCalledTimes(isVue2 ? 1 : 0)
expect(useEventListener(null, 'click', listener)).toBe(noop)
stop()

expect(removeSpy).toBeCalledTimes(1)
expect(removeSpy).toBeCalledWith(event, listener, options)
})
})

function getTargetName(useTarget: boolean) {
return useTarget ? 'element' : 'window'
}
describe('given array of events but single listener', () => {
const listener = vitest.fn()
const events = ['click', 'scroll', 'blur', 'resize']

function getArgs(useTarget: boolean) {
return (useTarget ? [target, 'click', listener] : ['click', listener])
}
beforeEach(() => {
listener.mockReset()
stop = useEventListener(target, events, listener, options)
})

function trigger(useTarget: boolean) {
(useTarget ? target.value : window)!.dispatchEvent(new MouseEvent('click'))
}
it('should add listener for all events', () => {
events.forEach(event => expect(addSpy).toBeCalledWith(event, listener, options))
})

function testTarget(useTarget: boolean) {
it(`should ${getTargetName(useTarget)} listen event`, async () => {
// @ts-expect-error mock different args
const stop = useEventListener(...getArgs(useTarget))
it('should trigger listener with all events', () => {
expect(listener).not.toBeCalled()
events.forEach((event, index) => {
target.dispatchEvent(new Event(event))
expect(listener).toBeCalledTimes(index + 1)
})
})

trigger(useTarget)
it('should remove listener with all events', () => {
expect(removeSpy).not.toBeCalled()

await nextTick()
stop()

expect(listener).toHaveBeenCalledTimes(1)
expect(removeSpy).toBeCalledTimes(events.length)
events.forEach(event => expect(removeSpy).toBeCalledWith(event, listener, options))
})
})

it(`should ${getTargetName(useTarget)} manually stop listening event`, async () => {
// @ts-expect-error mock different args
const stop = useEventListener(...getArgs(useTarget))
describe('given single event but array of listeners', () => {
const listeners = [vitest.fn(), vitest.fn(), vitest.fn()]
const event = 'click'

beforeEach(() => {
listeners.forEach(listener => listener.mockReset())
stop = useEventListener(target, event, listeners, options)
})

it('should add all listeners', () => {
listeners.forEach(listener => expect(addSpy).toBeCalledWith(event, listener, options))
})

it('should call all listeners with single click event', () => {
listeners.forEach(listener => expect(listener).not.toBeCalled())

target.dispatchEvent(new Event(event))

listeners.forEach(listener => expect(listener).toBeCalledTimes(1))
})

it('should remove listeners', () => {
expect(removeSpy).not.toBeCalled()

stop()

trigger(useTarget)
expect(removeSpy).toBeCalledTimes(listeners.length)
listeners.forEach(listener => expect(removeSpy).toBeCalledWith(event, listener, options))
})
})

await nextTick()
describe('given both array of events and listeners', () => {
const listeners = [vitest.fn(), vitest.fn(), vitest.fn()]
const events = ['click', 'scroll', 'blur', 'resize', 'custom-event']

expect(listener).toHaveBeenCalledTimes(0)
beforeEach(() => {
listeners.forEach(listener => listener.mockReset())
stop = useEventListener(target, events, listeners, options)
})

it(`should ${getTargetName(useTarget)} auto stop listening event`, async () => {
const scope = effectScope()
await scope.run(async () => {
// @ts-expect-error mock different args
useEventListener(...getArgs(useTarget))
it('should add all listeners for all events', () => {
listeners.forEach((listener) => {
events.forEach((event) => {
expect(addSpy).toBeCalledWith(event, listener, options)
})
})
})

it('should call all listeners with all events', () => {
events.forEach((event, index) => {
target.dispatchEvent(new Event(event))
listeners.forEach(listener => expect(listener).toBeCalledTimes(index + 1))
})
})

it('should remove all listeners with all events', () => {
stop()

listeners.forEach((listener) => {
events.forEach((event) => {
expect(removeSpy).toBeCalledWith(event, listener, options)
})
})
})
})

await scope.stop()
describe('multiple events', () => {
let target: Ref<HTMLDivElement | null>
let listener: () => any

trigger(useTarget)
beforeEach(() => {
target = ref(document.createElement('div'))
listener = vi.fn()
})

it('should not listen when target is invalid', async () => {
useEventListener(target, 'click', listener)
const el = target.value
target.value = null
await nextTick()
el?.dispatchEvent(new MouseEvent('click'))
await nextTick()

expect(listener).toHaveBeenCalledTimes(isVue2 ? 1 : 0)
expect(useEventListener(null, 'click', listener)).toBe(noop)
})
}

testTarget(false)
testTarget(true)
function getTargetName(useTarget: boolean) {
return useTarget ? 'element' : 'window'
}

function getArgs(useTarget: boolean) {
return (useTarget ? [target, 'click', listener] : ['click', listener])
}

function trigger(useTarget: boolean) {
(useTarget ? target.value : window)!.dispatchEvent(new MouseEvent('click'))
}

function testTarget(useTarget: boolean) {
it(`should ${getTargetName(useTarget)} listen event`, async () => {
// @ts-expect-error mock different args
const stop = useEventListener(...getArgs(useTarget))

trigger(useTarget)

await nextTick()

expect(listener).toHaveBeenCalledTimes(1)
})

it(`should ${getTargetName(useTarget)} manually stop listening event`, async () => {
// @ts-expect-error mock different args
const stop = useEventListener(...getArgs(useTarget))

stop()

trigger(useTarget)

await nextTick()

expect(listener).toHaveBeenCalledTimes(0)
})

it(`should ${getTargetName(useTarget)} auto stop listening event`, async () => {
const scope = effectScope()
await scope.run(async () => {
// @ts-expect-error mock different args
useEventListener(...getArgs(useTarget))
})

await scope.stop()

trigger(useTarget)

await nextTick()

expect(listener).toHaveBeenCalledTimes(isVue2 ? 1 : 0)
})
}

testTarget(false)
testTarget(true)
})
})

0 comments on commit ab0eea2

Please sign in to comment.