2026-02-22 18:46:43 +01:00
|
|
|
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
|
|
|
import { renderHook } from '@testing-library/react'
|
|
|
|
|
import { useAppKeyboard } from './useAppKeyboard'
|
|
|
|
|
|
feat: full-text search with qmd backend and SearchPanel UI (Cmd+Shift+F) (#64)
* feat: add search backend (qmd CLI) and design file
- Add search.rs: qmd integration for keyword (BM25), semantic (vsearch),
and hybrid search modes via CLI JSON output
- Register search_vault Tauri command in lib.rs
- Design file with 3 frames: empty, results, no-results states
- Fix pre-existing test failures: install @tauri-apps/plugin-opener,
add mock in test setup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add SearchPanel UI with Cmd+Shift+F shortcut
- SearchPanel component: full-text search overlay with keyword/semantic
mode toggle, debounced search (200ms), arrow key navigation, result
count + elapsed time display, note type badges from vault entries
- Cmd+Shift+F shortcut registered in useAppKeyboard
- Mock search_vault handler in mock-tauri for browser dev mode
- SearchResult/SearchResponse types in types.ts
- Tests: 15 new tests for SearchPanel + 2 for keyboard shortcut
- scrollIntoView mock in test setup for jsdom compatibility
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: correct SearchPanel imports for Tauri/browser dual-mode
Use invoke from @tauri-apps/api/core and mockInvoke from mock-tauri
with a searchCall wrapper, fixing the TypeScript build error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make test_detect_collection_fallback robust when qmd not installed
* fix: reset localStorage between App tests to prevent view mode state leak
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:09:50 +01:00
|
|
|
function fireKey(key: string, mods: { altKey?: boolean; metaKey?: boolean; ctrlKey?: boolean; shiftKey?: boolean } = {}) {
|
2026-02-22 18:46:43 +01:00
|
|
|
const event = new KeyboardEvent('keydown', {
|
|
|
|
|
key,
|
|
|
|
|
altKey: mods.altKey ?? false,
|
|
|
|
|
metaKey: mods.metaKey ?? false,
|
|
|
|
|
ctrlKey: mods.ctrlKey ?? false,
|
feat: full-text search with qmd backend and SearchPanel UI (Cmd+Shift+F) (#64)
* feat: add search backend (qmd CLI) and design file
- Add search.rs: qmd integration for keyword (BM25), semantic (vsearch),
and hybrid search modes via CLI JSON output
- Register search_vault Tauri command in lib.rs
- Design file with 3 frames: empty, results, no-results states
- Fix pre-existing test failures: install @tauri-apps/plugin-opener,
add mock in test setup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add SearchPanel UI with Cmd+Shift+F shortcut
- SearchPanel component: full-text search overlay with keyword/semantic
mode toggle, debounced search (200ms), arrow key navigation, result
count + elapsed time display, note type badges from vault entries
- Cmd+Shift+F shortcut registered in useAppKeyboard
- Mock search_vault handler in mock-tauri for browser dev mode
- SearchResult/SearchResponse types in types.ts
- Tests: 15 new tests for SearchPanel + 2 for keyboard shortcut
- scrollIntoView mock in test setup for jsdom compatibility
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: correct SearchPanel imports for Tauri/browser dual-mode
Use invoke from @tauri-apps/api/core and mockInvoke from mock-tauri
with a searchCall wrapper, fixing the TypeScript build error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make test_detect_collection_fallback robust when qmd not installed
* fix: reset localStorage between App tests to prevent view mode state leak
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:09:50 +01:00
|
|
|
shiftKey: mods.shiftKey ?? false,
|
2026-02-22 18:46:43 +01:00
|
|
|
bubbles: true,
|
|
|
|
|
cancelable: true,
|
|
|
|
|
})
|
|
|
|
|
window.dispatchEvent(event)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function makeActions() {
|
|
|
|
|
return {
|
|
|
|
|
onQuickOpen: vi.fn(),
|
2026-02-24 23:43:58 +01:00
|
|
|
onCommandPalette: vi.fn(),
|
feat: full-text search with qmd backend and SearchPanel UI (Cmd+Shift+F) (#64)
* feat: add search backend (qmd CLI) and design file
- Add search.rs: qmd integration for keyword (BM25), semantic (vsearch),
and hybrid search modes via CLI JSON output
- Register search_vault Tauri command in lib.rs
- Design file with 3 frames: empty, results, no-results states
- Fix pre-existing test failures: install @tauri-apps/plugin-opener,
add mock in test setup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add SearchPanel UI with Cmd+Shift+F shortcut
- SearchPanel component: full-text search overlay with keyword/semantic
mode toggle, debounced search (200ms), arrow key navigation, result
count + elapsed time display, note type badges from vault entries
- Cmd+Shift+F shortcut registered in useAppKeyboard
- Mock search_vault handler in mock-tauri for browser dev mode
- SearchResult/SearchResponse types in types.ts
- Tests: 15 new tests for SearchPanel + 2 for keyboard shortcut
- scrollIntoView mock in test setup for jsdom compatibility
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: correct SearchPanel imports for Tauri/browser dual-mode
Use invoke from @tauri-apps/api/core and mockInvoke from mock-tauri
with a searchCall wrapper, fixing the TypeScript build error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make test_detect_collection_fallback robust when qmd not installed
* fix: reset localStorage between App tests to prevent view mode state leak
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:09:50 +01:00
|
|
|
onSearch: vi.fn(),
|
2026-02-22 18:46:43 +01:00
|
|
|
onCreateNote: vi.fn(),
|
2026-03-02 03:08:15 +01:00
|
|
|
onOpenDailyNote: vi.fn(),
|
2026-02-22 18:46:43 +01:00
|
|
|
onSave: vi.fn(),
|
|
|
|
|
onOpenSettings: vi.fn(),
|
2026-04-06 12:21:56 +02:00
|
|
|
onDeleteNote: vi.fn(),
|
2026-02-22 18:46:43 +01:00
|
|
|
onArchiveNote: vi.fn(),
|
|
|
|
|
onSetViewMode: vi.fn(),
|
2026-02-28 12:41:57 +01:00
|
|
|
onZoomIn: vi.fn(),
|
|
|
|
|
onZoomOut: vi.fn(),
|
|
|
|
|
onZoomReset: vi.fn(),
|
2026-02-22 18:46:43 +01:00
|
|
|
activeTabPathRef: { current: '/vault/test.md' } as React.MutableRefObject<string | null>,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('useAppKeyboard', () => {
|
|
|
|
|
afterEach(() => vi.restoreAllMocks())
|
|
|
|
|
|
2026-02-23 08:13:32 +01:00
|
|
|
it('Cmd+1 sets view mode to editor-only', () => {
|
2026-02-22 18:46:43 +01:00
|
|
|
const actions = makeActions()
|
|
|
|
|
renderHook(() => useAppKeyboard(actions))
|
2026-02-23 08:13:32 +01:00
|
|
|
fireKey('1', { metaKey: true })
|
2026-02-22 18:46:43 +01:00
|
|
|
expect(actions.onSetViewMode).toHaveBeenCalledWith('editor-only')
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-23 08:13:32 +01:00
|
|
|
it('Cmd+2 sets view mode to editor-list', () => {
|
2026-02-22 18:46:43 +01:00
|
|
|
const actions = makeActions()
|
|
|
|
|
renderHook(() => useAppKeyboard(actions))
|
2026-02-23 08:13:32 +01:00
|
|
|
fireKey('2', { metaKey: true })
|
2026-02-22 18:46:43 +01:00
|
|
|
expect(actions.onSetViewMode).toHaveBeenCalledWith('editor-list')
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-23 08:13:32 +01:00
|
|
|
it('Cmd+3 sets view mode to all', () => {
|
2026-02-22 18:46:43 +01:00
|
|
|
const actions = makeActions()
|
|
|
|
|
renderHook(() => useAppKeyboard(actions))
|
2026-02-23 08:13:32 +01:00
|
|
|
fireKey('3', { metaKey: true })
|
2026-02-22 18:46:43 +01:00
|
|
|
expect(actions.onSetViewMode).toHaveBeenCalledWith('all')
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-23 08:13:32 +01:00
|
|
|
it('does not fire view mode when Cmd+Alt pressed', () => {
|
2026-02-22 18:46:43 +01:00
|
|
|
const actions = makeActions()
|
|
|
|
|
renderHook(() => useAppKeyboard(actions))
|
2026-02-23 08:13:32 +01:00
|
|
|
fireKey('1', { metaKey: true, altKey: true })
|
2026-02-22 18:46:43 +01:00
|
|
|
expect(actions.onSetViewMode).not.toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('Cmd+P triggers quick open', () => {
|
|
|
|
|
const actions = makeActions()
|
|
|
|
|
renderHook(() => useAppKeyboard(actions))
|
|
|
|
|
fireKey('p', { metaKey: true })
|
|
|
|
|
expect(actions.onQuickOpen).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('Cmd+N triggers create note', () => {
|
|
|
|
|
const actions = makeActions()
|
|
|
|
|
renderHook(() => useAppKeyboard(actions))
|
|
|
|
|
fireKey('n', { metaKey: true })
|
|
|
|
|
expect(actions.onCreateNote).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-03 17:20:33 +02:00
|
|
|
it('Cmd+D triggers toggle favorite on active note', () => {
|
|
|
|
|
const actions = makeActions()
|
|
|
|
|
actions.onToggleFavorite = vi.fn()
|
|
|
|
|
renderHook(() => useAppKeyboard(actions))
|
|
|
|
|
fireKey('d', { metaKey: true })
|
|
|
|
|
expect(actions.onToggleFavorite).toHaveBeenCalledWith('/vault/test.md')
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-02 03:08:15 +01:00
|
|
|
it('Cmd+J triggers open daily note', () => {
|
|
|
|
|
const actions = makeActions()
|
|
|
|
|
renderHook(() => useAppKeyboard(actions))
|
|
|
|
|
fireKey('j', { metaKey: true })
|
|
|
|
|
expect(actions.onOpenDailyNote).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-22 18:46:43 +01:00
|
|
|
it('Alt+4 does not trigger any view mode', () => {
|
|
|
|
|
const actions = makeActions()
|
|
|
|
|
renderHook(() => useAppKeyboard(actions))
|
|
|
|
|
fireKey('4', { altKey: true })
|
|
|
|
|
expect(actions.onSetViewMode).not.toHaveBeenCalled()
|
|
|
|
|
})
|
2026-02-24 23:43:58 +01:00
|
|
|
|
|
|
|
|
it('Cmd+K triggers command palette', () => {
|
|
|
|
|
const actions = makeActions()
|
|
|
|
|
renderHook(() => useAppKeyboard(actions))
|
|
|
|
|
fireKey('k', { metaKey: true })
|
|
|
|
|
expect(actions.onCommandPalette).toHaveBeenCalled()
|
|
|
|
|
})
|
feat: full-text search with qmd backend and SearchPanel UI (Cmd+Shift+F) (#64)
* feat: add search backend (qmd CLI) and design file
- Add search.rs: qmd integration for keyword (BM25), semantic (vsearch),
and hybrid search modes via CLI JSON output
- Register search_vault Tauri command in lib.rs
- Design file with 3 frames: empty, results, no-results states
- Fix pre-existing test failures: install @tauri-apps/plugin-opener,
add mock in test setup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add SearchPanel UI with Cmd+Shift+F shortcut
- SearchPanel component: full-text search overlay with keyword/semantic
mode toggle, debounced search (200ms), arrow key navigation, result
count + elapsed time display, note type badges from vault entries
- Cmd+Shift+F shortcut registered in useAppKeyboard
- Mock search_vault handler in mock-tauri for browser dev mode
- SearchResult/SearchResponse types in types.ts
- Tests: 15 new tests for SearchPanel + 2 for keyboard shortcut
- scrollIntoView mock in test setup for jsdom compatibility
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: correct SearchPanel imports for Tauri/browser dual-mode
Use invoke from @tauri-apps/api/core and mockInvoke from mock-tauri
with a searchCall wrapper, fixing the TypeScript build error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make test_detect_collection_fallback robust when qmd not installed
* fix: reset localStorage between App tests to prevent view mode state leak
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:09:50 +01:00
|
|
|
|
|
|
|
|
it('Cmd+Shift+F triggers search', () => {
|
|
|
|
|
const actions = makeActions()
|
|
|
|
|
renderHook(() => useAppKeyboard(actions))
|
|
|
|
|
fireKey('f', { metaKey: true, shiftKey: true })
|
|
|
|
|
expect(actions.onSearch).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('Cmd+Shift+F does not trigger other shortcuts', () => {
|
|
|
|
|
const actions = makeActions()
|
|
|
|
|
renderHook(() => useAppKeyboard(actions))
|
|
|
|
|
fireKey('f', { metaKey: true, shiftKey: true })
|
|
|
|
|
expect(actions.onQuickOpen).not.toHaveBeenCalled()
|
|
|
|
|
expect(actions.onCreateNote).not.toHaveBeenCalled()
|
|
|
|
|
})
|
2026-02-25 18:30:20 +01:00
|
|
|
|
|
|
|
|
function withFocusedInput(fn: () => void) {
|
|
|
|
|
const input = document.createElement('input')
|
|
|
|
|
document.body.appendChild(input)
|
|
|
|
|
input.focus()
|
|
|
|
|
try { fn() } finally { document.body.removeChild(input) }
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 12:21:56 +02:00
|
|
|
it('Cmd+Backspace does not delete note when text input is focused', () => {
|
2026-02-25 18:30:20 +01:00
|
|
|
const actions = makeActions()
|
|
|
|
|
renderHook(() => useAppKeyboard(actions))
|
|
|
|
|
withFocusedInput(() => {
|
|
|
|
|
fireKey('Backspace', { metaKey: true })
|
2026-04-06 12:21:56 +02:00
|
|
|
expect(actions.onDeleteNote).not.toHaveBeenCalled()
|
2026-02-25 18:30:20 +01:00
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-06 12:21:56 +02:00
|
|
|
it('Cmd+Backspace deletes note when no text input is focused', () => {
|
2026-02-25 18:30:20 +01:00
|
|
|
const actions = makeActions()
|
|
|
|
|
renderHook(() => useAppKeyboard(actions))
|
|
|
|
|
fireKey('Backspace', { metaKey: true })
|
2026-04-06 12:21:56 +02:00
|
|
|
expect(actions.onDeleteNote).toHaveBeenCalledWith('/vault/test.md')
|
2026-02-25 18:30:20 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('Cmd+K still works when text input is focused', () => {
|
|
|
|
|
const actions = makeActions()
|
|
|
|
|
renderHook(() => useAppKeyboard(actions))
|
|
|
|
|
withFocusedInput(() => {
|
|
|
|
|
fireKey('k', { metaKey: true })
|
|
|
|
|
expect(actions.onCommandPalette).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-02-28 12:41:57 +01:00
|
|
|
|
|
|
|
|
it('Cmd+= triggers zoom in', () => {
|
|
|
|
|
const actions = makeActions()
|
|
|
|
|
renderHook(() => useAppKeyboard(actions))
|
|
|
|
|
fireKey('=', { metaKey: true })
|
|
|
|
|
expect(actions.onZoomIn).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('Cmd++ triggers zoom in', () => {
|
|
|
|
|
const actions = makeActions()
|
|
|
|
|
renderHook(() => useAppKeyboard(actions))
|
|
|
|
|
fireKey('+', { metaKey: true })
|
|
|
|
|
expect(actions.onZoomIn).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('Cmd+- triggers zoom out', () => {
|
|
|
|
|
const actions = makeActions()
|
|
|
|
|
renderHook(() => useAppKeyboard(actions))
|
|
|
|
|
fireKey('-', { metaKey: true })
|
|
|
|
|
expect(actions.onZoomOut).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('Cmd+0 triggers zoom reset', () => {
|
|
|
|
|
const actions = makeActions()
|
|
|
|
|
renderHook(() => useAppKeyboard(actions))
|
|
|
|
|
fireKey('0', { metaKey: true })
|
|
|
|
|
expect(actions.onZoomReset).toHaveBeenCalled()
|
|
|
|
|
})
|
2026-03-02 05:00:29 +01:00
|
|
|
|
2026-04-02 11:28:04 +02:00
|
|
|
it('Cmd+Option+I triggers toggle AI chat', () => {
|
2026-03-02 05:00:29 +01:00
|
|
|
const actions = makeActions()
|
|
|
|
|
const onToggleAIChat = vi.fn()
|
|
|
|
|
renderHook(() => useAppKeyboard({ ...actions, onToggleAIChat }))
|
2026-04-02 11:28:04 +02:00
|
|
|
fireKey('i', { metaKey: true, altKey: true })
|
2026-03-02 05:00:29 +01:00
|
|
|
expect(onToggleAIChat).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-02 11:28:04 +02:00
|
|
|
it('Cmd+Option+I works when text input is focused', () => {
|
2026-03-02 05:00:29 +01:00
|
|
|
const actions = makeActions()
|
|
|
|
|
const onToggleAIChat = vi.fn()
|
|
|
|
|
renderHook(() => useAppKeyboard({ ...actions, onToggleAIChat }))
|
|
|
|
|
withFocusedInput(() => {
|
2026-04-02 11:28:04 +02:00
|
|
|
fireKey('i', { metaKey: true, altKey: true })
|
2026-03-02 05:00:29 +01:00
|
|
|
expect(onToggleAIChat).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-03-19 08:47:25 +01:00
|
|
|
|
2026-04-02 11:28:04 +02:00
|
|
|
it('Cmd+I does not trigger AI chat (reserved for italic)', () => {
|
|
|
|
|
const actions = makeActions()
|
|
|
|
|
const onToggleAIChat = vi.fn()
|
|
|
|
|
renderHook(() => useAppKeyboard({ ...actions, onToggleAIChat }))
|
|
|
|
|
fireKey('i', { metaKey: true })
|
|
|
|
|
expect(onToggleAIChat).not.toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-19 08:47:25 +01:00
|
|
|
it('Cmd+Shift+O triggers open in new window', () => {
|
|
|
|
|
const actions = makeActions()
|
|
|
|
|
const onOpenInNewWindow = vi.fn()
|
|
|
|
|
renderHook(() => useAppKeyboard({ ...actions, onOpenInNewWindow }))
|
|
|
|
|
fireKey('o', { metaKey: true, shiftKey: true })
|
|
|
|
|
expect(onOpenInNewWindow).toHaveBeenCalled()
|
|
|
|
|
})
|
2026-03-30 18:45:45 +02:00
|
|
|
|
|
|
|
|
it('Cmd+Shift+I triggers toggle inspector', () => {
|
|
|
|
|
const actions = makeActions()
|
|
|
|
|
const onToggleInspector = vi.fn()
|
|
|
|
|
renderHook(() => useAppKeyboard({ ...actions, onToggleInspector }))
|
|
|
|
|
fireKey('i', { metaKey: true, shiftKey: true })
|
|
|
|
|
expect(onToggleInspector).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('Cmd+Shift+I does not trigger AI chat toggle', () => {
|
|
|
|
|
const actions = makeActions()
|
|
|
|
|
const onToggleAIChat = vi.fn()
|
|
|
|
|
const onToggleInspector = vi.fn()
|
|
|
|
|
renderHook(() => useAppKeyboard({ ...actions, onToggleAIChat, onToggleInspector }))
|
|
|
|
|
fireKey('i', { metaKey: true, shiftKey: true })
|
|
|
|
|
expect(onToggleInspector).toHaveBeenCalled()
|
|
|
|
|
expect(onToggleAIChat).not.toHaveBeenCalled()
|
|
|
|
|
})
|
2026-04-02 11:28:04 +02:00
|
|
|
|
|
|
|
|
it('Cmd+Option+I does not trigger inspector toggle', () => {
|
|
|
|
|
const actions = makeActions()
|
|
|
|
|
const onToggleAIChat = vi.fn()
|
|
|
|
|
const onToggleInspector = vi.fn()
|
|
|
|
|
renderHook(() => useAppKeyboard({ ...actions, onToggleAIChat, onToggleInspector }))
|
|
|
|
|
fireKey('i', { metaKey: true, altKey: true })
|
|
|
|
|
expect(onToggleAIChat).toHaveBeenCalled()
|
|
|
|
|
expect(onToggleInspector).not.toHaveBeenCalled()
|
|
|
|
|
})
|
2026-02-22 18:46:43 +01:00
|
|
|
})
|