284 lines
9.8 KiB
TypeScript
284 lines
9.8 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
APP_COMMAND_IDS,
|
|
dispatchAppCommand,
|
|
executeAppCommand,
|
|
findShortcutCommandId,
|
|
findShortcutCommandIdForEvent,
|
|
isAppCommandId,
|
|
isNativeMenuCommandId,
|
|
resetAppCommandDispatchStateForTests,
|
|
type AppCommandHandlers,
|
|
} from './appCommandDispatcher'
|
|
import {
|
|
getDeterministicShortcutQaDefinition,
|
|
getShortcutEventInit,
|
|
} from './appCommandCatalog'
|
|
|
|
function makeHandlers(): AppCommandHandlers {
|
|
return {
|
|
onSetViewMode: vi.fn(),
|
|
onCreateNote: vi.fn(),
|
|
onCreateType: vi.fn(),
|
|
onQuickOpen: vi.fn(),
|
|
onSave: vi.fn(),
|
|
onOpenSettings: vi.fn(),
|
|
onToggleInspector: vi.fn(),
|
|
onCommandPalette: vi.fn(),
|
|
onZoomIn: vi.fn(),
|
|
onZoomOut: vi.fn(),
|
|
onZoomReset: vi.fn(),
|
|
onToggleOrganized: vi.fn(),
|
|
onToggleFavorite: vi.fn(),
|
|
onArchiveNote: vi.fn(),
|
|
onDeleteNote: vi.fn(),
|
|
onSearch: vi.fn(),
|
|
onToggleRawEditor: vi.fn(),
|
|
onToggleDiff: vi.fn(),
|
|
onToggleAIChat: vi.fn(),
|
|
onGoBack: vi.fn(),
|
|
onGoForward: vi.fn(),
|
|
onCheckForUpdates: vi.fn(),
|
|
onSelectFilter: vi.fn(),
|
|
onOpenVault: vi.fn(),
|
|
onRemoveActiveVault: vi.fn(),
|
|
onRestoreGettingStarted: vi.fn(),
|
|
onCommitPush: vi.fn(),
|
|
onPull: vi.fn(),
|
|
onResolveConflicts: vi.fn(),
|
|
onViewChanges: vi.fn(),
|
|
onInstallMcp: vi.fn(),
|
|
onOpenInNewWindow: vi.fn(),
|
|
onReloadVault: vi.fn(),
|
|
onRepairVault: vi.fn(),
|
|
onRestoreDeletedNote: vi.fn(),
|
|
activeTabPathRef: { current: '/vault/test.md' },
|
|
multiSelectionCommandRef: { current: null },
|
|
}
|
|
}
|
|
|
|
describe('appCommandDispatcher', () => {
|
|
afterEach(() => {
|
|
resetAppCommandDispatchStateForTests()
|
|
})
|
|
|
|
it('recognizes valid command ids', () => {
|
|
expect(isAppCommandId(APP_COMMAND_IDS.fileNewNote)).toBe(true)
|
|
expect(isAppCommandId('not-a-command')).toBe(false)
|
|
})
|
|
|
|
it('no longer recognizes the removed daily-note menu id', () => {
|
|
expect(isAppCommandId('file-daily-note')).toBe(false)
|
|
})
|
|
|
|
it('distinguishes native menu ids from keyboard-only ids', () => {
|
|
expect(isNativeMenuCommandId(APP_COMMAND_IDS.fileNewNote)).toBe(true)
|
|
expect(isNativeMenuCommandId(APP_COMMAND_IDS.noteToggleFavorite)).toBe(false)
|
|
})
|
|
|
|
it('finds raw editor and AI shortcuts from the shared catalog', () => {
|
|
expect(findShortcutCommandId('command-or-ctrl', 'o', 'KeyO')).toBe(APP_COMMAND_IDS.fileQuickOpen)
|
|
expect(findShortcutCommandId('command-or-ctrl', '\\')).toBe(APP_COMMAND_IDS.editToggleRawEditor)
|
|
expect(findShortcutCommandId('command-shift', '¬', 'KeyL')).toBe(APP_COMMAND_IDS.viewToggleAiChat)
|
|
})
|
|
|
|
it('gives every shortcut command an explicit deterministic QA strategy', () => {
|
|
expect(getDeterministicShortcutQaDefinition(APP_COMMAND_IDS.fileNewNote)).toMatchObject({
|
|
preferredMode: 'native-menu-command',
|
|
supportsRendererShortcutEvent: true,
|
|
supportsNativeMenuCommand: true,
|
|
requiresManualNativeAcceleratorQa: true,
|
|
})
|
|
expect(getDeterministicShortcutQaDefinition(APP_COMMAND_IDS.viewToggleProperties)).toMatchObject({
|
|
preferredMode: 'renderer-shortcut-event',
|
|
supportsRendererShortcutEvent: true,
|
|
supportsNativeMenuCommand: true,
|
|
requiresManualNativeAcceleratorQa: false,
|
|
})
|
|
expect(getDeterministicShortcutQaDefinition(APP_COMMAND_IDS.noteToggleFavorite)).toMatchObject({
|
|
preferredMode: 'renderer-shortcut-event',
|
|
supportsRendererShortcutEvent: true,
|
|
supportsNativeMenuCommand: false,
|
|
requiresManualNativeAcceleratorQa: true,
|
|
})
|
|
})
|
|
|
|
it('builds deterministic keyboard events from the shared shortcut manifest', () => {
|
|
expect(getShortcutEventInit(APP_COMMAND_IDS.viewToggleAiChat)).toMatchObject({
|
|
key: 'l',
|
|
code: 'KeyL',
|
|
metaKey: true,
|
|
ctrlKey: false,
|
|
shiftKey: true,
|
|
})
|
|
expect(getShortcutEventInit(APP_COMMAND_IDS.viewToggleAiChat, { preferControl: true })).toMatchObject({
|
|
key: 'l',
|
|
code: 'KeyL',
|
|
metaKey: false,
|
|
ctrlKey: true,
|
|
shiftKey: true,
|
|
})
|
|
expect(getShortcutEventInit(APP_COMMAND_IDS.viewGoBack)).toMatchObject({
|
|
key: 'ArrowLeft',
|
|
code: 'ArrowLeft',
|
|
metaKey: true,
|
|
ctrlKey: false,
|
|
shiftKey: false,
|
|
})
|
|
expect(getShortcutEventInit(APP_COMMAND_IDS.appCheckForUpdates)).toBeNull()
|
|
})
|
|
|
|
it('resolves event modifiers through the shared shortcut catalog', () => {
|
|
expect(
|
|
findShortcutCommandIdForEvent({
|
|
key: 'o',
|
|
code: 'KeyO',
|
|
altKey: false,
|
|
ctrlKey: false,
|
|
metaKey: true,
|
|
shiftKey: false,
|
|
}),
|
|
).toBe(APP_COMMAND_IDS.fileQuickOpen)
|
|
expect(
|
|
findShortcutCommandIdForEvent({
|
|
key: '¬',
|
|
code: 'KeyL',
|
|
altKey: false,
|
|
ctrlKey: false,
|
|
metaKey: true,
|
|
shiftKey: true,
|
|
}),
|
|
).toBe(APP_COMMAND_IDS.viewToggleAiChat)
|
|
expect(
|
|
findShortcutCommandIdForEvent({
|
|
key: 'I',
|
|
code: 'KeyI',
|
|
altKey: false,
|
|
ctrlKey: false,
|
|
metaKey: true,
|
|
shiftKey: true,
|
|
}),
|
|
).toBe(APP_COMMAND_IDS.viewToggleProperties)
|
|
expect(
|
|
findShortcutCommandIdForEvent({
|
|
key: 'ArrowLeft',
|
|
code: 'ArrowLeft',
|
|
altKey: false,
|
|
ctrlKey: false,
|
|
metaKey: true,
|
|
shiftKey: false,
|
|
}),
|
|
).toBe(APP_COMMAND_IDS.viewGoBack)
|
|
expect(
|
|
findShortcutCommandIdForEvent({
|
|
key: 'ArrowRight',
|
|
code: 'ArrowRight',
|
|
altKey: false,
|
|
ctrlKey: false,
|
|
metaKey: true,
|
|
shiftKey: false,
|
|
}),
|
|
).toBe(APP_COMMAND_IDS.viewGoForward)
|
|
expect(
|
|
findShortcutCommandIdForEvent({
|
|
key: 'l',
|
|
code: 'KeyL',
|
|
altKey: false,
|
|
ctrlKey: true,
|
|
metaKey: false,
|
|
shiftKey: true,
|
|
}),
|
|
).toBeNull()
|
|
})
|
|
|
|
it('dispatches create note through the shared command path', () => {
|
|
const handlers = makeHandlers()
|
|
expect(dispatchAppCommand(APP_COMMAND_IDS.fileNewNote, handlers)).toBe(true)
|
|
expect(handlers.onCreateNote).toHaveBeenCalled()
|
|
})
|
|
|
|
it('dispatches inspector toggle through the shared command path', () => {
|
|
const handlers = makeHandlers()
|
|
expect(dispatchAppCommand(APP_COMMAND_IDS.viewToggleProperties, handlers)).toBe(true)
|
|
expect(handlers.onToggleInspector).toHaveBeenCalled()
|
|
})
|
|
|
|
it('dispatches AI panel toggle through the shared command path', () => {
|
|
const handlers = makeHandlers()
|
|
expect(dispatchAppCommand(APP_COMMAND_IDS.viewToggleAiChat, handlers)).toBe(true)
|
|
expect(handlers.onToggleAIChat).toHaveBeenCalled()
|
|
})
|
|
|
|
it('uses the active note for note-scoped commands', () => {
|
|
const handlers = makeHandlers()
|
|
expect(dispatchAppCommand(APP_COMMAND_IDS.noteToggleFavorite, handlers)).toBe(true)
|
|
expect(dispatchAppCommand(APP_COMMAND_IDS.noteToggleOrganized, handlers)).toBe(true)
|
|
expect(dispatchAppCommand(APP_COMMAND_IDS.noteDelete, handlers)).toBe(true)
|
|
expect(handlers.onToggleFavorite).toHaveBeenCalledWith('/vault/test.md')
|
|
expect(handlers.onToggleOrganized).toHaveBeenCalledWith('/vault/test.md')
|
|
expect(handlers.onDeleteNote).toHaveBeenCalledWith('/vault/test.md')
|
|
})
|
|
|
|
it('uses the current multi-selection for delete and organize commands', () => {
|
|
const handlers = makeHandlers()
|
|
const deleteSelected = vi.fn()
|
|
const organizeSelected = vi.fn()
|
|
handlers.multiSelectionCommandRef.current = {
|
|
selectedPaths: ['/vault/a.md', '/vault/b.md'],
|
|
deleteSelected,
|
|
organizeSelected,
|
|
}
|
|
|
|
expect(dispatchAppCommand(APP_COMMAND_IDS.noteToggleOrganized, handlers)).toBe(true)
|
|
expect(dispatchAppCommand(APP_COMMAND_IDS.noteDelete, handlers)).toBe(true)
|
|
|
|
expect(organizeSelected).toHaveBeenCalledTimes(1)
|
|
expect(deleteSelected).toHaveBeenCalledTimes(1)
|
|
expect(handlers.onToggleOrganized).not.toHaveBeenCalled()
|
|
expect(handlers.onDeleteNote).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('does not fall back to the active note when multi-selection cannot handle the command', () => {
|
|
const handlers = makeHandlers()
|
|
handlers.multiSelectionCommandRef.current = {
|
|
selectedPaths: ['/vault/a.md', '/vault/b.md'],
|
|
}
|
|
|
|
expect(dispatchAppCommand(APP_COMMAND_IDS.noteToggleOrganized, handlers)).toBe(false)
|
|
expect(handlers.onToggleOrganized).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('no-ops note-scoped commands when there is no active note', () => {
|
|
const handlers = makeHandlers()
|
|
handlers.activeTabPathRef.current = null
|
|
expect(dispatchAppCommand(APP_COMMAND_IDS.noteToggleFavorite, handlers)).toBe(false)
|
|
expect(dispatchAppCommand(APP_COMMAND_IDS.noteToggleOrganized, handlers)).toBe(false)
|
|
expect(dispatchAppCommand(APP_COMMAND_IDS.noteDelete, handlers)).toBe(false)
|
|
expect(handlers.onToggleFavorite).not.toHaveBeenCalled()
|
|
expect(handlers.onToggleOrganized).not.toHaveBeenCalled()
|
|
expect(handlers.onDeleteNote).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('dispatches navigation filters through the same shared command path', () => {
|
|
const handlers = makeHandlers()
|
|
expect(dispatchAppCommand(APP_COMMAND_IDS.goChanges, handlers)).toBe(true)
|
|
expect(handlers.onSelectFilter).toHaveBeenCalledWith('changes')
|
|
})
|
|
|
|
it('suppresses a native-menu echo after renderer keyboard dispatch', () => {
|
|
const handlers = makeHandlers()
|
|
|
|
expect(executeAppCommand(APP_COMMAND_IDS.viewToggleProperties, handlers, 'renderer-keyboard')).toBe(true)
|
|
expect(executeAppCommand(APP_COMMAND_IDS.viewToggleProperties, handlers, 'native-menu')).toBe(false)
|
|
expect(handlers.onToggleInspector).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('suppresses a renderer keyboard echo after native-menu dispatch', () => {
|
|
const handlers = makeHandlers()
|
|
|
|
expect(executeAppCommand(APP_COMMAND_IDS.viewToggleAiChat, handlers, 'native-menu')).toBe(true)
|
|
expect(executeAppCommand(APP_COMMAND_IDS.viewToggleAiChat, handlers, 'renderer-keyboard')).toBe(false)
|
|
expect(handlers.onToggleAIChat).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|