Testing & Debugging Keyboard Events

Master the tools and techniques for testing keyboard functionality and debugging event handling issues in your JavaScript applications.

IntermediateMarch 28, 202418 min readTesting & Debugging

Browser DevTools for Keyboard Events

Modern browser DevTools provide powerful features for inspecting and debugging keyboard events. Learning to use these tools effectively can dramatically speed up your debugging process.

Chrome DevTools Event Listeners

// 1. Inspect element event listeners in Chrome DevTools
// Right-click element → Inspect → Event Listeners tab

// 2. Use getEventListeners() in Console
getEventListeners(document);
// Returns: {keydown: Array(2), keyup: Array(1), ...}

// 3. Monitor all keyboard events
monitorEvents(document, 'key');
// Now all keyboard events will be logged

// 4. Stop monitoring
unmonitorEvents(document, 'key');

// 5. Break on specific event
// Sources tab → Event Listener Breakpoints → Keyboard → keydown

// 6. Conditional breakpoints for specific keys
// Add this condition to the breakpoint:
event.key === 'Enter' && event.ctrlKey

DevTools Console Utilities

// Create a keyboard event monitor
const keyboardMonitor = {
  start() {
    this.handler = (e) => {
      console.table({
        type: e.type,
        key: e.key,
        code: e.code,
        keyCode: e.keyCode,
        ctrlKey: e.ctrlKey,
        altKey: e.altKey,
        shiftKey: e.shiftKey,
        metaKey: e.metaKey,
        repeat: e.repeat,
        timestamp: e.timeStamp
      });
    };
    
    ['keydown', 'keyup', 'keypress'].forEach(type => {
      document.addEventListener(type, this.handler);
    });
    
    console.log('🎹 Keyboard monitoring started');
  },
  
  stop() {
    ['keydown', 'keyup', 'keypress'].forEach(type => {
      document.removeEventListener(type, this.handler);
    });
    console.log('🛑 Keyboard monitoring stopped');
  }
};

// Usage in console
keyboardMonitor.start();
// Type some keys...
keyboardMonitor.stop();

Chrome DevTools Tips

  • • Use Event Listener Breakpoints for debugging
  • • Enable "Log XMLHttpRequests" to see event timing
  • • Use Performance tab to analyze event overhead
  • • Preserve log across page reloads

Firefox DevTools Features

  • • Event badges show listeners on elements
  • • "Break on events" in Debugger tab
  • • Event tooltip shows handler code
  • • Network monitor for event timing

Effective Logging Strategies

Strategic logging is crucial for understanding keyboard event flow and debugging issues in production:

Advanced Logging System

class KeyboardEventLogger {
  constructor(options = {}) {
    this.enabled = options.enabled ?? true;
    this.logLevel = options.logLevel ?? 'info';
    this.events = [];
    this.maxEvents = options.maxEvents ?? 100;
    this.filters = options.filters ?? {};
  }
  
  log(event, context = {}) {
    if (!this.enabled) return;
    
    const logEntry = {
      timestamp: Date.now(),
      type: event.type,
      key: event.key,
      code: event.code,
      modifiers: {
        ctrl: event.ctrlKey,
        alt: event.altKey,
        shift: event.shiftKey,
        meta: event.metaKey
      },
      target: {
        tagName: event.target.tagName,
        id: event.target.id,
        className: event.target.className
      },
      context,
      stackTrace: this.getStackTrace()
    };
    
    // Apply filters
    if (this.shouldLog(logEntry)) {
      this.events.push(logEntry);
      if (this.events.length > this.maxEvents) {
        this.events.shift();
      }
      
      this.output(logEntry);
    }
  }
  
  shouldLog(entry) {
    // Filter by key
    if (this.filters.keys && !this.filters.keys.includes(entry.key)) {
      return false;
    }
    
    // Filter by event type
    if (this.filters.types && !this.filters.types.includes(entry.type)) {
      return false;
    }
    
    // Filter by modifier
    if (this.filters.requireModifier) {
      const hasModifier = entry.modifiers.ctrl || 
                         entry.modifiers.alt || 
                         entry.modifiers.shift || 
                         entry.modifiers.meta;
      if (!hasModifier) return false;
    }
    
    return true;
  }
  
