mirror of
https://github.com/zadam/trilium.git
synced 2025-10-19 22:58:52 +02:00
356 lines
14 KiB
TypeScript
356 lines
14 KiB
TypeScript
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
import shortcuts, { keyMatches, matchesShortcut, isIMEComposing } from "./shortcuts.js";
|
|
|
|
// Mock utils module
|
|
vi.mock("./utils.js", () => ({
|
|
default: {
|
|
isDesktop: () => true
|
|
}
|
|
}));
|
|
|
|
// Mock jQuery globally since it's used in the shortcuts module
|
|
const mockElement = {
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn()
|
|
};
|
|
|
|
const mockJQuery = vi.fn(() => [mockElement]);
|
|
(mockJQuery as any).length = 1;
|
|
mockJQuery[0] = mockElement;
|
|
|
|
(global as any).$ = mockJQuery as any;
|
|
global.document = mockElement as any;
|
|
|
|
describe("shortcuts", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Clean up any active bindings after each test
|
|
shortcuts.removeGlobalShortcut("test-namespace");
|
|
});
|
|
|
|
describe("normalizeShortcut", () => {
|
|
it("should normalize shortcut to lowercase and remove whitespace", () => {
|
|
expect(shortcuts.normalizeShortcut("Ctrl + A")).toBe("ctrl+a");
|
|
expect(shortcuts.normalizeShortcut(" SHIFT + F1 ")).toBe("shift+f1");
|
|
expect(shortcuts.normalizeShortcut("Alt+Space")).toBe("alt+space");
|
|
});
|
|
|
|
it("should handle empty or null shortcuts", () => {
|
|
expect(shortcuts.normalizeShortcut("")).toBe("");
|
|
expect(shortcuts.normalizeShortcut(null as any)).toBe(null);
|
|
expect(shortcuts.normalizeShortcut(undefined as any)).toBe(undefined);
|
|
});
|
|
|
|
it("should handle shortcuts with multiple spaces", () => {
|
|
expect(shortcuts.normalizeShortcut("Ctrl + Shift + A")).toBe("ctrl+shift+a");
|
|
});
|
|
|
|
it("should warn about malformed shortcuts", () => {
|
|
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
|
|
shortcuts.normalizeShortcut("ctrl+");
|
|
shortcuts.normalizeShortcut("+a");
|
|
shortcuts.normalizeShortcut("ctrl++a");
|
|
|
|
expect(consoleSpy).toHaveBeenCalledTimes(3);
|
|
consoleSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe("keyMatches", () => {
|
|
const createKeyboardEvent = (key: string, code?: string) => ({
|
|
key,
|
|
code: code || `Key${key.toUpperCase()}`
|
|
} as KeyboardEvent);
|
|
|
|
it("should match regular letter keys using key code", () => {
|
|
const event = createKeyboardEvent("a", "KeyA");
|
|
expect(keyMatches(event, "a")).toBe(true);
|
|
expect(keyMatches(event, "A")).toBe(true);
|
|
});
|
|
|
|
it("should match number keys using digit codes", () => {
|
|
const event = createKeyboardEvent("1", "Digit1");
|
|
expect(keyMatches(event, "1")).toBe(true);
|
|
});
|
|
|
|
it("should match special keys using key mapping", () => {
|
|
expect(keyMatches({ key: "Enter" } as KeyboardEvent, "return")).toBe(true);
|
|
expect(keyMatches({ key: "Enter" } as KeyboardEvent, "enter")).toBe(true);
|
|
expect(keyMatches({ key: "Delete" } as KeyboardEvent, "del")).toBe(true);
|
|
expect(keyMatches({ key: "Escape" } as KeyboardEvent, "esc")).toBe(true);
|
|
expect(keyMatches({ key: " " } as KeyboardEvent, "space")).toBe(true);
|
|
expect(keyMatches({ key: "ArrowUp" } as KeyboardEvent, "up")).toBe(true);
|
|
});
|
|
|
|
it("should match function keys", () => {
|
|
expect(keyMatches({ key: "F1" } as KeyboardEvent, "f1")).toBe(true);
|
|
expect(keyMatches({ key: "F12" } as KeyboardEvent, "f12")).toBe(true);
|
|
});
|
|
|
|
it("should handle undefined or null keys", () => {
|
|
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
|
|
expect(keyMatches({} as KeyboardEvent, null as any)).toBe(false);
|
|
expect(keyMatches({} as KeyboardEvent, undefined as any)).toBe(false);
|
|
|
|
expect(consoleSpy).toHaveBeenCalled();
|
|
consoleSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe("matchesShortcut", () => {
|
|
const createKeyboardEvent = (options: {
|
|
key: string;
|
|
code?: string;
|
|
ctrlKey?: boolean;
|
|
altKey?: boolean;
|
|
shiftKey?: boolean;
|
|
metaKey?: boolean;
|
|
}) => ({
|
|
key: options.key,
|
|
code: options.code || `Key${options.key.toUpperCase()}`,
|
|
ctrlKey: options.ctrlKey || false,
|
|
altKey: options.altKey || false,
|
|
shiftKey: options.shiftKey || false,
|
|
metaKey: options.metaKey || false
|
|
} as KeyboardEvent);
|
|
|
|
it("should match shortcuts with modifiers", () => {
|
|
const event = createKeyboardEvent({ key: "a", code: "KeyA", ctrlKey: true });
|
|
expect(matchesShortcut(event, "ctrl+a")).toBe(true);
|
|
|
|
const shiftEvent = createKeyboardEvent({ key: "a", code: "KeyA", shiftKey: true });
|
|
expect(matchesShortcut(shiftEvent, "shift+a")).toBe(true);
|
|
});
|
|
|
|
it("should match complex modifier combinations", () => {
|
|
const event = createKeyboardEvent({
|
|
key: "a",
|
|
code: "KeyA",
|
|
ctrlKey: true,
|
|
shiftKey: true
|
|
});
|
|
expect(matchesShortcut(event, "ctrl+shift+a")).toBe(true);
|
|
});
|
|
|
|
it("should not match when modifiers don't match", () => {
|
|
const event = createKeyboardEvent({ key: "a", code: "KeyA", ctrlKey: true });
|
|
expect(matchesShortcut(event, "alt+a")).toBe(false);
|
|
expect(matchesShortcut(event, "a")).toBe(false);
|
|
});
|
|
|
|
it("should not match when no modifiers are used", () => {
|
|
const event = createKeyboardEvent({ key: "a", code: "KeyA" });
|
|
expect(matchesShortcut(event, "a")).toBe(false);
|
|
});
|
|
|
|
it("should handle alternative modifier names", () => {
|
|
const ctrlEvent = createKeyboardEvent({ key: "a", code: "KeyA", ctrlKey: true });
|
|
expect(matchesShortcut(ctrlEvent, "control+a")).toBe(true);
|
|
|
|
const metaEvent = createKeyboardEvent({ key: "a", code: "KeyA", metaKey: true });
|
|
expect(matchesShortcut(metaEvent, "cmd+a")).toBe(true);
|
|
expect(matchesShortcut(metaEvent, "command+a")).toBe(true);
|
|
});
|
|
|
|
it("should handle empty or invalid shortcuts", () => {
|
|
const event = createKeyboardEvent({ key: "a", code: "KeyA" });
|
|
expect(matchesShortcut(event, "")).toBe(false);
|
|
expect(matchesShortcut(event, null as any)).toBe(false);
|
|
});
|
|
|
|
it("should handle invalid events", () => {
|
|
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
|
|
expect(matchesShortcut(null as any, "a")).toBe(false);
|
|
expect(matchesShortcut({} as KeyboardEvent, "a")).toBe(false);
|
|
|
|
expect(consoleSpy).toHaveBeenCalled();
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it("should warn about invalid shortcut formats", () => {
|
|
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
const event = createKeyboardEvent({ key: "a", code: "KeyA" });
|
|
|
|
matchesShortcut(event, "ctrl+");
|
|
matchesShortcut(event, "+");
|
|
|
|
expect(consoleSpy).toHaveBeenCalled();
|
|
consoleSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe("bindGlobalShortcut", () => {
|
|
it("should bind a global shortcut", () => {
|
|
const handler = vi.fn();
|
|
shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace");
|
|
|
|
expect(mockElement.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
|
|
});
|
|
|
|
it("should not bind shortcuts when handler is null", () => {
|
|
shortcuts.bindGlobalShortcut("ctrl+a", null, "test-namespace");
|
|
|
|
expect(mockElement.addEventListener).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should remove previous bindings when namespace is reused", () => {
|
|
const handler1 = vi.fn();
|
|
const handler2 = vi.fn();
|
|
|
|
shortcuts.bindGlobalShortcut("ctrl+a", handler1, "test-namespace");
|
|
expect(mockElement.addEventListener).toHaveBeenCalledTimes(1);
|
|
|
|
shortcuts.bindGlobalShortcut("ctrl+b", handler2, "test-namespace");
|
|
expect(mockElement.removeEventListener).toHaveBeenCalledTimes(1);
|
|
expect(mockElement.addEventListener).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|
|
|
|
describe("bindElShortcut", () => {
|
|
it("should bind shortcut to specific element", () => {
|
|
const mockEl = { addEventListener: vi.fn(), removeEventListener: vi.fn() };
|
|
const mockJQueryEl = [mockEl] as any;
|
|
mockJQueryEl.length = 1;
|
|
|
|
const handler = vi.fn();
|
|
shortcuts.bindElShortcut(mockJQueryEl, "ctrl+a", handler, "test-namespace");
|
|
|
|
expect(mockEl.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
|
|
});
|
|
|
|
it("should fall back to document when element is empty", () => {
|
|
const emptyJQuery = [] as any;
|
|
emptyJQuery.length = 0;
|
|
|
|
const handler = vi.fn();
|
|
shortcuts.bindElShortcut(emptyJQuery, "ctrl+a", handler, "test-namespace");
|
|
|
|
expect(mockElement.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
|
|
});
|
|
});
|
|
|
|
describe("removeGlobalShortcut", () => {
|
|
it("should remove shortcuts for a specific namespace", () => {
|
|
const handler = vi.fn();
|
|
shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace");
|
|
|
|
shortcuts.removeGlobalShortcut("test-namespace");
|
|
|
|
expect(mockElement.removeEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
|
|
});
|
|
});
|
|
|
|
describe("event handling", () => {
|
|
it.skip("should call handler when shortcut matches", () => {
|
|
const handler = vi.fn();
|
|
shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace");
|
|
|
|
// Get the listener that was registered
|
|
expect(mockElement.addEventListener.mock.calls).toHaveLength(1);
|
|
const [, listener] = mockElement.addEventListener.mock.calls[0];
|
|
|
|
// First verify that matchesShortcut works directly
|
|
const testEvent = {
|
|
type: "keydown",
|
|
key: "a",
|
|
code: "KeyA",
|
|
ctrlKey: true,
|
|
altKey: false,
|
|
shiftKey: false,
|
|
metaKey: false,
|
|
preventDefault: vi.fn(),
|
|
stopPropagation: vi.fn()
|
|
} as any;
|
|
|
|
// Test matchesShortcut directly first
|
|
expect(matchesShortcut(testEvent, "ctrl+a")).toBe(true);
|
|
|
|
// Now test the actual listener
|
|
listener(testEvent);
|
|
|
|
expect(handler).toHaveBeenCalled();
|
|
expect(testEvent.preventDefault).toHaveBeenCalled();
|
|
expect(testEvent.stopPropagation).toHaveBeenCalled();
|
|
});
|
|
|
|
it("should not call handler for non-keyboard events", () => {
|
|
const handler = vi.fn();
|
|
shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace");
|
|
|
|
const [, listener] = mockElement.addEventListener.mock.calls[0];
|
|
|
|
// Simulate a non-keyboard event
|
|
const event = {
|
|
type: "click"
|
|
} as any;
|
|
|
|
listener(event);
|
|
|
|
expect(handler).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should not call handler when shortcut doesn't match", () => {
|
|
const handler = vi.fn();
|
|
shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace");
|
|
|
|
const [, listener] = mockElement.addEventListener.mock.calls[0];
|
|
|
|
// Simulate a non-matching keydown event
|
|
const event = {
|
|
type: "keydown",
|
|
key: "b",
|
|
code: "KeyB",
|
|
ctrlKey: true,
|
|
altKey: false,
|
|
shiftKey: false,
|
|
metaKey: false,
|
|
preventDefault: vi.fn(),
|
|
stopPropagation: vi.fn()
|
|
} as any;
|
|
|
|
listener(event);
|
|
|
|
expect(handler).not.toHaveBeenCalled();
|
|
expect(event.preventDefault).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('isIMEComposing', () => {
|
|
it('should return true when event.isComposing is true', () => {
|
|
const event = { isComposing: true, keyCode: 65 } as KeyboardEvent;
|
|
expect(isIMEComposing(event)).toBe(true);
|
|
});
|
|
|
|
it('should return true when keyCode is 229', () => {
|
|
const event = { isComposing: false, keyCode: 229 } as KeyboardEvent;
|
|
expect(isIMEComposing(event)).toBe(true);
|
|
});
|
|
|
|
it('should return true when both isComposing is true and keyCode is 229', () => {
|
|
const event = { isComposing: true, keyCode: 229 } as KeyboardEvent;
|
|
expect(isIMEComposing(event)).toBe(true);
|
|
});
|
|
|
|
it('should return false for normal keys', () => {
|
|
const event = { isComposing: false, keyCode: 65 } as KeyboardEvent;
|
|
expect(isIMEComposing(event)).toBe(false);
|
|
});
|
|
|
|
it('should return false when isComposing is undefined and keyCode is not 229', () => {
|
|
const event = { keyCode: 13 } as KeyboardEvent;
|
|
expect(isIMEComposing(event)).toBe(false);
|
|
});
|
|
|
|
it('should handle null/undefined events gracefully', () => {
|
|
expect(isIMEComposing(null as any)).toBe(false);
|
|
expect(isIMEComposing(undefined as any)).toBe(false);
|
|
});
|
|
});
|
|
});
|