[feat] Add automatic keybinding migration from event.key to event.code

- Implement keybindingMigration utility to convert old format to new format
- Add migration logic in registerUserKeybindings() for backward compatibility
- Update tests to use event.code format in KeyboardEvent creation
- Add comprehensive migration test suite
- Automatically migrate user keybindings on first load after update

This ensures existing user custom keybindings continue working after switching
from event.key to event.code for layout-independent shortcut handling.

Relates to #5252
This commit is contained in:
snomiao
2025-10-16 16:21:42 +00:00
parent 06ec45c1c5
commit 6606060802
4 changed files with 699 additions and 6 deletions

View File

@@ -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))
}
}

View File

@@ -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<string, string> = {
// 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
}

View File

@@ -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<typeof useKeybindingService>
let keybindingStore: ReturnType<typeof useKeybindingStore>
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')
})
})

View File

@@ -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')
})
})
})