Skip to content

Commit 7c2e6a7

Browse files
authored
test: add unit tests for useHotkeys hook (#354)
1 parent e293cc7 commit 7c2e6a7

1 file changed

Lines changed: 219 additions & 0 deletions

File tree

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import { renderHook } from '@testing-library/react';
2+
import { useHotkeys } from '../use-hotkeys';
3+
4+
describe('useHotkeys', () => {
5+
let callback: jest.Mock;
6+
7+
beforeEach(() => {
8+
callback = jest.fn();
9+
});
10+
11+
afterEach(() => {
12+
jest.clearAllMocks();
13+
});
14+
15+
it('should call callback when specified keys are pressed', () => {
16+
renderHook(() => useHotkeys(['ctrl', 's'], callback));
17+
18+
const event = new KeyboardEvent('keydown', {
19+
key: 's',
20+
ctrlKey: true,
21+
bubbles: true,
22+
});
23+
24+
window.dispatchEvent(event);
25+
26+
expect(callback).toHaveBeenCalledTimes(1);
27+
});
28+
29+
it('should handle multiple modifier keys', () => {
30+
renderHook(() => useHotkeys(['ctrl', 'shift', 'a'], callback));
31+
32+
const event = new KeyboardEvent('keydown', {
33+
key: 'a',
34+
ctrlKey: true,
35+
shiftKey: true,
36+
bubbles: true,
37+
});
38+
39+
window.dispatchEvent(event);
40+
41+
expect(callback).toHaveBeenCalledTimes(1);
42+
});
43+
44+
it('should be case insensitive for key matching', () => {
45+
renderHook(() => useHotkeys(['s'], callback));
46+
47+
const event = new KeyboardEvent('keydown', {
48+
key: 'S',
49+
bubbles: true,
50+
});
51+
52+
window.dispatchEvent(event);
53+
54+
expect(callback).toHaveBeenCalledTimes(1);
55+
});
56+
57+
it('should not trigger when keys do not match', () => {
58+
renderHook(() => useHotkeys(['ctrl', 's'], callback));
59+
60+
const event = new KeyboardEvent('keydown', {
61+
key: 'a',
62+
ctrlKey: true,
63+
bubbles: true,
64+
});
65+
66+
window.dispatchEvent(event);
67+
68+
expect(callback).not.toHaveBeenCalled();
69+
});
70+
71+
it('should not trigger when modifier keys are missing', () => {
72+
renderHook(() => useHotkeys(['ctrl', 's'], callback));
73+
74+
const event = new KeyboardEvent('keydown', {
75+
key: 's',
76+
bubbles: true,
77+
});
78+
79+
window.dispatchEvent(event);
80+
81+
expect(callback).not.toHaveBeenCalled();
82+
});
83+
84+
it('should not trigger when focus is in an input field', () => {
85+
renderHook(() => useHotkeys(['ctrl', 's'], callback));
86+
87+
const input = document.createElement('input');
88+
document.body.appendChild(input);
89+
90+
const event = new KeyboardEvent('keydown', {
91+
key: 's',
92+
ctrlKey: true,
93+
bubbles: true,
94+
});
95+
96+
Object.defineProperty(event, 'target', { value: input, enumerable: true });
97+
window.dispatchEvent(event);
98+
99+
expect(callback).not.toHaveBeenCalled();
100+
101+
document.body.removeChild(input);
102+
});
103+
104+
it('should not trigger when focus is in a textarea', () => {
105+
renderHook(() => useHotkeys(['ctrl', 's'], callback));
106+
107+
const textarea = document.createElement('textarea');
108+
document.body.appendChild(textarea);
109+
110+
const event = new KeyboardEvent('keydown', {
111+
key: 's',
112+
ctrlKey: true,
113+
bubbles: true,
114+
});
115+
116+
Object.defineProperty(event, 'target', {
117+
value: textarea,
118+
enumerable: true,
119+
});
120+
window.dispatchEvent(event);
121+
122+
expect(callback).not.toHaveBeenCalled();
123+
124+
document.body.removeChild(textarea);
125+
});
126+
127+
it('should not trigger when focus is in a select element', () => {
128+
renderHook(() => useHotkeys(['ctrl', 's'], callback));
129+
130+
const select = document.createElement('select');
131+
document.body.appendChild(select);
132+
133+
const event = new KeyboardEvent('keydown', {
134+
key: 's',
135+
ctrlKey: true,
136+
bubbles: true,
137+
});
138+
139+
Object.defineProperty(event, 'target', { value: select, enumerable: true });
140+
window.dispatchEvent(event);
141+
142+
expect(callback).not.toHaveBeenCalled();
143+
144+
document.body.removeChild(select);
145+
});
146+
147+
it('should not trigger when focus is in a contentEditable element', () => {
148+
renderHook(() => useHotkeys(['ctrl', 's'], callback));
149+
150+
const div = document.createElement('div');
151+
div.contentEditable = 'true';
152+
document.body.appendChild(div);
153+
154+
const event = new KeyboardEvent('keydown', {
155+
key: 's',
156+
ctrlKey: true,
157+
bubbles: true,
158+
});
159+
160+
// Mock both contentEditable and isContentEditable
161+
Object.defineProperty(div, 'isContentEditable', {
162+
value: true,
163+
writable: true,
164+
configurable: true,
165+
});
166+
Object.defineProperty(event, 'target', { value: div, enumerable: true });
167+
168+
window.dispatchEvent(event);
169+
170+
expect(callback).not.toHaveBeenCalled();
171+
172+
document.body.removeChild(div);
173+
});
174+
175+
it('should prevent default behavior when hotkey is triggered', () => {
176+
renderHook(() => useHotkeys(['ctrl', 's'], callback));
177+
178+
const event = new KeyboardEvent('keydown', {
179+
key: 's',
180+
ctrlKey: true,
181+
bubbles: true,
182+
});
183+
184+
const preventDefaultSpy = jest.spyOn(event, 'preventDefault');
185+
186+
window.dispatchEvent(event);
187+
188+
expect(preventDefaultSpy).toHaveBeenCalled();
189+
});
190+
191+
it('should handle alt modifier key', () => {
192+
renderHook(() => useHotkeys(['alt', 'f'], callback));
193+
194+
const event = new KeyboardEvent('keydown', {
195+
key: 'f',
196+
altKey: true,
197+
bubbles: true,
198+
});
199+
200+
window.dispatchEvent(event);
201+
202+
expect(callback).toHaveBeenCalledTimes(1);
203+
});
204+
205+
it('should cleanup event listener on unmount', () => {
206+
const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
207+
208+
const { unmount } = renderHook(() => useHotkeys(['ctrl', 's'], callback));
209+
210+
unmount();
211+
212+
expect(removeEventListenerSpy).toHaveBeenCalledWith(
213+
'keydown',
214+
expect.any(Function)
215+
);
216+
217+
removeEventListenerSpy.mockRestore();
218+
});
219+
});

0 commit comments

Comments
 (0)