  output(entry) {
    const style = 'color: #4ade80; font-weight: bold';
    const modifiers = Object.entries(entry.modifiers)
      .filter(([_, value]) => value)
      .map(([key]) => key)
      .join('+');
    
    console.groupCollapsed(
      `%c⌨️ ${entry.type}: ${modifiers ? modifiers + '+' : ''}${entry.key}`,
      style
    );
    console.log('Event:', entry);
    console.log('Target:', entry.target);
    console.log('Context:', entry.context);
    console.trace('Stack trace');
    console.groupEnd();
  }
  
  getStackTrace() {
    const stack = new Error().stack;
    return stack?.split('\n').slice(3, 8).join('\n');
  }
  
  getReport() {
    const summary = {
      totalEvents: this.events.length,
      byType: {},
      byKey: {},
      commonPatterns: []
    };
    
    this.events.forEach(event => {
      // Count by type
      summary.byType[event.type] = (summary.byType[event.type] || 0) + 1;
      
      // Count by key
      summary.byKey[event.key] = (summary.byKey[event.key] || 0) + 1;
    });
    
    // Find common patterns
    const sequences = this.findSequences();
    summary.commonPatterns = sequences.slice(0, 5);
    
    return summary;
  }
  
  findSequences() {
    const sequences = {};
    
    for (let i = 0; i < this.events.length - 1; i++) {
      const current = this.events[i];
      const next = this.events[i + 1];
      const timeDiff = next.timestamp - current.timestamp;
      
      if (timeDiff < 1000) { // Within 1 second
        const sequence = `${current.key} → ${next.key}`;
        sequences[sequence] = (sequences[sequence] || 0) + 1;
      }
    }
    
    return Object.entries(sequences)
      .sort(([, a], [, b]) => b - a)
      .map(([sequence, count]) => ({ sequence, count }));
  }
  
  exportLogs() {
    const data = {
      events: this.events,
      summary: this.getReport(),
      timestamp: new Date().toISOString()
    };
    
    const blob = new Blob([JSON.stringify(data, null, 2)], {
      type: 'application/json'
    });
    
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `keyboard-logs-${Date.now()}.json`;
    a.click();
    URL.revokeObjectURL(url);
  }
}

// Usage
const logger = new KeyboardEventLogger({
  enabled: true,
  filters: {
    requireModifier: true,
    types: ['keydown']
  }
});

document.addEventListener('keydown', (e) => {
  logger.log(e, { 
    feature: 'shortcut-handler',
    userId: 'user123'
  });
});

Production Tip: Use a feature flag to enable detailed logging in production for specific users when debugging issues.

Unit Testing Keyboard Events

Writing comprehensive unit tests for keyboard event handlers ensures your functionality works correctly across different scenarios:

Jest Testing Example

