@agor/stores (37 tests): - theme: 6 (17 themes, 3 groups, no duplicates) - notifications: 11 (types, rate limiter, window expiry) - health: 20 (scoring, burn rate, context pressure, tool tracking) Electrobun stores (90 tests): - agent-store: 27 (seqId, dedup, double-start guard, persistence) - workspace-store: 17 (CRUD, derived state, aggregates) - plugin-store: 14 (commands, events, permissions, meta) - keybinding-store: 18 (defaults, chords, conflicts, capture) Hardening (39 tests): - durable-sequencing: 10 (monotonic, dedup, restore) - file-conflict: 10 (mtime, atomic write, workflows) - backpressure: 7 (paste 64KB, buffer 50MB, line 10MB) - retention: 7 (count, age, running protected) - channel-acl: 9 (join/leave, rejection, isolation) Total across all suites: 1,020+ tests
211 lines
8.8 KiB
TypeScript
211 lines
8.8 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', () => {
|
|
// 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');
|
|
});
|
|
});
|