Testing React Keyboard Events with Vitest + Happy DOM
Handling keyboard events is a common requirement when developing React applications. This guide will walk you through setting up an efficient and reliable testing environment using Vitest and Happy DOM, specifically for testing keyboard events and shortcuts.
Introduction: Why Vitest + Happy DOM?
- Vitest: A blazing-fast unit test framework powered by Vite. It offers a Jest-compatible API, out-of-the-box TypeScript/JSX support, and leverages the many advantages of the Vite ecosystem.
- Happy DOM: A lightweight, standards-compliant JavaScript DOM implementation designed for Node.js. It's generally faster and uses less memory than JSDOM, making it ideal for running front-end unit tests.
- React Testing Library: Encourages writing tests that resemble how users interact with your components, focusing on behavior rather than implementation details.
This combination allows you to efficiently test your React components' keyboard interaction logic in a simulated browser environment.
Prerequisites
Before you begin, ensure your project is set up with Vite (or at least has a configuration compatible with Vitest), and you have a basic understanding of React and unit testing concepts.
Environment Setup
Install Core Dependencies
First, install the necessary development dependencies:
vitest
: The core test runner.@vitejs/plugin-react
: Allows Vitest to process React (JSX, Fast Refresh).happy-dom
: Provides the DOM environment needed for testing.@testing-library/react
: For rendering React components and querying the DOM.@testing-library/user-event
: Provides event dispatching methods that more closely resemble real user interactions (while this example primarily usesfireEvent
,user-event
is generally recommended for richer interactions).
Project Structure (Example)
A typical project structure might look like this:
project/ ├── src/ │ └── useTinykeys.ts # Our custom Hook to be tested │ └── SomeComponent.tsx # A component potentially using useTinykeys ├── tests/ │ └── useTinykeys.spec.tsx # Test file for useTinykeys ├── vitest.config.ts # Vitest configuration file ├── tsconfig.json # TypeScript configuration (if using TS) └── package.json
3. Configuring Vitest
Create or modify the vitest.config.ts
file in your project root:
import { configDefaults, defineConfig } from 'vitest/config'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [ react(), // Add React plugin support ], test: { // --- Core Configuration --- globals: true, // Enable global APIs (describe, it, expect, etc.) without manual imports environment: 'happy-dom', // Specify Happy DOM as the testing environment // --- Optional Configuration --- setupFiles: './tests/setup.ts', // (Optional) Global setup file for the test environment testTimeout: 1000 * 10, // Default timeout for a single test (10 seconds) // Exclude node_modules and other default exclusions exclude: [...configDefaults.exclude, '**/node_modules/**', '**/dist/**'], // Include all files ending with .test.tsx, .spec.tsx, etc. include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'], // --- Coverage Configuration (Optional) --- // coverage: { // provider: 'v8', // or 'istanbul' // reporter: ['text', 'json', 'html'], // }, }, });
Key Configuration Explained:
plugins: [react()]
: Ensures Vitest can correctly process JSX and other React features.globals: true
: Allows you to usedescribe
,it
,expect
, etc., directly in your test files without importing them from thevitest
module.environment: 'happy-dom'
: This is crucial! It tells Vitest to use Happy DOM to simulate a browser environment, making global objects likedocument
andwindow
available and enabling DOM event handling.setupFiles
: (Optional) If you have global mocks or setup that needs to run before all tests (e.g., extendingexpect
, setting up global mocks), you can specify the file here.testTimeout
: Adjust as needed to prevent complex tests from timing out.
4. Understanding useTinykeys
Hook
To demonstrate keyboard event testing, we'll focus on a custom React Hook named useTinykeys
. The purpose of this hook is to listen for specific keyboard combinations and execute a callback when they match.
Let's assume the core functionality of src/useTinykeys.ts
is as follows (this is a simplified concept; a real implementation might be more complex):
https://github.com/hyperse-io/tinykeys/blob/main/src/useTinykeys.ts
5. Writing Test Cases
Now, let's write tests for useTinykeys
. We'll use @testing-library/react
to render a simple component that incorporates this hook and fireEvent
to simulate keyboard events.
tests/useTinykeys.spec.tsx
:
import { type ComponentType, useEffect } from 'react'; import { vi } from 'vitest'; import type { RenderResult } from '@testing-library/react'; import { fireEvent, render } from '@testing-library/react'; import { useTinykeys } from '../src/useTinykeys.js'; const mockOnActionSelect = vi.fn(); const actionTree = { search: { id: 'search', shortcut: ['$mod+k'], }, sequence: { id: 'sequence', shortcut: ['a', 'b', 'c'], }, }; function Viewer() { const bindingEvents = useTinykeys({ actionTree, onActionSelect: mockOnActionSelect, }); useEffect(() => { const unsubscribe = bindingEvents(); return () => { unsubscribe(); }; }, [bindingEvents]); return <div />; } type Utils = RenderResult; const setup = (Component: ComponentType) => { const utils = render(<Component />); return { ...utils, } as Utils; }; describe('useTinykeys tests', () => { let utils: Utils; beforeEach(() => { utils = setup(Viewer); mockOnActionSelect.mockClear(); }); it('should call onActionSelect when Command+K is pressed', () => { // For macOS, use metaKey for Command fireEvent.keyDown(utils.container, { key: 'k', code: 'KeyK', metaKey: true, ctrlKey: false, altKey: false, shiftKey: false, }); // For Windows/Linux, use ctrlKey for Control fireEvent.keyDown(utils.container, { key: 'k', code: 'KeyK', metaKey: false, ctrlKey: true, altKey: false, shiftKey: false, }); expect(mockOnActionSelect).toHaveBeenCalledWith(actionTree.search); }); it('should call onActionSelect with sequence when keys are pressed in order', () => { fireEvent.keyDown(utils.container, { key: 'a', code: 'KeyA' }); fireEvent.keyDown(utils.container, { key: 'b', code: 'KeyB' }); fireEvent.keyDown(utils.container, { key: 'c', code: 'KeyC' }); expect(mockOnActionSelect).toHaveBeenCalledWith(actionTree.sequence); }); });
6. Running Tests
Add a test script to your package.json
:
{ "scripts": { "test": "vitest", "test:ui": "vitest --ui", // Use Vitest UI (optional) "coverage": "vitest run --coverage" // Generate coverage report } }
Then run:
npm test # Or, for watch mode: # npm test -- --watch
Vitest will find and execute test files matching the include
pattern (and outside the exclude
pattern).
7. Key Considerations and Best Practices
key
vs.code
:event.key
: Represents the character value produced by the key (e.g., "a", "A", "#", "Enter"). Affected by layout and modifier keys.event.code
: Represents the physical key (e.g., "KeyA", "Digit1", "Enter"). Not affected by layout.- When testing and implementing shortcuts,
event.key
is often preferred (especially for alphanumeric keys).event.code
might be better for non-character keys or when precise physical key control is needed.
- Focus Management: If your keyboard events are only active when a specific element is focused, ensure you correctly simulate focus in your tests (e.g.,
element.focus()
).@testing-library/user-event
often handles this more gracefully. fireEvent
vs.userEvent
:fireEvent
dispatches a single, raw DOM event.@testing-library/user-event
simulates a more complete user interaction sequence (e.g.,userEvent.keyboard('A')
simulateskeydown
,keypress
,input
,keyup
), which is closer to real user behavior. For complex interactions,userEvent
is generally the better choice. In this guide,fireEvent.keyDown
is sufficient for testing the core logic.
8. Conclusion
By combining Vitest's speed, Happy DOM's lightweight DOM environment, and React Testing Library's best practices, we can build an efficient and reliable testing process for keyboard events and shortcut logic in React applications.
9. Further Reading
- Vitest Official Documentation
- Happy DOM GitHub Repository
- React Testing Library Documentation
- Testing Library -
fireEvent
vsuserEvent
user-event
keyboard
API- MDN Web Docs: KeyboardEvent
Hopefully, this guide helps you better test keyboard event handling in your React applications! If you have any questions or suggestions, feel free to discuss.
Community
We're excited to see the community adopt Hyperse-io, raise issues, and provide feedback. Whether it's a feature request, bug report, or a project to showcase, please get involved!