diff --git a/src/services/keybindingService.ts b/src/services/keybindingService.ts index 30617dfc9..3d908ddde 100644 --- a/src/services/keybindingService.ts +++ b/src/services/keybindingService.ts @@ -8,6 +8,7 @@ import { KeybindingImpl, useKeybindingStore } from '@/stores/keybindingStore' +import { migrateKeybindings } from '@/utils/keybindingMigration' export const useKeybindingService = () => { const keybindingStore = useKeybindingStore() @@ -111,14 +112,41 @@ export const useKeybindingService = () => { } } - function registerUserKeybindings() { - // Unset bindings first as new bindings might conflict with default bindings. + async function registerUserKeybindings() { + // Load user keybindings from settings const unsetBindings = settingStore.get('Comfy.Keybinding.UnsetBindings') - for (const keybinding of unsetBindings) { + const newBindings = settingStore.get('Comfy.Keybinding.NewBindings') + + // Migrate keybindings from old event.key format to new event.code format + const migratedUnset = migrateKeybindings(unsetBindings) + const migratedNew = migrateKeybindings(newBindings) + + // Save migrated keybindings back to settings if any migration occurred + if (migratedUnset.migrated) { + await settingStore.set( + 'Comfy.Keybinding.UnsetBindings', + migratedUnset.keybindings + ) + console.warn( + '[Keybindings] Migrated unset keybindings to event.code format' + ) + } + + if (migratedNew.migrated) { + await settingStore.set( + 'Comfy.Keybinding.NewBindings', + migratedNew.keybindings + ) + console.warn( + '[Keybindings] Migrated custom keybindings to event.code format' + ) + } + + // Unset bindings first as new bindings might conflict with default bindings. + for (const keybinding of migratedUnset.keybindings) { keybindingStore.unsetKeybinding(new KeybindingImpl(keybinding)) } - const newBindings = settingStore.get('Comfy.Keybinding.NewBindings') - for (const keybinding of newBindings) { + for (const keybinding of migratedNew.keybindings) { keybindingStore.addUserKeybinding(new KeybindingImpl(keybinding)) } } diff --git a/src/utils/keybindingMigration.ts b/src/utils/keybindingMigration.ts new file mode 100644 index 000000000..75f045b26 --- /dev/null +++ b/src/utils/keybindingMigration.ts @@ -0,0 +1,263 @@ +import type { KeyCombo, Keybinding } from '@/schemas/keyBindingSchema' + +/** + * Migration utility for converting old event.key format to new event.code format + * This ensures backward compatibility for existing user keybindings + */ + +// Map from old event.key format to new event.code format +const KEY_MIGRATION_MAP: Record = { + // Letters (both cases) + a: 'KeyA', + A: 'KeyA', + b: 'KeyB', + B: 'KeyB', + c: 'KeyC', + C: 'KeyC', + d: 'KeyD', + D: 'KeyD', + e: 'KeyE', + E: 'KeyE', + f: 'KeyF', + F: 'KeyF', + g: 'KeyG', + G: 'KeyG', + h: 'KeyH', + H: 'KeyH', + i: 'KeyI', + I: 'KeyI', + j: 'KeyJ', + J: 'KeyJ', + k: 'KeyK', + K: 'KeyK', + l: 'KeyL', + L: 'KeyL', + m: 'KeyM', + M: 'KeyM', + n: 'KeyN', + N: 'KeyN', + o: 'KeyO', + O: 'KeyO', + p: 'KeyP', + P: 'KeyP', + q: 'KeyQ', + Q: 'KeyQ', + r: 'KeyR', + R: 'KeyR', + s: 'KeyS', + S: 'KeyS', + t: 'KeyT', + T: 'KeyT', + u: 'KeyU', + U: 'KeyU', + v: 'KeyV', + V: 'KeyV', + w: 'KeyW', + W: 'KeyW', + x: 'KeyX', + X: 'KeyX', + y: 'KeyY', + Y: 'KeyY', + z: 'KeyZ', + Z: 'KeyZ', + + // Numbers + '0': 'Digit0', + '1': 'Digit1', + '2': 'Digit2', + '3': 'Digit3', + '4': 'Digit4', + '5': 'Digit5', + '6': 'Digit6', + '7': 'Digit7', + '8': 'Digit8', + '9': 'Digit9', + + // Special keys that might be in old format + escape: 'Escape', + enter: 'Enter', + space: 'Space', + tab: 'Tab', + spacebar: 'Space', + Spacebar: 'Space', + esc: 'Escape', + Esc: 'Escape', + return: 'Enter', + Return: 'Enter', + backspace: 'Backspace', + delete: 'Delete', + + // Arrow keys + arrowup: 'ArrowUp', + arrowdown: 'ArrowDown', + arrowleft: 'ArrowLeft', + arrowright: 'ArrowRight', + + // Function keys (already correct format but handle lowercase) + f1: 'F1', + f2: 'F2', + f3: 'F3', + f4: 'F4', + f5: 'F5', + f6: 'F6', + f7: 'F7', + f8: 'F8', + f9: 'F9', + f10: 'F10', + f11: 'F11', + f12: 'F12', + + // Punctuation and symbols (old name -> new code) + '-': 'Minus', + '=': 'Equal', + '[': 'BracketLeft', + ']': 'BracketRight', + '\\': 'Backslash', + ';': 'Semicolon', + "'": 'Quote', + '`': 'Backquote', + ',': 'Comma', + '.': 'Period', + '/': 'Slash', + _: 'Minus', + '+': 'Equal', + '{': 'BracketLeft', + '}': 'BracketRight', + '|': 'Backslash', + ':': 'Semicolon', + '"': 'Quote', + '~': 'Backquote', + '<': 'Comma', + '>': 'Period', + '?': 'Slash', + + // Shifted digits + '!': 'Digit1', + '@': 'Digit2', + '#': 'Digit3', + $: 'Digit4', + '%': 'Digit5', + '^': 'Digit6', + '&': 'Digit7', + '*': 'Digit8', + '(': 'Digit9', + ')': 'Digit0', + + // Common aliases + ' ': 'Space' +} + +/** + * Checks if a key combo needs migration from old format to new format + */ +export function needsKeyMigration(keyCombo: KeyCombo): boolean { + if (!keyCombo.key) return false + + // Check if it's already in the new format + if ( + keyCombo.key.startsWith('Key') || + keyCombo.key.startsWith('Digit') || + (keyCombo.key.startsWith('F') && /^F\d+$/.test(keyCombo.key)) || + [ + 'Enter', + 'Escape', + 'Tab', + 'Space', + 'Backspace', + 'Delete', + 'ArrowUp', + 'ArrowDown', + 'ArrowLeft', + 'ArrowRight', + 'Minus', + 'Equal', + 'BracketLeft', + 'BracketRight', + 'Backslash', + 'Semicolon', + 'Quote', + 'Backquote', + 'Comma', + 'Period', + 'Slash', + 'NumpadAdd', + 'NumpadSubtract', + 'NumpadMultiply', + 'NumpadDivide' + ].includes(keyCombo.key) + ) { + return false + } + + // If it's in our migration map, it needs migration + return keyCombo.key in KEY_MIGRATION_MAP +} + +/** + * Migrates a single key combo from old format to new format + */ +export function migrateKeyCombo(keyCombo: KeyCombo): KeyCombo { + if (!needsKeyMigration(keyCombo)) { + return keyCombo + } + + const newKey = KEY_MIGRATION_MAP[keyCombo.key] + if (!newKey) { + console.warn(`Unknown key format for migration: ${keyCombo.key}`) + return keyCombo + } + + return { + ...keyCombo, + key: newKey + } +} + +/** + * Migrates a single keybinding + */ +export function migrateKeybinding(keybinding: Keybinding): Keybinding { + return { + ...keybinding, + combo: migrateKeyCombo(keybinding.combo) + } +} + +/** + * Migrates an array of keybindings and returns both the migrated keybindings + * and whether any migration was performed + */ +export function migrateKeybindings(keybindings: Keybinding[] | undefined): { + keybindings: Keybinding[] + migrated: boolean +} { + if (!Array.isArray(keybindings)) { + return { + keybindings: [], + migrated: false + } + } + + let migrated = false + const migratedKeybindings = keybindings.map((keybinding) => { + const migratedKeybinding = migrateKeybinding(keybinding) + if (migratedKeybinding.combo.key !== keybinding.combo.key) { + migrated = true + } + return migratedKeybinding + }) + + return { + keybindings: migratedKeybindings, + migrated + } +} + +/** + * Normalizes a key to the event.code format + * This handles both old and new formats + */ +export function normalizeKey(key: string): string { + const migrated = migrateKeyCombo({ key } as KeyCombo) + return migrated.key +} diff --git a/tests-ui/tests/services/keybindingService.escape.test.ts b/tests-ui/tests/services/keybindingService.escape.test.ts index 20b608e8d..9bc3d3f52 100644 --- a/tests-ui/tests/services/keybindingService.escape.test.ts +++ b/tests-ui/tests/services/keybindingService.escape.test.ts @@ -11,10 +11,14 @@ import { useKeybindingStore } from '@/stores/keybindingStore' +const settingStoreGetMock = vi.fn() +const settingStoreSetMock = vi.fn() + // Mock stores vi.mock('@/platform/settings/settingStore', () => ({ useSettingStore: vi.fn(() => ({ - get: vi.fn(() => []) + get: settingStoreGetMock, + set: settingStoreSetMock })) })) @@ -32,6 +36,9 @@ describe('keybindingService - Escape key handling', () => { vi.clearAllMocks() setActivePinia(createPinia()) + settingStoreGetMock.mockImplementation(() => []) + settingStoreSetMock.mockResolvedValue(undefined) + // Mock command store execute mockCommandExecute = vi.fn() const commandStore = useCommandStore() @@ -67,6 +74,7 @@ describe('keybindingService - Escape key handling', () => { it('should execute ExitSubgraph command when Escape is pressed', async () => { const event = new KeyboardEvent('keydown', { key: 'Escape', + code: 'Escape', bubbles: true, cancelable: true }) @@ -84,6 +92,7 @@ describe('keybindingService - Escape key handling', () => { it('should not execute command when Escape is pressed with modifiers', async () => { const event = new KeyboardEvent('keydown', { key: 'Escape', + code: 'Escape', ctrlKey: true, bubbles: true, cancelable: true @@ -101,6 +110,7 @@ describe('keybindingService - Escape key handling', () => { const inputElement = document.createElement('input') const event = new KeyboardEvent('keydown', { key: 'Escape', + code: 'Escape', bubbles: true, cancelable: true }) @@ -131,6 +141,7 @@ describe('keybindingService - Escape key handling', () => { const event = new KeyboardEvent('keydown', { key: 'Escape', + code: 'Escape', bubbles: true, cancelable: true }) @@ -159,6 +170,7 @@ describe('keybindingService - Escape key handling', () => { const event = new KeyboardEvent('keydown', { key: 'Escape', + code: 'Escape', bubbles: true, cancelable: true }) @@ -200,3 +212,108 @@ describe('keybindingService - Escape key handling', () => { expect(mockCommandExecute).not.toHaveBeenCalled() }) }) + +describe('keybindingService - migration support', () => { + let keybindingService: ReturnType + let keybindingStore: ReturnType + + beforeEach(() => { + vi.clearAllMocks() + setActivePinia(createPinia()) + + settingStoreSetMock.mockResolvedValue(undefined) + settingStoreGetMock.mockImplementation((key: string) => { + if (key === 'Comfy.Keybinding.UnsetBindings') { + return [] + } + if (key === 'Comfy.Keybinding.NewBindings') { + return [] + } + return [] + }) + + keybindingService = useKeybindingService() + keybindingService.registerCoreKeybindings() + keybindingStore = useKeybindingStore() + }) + + it('migrates legacy unset bindings using default combos', async () => { + // Legacy format used lowercase letters + // User wants to unset the 'R' shortcut (Comfy.RefreshNodeDefinitions) + const legacyUnset = [ + { + commandId: 'Comfy.RefreshNodeDefinitions', + combo: { key: 'r' } // Old format + } + ] + + settingStoreGetMock.mockImplementation((key: string) => { + if (key === 'Comfy.Keybinding.UnsetBindings') { + return legacyUnset + } + if (key === 'Comfy.Keybinding.NewBindings') { + return [] + } + return [] + }) + + await keybindingService.registerUserKeybindings() + + const unsetBindings = Object.values( + keybindingStore.getUserUnsetKeybindings() + ) + + // Should have migrated and unset the binding + expect(unsetBindings).toHaveLength(1) + expect(unsetBindings[0].combo.key).toBe('KeyR') + expect(unsetBindings[0].commandId).toBe('Comfy.RefreshNodeDefinitions') + + // Should have saved the migrated format + expect(settingStoreSetMock).toHaveBeenCalledWith( + 'Comfy.Keybinding.UnsetBindings', + expect.arrayContaining([ + expect.objectContaining({ + commandId: 'Comfy.RefreshNodeDefinitions', + combo: expect.objectContaining({ + key: 'KeyR' + }) + }) + ]) + ) + + // Verify the keybinding no longer matches + const eventCombo = new KeyComboImpl({ key: 'KeyR' }) + const resolved = keybindingStore.getKeybinding(eventCombo) + expect(resolved).toBeUndefined() + }) + + it('matches migrated event.code against legacy user bindings', async () => { + // User has a legacy binding in old format + const legacyBindings = [ + { + commandId: 'Custom.Legacy', + combo: { key: 'q' } + } + ] + + settingStoreGetMock.mockImplementation((key: string) => { + if (key === 'Comfy.Keybinding.UnsetBindings') { + return [] + } + if (key === 'Comfy.Keybinding.NewBindings') { + return legacyBindings + } + return [] + }) + + // Register user keybindings (which will migrate them) + await keybindingService.registerUserKeybindings() + + // Now press 'Q' key with event.code format + const eventCombo = new KeyComboImpl({ key: 'KeyQ' }) + + const resolved = keybindingStore.getKeybinding(eventCombo) + + expect(resolved?.commandId).toBe('Custom.Legacy') + }) +}) diff --git a/tests-ui/tests/utils/keybindingMigration.test.ts b/tests-ui/tests/utils/keybindingMigration.test.ts new file mode 100644 index 000000000..5fb1c0fca --- /dev/null +++ b/tests-ui/tests/utils/keybindingMigration.test.ts @@ -0,0 +1,285 @@ +import { describe, expect, it } from 'vitest' + +import type { KeyCombo, Keybinding } from '@/schemas/keyBindingSchema' +import { + migrateKeyCombo, + migrateKeybinding, + migrateKeybindings, + needsKeyMigration, + normalizeKey +} from '@/utils/keybindingMigration' + +describe('keybindingMigration', () => { + describe('needsKeyMigration', () => { + it('should return false for keys already in event.code format', () => { + expect(needsKeyMigration({ key: 'KeyA' })).toBe(false) + expect(needsKeyMigration({ key: 'KeyZ' })).toBe(false) + expect(needsKeyMigration({ key: 'Digit0' })).toBe(false) + expect(needsKeyMigration({ key: 'Digit9' })).toBe(false) + expect(needsKeyMigration({ key: 'F1' })).toBe(false) + expect(needsKeyMigration({ key: 'F12' })).toBe(false) + expect(needsKeyMigration({ key: 'Enter' })).toBe(false) + expect(needsKeyMigration({ key: 'Escape' })).toBe(false) + expect(needsKeyMigration({ key: 'ArrowUp' })).toBe(false) + expect(needsKeyMigration({ key: 'Minus' })).toBe(false) + }) + + it('should return true for keys in old event.key format', () => { + expect(needsKeyMigration({ key: 'a' })).toBe(true) + expect(needsKeyMigration({ key: 'z' })).toBe(true) + expect(needsKeyMigration({ key: 'A' })).toBe(true) + expect(needsKeyMigration({ key: 'Z' })).toBe(true) + expect(needsKeyMigration({ key: '0' })).toBe(true) + expect(needsKeyMigration({ key: '9' })).toBe(true) + expect(needsKeyMigration({ key: '-' })).toBe(true) + expect(needsKeyMigration({ key: '=' })).toBe(true) + }) + + it('should handle lowercase special keys', () => { + expect(needsKeyMigration({ key: 'escape' })).toBe(true) + expect(needsKeyMigration({ key: 'enter' })).toBe(true) + expect(needsKeyMigration({ key: 'space' })).toBe(true) + }) + + it('should return false for empty key', () => { + expect(needsKeyMigration({ key: '' })).toBe(false) + }) + }) + + describe('migrateKeyCombo', () => { + it('should migrate lowercase letters to KeyX format', () => { + expect(migrateKeyCombo({ key: 'a' }).key).toBe('KeyA') + expect(migrateKeyCombo({ key: 'r' }).key).toBe('KeyR') + expect(migrateKeyCombo({ key: 'z' }).key).toBe('KeyZ') + }) + + it('should migrate uppercase letters to KeyX format', () => { + expect(migrateKeyCombo({ key: 'A' }).key).toBe('KeyA') + expect(migrateKeyCombo({ key: 'R' }).key).toBe('KeyR') + expect(migrateKeyCombo({ key: 'Z' }).key).toBe('KeyZ') + }) + + it('should migrate digit characters to DigitX format', () => { + expect(migrateKeyCombo({ key: '0' }).key).toBe('Digit0') + expect(migrateKeyCombo({ key: '5' }).key).toBe('Digit5') + expect(migrateKeyCombo({ key: '9' }).key).toBe('Digit9') + }) + + it('should migrate special keys to proper case', () => { + expect(migrateKeyCombo({ key: 'escape' }).key).toBe('Escape') + expect(migrateKeyCombo({ key: 'enter' }).key).toBe('Enter') + expect(migrateKeyCombo({ key: 'space' }).key).toBe('Space') + expect(migrateKeyCombo({ key: 'tab' }).key).toBe('Tab') + }) + + it('should migrate punctuation to event.code names', () => { + expect(migrateKeyCombo({ key: '-' }).key).toBe('Minus') + expect(migrateKeyCombo({ key: '=' }).key).toBe('Equal') + expect(migrateKeyCombo({ key: '[' }).key).toBe('BracketLeft') + expect(migrateKeyCombo({ key: ']' }).key).toBe('BracketRight') + expect(migrateKeyCombo({ key: ';' }).key).toBe('Semicolon') + expect(migrateKeyCombo({ key: '/' }).key).toBe('Slash') + }) + + it('should migrate shifted punctuation correctly', () => { + expect(migrateKeyCombo({ key: '_' }).key).toBe('Minus') + expect(migrateKeyCombo({ key: '+' }).key).toBe('Equal') + expect(migrateKeyCombo({ key: '{' }).key).toBe('BracketLeft') + expect(migrateKeyCombo({ key: '}' }).key).toBe('BracketRight') + expect(migrateKeyCombo({ key: ':' }).key).toBe('Semicolon') + expect(migrateKeyCombo({ key: '?' }).key).toBe('Slash') + }) + + it('should migrate shifted digits correctly', () => { + expect(migrateKeyCombo({ key: '!' }).key).toBe('Digit1') + expect(migrateKeyCombo({ key: '@' }).key).toBe('Digit2') + expect(migrateKeyCombo({ key: '#' }).key).toBe('Digit3') + expect(migrateKeyCombo({ key: '$' }).key).toBe('Digit4') + expect(migrateKeyCombo({ key: '%' }).key).toBe('Digit5') + expect(migrateKeyCombo({ key: '^' }).key).toBe('Digit6') + expect(migrateKeyCombo({ key: '&' }).key).toBe('Digit7') + expect(migrateKeyCombo({ key: '*' }).key).toBe('Digit8') + expect(migrateKeyCombo({ key: '(' }).key).toBe('Digit9') + expect(migrateKeyCombo({ key: ')' }).key).toBe('Digit0') + }) + + it('should preserve modifier flags', () => { + const combo: KeyCombo = { + key: 's', + ctrl: true, + alt: true, + shift: true + } + const migrated = migrateKeyCombo(combo) + + expect(migrated.key).toBe('KeyS') + expect(migrated.ctrl).toBe(true) + expect(migrated.alt).toBe(true) + expect(migrated.shift).toBe(true) + }) + + it('should not modify keys already in event.code format', () => { + expect(migrateKeyCombo({ key: 'KeyR' }).key).toBe('KeyR') + expect(migrateKeyCombo({ key: 'Digit5' }).key).toBe('Digit5') + expect(migrateKeyCombo({ key: 'F1' }).key).toBe('F1') + expect(migrateKeyCombo({ key: 'Enter' }).key).toBe('Enter') + expect(migrateKeyCombo({ key: 'ArrowUp' }).key).toBe('ArrowUp') + }) + + it('should handle space character', () => { + expect(migrateKeyCombo({ key: ' ' }).key).toBe('Space') + }) + + it('should handle arrow key variations', () => { + expect(migrateKeyCombo({ key: 'arrowup' }).key).toBe('ArrowUp') + expect(migrateKeyCombo({ key: 'arrowdown' }).key).toBe('ArrowDown') + expect(migrateKeyCombo({ key: 'arrowleft' }).key).toBe('ArrowLeft') + expect(migrateKeyCombo({ key: 'arrowright' }).key).toBe('ArrowRight') + }) + + it('should handle function key variations', () => { + expect(migrateKeyCombo({ key: 'f1' }).key).toBe('F1') + expect(migrateKeyCombo({ key: 'f12' }).key).toBe('F12') + }) + }) + + describe('migrateKeybinding', () => { + it('should migrate a keybinding object', () => { + const keybinding: Keybinding = { + commandId: 'Test.Command', + combo: { key: 'r' } + } + + const migrated = migrateKeybinding(keybinding) + + expect(migrated.commandId).toBe('Test.Command') + expect(migrated.combo.key).toBe('KeyR') + }) + + it('should preserve targetElementId', () => { + const keybinding: Keybinding = { + commandId: 'Test.Command', + combo: { key: 's', ctrl: true }, + targetElementId: 'graph-canvas' + } + + const migrated = migrateKeybinding(keybinding) + + expect(migrated.targetElementId).toBe('graph-canvas') + expect(migrated.combo.key).toBe('KeyS') + expect(migrated.combo.ctrl).toBe(true) + }) + }) + + describe('migrateKeybindings', () => { + it('should migrate an array of keybindings', () => { + const keybindings: Keybinding[] = [ + { commandId: 'Test1', combo: { key: 'r' } }, + { commandId: 'Test2', combo: { key: 's', ctrl: true } }, + { commandId: 'Test3', combo: { key: 'q' } } + ] + + const result = migrateKeybindings(keybindings) + + expect(result.migrated).toBe(true) + expect(result.keybindings).toHaveLength(3) + expect(result.keybindings[0].combo.key).toBe('KeyR') + expect(result.keybindings[1].combo.key).toBe('KeyS') + expect(result.keybindings[2].combo.key).toBe('KeyQ') + }) + + it('should detect when no migration is needed', () => { + const keybindings: Keybinding[] = [ + { commandId: 'Test1', combo: { key: 'KeyR' } }, + { commandId: 'Test2', combo: { key: 'KeyS', ctrl: true } } + ] + + const result = migrateKeybindings(keybindings) + + expect(result.migrated).toBe(false) + expect(result.keybindings).toHaveLength(2) + }) + + it('should handle mixed old and new formats', () => { + const keybindings: Keybinding[] = [ + { commandId: 'Test1', combo: { key: 'r' } }, // Old format + { commandId: 'Test2', combo: { key: 'KeyS' } }, // New format + { commandId: 'Test3', combo: { key: 'q' } } // Old format + ] + + const result = migrateKeybindings(keybindings) + + expect(result.migrated).toBe(true) + expect(result.keybindings[0].combo.key).toBe('KeyR') + expect(result.keybindings[1].combo.key).toBe('KeyS') + expect(result.keybindings[2].combo.key).toBe('KeyQ') + }) + + it('should handle empty array', () => { + const result = migrateKeybindings([]) + + expect(result.migrated).toBe(false) + expect(result.keybindings).toHaveLength(0) + }) + + it('should handle undefined input', () => { + const result = migrateKeybindings(undefined) + + expect(result.migrated).toBe(false) + expect(result.keybindings).toHaveLength(0) + }) + }) + + describe('normalizeKey', () => { + it('should normalize old format keys', () => { + expect(normalizeKey('r')).toBe('KeyR') + expect(normalizeKey('s')).toBe('KeyS') + expect(normalizeKey('5')).toBe('Digit5') + expect(normalizeKey('-')).toBe('Minus') + }) + + it('should leave new format keys unchanged', () => { + expect(normalizeKey('KeyR')).toBe('KeyR') + expect(normalizeKey('Digit5')).toBe('Digit5') + expect(normalizeKey('Enter')).toBe('Enter') + }) + }) + + describe('real-world migration scenarios', () => { + it('should migrate common shortcuts', () => { + const commonShortcuts: Keybinding[] = [ + // Refresh nodes + { commandId: 'Comfy.RefreshNodeDefinitions', combo: { key: 'r' } }, + // Save + { commandId: 'Comfy.SaveWorkflow', combo: { key: 's', ctrl: true } }, + // Queue prompt + { commandId: 'Comfy.QueuePrompt', combo: { key: 'Enter', ctrl: true } }, + // Toggle sidebar + { commandId: 'Workspace.ToggleSidebar', combo: { key: 'q' } } + ] + + const result = migrateKeybindings(commonShortcuts) + + expect(result.migrated).toBe(true) + expect(result.keybindings[0].combo.key).toBe('KeyR') + expect(result.keybindings[1].combo.key).toBe('KeyS') + expect(result.keybindings[2].combo.key).toBe('Enter') + expect(result.keybindings[3].combo.key).toBe('KeyQ') + }) + + it('should handle keybindings with punctuation', () => { + const punctuationShortcuts: Keybinding[] = [ + { commandId: 'Zoom.In', combo: { key: '=', alt: true } }, + { commandId: 'Zoom.Out', combo: { key: '-', alt: true } }, + { commandId: 'Search', combo: { key: '/', ctrl: true } } + ] + + const result = migrateKeybindings(punctuationShortcuts) + + expect(result.migrated).toBe(true) + expect(result.keybindings[0].combo.key).toBe('Equal') + expect(result.keybindings[1].combo.key).toBe('Minus') + expect(result.keybindings[2].combo.key).toBe('Slash') + }) + }) +})