Something went wrong. Try again.
Ghostty for the web with xterm.js API compatibility
Something went wrong. Try again.
17 kB · 613 lines
TypeScript
at task8
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614/** * Unit tests for InputHandler */
import { describe, test, expect, beforeAll, beforeEach, mock } from 'bun:test';import { InputHandler } from './input-handler';import { Ghostty } from './ghostty';import { Key, KeyAction, Mods } from './types';
// Mock DOM types for testinginterface MockKeyboardEvent { code: string; key: string; ctrlKey: boolean; altKey: boolean; shiftKey: boolean; metaKey: boolean; repeat: boolean; preventDefault: () => void; stopPropagation: () => void;}
interface MockHTMLElement { addEventListener: (event: string, handler: (e: MockKeyboardEvent) => void) => void; removeEventListener: (event: string, handler: (e: MockKeyboardEvent) => void) => void;}
// Helper to create mock keyboard eventfunction createKeyEvent( code: string, key: string, modifiers: { ctrl?: boolean; alt?: boolean; shift?: boolean; meta?: boolean } = {}): MockKeyboardEvent { return { code, key, ctrlKey: modifiers.ctrl ?? false, altKey: modifiers.alt ?? false, shiftKey: modifiers.shift ?? false, metaKey: modifiers.meta ?? false, repeat: false, preventDefault: mock(() => {}), stopPropagation: mock(() => {}), };}
// Helper to create mock containerfunction createMockContainer(): MockHTMLElement & { _listeners: Map<string, ((e: MockKeyboardEvent) => void)[]> } { const listeners = new Map<string, ((e: MockKeyboardEvent) => void)[]>(); return { _listeners: listeners, addEventListener(event: string, handler: (e: MockKeyboardEvent) => void) { if (!listeners.has(event)) { listeners.set(event, []); } listeners.get(event)!.push(handler); }, removeEventListener(event: string, handler: (e: MockKeyboardEvent) => void) { const handlers = listeners.get(event); if (handlers) { const index = handlers.indexOf(handler); if (index >= 0) { handlers.splice(index, 1); } } }, };}
// Helper to simulate key eventfunction simulateKey( container: ReturnType<typeof createMockContainer>, event: MockKeyboardEvent): void { const handlers = container._listeners.get('keydown') || []; for (const handler of handlers) { handler(event); }}
describe('InputHandler', () => { let ghostty: Ghostty; let container: ReturnType<typeof createMockContainer>; let dataReceived: string[]; let bellCalled: boolean;
beforeAll(async () => { // Load WASM once for all tests (expensive operation) const wasmPath = new URL('../ghostty-vt.wasm', import.meta.url).href; ghostty = await Ghostty.load(wasmPath); });
beforeEach(() => { // Create mock container for each test container = createMockContainer(); // Reset data tracking dataReceived = []; bellCalled = false; });
describe('Constructor and Lifecycle', () => { test('creates handler and attaches listeners', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
expect(handler.isActive()).toBe(true); expect(container._listeners.has('keydown')).toBe(true); expect(container._listeners.get('keydown')!.length).toBe(1); });
test('dispose removes listeners and marks inactive', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
handler.dispose();
expect(handler.isActive()).toBe(false); expect(container._listeners.get('keydown')!.length).toBe(0); });
test('dispose is idempotent', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
handler.dispose(); handler.dispose(); // Second call should not throw
expect(handler.isActive()).toBe(false); }); });
describe('Printable Characters', () => { test('encodes lowercase letters', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('KeyA', 'a')); expect(dataReceived).toEqual(['a']);
simulateKey(container, createKeyEvent('KeyZ', 'z')); expect(dataReceived).toEqual(['a', 'z']); });
test('encodes uppercase letters (with shift)', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('KeyA', 'A', { shift: true })); expect(dataReceived).toEqual(['A']); });
test('encodes digits', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('Digit0', '0')); simulateKey(container, createKeyEvent('Digit5', '5')); simulateKey(container, createKeyEvent('Digit9', '9')); expect(dataReceived).toEqual(['0', '5', '9']); });
test('encodes punctuation', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('Comma', ',')); simulateKey(container, createKeyEvent('Period', '.')); simulateKey(container, createKeyEvent('Slash', '/')); expect(dataReceived).toEqual([',', '.', '/']); });
test('encodes space', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('Space', ' ')); expect(dataReceived).toEqual([' ']); }); });
describe('Control Characters', () => { test('encodes Ctrl+A', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('KeyA', 'a', { ctrl: true })); expect(dataReceived.length).toBe(1); // Ctrl+A should produce 0x01 expect(dataReceived[0].charCodeAt(0)).toBe(0x01); });
test('encodes Ctrl+C', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('KeyC', 'c', { ctrl: true })); expect(dataReceived.length).toBe(1); // Ctrl+C should produce 0x03 expect(dataReceived[0].charCodeAt(0)).toBe(0x03); });
test('encodes Ctrl+D', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('KeyD', 'd', { ctrl: true })); expect(dataReceived.length).toBe(1); // Ctrl+D should produce 0x04 expect(dataReceived[0].charCodeAt(0)).toBe(0x04); });
test('encodes Ctrl+Z', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('KeyZ', 'z', { ctrl: true })); expect(dataReceived.length).toBe(1); // Ctrl+Z should produce 0x1A (26) expect(dataReceived[0].charCodeAt(0)).toBe(0x1A); }); });
describe('Special Keys', () => { test('encodes Enter', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('Enter', 'Enter')); expect(dataReceived.length).toBe(1); // Enter should produce \r (0x0D) expect(dataReceived[0]).toBe('\r'); });
test('encodes Tab', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('Tab', 'Tab')); expect(dataReceived.length).toBe(1); // Tab should produce \t (0x09) expect(dataReceived[0]).toBe('\t'); });
test('encodes Escape', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('Escape', 'Escape')); expect(dataReceived.length).toBe(1); // Escape should produce ESC (0x1B) expect(dataReceived[0].charCodeAt(0)).toBe(0x1B); });
test('encodes Backspace', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('Backspace', 'Backspace')); expect(dataReceived.length).toBe(1); // Backspace should produce 0x7F (DEL) or 0x08 (BS) const code = dataReceived[0].charCodeAt(0); expect(code === 0x7F || code === 0x08).toBe(true); }); });
describe('Arrow Keys', () => { test('encodes Up arrow', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('ArrowUp', 'ArrowUp')); expect(dataReceived.length).toBe(1); // Arrow keys produce ESC[A, ESC[B, ESC[C, ESC[D or ESCOA, ESCOB, ESCOC, ESCOD expect(dataReceived[0]).toMatch(/\x1b(\[A|OA)/); });
test('encodes Down arrow', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('ArrowDown', 'ArrowDown')); expect(dataReceived.length).toBe(1); expect(dataReceived[0]).toMatch(/\x1b(\[B|OB)/); });
test('encodes Left arrow', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('ArrowLeft', 'ArrowLeft')); expect(dataReceived.length).toBe(1); expect(dataReceived[0]).toMatch(/\x1b(\[D|OD)/); });
test('encodes Right arrow', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('ArrowRight', 'ArrowRight')); expect(dataReceived.length).toBe(1); expect(dataReceived[0]).toMatch(/\x1b(\[C|OC)/); }); });
describe('Function Keys', () => { test('encodes F1', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('F1', 'F1')); expect(dataReceived.length).toBe(1); // F1 produces ESC[11~ or ESCOP expect(dataReceived[0]).toMatch(/\x1b(\[11~|OP)/); });
test('encodes F12', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('F12', 'F12')); expect(dataReceived.length).toBe(1); // F12 produces ESC[24~ expect(dataReceived[0].includes('\x1b')).toBe(true); }); });
describe('Navigation Keys', () => { test('encodes Home', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('Home', 'Home')); expect(dataReceived.length).toBe(1); expect(dataReceived[0].includes('\x1b')).toBe(true); });
test('encodes End', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('End', 'End')); expect(dataReceived.length).toBe(1); expect(dataReceived[0].includes('\x1b')).toBe(true); });
test('encodes PageUp', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('PageUp', 'PageUp')); expect(dataReceived.length).toBe(1); expect(dataReceived[0].includes('\x1b')).toBe(true); });
test('encodes PageDown', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('PageDown', 'PageDown')); expect(dataReceived.length).toBe(1); expect(dataReceived[0].includes('\x1b')).toBe(true); });
test('encodes Delete', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('Delete', 'Delete')); expect(dataReceived.length).toBe(1); expect(dataReceived[0].includes('\x1b')).toBe(true); });
test('encodes Insert', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('Insert', 'Insert')); expect(dataReceived.length).toBe(1); expect(dataReceived[0].includes('\x1b')).toBe(true); }); });
describe('Event Prevention', () => { test('prevents default on printable characters', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
const event = createKeyEvent('KeyA', 'a'); simulateKey(container, event); expect(event.preventDefault).toHaveBeenCalled(); });
test('prevents default on special keys', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
const event = createKeyEvent('Enter', 'Enter'); simulateKey(container, event); expect(event.preventDefault).toHaveBeenCalled(); });
test('prevents default on Ctrl+keys', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
const event = createKeyEvent('KeyC', 'c', { ctrl: true }); simulateKey(container, event); expect(event.preventDefault).toHaveBeenCalled(); }); });
describe('Unknown Keys', () => { test('ignores unmapped keys', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
// Simulate a key that's not in KEY_MAP simulateKey(container, createKeyEvent('Unknown', 'Unknown')); // Should not crash or produce output expect(dataReceived.length).toBe(0); }); });
describe('Modifier Combinations', () => { test('handles Ctrl+Shift combinations', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('KeyA', 'A', { ctrl: true, shift: true })); expect(dataReceived.length).toBe(1); // Should still encode something expect(dataReceived[0].length).toBeGreaterThan(0); });
test('handles Alt combinations', () => { const handler = new InputHandler( ghostty, container as any, (data) => dataReceived.push(data), () => { bellCalled = true; } );
simulateKey(container, createKeyEvent('KeyA', 'a', { alt: true })); expect(dataReceived.length).toBe(1); // Alt+A often produces ESC a or similar expect(dataReceived[0].length).toBeGreaterThan(0); }); });});