// keyboardHandler.test.js
import { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

describe('KeyboardHandler', () => {
  let mockHandler;
  
  beforeEach(() => {
    mockHandler = jest.fn();
    document.addEventListener('keydown', mockHandler);
  });
  
  afterEach(() => {
    document.removeEventListener('keydown', mockHandler);
    jest.clearAllMocks();
  });
  
  describe('Basic Key Events', () => {
    test('handles single key press', () => {
      const event = new KeyboardEvent('keydown', {
        key: 'a',
        code: 'KeyA',
        keyCode: 65,
        bubbles: true
      });
      
      document.dispatchEvent(event);
      
      expect(mockHandler).toHaveBeenCalledTimes(1);
      expect(mockHandler).toHaveBeenCalledWith(
        expect.objectContaining({
          key: 'a',
          code: 'KeyA'
        })
      );
    });
    
    test('handles modifier combinations', () => {
      const event = new KeyboardEvent('keydown', {
        key: 's',
        code: 'KeyS',
        ctrlKey: true,
        bubbles: true
      });
      
      document.dispatchEvent(event);
      
      expect(mockHandler).toHaveBeenCalledWith(
        expect.objectContaining({
          key: 's',
          ctrlKey: true
        })
      );
    });
  });
  
  describe('React Component Testing', () => {
    test('component responds to keyboard shortcuts', async () => {
      const user = userEvent.setup();
      
      const TestComponent = () => {
        const [saved, setSaved] = React.useState(false);
        
        React.useEffect(() => {
          const handleKeydown = (e) => {
            if ((e.ctrlKey || e.metaKey) && e.key === 's') {
              e.preventDefault();
              setSaved(true);
            }
          };
          
          document.addEventListener('keydown', handleKeydown);
          return () => document.removeEventListener('keydown', handleKeydown);
        }, []);
        
        return <div>{saved ? 'Saved!' : 'Press Ctrl+S'}</div>;
      };
      
      render(<TestComponent />);
      
      expect(screen.getByText('Press Ctrl+S')).toBeInTheDocument();
      
      // Simulate Ctrl+S
      await user.keyboard('{Control>}s{/Control}');
      
      expect(screen.getByText('Saved!')).toBeInTheDocument();
    });
  });
  
  describe('Event Sequences', () => {
    test('handles rapid key sequences', async () => {
      const sequenceHandler = jest.fn();
      const sequence = [];
      
      const handleKeydown = (e) => {
        sequence.push(e.key);
        if (sequence.join('') === 'gg') {
          sequenceHandler();
          sequence.length = 0;
        }
        
        // Clear sequence after delay
        setTimeout(() => sequence.length = 0, 500);
      };
      
      document.addEventListener('keydown', handleKeydown);
      
      // Simulate g → g sequence
      fireEvent.keyDown(document, { key: 'g' });
      fireEvent.keyDown(document, { key: 'g' });
      
      expect(sequenceHandler).toHaveBeenCalledTimes(1);
      
      // Test timeout
      fireEvent.keyDown(document, { key: 'g' });
      await new Promise(resolve => setTimeout(resolve, 600));
      fireEvent.keyDown(document, { key: 'g' });
      
      expect(sequenceHandler).toHaveBeenCalledTimes(1); // Not called again
    });
  });
});

// Custom matchers for keyboard events
expect.extend({
  toBeKeyboardEvent(received, expected) {
    const pass = received instanceof KeyboardEvent &&
                 received.key === expected.key &&
                 received.code === expected.code;
    
    return {
      pass,
      message: () => pass
        ? `Expected not to be keyboard event with key "${expected.key}"`
        : `Expected keyboard event with key "${expected.key}", got "${received.key}"`
    };
  }
});

Testing Utilities

// testUtils/keyboard.js
export class KeyboardTestUtils {
  static createEvent(key, options = {}) {
    const defaults = {
      key,
      code: this.getCodeFromKey(key),
      keyCode: this.getKeyCodeFromKey(key),
      bubbles: true,
      cancelable: true,
      ...options
    };
    
    return new KeyboardEvent('keydown', defaults);
  }
  
  static getCodeFromKey(key) {
    const codeMap = {
      'a': 'KeyA', 'b': 'KeyB', 'c': 'KeyC',
      'Enter': 'Enter', 'Escape': 'Escape',
      ' ': 'Space', 'Tab': 'Tab',
      'ArrowUp': 'ArrowUp', 'ArrowDown': 'ArrowDown'
    };
    return codeMap[key] || `Key${key.toUpperCase()}`;
  }
  
  static getKeyCodeFromKey(key) {
    const keyCodeMap = {
      'a': 65, 'b': 66, 'c': 67,
      'Enter': 13, 'Escape': 27,
      ' ': 32, 'Tab': 9
    };
    return keyCodeMap[key] || 0;
  }
  
  static async typeSequence(element, keys, delay = 50) {
    for (const key of keys) {
      element.dispatchEvent(this.createEvent(key));
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
  
  static mockKeyboardEvent() {
    const original = window.KeyboardEvent;
    
    window.KeyboardEvent = function(type, init) {
      const event = new Event(type, init);
      Object.assign(event, init);
      return event;
    };
    
    return () => {
      window.KeyboardEvent = original;
    };
  }
}

End-to-End Testing

E2E tests ensure keyboard functionality works correctly in real browser environments:

Playwright Keyboard Testing

// keyboard.e2e.spec.js
import { test, expect } from '@playwright/test';

test.describe('Keyboard Shortcuts', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/app');
  });
  
  test('save shortcut works across platforms', async ({ page, browserName }) => {
    // Platform-specific modifier
    const modifier = process.platform === 'darwin' ? 'Meta' : 'Control';
    
    // Focus the editor
    await page.click('[data-testid="editor"]');
    
    // Press save shortcut
    await page.keyboard.press(`${modifier}+s`);
    
    // Verify save indicator appears
    await expect(page.locator('[data-testid="save-indicator"]')).toBeVisible();
    await expect(page.locator('[data-testid="save-indicator"]')).toHaveText('Saved');
  });
  
  test('command palette opens with correct shortcut', async ({ page }) => {
    const modifier = process.platform === 'darwin' ? 'Meta' : 'Control';
    
    // Press command palette shortcut
    await page.keyboard.press(`${modifier}+Shift+p`);
    
    // Verify palette is visible
    await expect(page.locator('[role="dialog"][aria-label="Command Palette"]')).toBeVisible();
    
    // Type to search
    await page.keyboard.type('save');
    
    // Verify filtered results
    await expect(page.locator('[role="option"]')).toHaveCount(3);
    await expect(page.locator('[role="option"]').first()).toContainText('Save');
  });
  
  test('vim-style navigation', async ({ page }) => {
    // Navigate to document
    await page.goto('/document/123');
    
    // Press g twice to go to top
    await page.keyboard.press('g');
    await page.keyboard.press('g');
    
    // Verify scrolled to top
    const scrollPosition = await page.evaluate(() => window.scrollY);
    expect(scrollPosition).toBe(0);
    
    // Press G to go to bottom
    await page.keyboard.press('Shift+g');
    
    // Verify scrolled to bottom
    const newScrollPosition = await page.evaluate(() => window.scrollY);
    const maxScroll = await page.evaluate(() => {
      return document.documentElement.scrollHeight - window.innerHeight;
    });
    expect(newScrollPosition).toBe(maxScroll);
  });
  
  test('keyboard navigation in dropdown', async ({ page }) => {
    // Open dropdown
    await page.click('[data-testid="dropdown-trigger"]');
    
    // Navigate with arrow keys
    await page.keyboard.press('ArrowDown');
    await page.keyboard.press('ArrowDown');
    
    // Verify third item is focused
    const focusedText = await page.evaluate(() => {
      return document.activeElement?.textContent;
    });
    expect(focusedText).toBe('Option 3');
    
    // Select with Enter
    await page.keyboard.press('Enter');
    
    // Verify selection
    await expect(page.locator('[data-testid="dropdown-trigger"]')).toHaveText('Option 3');
  });
  
  test('form keyboard submission', async ({ page }) => {
    // Fill form using keyboard
    await page.focus('[name="username"]');
    await page.keyboard.type('testuser');
    
    await page.keyboard.press('Tab');
    await page.keyboard.type('password123');
    
    // Submit with Enter
    await page.keyboard.press('Enter');
    
    // Verify submission
    await expect(page).toHaveURL('/dashboard');
  });
});

