feat(keybindings): 快捷键自定义系统 — store+冲突检测+交互式重新绑定UI | 13 files +1032/-137 | 72 tests passing tsc零错误

This commit is contained in:
OpsXShell Dev committed 2026-04-24 05:11:49 +08:00
1 parent 514d62644e
commit 3f636b1b0e
13 files changed
+1033 -138

No files matched your search

+2
View File
@@ -37,6 +37,8 @@ const mockElectronAPI = {
// Settings
getSettings: vi.fn().mockResolvedValue({ success: true, settings: {} }),
setSettings: vi.fn().mockResolvedValue({ success: true }),
settingsGet: vi.fn().mockResolvedValue({ success: true, value: undefined }),
settingsSet: vi.fn().mockResolvedValue({ success: true }),
// Dialog
showSaveDialog: vi.fn().mockResolvedValue({ success: true, canceled: false, filePath: '/tmp/test' }),
+305
View File
@@ -0,0 +1,305 @@
/**
* KeybindingStore 测试 — 快捷键自定义系统
*
* 测试覆盖:
* - 初始化加载默认键绑定
* - 自定义键绑定 (setCustomKeybinding)
* - 重置为默认 (resetKeybinding / resetAll)
* - 冲突检测 (getConflicts)
* - 键序列规范化 (normalizeKeySequence)
* - 持久化到 settings
* - 导出/导入
*/
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { useKeybindingStore } from '../../src/stores/keybindingStore'
import { KeybindingAction } from '../../src/constants/keybindings'
describe('KeybindingStore', () => {
beforeEach(() => {
// 重置 store 到初始状态
useKeybindingStore.setState({
customKeybindings: {},
isLoaded: false,
})
// 清除 mock 调用记录
vi.clearAllMocks()
})
// ─── 1. 初始化与加载 ────────────────────────────────
describe('初始化与加载', () => {
it('应有正确的默认状态', () => {
const state = useKeybindingStore.getState()
expect(state.customKeybindings).toEqual({})
expect(state.isLoaded).toBe(false)
})
it('loadFromSettings 应从 electronAPI 加载自定义快捷键', async () => {
const customData = {
[KeybindingAction.TAB_CLOSE]: 'Ctrl+Q',
[KeybindingAction.TAB_NEW]: 'Ctrl+T',
}
// Mock electronAPI 返回自定义快捷键
vi.mocked(window.electronAPI.settingsGet).mockResolvedValueOnce({
success: true,
value: customData,
})
await useKeybindingStore.getState().loadFromSettings()
const state = useKeybindingStore.getState()
expect(state.isLoaded).toBe(true)
expect(state.customKeybindings).toEqual(customData)
})
it('loadFromSettings 无保存数据时应使用空自定义', async () => {
vi.mocked(window.electronAPI.settingsGet).mockResolvedValueOnce({
success: false,
})
await useKeybindingStore.getState().loadFromSettings()
const state = useKeybindingStore.getState()
expect(state.isLoaded).toBe(true)
expect(state.customKeybindings).toEqual({})
})
it('loadFromSettings API 报错时应安全降级', async () => {
vi.mocked(window.electronAPI.settingsGet).mockRejectedValueOnce(new Error('API error'))
await useKeybindingStore.getState().loadFromSettings()
const state = useKeybindingStore.getState()
expect(state.isLoaded).toBe(true)
expect(state.customKeybindings).toEqual({})
})
})
// ─── 2. 获取有效快捷键 ──────────────────────────────
describe('getEffectiveKeys', () => {
it('无自定义时应返回默认键', () => {
const keys = useKeybindingStore.getState().getEffectiveKeys(KeybindingAction.TAB_NEXT)
expect(keys).toBe('Ctrl+Tab')
})
it('有自定义时应返回自定义键', () => {
useKeybindingStore.setState({
customKeybindings: {
[KeybindingAction.TAB_NEXT]: 'Alt+Right',
},
})
const keys = useKeybindingStore.getState().getEffectiveKeys(KeybindingAction.TAB_NEXT)
expect(keys).toBe('Alt+Right')
})
it('自定义为空字符串时应返回默认键', () => {
useKeybindingStore.setState({
customKeybindings: {
[KeybindingAction.TAB_NEXT]: '',
},
})
const keys = useKeybindingStore.getState().getEffectiveKeys(KeybindingAction.TAB_NEXT)
expect(keys).toBe('Ctrl+Tab')
})
})
// ─── 3. 设置自定义快捷键 ────────────────────────────
describe('setCustomKeybinding', () => {
it('应设置自定义键绑定并持久化', async () => {
await useKeybindingStore.getState().setCustomKeybinding(
KeybindingAction.TAB_CLOSE,
'Ctrl+Q'
)
const state = useKeybindingStore.getState()
expect(state.customKeybindings[KeybindingAction.TAB_CLOSE]).toBe('Ctrl+Q')
// 应调用 settingsSet 持久化
expect(window.electronAPI.settingsSet).toHaveBeenCalledWith(
'keybindings',
{ [KeybindingAction.TAB_CLOSE]: 'Ctrl+Q' }
)
})
it('多次设置应合并而非覆盖其他', async () => {
await useKeybindingStore.getState().setCustomKeybinding(
KeybindingAction.TAB_CLOSE,
'Ctrl+Q'
)
await useKeybindingStore.getState().setCustomKeybinding(
KeybindingAction.TAB_NEW,
'Ctrl+T'
)
const state = useKeybindingStore.getState()
expect(state.customKeybindings[KeybindingAction.TAB_CLOSE]).toBe('Ctrl+Q')
expect(state.customKeybindings[KeybindingAction.TAB_NEW]).toBe('Ctrl+T')
// 第二次调用应包含两个
const lastCall = vi.mocked(window.electronAPI.settingsSet).mock.calls.at(-1)
expect(lastCall?.[1]).toEqual({
[KeybindingAction.TAB_CLOSE]: 'Ctrl+Q',
[KeybindingAction.TAB_NEW]: 'Ctrl+T',
})
})
it('设置为与默认相同的值时仍应记录', async () => {
await useKeybindingStore.getState().setCustomKeybinding(
KeybindingAction.TAB_CLOSE,
'Ctrl+W' // 等同默认
)
const state = useKeybindingStore.getState()
expect(state.customKeybindings[KeybindingAction.TAB_CLOSE]).toBe('Ctrl+W')
})
})
// ─── 4. 重置快捷键 ──────────────────────────────────
describe('重置操作', () => {
it('resetKeybinding 应移除单个自定义', async () => {
useKeybindingStore.setState({
customKeybindings: {
[KeybindingAction.TAB_CLOSE]: 'Ctrl+Q',
[KeybindingAction.TAB_NEW]: 'Ctrl+T',
},
})
await useKeybindingStore.getState().resetKeybinding(KeybindingAction.TAB_CLOSE)
const state = useKeybindingStore.getState()
expect(state.customKeybindings[KeybindingAction.TAB_CLOSE]).toBeUndefined()
expect(state.customKeybindings[KeybindingAction.TAB_NEW]).toBe('Ctrl+T')
})
it('resetAll 应清除所有自定义', async () => {
useKeybindingStore.setState({
customKeybindings: {
[KeybindingAction.TAB_CLOSE]: 'Ctrl+Q',
[KeybindingAction.TAB_NEW]: 'Ctrl+T',
},
})
await useKeybindingStore.getState().resetAll()
const state = useKeybindingStore.getState()
expect(state.customKeybindings).toEqual({})
expect(window.electronAPI.settingsSet).toHaveBeenCalledWith('keybindings', {})
})
})
// ─── 5. 冲突检测 ────────────────────────────────────
describe('冲突检测', () => {
it('getConflicts 应检测到与其他操作的冲突', () => {
// TAB_CLOSE 默认是 Ctrl+W,设置 TAB_NEW 也为 Ctrl+W 应产生冲突
useKeybindingStore.setState({
customKeybindings: {
[KeybindingAction.TAB_NEW]: 'Ctrl+W',
},
})
const conflicts = useKeybindingStore.getState().getConflicts(
KeybindingAction.TAB_NEW,
'Ctrl+W'
)
// 应检测到 TAB_CLOSE (默认 Ctrl+W) 和 TAB_NEW (自定义 Ctrl+W) 冲突
expect(conflicts.length).toBeGreaterThan(0)
expect(conflicts.some((c) => c.id === KeybindingAction.TAB_CLOSE)).toBe(true)
})
it('getConflicts 不应报告自身为冲突', () => {
const conflicts = useKeybindingStore.getState().getConflicts(
KeybindingAction.TAB_CLOSE,
'Ctrl+W' // 它自己的默认值
)
expect(conflicts).toEqual([])
})
it('getConflicts 应考虑自定义快捷键', () => {
useKeybindingStore.setState({
customKeybindings: {
[KeybindingAction.TAB_CLOSE]: 'Alt+F4',
},
})
// 设置另一个操作为 Alt+F4
const conflicts = useKeybindingStore.getState().getConflicts(
KeybindingAction.TAB_NEW,
'Alt+F4'
)
expect(conflicts.some((c) => c.id === KeybindingAction.TAB_CLOSE)).toBe(true)
})
it('无冲突时应返回空数组', () => {
const conflicts = useKeybindingStore.getState().getConflicts(
KeybindingAction.TAB_NEW,
'Ctrl+Alt+X' // 不冲突
)
expect(conflicts).toEqual([])
})
it('特殊的 Ctrl+1…9 范围不应产生冲突', () => {
const conflicts = useKeybindingStore.getState().getConflicts(
KeybindingAction.TAB_GOTO_N,
'Ctrl+1…9'
)
expect(conflicts).toEqual([])
})
})
// ─── 6. 获取所有有效快捷键映射 ──────────────────────
describe('getAllEffective', () => {
it('应返回所有操作的有效键绑定', () => {
useKeybindingStore.setState({
customKeybindings: {
[KeybindingAction.TAB_CLOSE]: 'Ctrl+Q',
},
})
const all = useKeybindingStore.getState().getAllEffective()
expect(all[KeybindingAction.TAB_CLOSE]).toBe('Ctrl+Q') // 自定义
expect(all[KeybindingAction.TAB_NEXT]).toBe('Ctrl+Tab') // 默认
expect(all[KeybindingAction.TOOLS_COMMAND_PALETTE]).toBe('Ctrl+Shift+P') // 默认
})
})
// ─── 7. 键序列规范化 ────────────────────────────────
describe('normalizeKeySequence', () => {
it('应规范化修饰键顺序', () => {
const { normalizeKeySequence } = useKeybindingStore.getState()
expect(normalizeKeySequence('Shift+Ctrl+Tab')).toBe('Ctrl+Shift+Tab')
})
it('应处理小写键名', () => {
const { normalizeKeySequence } = useKeybindingStore.getState()
expect(normalizeKeySequence('ctrl+shift+p')).toBe('Ctrl+Shift+P')
})
it('应处理空格和多余分隔符', () => {
const { normalizeKeySequence } = useKeybindingStore.getState()
expect(normalizeKeySequence(' Ctrl + Tab ')).toBe('Ctrl+Tab')
})
it('应正确处理单键 (F11)', () => {
const { normalizeKeySequence } = useKeybindingStore.getState()
expect(normalizeKeySequence('F11')).toBe('F11')
})
it('Meta 应映射为 Ctrl', () => {
const { normalizeKeySequence } = useKeybindingStore.getState()
expect(normalizeKeySequence('Meta+W')).toBe('Ctrl+W')
})
})
})