From f1cd643321f8514544e983bbec8dd061bec48766 Mon Sep 17 00:00:00 2001 From: forckes Date: Sat, 14 Mar 2026 16:16:05 +0200 Subject: [PATCH] =?UTF-8?q?test/useKeyboard=20=F0=9F=A7=8A=20added=20tests?= =?UTF-8?q?=20and=20fixed=20window=20to=20be=20undefined=20in=20ssr=20rend?= =?UTF-8?q?er?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bundle/hooks/useKeyboard/useKeyboard.js | 5 +- .../src/hooks/useKeyboard/useKeyboard.test.ts | 198 ++++++++++++++++++ .../core/src/hooks/useKeyboard/useKeyboard.ts | 11 +- 3 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 packages/core/src/hooks/useKeyboard/useKeyboard.test.ts diff --git a/packages/core/src/bundle/hooks/useKeyboard/useKeyboard.js b/packages/core/src/bundle/hooks/useKeyboard/useKeyboard.js index 40837e5d..efced5fb 100644 --- a/packages/core/src/bundle/hooks/useKeyboard/useKeyboard.js +++ b/packages/core/src/bundle/hooks/useKeyboard/useKeyboard.js @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { isTarget } from '@/utils/helpers'; import { useRefState } from '../useRefState/useRefState'; /** @@ -48,7 +48,8 @@ export const useKeyboard = (...params) => { : typeof params[0] === 'object' ? params[0] : { onKeyDown: params[0] }; - const internalRef = useRefState(window); + const [initialValue] = useState(() => (typeof window !== 'undefined' ? window : undefined)); + const internalRef = useRefState(initialValue); const internalOptionsRef = useRef(options); internalOptionsRef.current = options; useEffect(() => { diff --git a/packages/core/src/hooks/useKeyboard/useKeyboard.test.ts b/packages/core/src/hooks/useKeyboard/useKeyboard.test.ts new file mode 100644 index 00000000..dc92c131 --- /dev/null +++ b/packages/core/src/hooks/useKeyboard/useKeyboard.test.ts @@ -0,0 +1,198 @@ +import { act, renderHook } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +import { target } from '@/utils/helpers'; + +import type { StateRef } from '../useRefState/useRefState'; + +import { renderHookServer } from '../../../tests/renderHookServer'; +import { useKeyboard } from './useKeyboard'; + +type UseKeyboardReturn = StateRef; + +const targets = [ + undefined, + target('#target'), + target(document.getElementById('target')!), + target(() => document.getElementById('target')!), + { current: document.getElementById('target') }, + Object.assign(() => {}, { + state: document.getElementById('target'), + current: document.getElementById('target') + }) +]; + +const element = document.getElementById('target') as HTMLDivElement; + +targets.forEach((target) => { + describe(`${target}`, () => { + it('Should use keyboard', () => { + const { result } = renderHook(() => { + if (target) return useKeyboard(target, () => {}) as unknown as UseKeyboardReturn; + return useKeyboard(() => {}); + }); + + if (target) expect(result.current).toBeUndefined(); + if (!target) expect(result.current).toBeTypeOf('function'); + }); + + it('Should use keyboard on server side', () => { + const { result } = renderHookServer(() => { + if (target) return useKeyboard(target, () => {}) as unknown as UseKeyboardReturn; + return useKeyboard(() => {}); + }); + + if (target) expect(result.current).toBeUndefined(); + if (!target) expect(result.current).toBeTypeOf('function'); + }); + + it('Should call callback on key down', () => { + const callback = vi.fn(); + + const { result } = renderHook(() => { + if (target) return useKeyboard(target, callback) as unknown as UseKeyboardReturn; + return useKeyboard(callback); + }); + + if (!target) act(() => (result.current as UseKeyboardReturn)(element)); + + act(() => { + element.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'Enter' })); + }); + + expect(callback).toHaveBeenCalledOnce(); + expect(callback).toHaveBeenCalledWith(expect.any(KeyboardEvent)); + }); + + it('Should call onKeyDown option on key down', () => { + const onKeyDown = vi.fn(); + + const { result } = renderHook(() => { + if (target) return useKeyboard(target, { onKeyDown }) as unknown as UseKeyboardReturn; + return useKeyboard({ onKeyDown }); + }); + + if (!target) act(() => (result.current as UseKeyboardReturn)(element)); + + act(() => { + element.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'a' })); + }); + + expect(onKeyDown).toHaveBeenCalledOnce(); + expect(onKeyDown).toHaveBeenCalledWith(expect.any(KeyboardEvent)); + }); + + it('Should call onKeyUp option on key up', () => { + const onKeyUp = vi.fn(); + + const { result } = renderHook(() => { + if (target) return useKeyboard(target, { onKeyUp }) as unknown as UseKeyboardReturn; + return useKeyboard({ onKeyUp }); + }); + + if (!target) act(() => (result.current as UseKeyboardReturn)(element)); + + act(() => { + element.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: 'Escape' })); + }); + + expect(onKeyUp).toHaveBeenCalledOnce(); + expect(onKeyUp).toHaveBeenCalledWith(expect.any(KeyboardEvent)); + }); + + it('Should call both onKeyDown and onKeyUp options', () => { + const onKeyDown = vi.fn(); + const onKeyUp = vi.fn(); + + const { result } = renderHook(() => { + if (target) + return useKeyboard(target, { onKeyDown, onKeyUp }) as unknown as UseKeyboardReturn; + return useKeyboard({ onKeyDown, onKeyUp }); + }); + + if (!target) act(() => (result.current as UseKeyboardReturn)(element)); + + act(() => { + element.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'Tab' })); + element.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: 'Tab' })); + }); + + expect(onKeyDown).toHaveBeenCalledOnce(); + expect(onKeyUp).toHaveBeenCalledOnce(); + }); + + it('Should cleanup on unmount', () => { + const removeEventListenerSpy = vi.spyOn(element, 'removeEventListener'); + + const { result, unmount } = renderHook(() => { + if (target) return useKeyboard(target, () => {}) as unknown as UseKeyboardReturn; + return useKeyboard(() => {}); + }); + + if (!target) act(() => (result.current as UseKeyboardReturn)(element)); + + unmount(); + + expect(removeEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function)); + expect(removeEventListenerSpy).toHaveBeenCalledWith('keyup', expect.any(Function)); + }); + + it('Should pass correct key in keyboard event', () => { + const callback = vi.fn(); + + const { result } = renderHook(() => { + if (target) return useKeyboard(target, callback) as unknown as UseKeyboardReturn; + return useKeyboard(callback); + }); + + if (!target) act(() => (result.current as UseKeyboardReturn)(element)); + + act(() => { + element.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'Enter' })); + }); + + expect(callback).toHaveBeenCalledOnce(); + expect(callback.mock.calls[0][0].key).toBe('Enter'); + }); + + it('Should attach listener to window when no target provided', () => { + const callback = vi.fn(); + + renderHook(() => useKeyboard(callback)); + + act(() => { + window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); + }); + + expect(callback).toHaveBeenCalledOnce(); + }); + + it('Should update options after rerender', () => { + if (target) return; + + const onKeyDown1 = vi.fn(); + const onKeyDown2 = vi.fn(); + + const { result, rerender } = renderHook( + ({ handler }) => useKeyboard({ onKeyDown: handler }), + { initialProps: { handler: onKeyDown1 } } + ); + + act(() => (result.current as unknown as UseKeyboardReturn)(element)); + + act(() => { + element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); + }); + + expect(onKeyDown1).toHaveBeenCalledOnce(); + + rerender({ handler: onKeyDown2 }); + + act(() => { + element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); + }); + + expect(onKeyDown2).toHaveBeenCalledOnce(); + }); + }); +}); diff --git a/packages/core/src/hooks/useKeyboard/useKeyboard.ts b/packages/core/src/hooks/useKeyboard/useKeyboard.ts index 54bcc327..072f18cc 100644 --- a/packages/core/src/hooks/useKeyboard/useKeyboard.ts +++ b/packages/core/src/hooks/useKeyboard/useKeyboard.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import type { HookTarget } from '@/utils/helpers'; @@ -8,6 +8,9 @@ import type { StateRef } from '../useRefState/useRefState'; import { useRefState } from '../useRefState/useRefState'; +/** The use keyboard return type */ +export type UseKeyboardReturn = StateRef; + /** The use keyboard event handler type */ export type KeyboardEventHandler = (event: KeyboardEvent) => void; @@ -87,7 +90,11 @@ export const useKeyboard = ((...params: any[]) => { : { onKeyDown: params[0] } ) as UseKeyboardEventOptions; - const internalRef = useRefState(window); + const [initialValue] = useState(() => + typeof window !== 'undefined' ? window : undefined + ); + + const internalRef = useRefState(initialValue as Window); const internalOptionsRef = useRef(options); internalOptionsRef.current = options;