// Accessibility keyboard tests
test.describe('Keyboard Accessibility', () => {
  test('all interactive elements are keyboard accessible', async ({ page }) => {
    await page.goto('/');
    
    const interactiveElements = await page.locator(`
      a[href],
      button:not([disabled]),
      input:not([disabled]),
      select:not([disabled]),
      textarea:not([disabled]),
      [tabindex]:not([tabindex="-1"])
    `).all();
    
    for (const element of interactiveElements) {
      await element.focus();
      const isFocused = await element.evaluate(el => el === document.activeElement);
      expect(isFocused).toBe(true);
    }
  });
  
  test('focus trap in modal', async ({ page }) => {
    // Open modal
    await page.click('[data-testid="open-modal"]');
    
    // Get focusable elements in modal
    const modal = page.locator('[role="dialog"]');
    const focusableElements = await modal.locator(`
      button, [href], input, select, textarea, 
      [tabindex]:not([tabindex="-1"])
    `).all();
    
    // Tab through all elements
    for (let i = 0; i < focusableElements.length + 1; i++) {
      await page.keyboard.press('Tab');
    }
    
    // Verify focus wrapped to first element
    const firstElement = focusableElements[0];
    const isFocused = await firstElement.evaluate(el => el === document.activeElement);
    expect(isFocused).toBe(true);
    
    // Escape closes modal
    await page.keyboard.press('Escape');
    await expect(modal).not.toBeVisible();
  });
});

