Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(useEventListener): accept multiple events or listeners #2180

Merged
merged 7 commits into from Nov 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)
})
})