Files
MyGoNavi/frontend/src/utils/shortcuts.test.ts
2026-06-22 15:12:42 +08:00

425 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { beforeEach, describe, expect, it } from 'vitest';
import {
setCurrentLanguage,
} from '../i18n';
import {
DEFAULT_SHORTCUT_OPTIONS,
findReservedConflict,
findReservedConflicts,
describeConflictContext,
normalizeShortcutCombo,
RESERVED_SHORTCUTS,
comboToMonacoKeyBinding,
getPrimaryShortcutDisplayLabel,
getShortcutDisplayLabel,
getShortcutPrimaryModifierDisplayLabel,
resolveShortcutBinding,
resolveShortcutDisplay,
sanitizeShortcutOptions,
SHORTCUT_ACTION_META,
} from './shortcuts';
import type { ConflictInfo } from './shortcuts';
beforeEach(() => {
setCurrentLanguage('zh-CN');
});
// ─── findReservedConflict ────────────────────────────────────────────
describe('findReservedConflict', () => {
it('finds Ctrl+F conflict (Monaco Find)', () => {
const result = findReservedConflict('Ctrl+F');
expect(result).not.toBeNull();
expect(result!.label).toBe('编辑器查找');
expect(result!.context).toBe('monaco');
expect(result!.monacoCommandId).toBe('actions.find');
});
it('finds Ctrl+S conflict (browser save)', () => {
const result = findReservedConflict('Ctrl+S');
expect(result).not.toBeNull();
expect(result!.label).toBe('浏览器保存');
expect(result!.context).toBe('global');
});
it('returns null for non-reserved combo', () => {
expect(findReservedConflict('Ctrl+Shift+R')).toBeNull();
});
it('returns null for empty string', () => {
expect(findReservedConflict('')).toBeNull();
});
it('finds Meta+F (macOS variant)', () => {
const result = findReservedConflict('Meta+F');
expect(result).not.toBeNull();
expect(result!.label).toBe('编辑器查找');
expect(result!.context).toBe('monaco');
});
it('matches after normalization (ctrl+f → Ctrl+F)', () => {
const result = findReservedConflict(normalizeShortcutCombo('ctrl+f'));
expect(result).not.toBeNull();
expect(result!.label).toBe('编辑器查找');
});
it('finds F2 conflict', () => {
const result = findReservedConflict('F2');
expect(result).not.toBeNull();
expect(result!.context).toBe('monaco');
});
});
// ─── findReservedConflicts ───────────────────────────────────────────
describe('findReservedConflicts', () => {
it('returns multiple conflicts for Ctrl+Enter', () => {
const results = findReservedConflicts('Ctrl+Enter');
expect(results.length).toBeGreaterThanOrEqual(1);
const labels = results.map(r => r.label);
expect(labels).toContain('编辑器在下方插入行');
});
it('returns empty array for non-reserved combo', () => {
expect(findReservedConflicts('Ctrl+Shift+Q')).toEqual([]);
});
it('preserves monacoCommandId in results', () => {
const results = findReservedConflicts('Ctrl+F');
expect(results[0].monacoCommandId).toBe('actions.find');
});
it('uses Command instead of Control for macOS find shortcut conflicts', () => {
expect(findReservedConflicts('Ctrl+F', 'mac')).toEqual([]);
expect(findReservedConflicts('Meta+F', 'mac')[0]).toMatchObject({
label: '编辑器查找',
monacoCommandId: 'actions.find',
});
expect(findReservedConflicts('Ctrl+F', 'windows')[0]).toMatchObject({
label: '编辑器查找',
monacoCommandId: 'actions.find',
});
});
});
// ─── describeConflictContext ─────────────────────────────────────────
describe('describeConflictContext', () => {
it('describes global context', () => {
expect(describeConflictContext('global')).toBe('浏览器');
});
it('describes monaco context', () => {
expect(describeConflictContext('monaco')).toBe('编辑器');
});
it('describes datagrid context', () => {
expect(describeConflictContext('datagrid')).toBe('数据表格');
});
});
describe('shortcut localization', () => {
it('localizes action meta and reserved conflict copy for the active language while preserving raw combos', () => {
setCurrentLanguage('en-US');
try {
expect(SHORTCUT_ACTION_META.runQuery.label).toBe('Run SQL');
expect(SHORTCUT_ACTION_META.saveQuery.description).toBe('Save the current query tab; unnamed queries open the save dialog');
expect(SHORTCUT_ACTION_META.toggleQueryResultsPanel.label).toBe('Toggle Results Panel');
expect(SHORTCUT_ACTION_META.toggleQueryResultsPanel.description).toBe('Show or hide the results area below the query editor');
expect(SHORTCUT_ACTION_META.sendAIChatMessage.description).toContain('Shift+Enter');
expect(describeConflictContext('global')).toBe('Browser');
const browserSave = findReservedConflict('Ctrl+S');
expect(browserSave).toMatchObject({
label: 'Browser Save',
context: 'global',
});
setCurrentLanguage('zh-CN');
expect(SHORTCUT_ACTION_META.runQuery.label).toBe('执行 SQL');
expect(findReservedConflict('Ctrl+S')?.label).toBe('浏览器保存');
} finally {
setCurrentLanguage('zh-CN');
}
});
});
// ─── RESERVED_SHORTCUTS sanity ───────────────────────────────────────
describe('RESERVED_SHORTCUTS', () => {
it('all combos are already normalized', () => {
for (const entry of RESERVED_SHORTCUTS) {
expect(entry.combo).toBe(normalizeShortcutCombo(entry.combo));
}
});
it('has at least 10 entries', () => {
expect(RESERVED_SHORTCUTS.length).toBeGreaterThanOrEqual(10);
});
it('every entry has a label and context', () => {
for (const entry of RESERVED_SHORTCUTS) {
expect(entry.label).toBeTruthy();
expect(['global', 'monaco', 'datagrid']).toContain(entry.context);
}
});
});
// ─── shortcut defaults ───────────────────────────────────────────────
describe('shortcut defaults', () => {
it('registers select current statement as a query editor shortcut', () => {
expect(DEFAULT_SHORTCUT_OPTIONS.selectCurrentStatement).toEqual({
mac: { combo: 'Meta+E', enabled: true },
windows: { combo: 'Ctrl+E', enabled: true },
});
expect(SHORTCUT_ACTION_META.selectCurrentStatement).toMatchObject({
label: '选择当前语句',
scope: 'queryEditor',
});
});
it('registers save query as a query editor shortcut', () => {
expect(DEFAULT_SHORTCUT_OPTIONS.saveQuery).toEqual({
mac: { combo: 'Meta+S', enabled: true },
windows: { combo: 'Ctrl+S', enabled: true },
});
expect(SHORTCUT_ACTION_META.saveQuery).toMatchObject({
label: '保存查询',
scope: 'queryEditor',
allowInEditable: true,
});
});
it('registers query results panel toggle as a query editor shortcut', () => {
expect(DEFAULT_SHORTCUT_OPTIONS.toggleQueryResultsPanel).toEqual({
mac: { combo: 'Meta+Shift+M', enabled: true },
windows: { combo: 'Ctrl+Shift+M', enabled: true },
});
expect(SHORTCUT_ACTION_META.toggleQueryResultsPanel).toMatchObject({
label: '切换结果区',
scope: 'queryEditor',
allowInEditable: true,
});
});
// Windows 任务栏恢复后字体异常变大的兜底入口(方案 3
// 自动 fix 路径9848b8b2刻意不再 toggle 以避免可见动画,由该快捷键给用户主动触发的修复入口。
it('registers reset window zoom shortcut with default Ctrl+Shift+0', () => {
expect(DEFAULT_SHORTCUT_OPTIONS.resetWindowZoom).toEqual({
mac: { combo: '', enabled: false },
windows: { combo: 'Ctrl+Shift+0', enabled: true },
});
expect(SHORTCUT_ACTION_META.resetWindowZoom).toMatchObject({
label: '重置窗口缩放',
allowInEditable: true,
});
});
it('keeps configurable shortcut descriptions free of hardcoded shortcut labels', () => {
Object.values(SHORTCUT_ACTION_META).forEach((meta) => {
expect(meta.description).not.toMatch(/⌘|⌃|Ctrl|Meta|Cmd|Command|Alt\+/);
});
});
it('keeps enabled default shortcuts unique per platform', () => {
for (const platform of ['mac', 'windows'] as const) {
const seen = new Map<string, string>();
Object.entries(DEFAULT_SHORTCUT_OPTIONS).forEach(([action, bindings]) => {
const binding = bindings[platform];
if (!binding.enabled || !binding.combo) return;
const existingAction = seen.get(binding.combo);
expect(existingAction, `${platform} ${binding.combo} is shared by ${existingAction} and ${action}`).toBeUndefined();
seen.set(binding.combo, action);
});
}
});
it('uses Navicat-inspired defaults separately for macOS and Windows/Linux', () => {
expect(DEFAULT_SHORTCUT_OPTIONS.runQuery).toEqual({
mac: { combo: 'Meta+R', enabled: true },
windows: { combo: 'Ctrl+R', enabled: true },
});
expect(DEFAULT_SHORTCUT_OPTIONS.newQueryTab).toEqual({
mac: { combo: 'Meta+N', enabled: true },
windows: { combo: 'Ctrl+N', enabled: true },
});
expect(DEFAULT_SHORTCUT_OPTIONS.switchToNextTab).toEqual({
mac: { combo: 'Ctrl+Tab', enabled: true },
windows: { combo: 'Ctrl+Tab', enabled: true },
});
expect(DEFAULT_SHORTCUT_OPTIONS.switchToPreviousTab).toEqual({
mac: { combo: 'Ctrl+Shift+Tab', enabled: true },
windows: { combo: 'Ctrl+Shift+Tab', enabled: true },
});
expect(DEFAULT_SHORTCUT_OPTIONS.toggleLogPanel).toEqual({
mac: { combo: 'Meta+Shift+H', enabled: true },
windows: { combo: 'Ctrl+H', enabled: true },
});
});
it('registers connection and AI panel actions as real shortcuts', () => {
expect(DEFAULT_SHORTCUT_OPTIONS.newConnection).toEqual({
mac: { combo: 'Meta+Shift+N', enabled: true },
windows: { combo: 'Ctrl+Shift+N', enabled: true },
});
expect(DEFAULT_SHORTCUT_OPTIONS.toggleAIPanel).toEqual({
mac: { combo: 'Meta+J', enabled: true },
windows: { combo: 'Ctrl+J', enabled: true },
});
expect(SHORTCUT_ACTION_META.newConnection.label).toBe('新建数据源');
expect(SHORTCUT_ACTION_META.toggleAIPanel.label).toBe('打开 AI 数据洞察');
});
it('migrates legacy single-platform shortcut bindings into both platform slots', () => {
const options = sanitizeShortcutOptions({
runQuery: { combo: 'Ctrl+Shift+R', enabled: false },
});
expect(options.runQuery).toEqual({
mac: { combo: 'Ctrl+Shift+R', enabled: false },
windows: { combo: 'Ctrl+Shift+R', enabled: false },
});
expect(options.newQueryTab.windows.combo).toBe('Ctrl+N');
});
it('sanitizes partial platform shortcut bindings without losing defaults', () => {
const options = sanitizeShortcutOptions({
newQueryTab: {
mac: { combo: 'Meta+N', enabled: false },
},
sendAIChatMessage: {
windows: { combo: 'A', enabled: true },
},
});
expect(options.newQueryTab.mac).toEqual({ combo: 'Meta+N', enabled: false });
expect(options.newQueryTab.windows).toEqual({ combo: 'Ctrl+N', enabled: true });
expect(options.saveQuery.windows).toEqual({ combo: 'Ctrl+S', enabled: true });
expect(options.sendAIChatMessage.windows).toEqual({ combo: 'Enter', enabled: true });
});
it('resolves and displays platform-specific bindings', () => {
const options = sanitizeShortcutOptions({
newQueryTab: {
mac: { combo: 'Meta+N', enabled: true },
windows: { combo: 'Ctrl+N', enabled: true },
},
});
expect(resolveShortcutBinding(options, 'newQueryTab', 'mac')).toEqual({
combo: 'Meta+N',
enabled: true,
});
expect(getShortcutDisplayLabel('Meta+N', 'mac')).toBe('⌘N');
expect(getShortcutDisplayLabel('Meta+Shift+H', 'mac')).toBe('⌘⇧H');
expect(getShortcutDisplayLabel('Ctrl+Meta+F', 'mac')).toBe('⌃⌘F');
expect(getShortcutDisplayLabel('Meta+S', 'mac')).toBe('⌘S');
expect(getShortcutDisplayLabel('Ctrl+S', 'windows')).toBe('Ctrl+S');
expect(getShortcutPrimaryModifierDisplayLabel('mac')).toBe('⌘');
expect(getShortcutPrimaryModifierDisplayLabel('windows')).toBe('Ctrl');
expect(getPrimaryShortcutDisplayLabel('C', 'mac')).toBe('⌘C');
expect(getPrimaryShortcutDisplayLabel('C', 'windows')).toBe('Ctrl+C');
expect(getPrimaryShortcutDisplayLabel('Enter', 'mac')).toBe('⌘↵');
expect(getPrimaryShortcutDisplayLabel('Enter', 'windows')).toBe('Ctrl+Enter');
expect(resolveShortcutDisplay(options, 'newQueryTab', 'windows')).toBe('Ctrl+N');
});
});
// ─── comboToMonacoKeyBinding ─────────────────────────────────────────
describe('comboToMonacoKeyBinding', () => {
const mockKeyMod = {
CtrlCmd: 2048,
WinCtrl: 256,
Alt: 512,
Shift: 1024,
};
const mockKeyCode = {
Enter: 3,
Tab: 2,
Escape: 9,
Space: 10,
Backspace: 1,
Delete: 20,
Home: 14,
End: 13,
PageUp: 11,
PageDown: 12,
UpArrow: 16,
DownArrow: 17,
LeftArrow: 15,
RightArrow: 18,
Insert: 19,
KeyA: 31, KeyB: 32, KeyC: 33, KeyD: 34, KeyE: 35,
KeyF: 41,
KeyG: 42, KeyH: 43, KeyK: 47, KeyN: 50, KeyP: 52, KeyR: 54, KeyS: 55,
Digit0: 21, Digit1: 22, Digit2: 23, Digit3: 24, Digit4: 25,
Digit5: 26, Digit6: 27, Digit7: 28, Digit8: 29, Digit9: 30,
F1: 61, F2: 62, F3: 63, F4: 64, F5: 65, F6: 66,
F7: 67, F8: 68, F9: 69, F10: 70, F11: 71, F12: 72,
Oem1: 80, Oem2: 81, Oem3: 82, Oem4: 83, Oem5: 84,
Oem6: 85, Oem7: 86, OemComma: 87, OemMinus: 88,
OemPlus: 89, OemPeriod: 90,
};
it('maps Ctrl+Enter correctly', () => {
expect(comboToMonacoKeyBinding('Ctrl+Enter', mockKeyMod, mockKeyCode)).toEqual({
keyMod: mockKeyMod.CtrlCmd,
keyCode: mockKeyCode.Enter,
});
});
it('maps Ctrl+Shift+R correctly', () => {
expect(comboToMonacoKeyBinding('Ctrl+Shift+R', mockKeyMod, mockKeyCode)).toEqual({
keyMod: mockKeyMod.CtrlCmd | mockKeyMod.Shift,
keyCode: mockKeyCode.KeyR,
});
});
it('maps Meta+Enter (macOS variant)', () => {
expect(comboToMonacoKeyBinding('Meta+Enter', mockKeyMod, mockKeyCode)).toEqual({
keyMod: mockKeyMod.WinCtrl,
keyCode: mockKeyCode.Enter,
});
});
it('maps F2 key', () => {
expect(comboToMonacoKeyBinding('F2', mockKeyMod, mockKeyCode)).toEqual({
keyMod: 0,
keyCode: mockKeyCode.F2,
});
});
it('maps Ctrl+, (comma)', () => {
expect(comboToMonacoKeyBinding('Ctrl+,', mockKeyMod, mockKeyCode)).toEqual({
keyMod: mockKeyMod.CtrlCmd,
keyCode: mockKeyCode.OemComma,
});
});
it('returns null for empty combo', () => {
expect(comboToMonacoKeyBinding('', mockKeyMod, mockKeyCode)).toBeNull();
});
it('returns null for combo with only modifiers', () => {
expect(comboToMonacoKeyBinding('Ctrl+Shift', mockKeyMod, mockKeyCode)).toBeNull();
});
it('maps Ctrl+Digit1', () => {
expect(comboToMonacoKeyBinding('Ctrl+1', mockKeyMod, mockKeyCode)).toEqual({
keyMod: mockKeyMod.CtrlCmd,
keyCode: mockKeyCode.Digit1,
});
});
it('maps Ctrl+Alt+Delete', () => {
expect(comboToMonacoKeyBinding('Ctrl+Alt+Delete', mockKeyMod, mockKeyCode)).toEqual({
keyMod: mockKeyMod.CtrlCmd | mockKeyMod.Alt,
keyCode: mockKeyCode.Delete,
});
});
});