Cypress Keyboard Testing

// cypress/e2e/keyboard.cy.js
describe('Keyboard Events', () => {
  beforeEach(() => {
    cy.visit('/app');
  });
  
  it('handles keyboard shortcuts', () => {
    // Get OS-specific modifier
    const modifier = Cypress.platform === 'darwin' ? '{cmd}' : '{ctrl}';
    
    // Test save shortcut
    cy.get('[data-testid="editor"]').type(`${modifier}s`);
    cy.get('[data-testid="save-status"]').should('contain', 'Saved');
    
    // Test undo/redo
    cy.get('[data-testid="editor"]')
      .type('Hello World')
      .type(`${modifier}z`)
      .should('have.value', '')
      .type(`${modifier}{shift}z`)
      .should('have.value', 'Hello World');
  });
  
  it('navigates with keyboard', () => {
    // Tab navigation
    cy.get('body').tab();
    cy.focused().should('have.attr', 'data-testid', 'search-input');
    
    cy.focused().tab();
    cy.focused().should('have.attr', 'data-testid', 'nav-button');
    
    // Arrow key navigation in menu
    cy.get('[data-testid="menu-trigger"]').click();
    cy.focused()
      .type('{downarrow}')
      .type('{downarrow}')
      .type('{enter}');
    
    cy.url().should('include', '/settings');
  });
  
  // Custom command for keyboard testing
  Cypress.Commands.add('typeShortcut', (shortcut) => {
    const keys = shortcut.split('+').map(key => {
      const keyMap = {
        'cmd': '{cmd}',
        'ctrl': '{ctrl}',
        'alt': '{alt}',
        'shift': '{shift}',
        'enter': '{enter}',
        'esc': '{esc}'
      };
      return keyMap[key.toLowerCase()] || key;
    });
    
    cy.focused().type(keys.join(''));
  });
  
  it('uses custom shortcuts command', () => {
    cy.get('[data-testid="editor"]').focus();
    cy.typeShortcut('ctrl+b');
    cy.get('[data-testid="bold-indicator"]').should('be.visible');
  });
});

Common Debugging Scenarios

Learn how to debug the most common keyboard event issues developers encounter:

Issue: Events Not Firing

// Debugging checklist for events not firing
const debugEventNotFiring = () => {
  // 1. Check element focus
  console.log('Active element:', document.activeElement);
  console.log('Has focus:', document.hasFocus());
  
  // 2. Check event listeners
  const listeners = getEventListeners(document);
  console.log('Registered listeners:', listeners);
  
  // 3. Check for stopPropagation
  const originalStopProp = Event.prototype.stopPropagation;
  Event.prototype.stopPropagation = function() {
    console.trace('stopPropagation called');
    originalStopProp.call(this);
  };
  
  // 4. Check for preventDefault
  const originalPrevent = Event.prototype.preventDefault;
  Event.prototype.preventDefault = function() {
    console.trace('preventDefault called');
    originalPrevent.call(this);
  };
  
  // 5. Monitor all phases
  ['capture', 'bubble'].forEach(phase => {
    document.addEventListener('keydown', (e) => {
      console.log(`keydown (${phase}):`, e.key);
    }, phase === 'capture');
  });
};

