Files
joplin/packages/lib/services/KeymapService.test.ts
T

370 lines
15 KiB
TypeScript

import { tempFilePath } from '../testing/test-utils';
import KeymapService, { KeymapItem } from '../services/KeymapService';
const keymapService = KeymapService.instance();
keymapService.initialize([]);
describe('KeymapService', () => {
describe('validateAccelerator', () => {
it('should identify valid Accelerators', () => {
const testCases = {
darwin: [
'F4',
'Cmd+F9',
'Option+Z',
'Option+Shift+F',
'Ctrl+Option+U',
'Option+Shift+Cmd+F9',
'Ctrl+Shift+Z',
'Option+Shift+Cmd+B',
],
linux: [
'F4',
'Ctrl+F9',
'Alt+Shift+F',
'Shift+U',
'Ctrl+Shift+T',
'Ctrl+Alt+Shift+Z',
'Alt+E',
'Alt+Shift+F9',
],
};
// eslint-disable-next-line github/array-foreach -- Old code before rule was applied
Object.entries(testCases).forEach(([platform, accelerators]) => {
keymapService.initialize([], platform);
// eslint-disable-next-line github/array-foreach -- Old code before rule was applied
accelerators.forEach(accelerator => {
expect(() => keymapService.validateAccelerator(accelerator)).not.toThrow();
});
});
});
it('should identify invalid Accelerators', () => {
const testCases = {
darwin: [
'',
'A+Z',
'Cmd',
'Ctrl+',
'Option+Cmd',
'Ctrl+Shift',
'Cmd+H+A',
'Option+Shift+Cmd+J+O+P',
'Opptionn+F9',
'Ctrl+Shiftt+X',
'Cmd+Option+Shoft+T',
],
win32: [
'+',
'B+F4',
'Ctrl+',
'Ctrl+Shift+',
'Cmd+Alt',
'Ctrl+Shift+Alt',
'Cmd+H+A',
'Ctrl+Shift+Alt+J+O+P',
'Contrl+F9',
'Controller+Shift+X',
'Cmd+Option+Shoft+T',
],
};
// eslint-disable-next-line github/array-foreach -- Old code before rule was applied
Object.entries(testCases).forEach(([platform, accelerators]) => {
keymapService.initialize([], platform);
// eslint-disable-next-line github/array-foreach -- Old code before rule was applied
accelerators.forEach(accelerator => {
expect(() => keymapService.validateAccelerator(accelerator)).toThrow();
});
});
});
});
describe('registerCommandAccelerator', () => {
beforeEach(() => keymapService.initialize());
it('should allow registering new commands', async () => {
keymapService.initialize([], 'linux');
keymapService.registerCommandAccelerator('myCustomCommand', 'Ctrl+Shift+Alt+B');
expect(keymapService.getAccelerator('myCustomCommand')).toEqual('Ctrl+Shift+Alt+B');
// Check that macOS key conversion is working
keymapService.initialize([], 'darwin');
keymapService.registerCommandAccelerator('myCustomCommand', 'CmdOrCtrl+Shift+Alt+B');
expect(keymapService.getAccelerator('myCustomCommand')).toEqual('Cmd+Shift+Option+B');
keymapService.setAccelerator('myCustomCommand', 'Cmd+Shift+Option+X');
// Check that the new custom shortcut is being saved and loaded
const keymapFilePath = tempFilePath('json');
await keymapService.saveCustomKeymap(keymapFilePath);
keymapService.initialize([], 'darwin');
await keymapService.loadCustomKeymap(keymapFilePath);
expect(keymapService.getAccelerator('myCustomCommand')).toEqual('Cmd+Shift+Option+X');
});
});
describe('getAccelerator', () => {
beforeEach(() => keymapService.initialize());
it('should return the platform-specific default Accelerator', () => {
keymapService.initialize([], 'darwin');
expect(keymapService.getAccelerator('newNote')).toEqual('Cmd+N');
expect(keymapService.getAccelerator('synchronize')).toEqual('Cmd+S');
expect(keymapService.getAccelerator('textSelectAll')).toEqual('Cmd+A');
expect(keymapService.getAccelerator('textBold')).toEqual('Cmd+B');
expect(keymapService.getAccelerator('closeWindow')).toEqual('Cmd+W');
keymapService.initialize([], 'linux');
expect(keymapService.getAccelerator('newNote')).toEqual('Ctrl+N');
expect(keymapService.getAccelerator('synchronize')).toEqual('Ctrl+S');
expect(keymapService.getAccelerator('closeWindow')).toEqual('Ctrl+W');
keymapService.initialize([], 'win32');
expect(keymapService.getAccelerator('textSelectAll')).toEqual('Ctrl+A');
expect(keymapService.getAccelerator('textBold')).toEqual('Ctrl+B');
expect(keymapService.getAccelerator('closeWindow')).toEqual('Ctrl+W');
});
it('should throw when an invalid command is requested', () => {
expect(() => keymapService.getAccelerator('totallyNonExistentCommand')).toThrow();
});
});
describe('setAccelerator', () => {
beforeEach(() => keymapService.initialize());
it('should update the Accelerator', () => {
keymapService.initialize(['print'], 'darwin');
const testCases_Darwin = [
{ command: 'newNote', accelerator: 'Ctrl+Option+Shift+N' },
{ command: 'synchronize', accelerator: 'F11' },
{ command: 'textBold', accelerator: 'Shift+F5' },
{ command: 'showLocalSearch', accelerator: 'Option+Cmd+S' },
{ command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' },
{ command: 'print', accelerator: null /* Disabled */ },
{ command: 'focusElementNoteTitle', accelerator: 'Option+Shift+Cmd+T' },
{ command: 'focusElementNoteBody', accelerator: 'Ctrl+Option+Shift+Cmd+B' },
];
// eslint-disable-next-line github/array-foreach -- Old code before rule was applied
testCases_Darwin.forEach(({ command, accelerator }) => {
keymapService.setAccelerator(command, accelerator);
expect(keymapService.getAccelerator(command)).toEqual(accelerator);
});
keymapService.initialize(['print'], 'linux');
const testCases_Linux = [
{ command: 'newNote', accelerator: 'Ctrl+Alt+Shift+N' },
{ command: 'synchronize', accelerator: 'F15' },
{ command: 'textBold', accelerator: 'Shift+F5' },
{ command: 'showLocalSearch', accelerator: 'Ctrl+Alt+L' },
{ command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' },
{ command: 'print', accelerator: null /* Disabled */ },
{ command: 'focusElementNoteTitle', accelerator: 'Ctrl+Alt+Shift+T' },
{ command: 'focusElementNoteBody', accelerator: 'Ctrl+Alt+Shift+B' },
];
// eslint-disable-next-line github/array-foreach -- Old code before rule was applied
testCases_Linux.forEach(({ command, accelerator }) => {
keymapService.setAccelerator(command, accelerator);
expect(keymapService.getAccelerator(command)).toEqual(accelerator);
});
});
});
describe('getDefaultAccelerator', () => {
beforeEach(() => keymapService.initialize(['print', 'linux']));
it('should return the default accelerator', () => {
const testCases = [
{ command: 'newNote', accelerator: 'Ctrl+Alt+Shift+N' },
{ command: 'synchronize', accelerator: null /* Disabled */ },
{ command: 'textBold', accelerator: 'Shift+F5' },
{ command: 'showLocalSearch', accelerator: 'Ctrl+Alt+L' },
{ command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' },
{ command: 'print', accelerator: 'Alt+P' },
{ command: 'focusElementNoteTitle', accelerator: 'Ctrl+Alt+Shift+T' },
{ command: 'focusElementNoteBody', accelerator: 'Ctrl+Alt+Shift+B' },
];
// eslint-disable-next-line github/array-foreach -- Old code before rule was applied
testCases.forEach(({ command, accelerator }) => {
// Remember the real default Accelerator value
const defaultAccelerator = keymapService.getAccelerator(command);
// Update the Accelerator and then retrieve the default accelerator
keymapService.setAccelerator(command, accelerator);
expect(keymapService.getDefaultAccelerator(command)).toEqual(defaultAccelerator);
});
});
});
describe('overrideKeymap', () => {
beforeEach(() => keymapService.initialize());
it('should update the keymap', () => {
keymapService.initialize([], 'darwin');
const customKeymapItems_Darwin = [
{ command: 'newNote', accelerator: 'Option+Shift+Cmd+N' },
{ command: 'synchronize', accelerator: 'Ctrl+F11' },
{ command: 'textBold', accelerator: 'Shift+F5' },
{ command: 'showLocalSearch', accelerator: 'Ctrl+Option+S' },
{ command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' },
{ command: 'print', accelerator: 'Option+P' },
{ command: 'help', accelerator: null /* Disabled */ },
{ command: 'focusElementNoteTitle', accelerator: 'Ctrl+Option+Shift+T' },
{ command: 'focusElementNoteBody', accelerator: 'Option+Shift+Cmd+B' },
{ command: 'focusElementSideBar', accelerator: 'Shift+Cmd+L' /* Default of focusElementNoteList */ },
{ command: 'focusElementNoteList', accelerator: 'Shift+Cmd+S' /* Default of focusElementSideBar */ },
];
expect(() => keymapService.overrideKeymap(customKeymapItems_Darwin)).not.toThrow();
// eslint-disable-next-line github/array-foreach -- Old code before rule was applied
customKeymapItems_Darwin.forEach(({ command, accelerator }) => {
// Also check if keymap is updated or not
expect(keymapService.getAccelerator(command)).toEqual(accelerator);
});
keymapService.initialize([], 'win32');
const customKeymapItems_Win32 = [
{ command: 'newNote', accelerator: 'Ctrl+Alt+Shift+N' },
{ command: 'synchronize', accelerator: 'Ctrl+F11' },
{ command: 'textBold', accelerator: 'Shift+F5' },
{ command: 'showLocalSearch', accelerator: 'Ctrl+Alt+L' },
{ command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' },
{ command: 'print', accelerator: 'Alt+P' },
{ command: 'help', accelerator: null /* Disabled */ },
{ command: 'focusElementNoteTitle', accelerator: 'Ctrl+Alt+Shift+T' },
{ command: 'focusElementNoteBody', accelerator: 'Ctrl+Alt+Shift+B' },
{ command: 'focusElementSideBar', accelerator: 'Ctrl+Shift+L' /* Default of focusElementNoteList */ },
{ command: 'focusElementNoteList', accelerator: 'Ctrl+Shift+S' /* Default of focusElementSideBar */ },
];
expect(() => keymapService.overrideKeymap(customKeymapItems_Win32)).not.toThrow();
// eslint-disable-next-line github/array-foreach -- Old code before rule was applied
customKeymapItems_Win32.forEach(({ command, accelerator }) => {
// Also check if keymap is updated or not
expect(keymapService.getAccelerator(command)).toEqual(accelerator);
});
});
it('should import legacy undo and redo command names', () => {
// Regression test for #15308: keymap files saved before 3.6.11 used
// "editor.undo" / "editor.redo" for the global undo/redo accelerators.
// In 3.6.11 these were renamed to "globalUndo" / "globalRedo", so
// importing the old file would clash with the new defaults that
// already bind Ctrl+Z / Ctrl+Y to the renamed commands.
keymapService.initialize([], 'win32');
const customKeymapItems = [
{ command: 'editor.undo', accelerator: 'Ctrl+Z' },
{ command: 'editor.redo', accelerator: 'Ctrl+Y' },
];
expect(() => keymapService.overrideKeymap(customKeymapItems)).not.toThrow();
expect(keymapService.getAccelerator('globalUndo')).toEqual('Ctrl+Z');
expect(keymapService.getAccelerator('globalRedo')).toEqual('Ctrl+Y');
expect(keymapService.getCommandNames()).not.toContain('editor.undo');
expect(keymapService.getCommandNames()).not.toContain('editor.redo');
});
it('should throw when the required properties are missing', () => {
const customKeymaps = [
[
{ commmmmand: 'gotoAnything', accelerator: 'Ctrl+Shift+G' },
{ command: 'showLocalSearch', accelerator: 'Ctrl+Alt+L' },
{ command: 'print', accelerator: 'Alt+P' },
],
[
{ accelerator: 'Alt+P' },
{ command: 'showLocalSearch', accelerator: 'Ctrl+Alt+S' },
{ command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' },
],
[
{ command: 'showLocalSearch', accel: 'Ctrl+Alt+L' },
{ command: 'print', accelerator: 'Alt+P' },
{ command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' },
],
[
{ command: 'print' },
{ command: 'showLocalSearch', accelerator: 'Ctrl+Alt+L' },
{ command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' },
],
];
for (let i = 0; i < customKeymaps.length; i++) {
// Intentionally malformed test data — cast through unknown so the type
// checker doesn't reject the wrong-keyed entries that the test relies on.
const customKeymapItems = customKeymaps[i] as unknown as KeymapItem[];
expect(() => keymapService.overrideKeymap(customKeymapItems)).toThrow();
}
});
it('should throw when the provided Accelerators are invalid', () => {
// Only one test case is provided since KeymapService.validateAccelerator() is already tested
const customKeymapItems = [
{ command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' },
{ command: 'print', accelerator: 'Alt+P' },
{ command: 'focusElementNoteTitle', accelerator: 'Ctrl+Alt+Shift+J+O+P+L+I+N' },
];
expect(() => keymapService.overrideKeymap(customKeymapItems)).toThrow();
});
// it('should throw when the provided commands are invalid', () => {
// const customKeymapItems = [
// { command: 'totallyInvalidCommand', accelerator: 'Ctrl+Shift+G' },
// { command: 'print', accelerator: 'Alt+P' },
// { command: 'focusElementNoteTitle', accelerator: 'Ctrl+Alt+Shift+J' },
// ];
// expect(() => keymapService.overrideKeymap(customKeymapItems)).toThrow();
// });
it('should throw when duplicate accelerators are provided', () => {
const customKeymaps_Darwin = [
[
{ command: 'showLocalSearch', accelerator: 'Option+Cmd+S' /* Duplicate */ },
{ command: 'gotoAnything', accelerator: 'Option+Cmd+S' /* Duplicate */ },
{ command: 'print', accelerator: 'Option+P' },
],
[
{ command: 'showLocalSearch', accelerator: 'Option+Cmd+S' },
{ command: 'print', accelerator: 'Cmd+P' /* Default of gotoAnything */ },
{ command: 'focusElementNoteTitle', accelerator: 'Option+Shift+Cmd+J' },
],
];
for (let i = 0; i < customKeymaps_Darwin.length; i++) {
const customKeymapItems = customKeymaps_Darwin[i];
expect(() => keymapService.overrideKeymap(customKeymapItems)).toThrow();
}
const customKeymaps_Linux = [
[
{ command: 'showLocalSearch', accelerator: 'Ctrl+Alt+L' /* Duplicate */ },
{ command: 'print', accelerator: 'Alt+P' },
{ command: 'gotoAnything', accelerator: 'Ctrl+Alt+L' /* Duplicate */ },
],
[
{ command: 'showLocalSearch', accelerator: 'Ctrl+Alt+L' },
{ command: 'print', accelerator: 'Ctrl+P' /* Default of gotoAnything */ },
{ command: 'focusElementNoteTitle', accelerator: 'Ctrl+Alt+Shift+J' },
],
];
for (let i = 0; i < customKeymaps_Linux.length; i++) {
const customKeymapItems = customKeymaps_Linux[i];
expect(() => keymapService.overrideKeymap(customKeymapItems)).toThrow();
}
});
});
it('getAriaKeyShortcuts should return undefined if there is no associated shortcut', () => {
keymapService.registerCommandAccelerator('some-command', null);
expect(keymapService.getAriaKeyShortcuts('some-command')).toBe(undefined);
});
});