Skip to content

Commit 07a4107

Browse files
committed
main 🧊 add test for use event listener
1 parent 16a26ce commit 07a4107

File tree

3 files changed

+190
-21
lines changed

3 files changed

+190
-21
lines changed

‎packages/core/src/bundle/hooks/useEventListener/useEventListener.js‎

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { useEffect } from 'react';
1+
import { useEffect, useRef } from 'react';
22
import { getElement, isTarget } from '@/utils/helpers';
3-
import { useEvent } from '../useEvent/useEvent';
43
import { useRefState } from '../useRefState/useRefState';
54
/**
65
* @name useEventListener
@@ -58,17 +57,21 @@ export const useEventListener = (...params) => {
5857
const event = target ? params[1] : params[0];
5958
const listener = target ? params[2] : params[1];
6059
const options = target ? params[3] : params[2];
61-
const internalRef = useRefState(window);
62-
const internalListener = useEvent(listener);
60+
const enabled = options?.enabled ?? true;
61+
const internalRef = useRefState();
62+
const internalListenerRef = useRef(listener);
63+
internalListenerRef.current = listener;
64+
const internalOptionsRef = useRef(options);
65+
internalOptionsRef.current = options;
6366
useEffect(() => {
64-
const element = target ? getElement(target) : internalRef.current;
65-
if (!element) return;
66-
const callback = (event) => internalListener(event);
67-
element.addEventListener(event, callback, options);
67+
if (!enabled || (!target && !internalRef.state)) return;
68+
const element = (target ? getElement(target) : internalRef.current) ?? window;
69+
const listener = (event) => internalListenerRef.current(event);
70+
element.addEventListener(event, listener, options);
6871
return () => {
69-
element.removeEventListener(event, callback, options);
72+
element.removeEventListener(event, listener, options);
7073
};
71-
}, [target, internalRef.state, event, options]);
74+
}, [target, internalRef.state, event, enabled]);
7275
if (target) return;
7376
return internalRef;
7477
};
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { act, renderHook } from '@testing-library/react';
2+
import { afterEach, describe, expect, it, vi } from 'vitest';
3+
4+
import { target } from '@/utils/helpers';
5+
6+
import type { UseEventListenerReturn } from './useEventListener';
7+
8+
import { renderHookServer } from '../../../tests/renderHookServer';
9+
import { useEventListener } from './useEventListener';
10+
11+
afterEach(vi.clearAllMocks);
12+
13+
const targets = [
14+
undefined,
15+
target('#target'),
16+
target(document.getElementById('target')!),
17+
target(() => document.getElementById('target')!),
18+
{ current: document.getElementById('target') }
19+
];
20+
21+
const element = document.getElementById('target') as HTMLDivElement;
22+
23+
targets.forEach((target) => {
24+
describe(`${target}`, () => {
25+
it('Should use event listener', () => {
26+
const callback = vi.fn();
27+
28+
const { result } = renderHook(() => {
29+
if (target)
30+
return useEventListener(
31+
target,
32+
'click',
33+
callback
34+
) as unknown as UseEventListenerReturn<HTMLDivElement>;
35+
return useEventListener('click', callback);
36+
});
37+
38+
if (!target) act(() => result.current(element));
39+
40+
if (!target) expect(result.current).toBeTypeOf('function');
41+
if (target) expect(result.current).toBeUndefined();
42+
});
43+
44+
it('Should use event listener on server side', () => {
45+
const listener = vi.fn();
46+
47+
const { result } = renderHookServer(() => {
48+
if (target)
49+
return useEventListener(
50+
target,
51+
'click',
52+
listener
53+
) as unknown as UseEventListenerReturn<HTMLDivElement>;
54+
return useEventListener('click', listener);
55+
});
56+
57+
if (!target) act(() => result.current(element));
58+
59+
if (!target) expect(result.current).toBeTypeOf('function');
60+
if (target) expect(result.current).toBeUndefined();
61+
});
62+
63+
it('Should call listener when event is triggered', () => {
64+
const listener = vi.fn();
65+
66+
const { result } = renderHook(() => {
67+
if (target)
68+
return useEventListener(
69+
target,
70+
'click',
71+
listener
72+
) as unknown as UseEventListenerReturn<HTMLDivElement>;
73+
return useEventListener('click', listener);
74+
});
75+
76+
if (!target) act(() => result.current(element));
77+
78+
if (!target) expect(result.current).toBeTypeOf('function');
79+
if (target) expect(result.current).toBeUndefined();
80+
81+
act(() => element.dispatchEvent(new Event('click')));
82+
83+
expect(listener).toHaveBeenCalled();
84+
});
85+
86+
it('Should handle enabled option', () => {
87+
const listener = vi.fn();
88+
89+
const { result } = renderHook(() => {
90+
if (target)
91+
return useEventListener(target, 'click', listener, {
92+
enabled: false
93+
}) as unknown as UseEventListenerReturn<HTMLDivElement>;
94+
return useEventListener('click', listener, { enabled: false });
95+
});
96+
97+
if (!target) act(() => result.current(element));
98+
99+
if (!target) expect(result.current).toBeTypeOf('function');
100+
if (target) expect(result.current).toBeUndefined();
101+
102+
act(() => element.dispatchEvent(new Event('click')));
103+
104+
expect(listener).not.toHaveBeenCalled();
105+
});
106+
107+
it('Should handle target changes', () => {
108+
const listener = vi.fn();
109+
const addEventListenerSpy = vi.spyOn(element, 'addEventListener');
110+
const removeEventListenerSpy = vi.spyOn(element, 'removeEventListener');
111+
112+
const { result, rerender } = renderHook(
113+
(target) => {
114+
if (target)
115+
return useEventListener(
116+
target,
117+
'click',
118+
listener
119+
) as unknown as UseEventListenerReturn<HTMLDivElement>;
120+
return useEventListener<HTMLDivElement>('click', listener);
121+
},
122+
{ initialProps: target }
123+
);
124+
125+
if (!target) act(() => result.current(element));
126+
127+
expect(addEventListenerSpy).toHaveBeenCalledOnce();
128+
expect(removeEventListenerSpy).not.toHaveBeenCalled();
129+
130+
rerender({ current: document.getElementById('target') });
131+
132+
expect(addEventListenerSpy).toHaveBeenCalledTimes(2);
133+
expect(removeEventListenerSpy).toHaveBeenCalledOnce();
134+
});
135+
136+
it('Should disconnect observer on unmount', () => {
137+
const listener = vi.fn();
138+
const addEventListenerSpy = vi.spyOn(element, 'addEventListener');
139+
const removeEventListenerSpy = vi.spyOn(element, 'removeEventListener');
140+
141+
const { result, unmount } = renderHook(() => {
142+
if (target)
143+
return useEventListener(
144+
target,
145+
'click',
146+
listener
147+
) as unknown as UseEventListenerReturn<HTMLDivElement>;
148+
return useEventListener<HTMLDivElement>('click', listener);
149+
});
150+
151+
if (!target) act(() => result.current(element));
152+
153+
unmount();
154+
155+
expect(addEventListenerSpy).toHaveBeenCalledOnce();
156+
expect(removeEventListenerSpy).toHaveBeenCalledOnce();
157+
});
158+
});
159+
});

‎packages/core/src/hooks/useEventListener/useEventListener.ts‎

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
import { useEffect } from 'react';
1+
import { useEffect, useRef } from 'react';
22

33
import type { HookTarget } from '@/utils/helpers';
44

55
import { getElement, isTarget } from '@/utils/helpers';
66

77
import type { StateRef } from '../useRefState/useRefState';
88

9-
import { useEvent } from '../useEvent/useEvent';
109
import { useRefState } from '../useRefState/useRefState';
1110

1211
/** The use event listener options */
13-
export type UseEventListenerOptions = boolean | AddEventListenerOptions;
12+
export type UseEventListenerOptions = {
13+
enabled?: boolean;
14+
} & AddEventListenerOptions;
1415