Common causes: Element not focused, event stopped by parent handler, or browser extension interference.

Issue: Wrong Key Values

// Debug key value issues
class KeyValueDebugger {
  static analyze(event) {
    const report = {
      timestamp: new Date().toISOString(),
      browser: navigator.userAgent,
      event: {
        key: event.key,
        code: event.code,
        keyCode: event.keyCode,
        which: event.which,
        charCode: event.charCode,
        location: event.location
      },
      modifiers: {
        altKey: event.altKey,
        ctrlKey: event.ctrlKey,
        metaKey: event.metaKey,
        shiftKey: event.shiftKey
      },
      keyboard: {
        locale: navigator.language,
        layout: this.detectLayout(event)
      }
    };
    
    console.table(report.event);
    console.log('Full report:', report);
    
    // Check for common issues
    if (event.key === 'Unidentified') {
      console.warn('⚠️ Key is "Unidentified" - possible IME or special key');
    }
    
    if (event.keyCode === 229) {
      console.warn('⚠️ keyCode 229 - IME composition in progress');
    }
    
    return report;
  }
  
  static detectLayout(event) {
    // Simple layout detection based on key/code mismatch
    if (event.code === 'KeyQ' && event.key === 'a') {
      return 'AZERTY';
    }
    if (event.code === 'KeyZ' && event.key === 'y') {
      return 'QWERTZ';
    }
    return 'QWERTY';
  }
}

Common causes: Different keyboard layouts, IME input, or legacy browser behavior.

Issue: Timing and Race Conditions

// Debug timing issues
class EventTimingDebugger {
  constructor() {
    this.events = [];
    this.startTime = performance.now();
  }
  
  track(event) {
    const now = performance.now();
    const entry = {
      type: event.type,
      key: event.key,
      timestamp: now - this.startTime,
      delta: this.events.length > 0 
        ? now - this.events[this.events.length - 1].timestamp 
        : 0,
      target: event.target.tagName
    };
    
    this.events.push(entry);
    
    // Detect potential issues
    if (entry.delta < 10 && this.events.length > 1) {
      console.warn('⚠️ Rapid fire events detected:', entry);
    }
    
    if (entry.delta > 1000) {
      console.warn('⚠️ Large gap between events:', entry);
    }
  }
  
  visualize() {
    console.log('Event Timeline:');
    this.events.forEach((event, i) => {
      const bar = '█'.repeat(Math.floor(event.delta / 10));
      console.log(
        `${event.timestamp.toFixed(0).padStart(6)}ms ${bar} ${event.type}:${event.key}`
      );
    });
  }
  
  findPatterns() {
    const patterns = {
      doubleTaps: [],
      sequences: [],
      simultaneousmods: []
    };
    
    // Find double taps
    for (let i = 1; i < this.events.length; i++) {
      if (this.events[i].key === this.events[i-1].key && 
          this.events[i].delta < 300) {
        patterns.doubleTaps.push({
          key: this.events[i].key,
          interval: this.events[i].delta
        });
      }
    }
    
    return patterns;
  }
}

Common causes: Debouncing issues, event handler order, or asynchronous state updates.

Testing Tools & Libraries

Essential tools and libraries for testing keyboard functionality:

Testing Libraries

  • @testing-library/user-event

    Simulates real user keyboard interactions

  • Puppeteer

    Headless browser testing with keyboard API

  • Playwright

    Cross-browser automation with keyboard support

  • Cypress

    E2E testing with .type() commands

Debugging Tools

  • keyboard-event-viewer

    Online tool for viewing event properties

  • Chrome DevTools

    Event listeners, breakpoints, monitoring

  • React DevTools

    Component prop/state inspection

  • Redux DevTools

    Action tracking for keyboard events

Performance Testing

