Skip to main content
Glama
keyboard.test.ts15.5 kB
/** * Keyboard Input Tests * * @package WP_Navigator_Pro * @since 2.5.0 */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { parseKey, createKeyboardInput, isQuitKey, isBackKey, isHelpKey, isRetryKey, isContinueKey, type KeyEvent, } from './keyboard.js'; describe('Keyboard input', () => { describe('parseKey', () => { describe('single printable characters', () => { it('should parse lowercase letters', () => { const event = parseKey(Buffer.from('a')); expect(event.key).toBe('a'); expect(event.ctrl).toBe(false); expect(event.isNavKey).toBe(false); }); it('should normalize uppercase to lowercase', () => { const event = parseKey(Buffer.from('A')); expect(event.key).toBe('a'); }); it('should parse navigation key b', () => { const event = parseKey(Buffer.from('b')); expect(event.key).toBe('b'); expect(event.isNavKey).toBe(true); }); it('should parse navigation key h', () => { const event = parseKey(Buffer.from('h')); expect(event.key).toBe('h'); expect(event.isNavKey).toBe(true); }); it('should parse navigation key q', () => { const event = parseKey(Buffer.from('q')); expect(event.key).toBe('q'); expect(event.isNavKey).toBe(true); }); it('should parse navigation key r', () => { const event = parseKey(Buffer.from('r')); expect(event.key).toBe('r'); expect(event.isNavKey).toBe(true); }); it('should parse navigation key y', () => { const event = parseKey(Buffer.from('y')); expect(event.key).toBe('y'); expect(event.isNavKey).toBe(true); }); it('should parse navigation key n', () => { const event = parseKey(Buffer.from('n')); expect(event.key).toBe('n'); expect(event.isNavKey).toBe(true); }); it('should mark non-navigation keys correctly', () => { const event = parseKey(Buffer.from('x')); expect(event.isNavKey).toBe(false); }); it('should parse numbers', () => { const event = parseKey(Buffer.from('5')); expect(event.key).toBe('5'); }); it('should parse special characters', () => { const event = parseKey(Buffer.from('?')); expect(event.key).toBe('?'); }); }); describe('control characters', () => { it('should parse Enter key (CR)', () => { const event = parseKey(Buffer.from([13])); // Carriage Return expect(event.key).toBe('enter'); expect(event.isNavKey).toBe(true); }); it('should parse Enter key (LF)', () => { const event = parseKey(Buffer.from([10])); // Line Feed expect(event.key).toBe('enter'); expect(event.isNavKey).toBe(true); }); it('should parse Tab key', () => { const event = parseKey(Buffer.from([9])); expect(event.key).toBe('tab'); }); it('should parse Backspace (DEL)', () => { const event = parseKey(Buffer.from([127])); expect(event.key).toBe('backspace'); }); it('should parse Backspace (Ctrl+H)', () => { const event = parseKey(Buffer.from([8])); expect(event.key).toBe('backspace'); }); it('should parse Ctrl+C', () => { const event = parseKey(Buffer.from([3])); expect(event.key).toBe('ctrl+c'); expect(event.ctrl).toBe(true); }); it('should parse Ctrl+D', () => { const event = parseKey(Buffer.from([4])); expect(event.key).toBe('ctrl+d'); expect(event.ctrl).toBe(true); }); it('should parse Escape key', () => { const event = parseKey(Buffer.from([27])); expect(event.key).toBe('escape'); expect(event.isNavKey).toBe(true); }); }); describe('ANSI escape sequences', () => { it('should parse Up arrow key', () => { const event = parseKey(Buffer.from('\x1b[A')); expect(event.key).toBe('up'); }); it('should parse Down arrow key', () => { const event = parseKey(Buffer.from('\x1b[B')); expect(event.key).toBe('down'); }); it('should parse Right arrow key', () => { const event = parseKey(Buffer.from('\x1b[C')); expect(event.key).toBe('right'); }); it('should parse Left arrow key', () => { const event = parseKey(Buffer.from('\x1b[D')); expect(event.key).toBe('left'); }); it('should parse alternative Up arrow (Application mode)', () => { const event = parseKey(Buffer.from('\x1bOA')); expect(event.key).toBe('up'); }); it('should parse Home key', () => { const event = parseKey(Buffer.from('\x1b[H')); expect(event.key).toBe('home'); }); it('should parse End key', () => { const event = parseKey(Buffer.from('\x1b[F')); expect(event.key).toBe('end'); }); it('should parse Page Up key', () => { const event = parseKey(Buffer.from('\x1b[5~')); expect(event.key).toBe('pageup'); }); it('should parse Page Down key', () => { const event = parseKey(Buffer.from('\x1b[6~')); expect(event.key).toBe('pagedown'); }); it('should parse Delete key', () => { const event = parseKey(Buffer.from('\x1b[3~')); expect(event.key).toBe('delete'); }); it('should parse F1 key', () => { const event = parseKey(Buffer.from('\x1bOP')); expect(event.key).toBe('f1'); }); it('should handle unknown escape sequence as escape', () => { const event = parseKey(Buffer.from('\x1b[99~')); expect(event.key).toBe('escape'); }); }); describe('raw buffer preservation', () => { it('should preserve raw buffer for single character', () => { const buffer = Buffer.from('a'); const event = parseKey(buffer); expect(event.raw).toEqual(buffer); }); it('should preserve raw buffer for escape sequence', () => { const buffer = Buffer.from('\x1b[A'); const event = parseKey(buffer); expect(event.raw).toEqual(buffer); }); }); describe('multi-byte UTF-8 characters', () => { it('should handle emoji', () => { const event = parseKey(Buffer.from('\u{1F600}')); // Grinning face expect(event.key.length).toBeGreaterThan(0); expect(event.isNavKey).toBe(false); }); it('should handle unicode characters', () => { const event = parseKey(Buffer.from('\u00E9')); // e with acute expect(event.key).toBe('\u00E9'); expect(event.isNavKey).toBe(false); }); }); }); describe('key type helpers', () => { describe('isQuitKey', () => { it('should return true for q key', () => { const event: KeyEvent = { key: 'q', raw: Buffer.from('q'), ctrl: false, isNavKey: true }; expect(isQuitKey(event)).toBe(true); }); it('should return true for Ctrl+C', () => { const event: KeyEvent = { key: 'ctrl+c', raw: Buffer.from([3]), ctrl: true, isNavKey: false, }; expect(isQuitKey(event)).toBe(true); }); it('should return false for other keys', () => { const event: KeyEvent = { key: 'x', raw: Buffer.from('x'), ctrl: false, isNavKey: false }; expect(isQuitKey(event)).toBe(false); }); }); describe('isBackKey', () => { it('should return true for b key', () => { const event: KeyEvent = { key: 'b', raw: Buffer.from('b'), ctrl: false, isNavKey: true }; expect(isBackKey(event)).toBe(true); }); it('should return true for escape key', () => { const event: KeyEvent = { key: 'escape', raw: Buffer.from([27]), ctrl: false, isNavKey: true, }; expect(isBackKey(event)).toBe(true); }); it('should return false for other keys', () => { const event: KeyEvent = { key: 'x', raw: Buffer.from('x'), ctrl: false, isNavKey: false }; expect(isBackKey(event)).toBe(false); }); }); describe('isHelpKey', () => { it('should return true for h key', () => { const event: KeyEvent = { key: 'h', raw: Buffer.from('h'), ctrl: false, isNavKey: true }; expect(isHelpKey(event)).toBe(true); }); it('should return true for ? key', () => { const event: KeyEvent = { key: '?', raw: Buffer.from('?'), ctrl: false, isNavKey: false }; expect(isHelpKey(event)).toBe(true); }); it('should return false for other keys', () => { const event: KeyEvent = { key: 'x', raw: Buffer.from('x'), ctrl: false, isNavKey: false }; expect(isHelpKey(event)).toBe(false); }); }); describe('isRetryKey', () => { it('should return true for r key', () => { const event: KeyEvent = { key: 'r', raw: Buffer.from('r'), ctrl: false, isNavKey: true }; expect(isRetryKey(event)).toBe(true); }); it('should return false for other keys', () => { const event: KeyEvent = { key: 'x', raw: Buffer.from('x'), ctrl: false, isNavKey: false }; expect(isRetryKey(event)).toBe(false); }); }); describe('isContinueKey', () => { it('should return true for enter key', () => { const event: KeyEvent = { key: 'enter', raw: Buffer.from([13]), ctrl: false, isNavKey: true, }; expect(isContinueKey(event)).toBe(true); }); it('should return true for y key', () => { const event: KeyEvent = { key: 'y', raw: Buffer.from('y'), ctrl: false, isNavKey: true }; expect(isContinueKey(event)).toBe(true); }); it('should return false for other keys', () => { const event: KeyEvent = { key: 'x', raw: Buffer.from('x'), ctrl: false, isNavKey: false }; expect(isContinueKey(event)).toBe(false); }); }); }); describe('createKeyboardInput', () => { describe('isSupported', () => { it('should return false when stdin is not a TTY', () => { const mockInput = { isTTY: false, setRawMode: vi.fn(), } as unknown as NodeJS.ReadStream; const keyboard = createKeyboardInput({ input: mockInput, autoCleanup: false }); expect(keyboard.isSupported()).toBe(false); }); it('should return false when setRawMode is not available', () => { const mockInput = { isTTY: true, // No setRawMode } as unknown as NodeJS.ReadStream; const keyboard = createKeyboardInput({ input: mockInput, autoCleanup: false }); expect(keyboard.isSupported()).toBe(false); }); it('should return true when TTY with setRawMode', () => { const mockInput = { isTTY: true, setRawMode: vi.fn(), } as unknown as NodeJS.ReadStream; const keyboard = createKeyboardInput({ input: mockInput, autoCleanup: false }); expect(keyboard.isSupported()).toBe(true); }); }); describe('setup and cleanup', () => { it('should return false when not supported', () => { const mockInput = { isTTY: false, setRawMode: vi.fn(), } as unknown as NodeJS.ReadStream; const keyboard = createKeyboardInput({ input: mockInput, autoCleanup: false }); expect(keyboard.setup()).toBe(false); expect(keyboard.isActive()).toBe(false); }); it('should enable raw mode when supported', () => { const setRawMode = vi.fn(); const resume = vi.fn(); const mockInput = { isTTY: true, isRaw: false, setRawMode, resume, } as unknown as NodeJS.ReadStream; const keyboard = createKeyboardInput({ input: mockInput, autoCleanup: false }); const result = keyboard.setup(); expect(result).toBe(true); expect(setRawMode).toHaveBeenCalledWith(true); expect(resume).toHaveBeenCalled(); expect(keyboard.isActive()).toBe(true); }); it('should restore raw mode on cleanup', () => { const setRawMode = vi.fn(); const resume = vi.fn(); const pause = vi.fn(); const removeListener = vi.fn(); const mockInput = { isTTY: true, isRaw: false, setRawMode, resume, pause, removeListener, } as unknown as NodeJS.ReadStream; const keyboard = createKeyboardInput({ input: mockInput, autoCleanup: false }); keyboard.setup(); keyboard.cleanup(); expect(setRawMode).toHaveBeenCalledWith(false); expect(pause).toHaveBeenCalled(); expect(keyboard.isActive()).toBe(false); }); it('should be idempotent - multiple setup calls are safe', () => { const setRawMode = vi.fn(); const resume = vi.fn(); const mockInput = { isTTY: true, isRaw: false, setRawMode, resume, } as unknown as NodeJS.ReadStream; const keyboard = createKeyboardInput({ input: mockInput, autoCleanup: false }); keyboard.setup(); keyboard.setup(); // Second call expect(setRawMode).toHaveBeenCalledTimes(1); // Only once }); it('should be idempotent - multiple cleanup calls are safe', () => { const setRawMode = vi.fn(); const resume = vi.fn(); const pause = vi.fn(); const removeListener = vi.fn(); const mockInput = { isTTY: true, isRaw: false, setRawMode, resume, pause, removeListener, } as unknown as NodeJS.ReadStream; const keyboard = createKeyboardInput({ input: mockInput, autoCleanup: false }); keyboard.setup(); keyboard.cleanup(); keyboard.cleanup(); // Second call expect(pause).toHaveBeenCalledTimes(1); // Only once }); }); describe('waitForKey', () => { it('should reject if not set up', async () => { const mockInput = { isTTY: false, } as unknown as NodeJS.ReadStream; const keyboard = createKeyboardInput({ input: mockInput, autoCleanup: false }); await expect(keyboard.waitForKey()).rejects.toThrow('Keyboard not set up'); }); it('should resolve with key event when data received', async () => { const dataHandlers: ((data: Buffer) => void)[] = []; const mockInput = { isTTY: true, isRaw: false, setRawMode: vi.fn(), resume: vi.fn(), on: vi.fn((event: string, handler: (data: Buffer) => void) => { if (event === 'data') { dataHandlers.push(handler); } }), removeListener: vi.fn(), } as unknown as NodeJS.ReadStream; const keyboard = createKeyboardInput({ input: mockInput, autoCleanup: false }); keyboard.setup(); // Start waiting for key const keyPromise = keyboard.waitForKey(); // Simulate key press expect(dataHandlers.length).toBe(1); dataHandlers[0](Buffer.from('a')); const event = await keyPromise; expect(event.key).toBe('a'); }); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/littlebearapps/wp-navigator-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server