import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { createKeyboardController } from './keyboard' import type { RouteName } from './keyboard' // ── Helpers ────────────────────────────────────────────────────────────────── function makeOpts() { const opts = { go: vi.fn(), openPalette: vi.fn(), closePalette: vi.fn(), isPaletteOpen: vi.fn().mockReturnValue(false) as () => boolean, cursor: { move: vi.fn(), activate: vi.fn(), }, } return opts } type Opts = ReturnType function makeCtrl(opts: Opts) { // Cast to KeyboardOpts — vi.fn() satisfies the shape at runtime. return createKeyboardController(opts as Parameters[0]) } function makeKey( key: string, init: Partial & { target?: EventTarget } = {}, ): KeyboardEvent { const { target: tgt, ...rest } = init const e = new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true, ...rest }) if (tgt) { Object.defineProperty(e, 'target', { value: tgt, configurable: true }) } return e } // ── Tests ──────────────────────────────────────────────────────────────────── describe('g + letter navigation', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('g then h within 800 ms → go("home") once; gPending reset', () => { const opts = makeOpts() const ctrl = makeCtrl(opts) ctrl.onKeyDown(makeKey('g')) vi.advanceTimersByTime(400) // still within 800 ms window ctrl.onKeyDown(makeKey('h')) expect(opts.go).toHaveBeenCalledOnce() expect(opts.go).toHaveBeenCalledWith('home' satisfies RouteName) // gPending should be cleared — a second 'h' without 'g' should NOT call go opts.go.mockClear() ctrl.onKeyDown(makeKey('h')) expect(opts.go).not.toHaveBeenCalled() }) it('g then idle 1 s, then h → no call', () => { const opts = makeOpts() const ctrl = makeCtrl(opts) ctrl.onKeyDown(makeKey('g')) vi.advanceTimersByTime(1000) // timeout fires, gPending cleared ctrl.onKeyDown(makeKey('h')) expect(opts.go).not.toHaveBeenCalled() }) it('g then p → go("projects")', () => { const opts = makeOpts() const ctrl = makeCtrl(opts) ctrl.onKeyDown(makeKey('g')) ctrl.onKeyDown(makeKey('p')) expect(opts.go).toHaveBeenCalledWith('projects' satisfies RouteName) }) it('g then s → go("stats")', () => { const opts = makeOpts() const ctrl = makeCtrl(opts) ctrl.onKeyDown(makeKey('g')) ctrl.onKeyDown(makeKey('s')) expect(opts.go).toHaveBeenCalledWith('stats' satisfies RouteName) }) it('g then i → go("health")', () => { const opts = makeOpts() const ctrl = makeCtrl(opts) ctrl.onKeyDown(makeKey('g')) ctrl.onKeyDown(makeKey('i')) expect(opts.go).toHaveBeenCalledWith('health' satisfies RouteName) }) it('g then h while palette is open → no go() (palette guards prevent navigation mid-query)', () => { const opts = makeOpts() opts.isPaletteOpen = vi.fn().mockReturnValue(true) const ctrl = makeCtrl(opts) ctrl.onKeyDown(makeKey('g')) ctrl.onKeyDown(makeKey('h')) expect(opts.go).not.toHaveBeenCalled() }) it('g while target is → no gPending set, follow-up h does nothing', () => { const opts = makeOpts() const ctrl = makeCtrl(opts) const input = document.createElement('input') ctrl.onKeyDown(makeKey('g', { target: input })) ctrl.onKeyDown(makeKey('h', { target: input })) expect(opts.go).not.toHaveBeenCalled() }) it('teardown clears pending timer', () => { const opts = makeOpts() const ctrl = makeCtrl(opts) ctrl.onKeyDown(makeKey('g')) ctrl.teardown() // should not throw; timer cleared vi.advanceTimersByTime(1000) ctrl.onKeyDown(makeKey('h')) expect(opts.go).not.toHaveBeenCalled() }) }) describe('j / k cursor movement', () => { it('j while body is target → cursor.move(1)', () => { const opts = makeOpts() const ctrl = makeCtrl(opts) ctrl.onKeyDown(makeKey('j', { target: document.body })) expect(opts.cursor.move).toHaveBeenCalledWith(1) }) it('k while body is target → cursor.move(-1)', () => { const opts = makeOpts() const ctrl = makeCtrl(opts) ctrl.onKeyDown(makeKey('k', { target: document.body })) expect(opts.cursor.move).toHaveBeenCalledWith(-1) }) it('j while target is → no cursor.move', () => { const opts = makeOpts() const ctrl = makeCtrl(opts) const input = document.createElement('input') ctrl.onKeyDown(makeKey('j', { target: input })) expect(opts.cursor.move).not.toHaveBeenCalled() }) it('j while target is