1516
/** The use event listener return type */
1617
export type UseEventListenerReturn<Target extends Element> = StateRef<Target>;
@@ -112,20 +113,26 @@ export const useEventListener = ((...params: any[]) => {
112113
const listener = (target ? params[2] : params[1]) as (...arg: any[]) => undefined | void;
113114
const options = (target ? params[3] : params[2]) as UseEventListenerOptions | undefined;
114115

115-
const internalRef = useRefState(window);
116-
const internalListener = useEvent(listener);
116+
const enabled = options?.enabled ?? true;
117+
118+
const internalRef = useRefState();
119+
const internalListenerRef = useRef(listener);
120+
internalListenerRef.current = listener;
121+
const internalOptionsRef = useRef(options);
122+
internalOptionsRef.current = options;
117123

118124
useEffect(() => {
119-
const element = target ? getElement(target) : internalRef.current;
120-
if (!element) return;
125+
if (!enabled || (!target && !internalRef.state)) return;
126+
127+
const element = ((target ? getElement(target) : internalRef.current) as Element) ?? window;
121128

122-
const callback = (event: Event) => internalListener(event);
129+
const listener = (event: Event) => internalListenerRef.current(event);
123130

124-
element.addEventListener(event, callback, options);
131+
element.addEventListener(event, listener, options);
125132
return () => {
126-
element.removeEventListener(event, callback, options);
133+
element.removeEventListener(event, listener, options);
127134
};
128-
}, [target, internalRef.state, event, options]);
135+
}, [target, internalRef.state, event, enabled]);
129136

130137
if (target) return;
131138
return internalRef;

0 commit comments

Comments
 (0)