import { describe, expect, it, vi } from 'vitest'; import { DEFAULT_MECHANICA_SHORTCUTS, eventToShortcut, findShortcutConflicts, isEditableTarget, isInteractiveTarget, matchesShortcut, normalizeShortcut, parseShortcut, shortcutSignature, shortcutToLabel, shouldIgnoreKeyboardEvent, type KeyboardShortcutDefinition, type KeyboardShortcutEventLike, } from '../src/utils/keyboardShortcuts'; interface FakeTargetOptions { tagName?: string; attrs?: Record; parent?: EventTarget | null; contentEditable?: boolean; } function makeTarget({ tagName, attrs = {}, parent = null, contentEditable = false, }: FakeTargetOptions = {}): EventTarget { return { tagName, parentElement: parent, isContentEditable: contentEditable, getAttribute: (name: string) => Object.prototype.hasOwnProperty.call(attrs, name) ? attrs[name] ?? null : null, } as unknown as EventTarget; } function keyEvent(overrides: Partial = {}): KeyboardShortcutEventLike { return { key: 'e', ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, repeat: false, target: null, preventDefault: vi.fn(), ...overrides, }; } describe('keyboard shortcut utilities', () => { it('normalizes aliases, duplicate modifiers, and canonical signature order', () => { expect( normalizeShortcut({ key: 'A', modifiers: ['shift', 'ctrl', 'shift'] }), ).toEqual({ key: 'a', modifiers: ['ctrl', 'shift'], }); expect(shortcutSignature('Command+Option+Shift+P')).toBe('alt+shift+meta+p'); expect(parseShortcut('Ctrl++')).toEqual({ key: '=', modifiers: ['ctrl', 'shift'] }); }); it('matches shifted printable keys as browsers report them', () => { const browserQuestionMarkEvent = keyEvent({ key: '?', shiftKey: true }); expect(eventToShortcut(browserQuestionMarkEvent)).toEqual({ key: '/', modifiers: ['shift'], }); expect(matchesShortcut(browserQuestionMarkEvent, '?')).toBe(true); expect(matchesShortcut(browserQuestionMarkEvent, '/')).toBe(false); }); it('protects editable fields and semantic controls unless explicitly allowed', () => { const input = makeTarget({ tagName: 'INPUT' }); const button = makeTarget({ tagName: 'BUTTON' }); expect(isEditableTarget(input)).toBe(true); expect(matchesShortcut(keyEvent({ key: 'e', target: input }), 'E')).toBe(false); expect( matchesShortcut(keyEvent({ key: 'e', target: input }), 'E', { allowInEditable: true, }), ).toBe(true); expect(isInteractiveTarget(button)).toBe(true); expect(matchesShortcut(keyEvent({ key: ' ', target: button }), 'Space')).toBe(false); expect( matchesShortcut(keyEvent({ key: ' ', target: button }), 'Space', { allowInInteractive: true, }), ).toBe(true); }); it('supports explicit shortcut policies on focused targets or ancestors', () => { const ignoredPanel = makeTarget({ attrs: { 'data-mechanica-shortcuts': 'ignore' }, }); const canvasInsideIgnoredPanel = makeTarget({ tagName: 'CANVAS', parent: ignoredPanel, }); const explicitlyAllowedInput = makeTarget({ tagName: 'INPUT', attrs: { 'data-mechanica-shortcuts': 'allow' }, }); expect(shouldIgnoreKeyboardEvent(keyEvent({ target: canvasInsideIgnoredPanel }))).toBe( true, ); expect(shouldIgnoreKeyboardEvent(keyEvent({ target: explicitlyAllowedInput }))).toBe( false, ); }); it('rejects key repeats by default and only prevents default after a match', () => { const preventDefault = vi.fn(); const repeatedArrow = keyEvent({ key: 'ArrowRight', repeat: true, preventDefault, }); expect( matchesShortcut(repeatedArrow, 'ArrowRight', { preventDefault: true, }), ).toBe(false); expect(preventDefault).toHaveBeenCalledTimes(0); expect( matchesShortcut(repeatedArrow, 'ArrowRight', { allowRepeat: true, preventDefault: true, }), ).toBe(true); expect(preventDefault).toHaveBeenCalledTimes(1); }); it('does not hijack AltGraph text composition', () => { expect( matchesShortcut( keyEvent({ key: 'e', ctrlKey: true, altKey: true, getModifierState: (modifier) => modifier === 'AltGraph', }), 'Ctrl+Alt+E', ), ).toBe(false); }); it('formats shortcut labels for Mac and text platforms', () => { expect(shortcutToLabel('Meta+Shift+1', 'mac')).toBe('โ‡งโŒ˜1'); expect(shortcutToLabel('Ctrl+Alt+ArrowLeft', 'windows')).toBe('Ctrl + Alt + โ†'); }); it('keeps route-local scopes independent while flagging global overlaps', () => { const routeScopedDefinitions: KeyboardShortcutDefinition[] = [ { id: 'viewer.e', scope: 'viewer', description: 'Viewer local action.', shortcut: 'E', }, { id: 'catalogue.e', scope: 'catalogue', description: 'Catalogue local action.', shortcut: 'E', }, ]; expect(findShortcutConflicts(routeScopedDefinitions)).toEqual([]); const conflicts = findShortcutConflicts([ ...routeScopedDefinitions, { id: 'global.e', scope: 'global', description: 'Global action using the same binding.', shortcut: 'E', }, ]); expect(conflicts).toHaveLength(1); expect(conflicts[0]?.signature).toBe('e'); expect(conflicts[0]?.definitions.map((definition) => definition.id).sort()).toEqual([ 'catalogue.e', 'global.e', 'viewer.e', ]); }); it('ships the default Mechanica bindings with complete metadata and no conflicts', () => { const ids = new Set(); expect(DEFAULT_MECHANICA_SHORTCUTS.length).toBeGreaterThan(15); for (const shortcut of DEFAULT_MECHANICA_SHORTCUTS) { expect(shortcut.id).toMatch(/^[a-z0-9.-]+$/); expect(ids.has(shortcut.id)).toBe(false); expect(shortcut.category).toBeTruthy(); expect(shortcut.description.length).toBeGreaterThan(12); expect(() => normalizeShortcut(shortcut.shortcut)).not.toThrow(); ids.add(shortcut.id); } expect(findShortcutConflicts(DEFAULT_MECHANICA_SHORTCUTS)).toEqual([]); }); });