Measure and optimize the performance impact of keyboard event handlers:

Performance Measurement

// Performance monitoring for keyboard events
class KeyboardPerformanceMonitor {
  constructor() {
    this.metrics = {
      eventCount: 0,
      totalTime: 0,
      maxTime: 0,
      handlers: new Map()
    };
  }
  
  wrapHandler(name, handler) {
    return (event) => {
      const start = performance.now();
      
      try {
        handler(event);
      } finally {
        const duration = performance.now() - start;
        this.recordMetric(name, duration);
        
        if (duration > 16) { // Longer than one frame
          console.warn(`Slow keyboard handler "${name}": ${duration.toFixed(2)}ms`);
        }
      }
    };
  }
  
  recordMetric(handler, duration) {
    this.metrics.eventCount++;
    this.metrics.totalTime += duration;
    this.metrics.maxTime = Math.max(this.metrics.maxTime, duration);
    
    if (!this.metrics.handlers.has(handler)) {
      this.metrics.handlers.set(handler, {
        count: 0,
        totalTime: 0,
        maxTime: 0
      });
    }
    
    const handlerMetrics = this.metrics.handlers.get(handler);
    handlerMetrics.count++;
    handlerMetrics.totalTime += duration;
    handlerMetrics.maxTime = Math.max(handlerMetrics.maxTime, duration);
  }
  
  getReport() {
    const avgTime = this.metrics.totalTime / this.metrics.eventCount;
    
    const report = {
      summary: {
        totalEvents: this.metrics.eventCount,
        averageTime: avgTime.toFixed(2) + 'ms',
        maxTime: this.metrics.maxTime.toFixed(2) + 'ms',
        totalTime: this.metrics.totalTime.toFixed(2) + 'ms'
      },
      handlers: []
    };
    
    this.metrics.handlers.forEach((metrics, name) => {
      report.handlers.push({
        name,
        count: metrics.count,
        avgTime: (metrics.totalTime / metrics.count).toFixed(2) + 'ms',
        maxTime: metrics.maxTime.toFixed(2) + 'ms',
        totalTime: metrics.totalTime.toFixed(2) + 'ms'
      });
    });
    
    return report;
  }
  
  startProfiling() {
    if ('PerformanceObserver' in window) {
      const observer = new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          if (entry.name.includes('keydown') || entry.name.includes('keyup')) {
            console.log('Event timing:', entry.name, entry.duration);
          }
        }
      });
      
      observer.observe({ entryTypes: ['event'] });
      return () => observer.disconnect();
    }
  }
}

// Usage
const monitor = new KeyboardPerformanceMonitor();

// Wrap handlers
document.addEventListener('keydown', 
  monitor.wrapHandler('main-handler', (e) => {
    // Your handler logic
  })
);

// Get performance report
setTimeout(() => {
  console.table(monitor.getReport());
}, 10000);

Best Practices

1. Comprehensive Test Coverage

  • • Test all modifier combinations
  • • Include edge cases (rapid typing, held keys)
  • • Test across different keyboard layouts
  • • Verify accessibility requirements
  • • Test on real devices, not just emulators

2. Debugging Strategy

  • • Start with browser DevTools
  • • Add strategic logging points
  • • Use breakpoints for complex flows
  • • Monitor performance impact
  • • Test in production-like environments

3. Continuous Testing

  • • Automate keyboard tests in CI/CD
  • • Run cross-browser tests regularly
  • • Monitor production errors
  • • Collect user feedback on shortcuts
  • • Update tests for new features

Conclusion

Effective testing and debugging of keyboard events requires a combination of the right tools, techniques, and practices. By implementing comprehensive tests and using proper debugging strategies, you can ensure your keyboard functionality works reliably for all users.

Testing Checklist

  • ✓ Unit tests for all keyboard handlers
  • ✓ E2E tests for critical user flows
  • ✓ Cross-browser compatibility tests
  • ✓ Performance monitoring in place
  • ✓ Debugging utilities available
  • ✓ Accessibility tests passing
Try the Event Tester