// Tests for Electrobun keybinding-store — pure logic. // Uses bun:test. Tests default bindings, chord serialization, conflict detection. import { describe, it, expect, beforeEach } from 'bun:test'; // ── Replicated types ────────────────────────────────────────────────────────── interface Keybinding { id: string; label: string; category: 'Global' | 'Navigation' | 'Terminal' | 'Settings'; chord: string; defaultChord: string; } // ── Default bindings (replicated from keybinding-store.svelte.ts) ─────────── const DEFAULTS: Keybinding[] = [ { id: 'palette', label: 'Command Palette', category: 'Global', chord: 'Ctrl+K', defaultChord: 'Ctrl+K' }, { id: 'settings', label: 'Open Settings', category: 'Global', chord: 'Ctrl+,', defaultChord: 'Ctrl+,' }, { id: 'group1', label: 'Switch to Group 1', category: 'Navigation', chord: 'Ctrl+1', defaultChord: 'Ctrl+1' }, { id: 'group2', label: 'Switch to Group 2', category: 'Navigation', chord: 'Ctrl+2', defaultChord: 'Ctrl+2' }, { id: 'group3', label: 'Switch to Group 3', category: 'Navigation', chord: 'Ctrl+3', defaultChord: 'Ctrl+3' }, { id: 'group4', label: 'Switch to Group 4', category: 'Navigation', chord: 'Ctrl+4', defaultChord: 'Ctrl+4' }, { id: 'newTerminal', label: 'New Terminal Tab', category: 'Terminal', chord: 'Ctrl+Shift+T', defaultChord: 'Ctrl+Shift+T' }, { id: 'closeTab', label: 'Close Terminal Tab', category: 'Terminal', chord: 'Ctrl+Shift+W', defaultChord: 'Ctrl+Shift+W' }, { id: 'nextTab', label: 'Next Terminal Tab', category: 'Terminal', chord: 'Ctrl+]', defaultChord: 'Ctrl+]' }, { id: 'prevTab', label: 'Previous Terminal Tab', category: 'Terminal', chord: 'Ctrl+[', defaultChord: 'Ctrl+[' }, { id: 'search', label: 'Global Search', category: 'Global', chord: 'Ctrl+Shift+F', defaultChord: 'Ctrl+Shift+F' }, { id: 'notifications', label: 'Notification Center', category: 'Global', chord: 'Ctrl+Shift+N', defaultChord: 'Ctrl+Shift+N' }, { id: 'minimize', label: 'Minimize Window', category: 'Global', chord: 'Ctrl+M', defaultChord: 'Ctrl+M' }, { id: 'toggleFiles', label: 'Toggle Files Tab', category: 'Navigation', chord: 'Ctrl+Shift+E', defaultChord: 'Ctrl+Shift+E' }, { id: 'toggleMemory', label: 'Toggle Memory Tab', category: 'Navigation', chord: 'Ctrl+Shift+M', defaultChord: 'Ctrl+Shift+M' }, { id: 'reload', label: 'Reload App', category: 'Settings', chord: 'Ctrl+R', defaultChord: 'Ctrl+R' }, ]; // ── Chord serialization (replicated) ───────────────────────────────────────── interface MockKeyboardEvent { ctrlKey: boolean; metaKey: boolean; shiftKey: boolean; altKey: boolean; key: string; } function chordFromEvent(e: MockKeyboardEvent): string { const parts: string[] = []; if (e.ctrlKey || e.metaKey) parts.push('Ctrl'); if (e.shiftKey) parts.push('Shift'); if (e.altKey) parts.push('Alt'); const key = e.key === ' ' ? 'Space' : e.key; if (!['Control', 'Shift', 'Alt', 'Meta'].includes(key)) { parts.push(key.length === 1 ? key.toUpperCase() : key); } return parts.join('+'); } // ── Store logic (replicated without runes) ────────────────────────────────── function createKeybindingState() { let bindings: Keybinding[] = DEFAULTS.map(b => ({ ...b })); return { getBindings: () => bindings, setChord(id: string, chord: string): void { bindings = bindings.map(b => b.id === id ? { ...b, chord } : b); }, resetChord(id: string): void { const def = DEFAULTS.find(b => b.id === id); if (!def) return; bindings = bindings.map(b => b.id === id ? { ...b, chord: def.defaultChord } : b); }, findConflicts(chord: string, excludeId?: string): Keybinding[] { return bindings.filter(b => b.chord === chord && b.id !== excludeId); }, }; } // ── Tests ─────────────────────────────────────────────────────────────────── describe('default bindings', () => { it('has exactly 16 default bindings', () => { expect(DEFAULTS).toHaveLength(16); }); it('all bindings have unique ids', () => { const ids = DEFAULTS.map(b => b.id); expect(new Set(ids).size).toBe(ids.length); }); it('all chords match defaultChord initially', () => { for (const b of DEFAULTS) { expect(b.chord).toBe(b.defaultChord); } }); it('covers all 4 categories', () => { const categories = new Set(DEFAULTS.map(b => b.category)); expect(categories.has('Global')).toBe(true); expect(categories.has('Navigation')).toBe(true); expect(categories.has('Terminal')).toBe(true); expect(categories.has('Settings')).toBe(true); }); it('command palette is Ctrl+K', () => { const palette = DEFAULTS.find(b => b.id === 'palette'); expect(palette?.chord).toBe('Ctrl+K'); }); }); describe('chordFromEvent', () => { it('serializes Ctrl+K', () => { expect(chordFromEvent({ ctrlKey: true, metaKey: false, shiftKey: false, altKey: false, key: 'k' })).toBe('Ctrl+K'); }); it('serializes Ctrl+Shift+F', () => { expect(chordFromEvent({ ctrlKey: true, metaKey: false, shiftKey: true, altKey: false, key: 'f' })).toBe('Ctrl+Shift+F'); }); it('serializes Alt+1', () => { expect(chordFromEvent({ ctrlKey: false, metaKey: false, shiftKey: false, altKey: true, key: '1' })).toBe('Alt+1'); }); it('maps space to Space', () => { expect(chordFromEvent({ ctrlKey: true, metaKey: false, shiftKey: false, altKey: false, key: ' ' })).toBe('Ctrl+Space'); }); it('ignores pure modifier keys', () => { expect(chordFromEvent({ ctrlKey: true, metaKey: false, shiftKey: false, altKey: false, key: 'Control' })).toBe('Ctrl'); }); it('metaKey treated as Ctrl', () => { expect(chordFromEvent({ ctrlKey: false, metaKey: true, shiftKey: false, altKey: false, key: 'k' })).toBe('Ctrl+K'); }); it('preserves multi-char key names', () => { expect(chordFromEvent({ ctrlKey: false, metaKey: false, shiftKey: false, altKey: false, key: 'Escape' })).toBe('Escape'); }); }); describe('setChord / resetChord', () => { let state: ReturnType; beforeEach(() => { state = createKeybindingState(); }); it('setChord updates the binding', () => { state.setChord('palette', 'Ctrl+P'); const b = state.getBindings().find(b => b.id === 'palette'); expect(b?.chord).toBe('Ctrl+P'); expect(b?.defaultChord).toBe('Ctrl+K'); // default unchanged }); it('resetChord restores default', () => { state.setChord('palette', 'Ctrl+P'); state.resetChord('palette'); const b = state.getBindings().find(b => b.id === 'palette'); expect(b?.chord).toBe('Ctrl+K'); }); it('resetChord ignores unknown id', () => { const before = state.getBindings().length; state.resetChord('nonexistent'); expect(state.getBindings().length).toBe(before); }); }); describe('conflict detection', () => { let state: ReturnType; beforeEach(() => { state = createKeybindingState(); }); it('detects conflict when two bindings share a chord', () => { state.setChord('settings', 'Ctrl+K'); // same as palette const conflicts = state.findConflicts('Ctrl+K', 'settings'); expect(conflicts).toHaveLength(1); expect(conflicts[0].id).toBe('palette'); }); it('no conflict when chord is unique', () => { state.setChord('palette', 'Ctrl+Shift+P'); const conflicts = state.findConflicts('Ctrl+Shift+P', 'palette'); expect(conflicts).toHaveLength(0); }); it('excludes self from conflict check', () => { const conflicts = state.findConflicts('Ctrl+K', 'palette'); expect(conflicts).toHaveLength(0); }); it('finds multiple conflicts', () => { state.setChord('search', 'Ctrl+K'); state.setChord('reload', 'Ctrl+K'); const conflicts = state.findConflicts('Ctrl+K', 'settings'); expect(conflicts).toHaveLength(3); // palette, search, reload }); }); describe('capture mode', () => { it('chordFromEvent records full chord for capture', () => { // Simulate user pressing Ctrl+Shift+X in capture mode const chord = chordFromEvent({ ctrlKey: true, metaKey: false, shiftKey: true, altKey: false, key: 'x', }); expect(chord).toBe('Ctrl+Shift+X'); }); });