agent-orchestrator/ui-electrobun/tests/unit/keybinding-store.test.ts
Hibryda 1de6c93e01 feat(electrobun): settings overhaul — fonts, shells, providers, retention, chords
- Settings drawer: responsive width clamp(24rem, 45vw, 50rem)
- System font detection: fc-list for UI fonts (preferred sans-serif starred)
  and mono fonts (Nerd Fonts starred), fallback to hardcoded lists
- Scrollback: default 5000, min 1000, step 500
- Shell detection: system.shells RPC, pre-selects $SHELL login shell
- Provider enablement: provider.scan gates toggle, unavailable shown as N/A
- Session retention: count 0-100 (0=Keep all), age 0-365 (0=Forever)
- Chord keybindings: Ctrl+K → Ctrl+S style multi-key sequences,
  1s prefix wait, arrow separator display, 26 tests passing
2026-03-25 01:42:34 +01:00

254 lines
10 KiB
TypeScript

// 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<typeof createKeybindingState>;
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<typeof createKeybindingState>;
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', () => {
const chord = chordFromEvent({
ctrlKey: true, metaKey: false, shiftKey: true, altKey: false, key: 'x',
});
expect(chord).toBe('Ctrl+Shift+X');
});
});
// ── Chord sequence helpers ──────────────────────────────────────────────────
function chordParts(chord: string): string[] {
return chord.split(' ').filter(Boolean);
}
function formatChord(chord: string): string {
return chordParts(chord).join(' \u2192 ');
}
describe('chord sequences', () => {
it('chordParts splits single chord', () => {
expect(chordParts('Ctrl+K')).toEqual(['Ctrl+K']);
});
it('chordParts splits multi-key chord', () => {
expect(chordParts('Ctrl+K Ctrl+S')).toEqual(['Ctrl+K', 'Ctrl+S']);
});
it('formatChord adds arrow separator', () => {
expect(formatChord('Ctrl+K Ctrl+S')).toBe('Ctrl+K \u2192 Ctrl+S');
});
it('formatChord passes through single chord', () => {
expect(formatChord('Ctrl+Shift+F')).toBe('Ctrl+Shift+F');
});
it('setChord stores chord sequence', () => {
const state = createKeybindingState();
state.setChord('palette', 'Ctrl+K Ctrl+P');
const b = state.getBindings().find(b => b.id === 'palette');
expect(b?.chord).toBe('Ctrl+K Ctrl+P');
});
it('conflict detection works with chord sequences', () => {
const state = createKeybindingState();
state.setChord('palette', 'Ctrl+K Ctrl+P');
state.setChord('settings', 'Ctrl+K Ctrl+P');
const conflicts = state.findConflicts('Ctrl+K Ctrl+P', 'palette');
expect(conflicts).toHaveLength(1);
expect(conflicts[0].id).toBe('settings');
});
});