rx-hotkeys 2.6.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -98
- package/dist/hotkeys.d.ts +88 -37
- package/dist/hotkeys.d.ts.map +1 -1
- package/dist/hotkeys.js +228 -97
- package/dist/hotkeys.test.js +402 -238
- package/dist/keys.d.ts +6 -0
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +21 -0
- package/dist/testutils.d.ts +7 -4
- package/dist/testutils.d.ts.map +1 -1
- package/dist/testutils.js +9 -6
- package/package.json +1 -1
- package/dist/tt.d.ts +0 -125
- package/dist/tt.d.ts.map +0 -1
- package/dist/tt.js +0 -384
- package/dist/ttt.d.ts +0 -164
- package/dist/ttt.d.ts.map +0 -1
- package/dist/ttt.js +0 -439
- package/dist/tttt.d.ts +0 -67
- package/dist/tttt.d.ts.map +0 -1
- package/dist/tttt.js +0 -301
package/dist/hotkeys.test.js
CHANGED
|
@@ -2,7 +2,7 @@ import { describe, it, before, beforeEach, afterEach, mock } from "node:test";
|
|
|
2
2
|
import assert from "node:assert";
|
|
3
3
|
import { Hotkeys, ShortcutTypes } from "./hotkeys.js";
|
|
4
4
|
import { Keys } from "./keys.js";
|
|
5
|
-
import { fromEvent, BehaviorSubject } from "rxjs";
|
|
5
|
+
import { fromEvent, BehaviorSubject, Observable, EMPTY } from "rxjs";
|
|
6
6
|
import { createMockFn, dispatchKeyEvent } from "./testutils.js";
|
|
7
7
|
import { JSDOM } from "jsdom";
|
|
8
8
|
// --- JSDOM and RxJS setup for Node.js tests ---
|
|
@@ -10,21 +10,31 @@ let dom;
|
|
|
10
10
|
let window;
|
|
11
11
|
let document;
|
|
12
12
|
let originalPerformanceNow;
|
|
13
|
+
let testArea; // For target tests
|
|
13
14
|
before(() => {
|
|
14
|
-
dom = new JSDOM(
|
|
15
|
+
dom = new JSDOM(`<!DOCTYPE html><html><body><div id="test-area"></div></body></html>`, {
|
|
15
16
|
url: "http://localhost",
|
|
16
17
|
});
|
|
17
18
|
window = dom.window;
|
|
18
19
|
document = window.document;
|
|
20
|
+
testArea = document.getElementById("test-area");
|
|
19
21
|
// @ts-ignore
|
|
20
22
|
global.document = document;
|
|
21
23
|
// @ts-ignore
|
|
24
|
+
global.window = window;
|
|
25
|
+
// @ts-ignore
|
|
26
|
+
global.HTMLElement = window.HTMLElement;
|
|
27
|
+
// @ts-ignore
|
|
22
28
|
global.KeyboardEvent = window.KeyboardEvent;
|
|
23
29
|
// @ts-ignore
|
|
24
30
|
global.BehaviorSubject = BehaviorSubject;
|
|
25
31
|
// @ts-ignore
|
|
26
32
|
global.fromEvent = fromEvent;
|
|
27
33
|
// @ts-ignore
|
|
34
|
+
global.Observable = Observable;
|
|
35
|
+
// @ts-ignore
|
|
36
|
+
global.EMPTY = EMPTY;
|
|
37
|
+
// @ts-ignore
|
|
28
38
|
if (typeof global.performance === "undefined") {
|
|
29
39
|
// @ts-ignore
|
|
30
40
|
global.performance = {};
|
|
@@ -242,82 +252,92 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
242
252
|
});
|
|
243
253
|
});
|
|
244
254
|
describe("addCombination", () => {
|
|
245
|
-
it(`should
|
|
246
|
-
const config = { id: "simpleA", keys: { key: Keys.A }
|
|
247
|
-
const
|
|
248
|
-
assert
|
|
249
|
-
|
|
255
|
+
it(`should return an Observable that emits on a simple key combination (e.g., "A")`, () => {
|
|
256
|
+
const config = { id: "simpleA", keys: { key: Keys.A } };
|
|
257
|
+
const combo$ = keyManager.addCombination(config);
|
|
258
|
+
assert(combo$ instanceof Observable, "Did not return an Observable");
|
|
259
|
+
combo$.subscribe(mockCallback);
|
|
260
|
+
dispatchKeyEvent(document, "a");
|
|
250
261
|
assert.strictEqual(mockCallback.calledCount, 1, `Callback for "a" not called`);
|
|
251
262
|
mockCallback.mockClear();
|
|
252
|
-
dispatchKeyEvent("A");
|
|
263
|
+
dispatchKeyEvent(document, "A");
|
|
253
264
|
assert.strictEqual(mockCallback.calledCount, 1, `Callback for "A" not called`);
|
|
254
265
|
});
|
|
255
|
-
it("should return
|
|
256
|
-
const config = { id: "nullKey", keys: { key: null }
|
|
257
|
-
const
|
|
258
|
-
|
|
266
|
+
it("should return an empty observable and warn if keys.key is null or undefined", () => {
|
|
267
|
+
const config = { id: "nullKey", keys: { key: null } };
|
|
268
|
+
const combo$ = keyManager.addCombination(config);
|
|
269
|
+
combo$.subscribe(mockCallback);
|
|
259
270
|
assert.strictEqual(consoleWarnMock.mock.calls.length, 1);
|
|
260
|
-
assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Invalid "key" property in shortcut "nullKey"
|
|
271
|
+
assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Invalid "key" property in shortcut "nullKey"`));
|
|
272
|
+
dispatchKeyEvent(document, "a");
|
|
273
|
+
assert.strictEqual(mockCallback.calledCount, 0);
|
|
261
274
|
});
|
|
262
|
-
it("should pass the KeyboardEvent to the
|
|
263
|
-
const config = { id: "eventPass", keys: { key: Keys.E }
|
|
264
|
-
keyManager.addCombination(config);
|
|
265
|
-
const event = dispatchKeyEvent("e");
|
|
275
|
+
it("should pass the KeyboardEvent to the subscriber", () => {
|
|
276
|
+
const config = { id: "eventPass", keys: { key: Keys.E } };
|
|
277
|
+
keyManager.addCombination(config).subscribe(mockCallback);
|
|
278
|
+
const event = dispatchKeyEvent(document, "e");
|
|
266
279
|
assert.strictEqual(mockCallback.calledCount, 1);
|
|
267
280
|
assert.deepStrictEqual(mockCallback.lastArgs, [event]);
|
|
268
281
|
});
|
|
269
|
-
it("should
|
|
270
|
-
const config = { id: "ctrlS", keys: { key: Keys.S, ctrlKey: true }
|
|
271
|
-
keyManager.addCombination(config);
|
|
272
|
-
dispatchKeyEvent("s", { ctrlKey: true });
|
|
282
|
+
it("should emit for a combination with Ctrl key", () => {
|
|
283
|
+
const config = { id: "ctrlS", keys: { key: Keys.S, ctrlKey: true } };
|
|
284
|
+
keyManager.addCombination(config).subscribe(mockCallback);
|
|
285
|
+
dispatchKeyEvent(document, "s", "keydown", { ctrlKey: true });
|
|
273
286
|
assert.strictEqual(mockCallback.calledCount, 1);
|
|
274
287
|
});
|
|
275
|
-
it("should NOT
|
|
276
|
-
const config = { id: "noCtrlA", keys: { key: Keys.A, ctrlKey: false }
|
|
277
|
-
keyManager.addCombination(config);
|
|
278
|
-
dispatchKeyEvent("a", { ctrlKey: true });
|
|
288
|
+
it("should NOT emit if specified modifier key (ctrlKey: false) is false and event has it true", () => {
|
|
289
|
+
const config = { id: "noCtrlA", keys: { key: Keys.A, ctrlKey: false } };
|
|
290
|
+
keyManager.addCombination(config).subscribe(mockCallback);
|
|
291
|
+
dispatchKeyEvent(document, "a", "keydown", { ctrlKey: true });
|
|
279
292
|
assert.strictEqual(mockCallback.calledCount, 0);
|
|
280
|
-
dispatchKeyEvent("a", { ctrlKey: false });
|
|
293
|
+
dispatchKeyEvent(document, "a", "keydown", { ctrlKey: false });
|
|
281
294
|
assert.strictEqual(mockCallback.calledCount, 1);
|
|
282
295
|
});
|
|
283
|
-
it("should
|
|
284
|
-
const config = { id: "escapeKeyObj", keys: { key: Keys.Escape }
|
|
285
|
-
keyManager.addCombination(config);
|
|
286
|
-
dispatchKeyEvent("Escape"); // Event key matches Keys.Escape
|
|
296
|
+
it("should emit for special keys like Escape (object form)", () => {
|
|
297
|
+
const config = { id: "escapeKeyObj", keys: { key: Keys.Escape } };
|
|
298
|
+
keyManager.addCombination(config).subscribe(mockCallback);
|
|
299
|
+
dispatchKeyEvent(document, "Escape"); // Event key matches Keys.Escape
|
|
287
300
|
assert.strictEqual(mockCallback.calledCount, 1);
|
|
288
301
|
});
|
|
289
302
|
it("should handle preventDefault correctly (object form)", () => {
|
|
290
|
-
const config = { id: "preventAObj", keys: { key: Keys.A },
|
|
291
|
-
keyManager.addCombination(config);
|
|
292
|
-
const event = dispatchKeyEvent("a");
|
|
303
|
+
const config = { id: "preventAObj", keys: { key: Keys.A }, preventDefault: true };
|
|
304
|
+
keyManager.addCombination(config).subscribe(mockCallback);
|
|
305
|
+
const event = dispatchKeyEvent(document, "a");
|
|
293
306
|
assert.strictEqual(mockCallback.calledCount, 1);
|
|
294
307
|
assert.strictEqual(event.defaultPrevented, true);
|
|
295
308
|
});
|
|
296
|
-
it("should overwrite an existing combination
|
|
309
|
+
it("should overwrite an existing combination, warn, and terminate the old stream", () => {
|
|
297
310
|
const firstCallback = createMockFn();
|
|
311
|
+
const firstComplete = createMockFn();
|
|
298
312
|
const secondCallback = createMockFn();
|
|
299
|
-
keyManager.addCombination({ id: "combo1", keys: { key: Keys.K }
|
|
313
|
+
const first$ = keyManager.addCombination({ id: "combo1", keys: { key: Keys.K } });
|
|
314
|
+
first$.subscribe({ next: firstCallback, complete: firstComplete });
|
|
300
315
|
consoleWarnMock.mock.resetCalls();
|
|
301
|
-
keyManager.addCombination({ id: "combo1", keys: { key: Keys.K, ctrlKey: true }
|
|
316
|
+
const second$ = keyManager.addCombination({ id: "combo1", keys: { key: Keys.K, ctrlKey: true } });
|
|
317
|
+
second$.subscribe(secondCallback);
|
|
302
318
|
assert.strictEqual(consoleWarnMock.mock.calls.length, 1);
|
|
303
319
|
assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Shortcut with ID "combo1" already exists`));
|
|
304
|
-
|
|
320
|
+
assert.strictEqual(firstComplete.calledCount, 1, "First observable should have completed");
|
|
321
|
+
dispatchKeyEvent(document, "k");
|
|
305
322
|
assert.strictEqual(firstCallback.calledCount, 0);
|
|
306
|
-
dispatchKeyEvent("k", { ctrlKey: true });
|
|
323
|
+
dispatchKeyEvent(document, "k", "keydown", { ctrlKey: true });
|
|
307
324
|
assert.strictEqual(secondCallback.calledCount, 1);
|
|
308
325
|
});
|
|
309
|
-
it("should
|
|
310
|
-
const errorCallback = () => { throw new Error("Test callback error"); };
|
|
326
|
+
it("should not affect other shortcuts if one subscription throws an error", () => {
|
|
311
327
|
const workingCallback = createMockFn();
|
|
312
|
-
|
|
313
|
-
keyManager.addCombination({ id: "
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
328
|
+
const erroringCallback = () => { throw new Error("Test callback error"); };
|
|
329
|
+
const error$ = keyManager.addCombination({ id: "errorCombo", keys: { key: Keys.E } });
|
|
330
|
+
// Suppress unhandled exception message in test runner output
|
|
331
|
+
error$.subscribe(erroringCallback, () => { });
|
|
332
|
+
const working$ = keyManager.addCombination({ id: "workingCombo", keys: { key: Keys.W } });
|
|
333
|
+
working$.subscribe(workingCallback);
|
|
334
|
+
// Dispatch event that causes an error in the subscription
|
|
335
|
+
//dispatchKeyEvent(document, "e");
|
|
336
|
+
// Dispatch another event to ensure the other shortcut still works
|
|
337
|
+
dispatchKeyEvent(document, "w");
|
|
338
|
+
assert.strictEqual(workingCallback.calledCount, 1, "Working callback should have been called");
|
|
339
|
+
});
|
|
340
|
+
it("should emit if any of multiple key combinations are pressed", () => {
|
|
321
341
|
const config = {
|
|
322
342
|
id: "multiCombo",
|
|
323
343
|
keys: [
|
|
@@ -325,102 +345,124 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
325
345
|
Keys.Escape, // Escape
|
|
326
346
|
{ key: Keys.B, shiftKey: true, altKey: true } // Shift+Alt+B
|
|
327
347
|
],
|
|
328
|
-
callback: mockCallback
|
|
329
348
|
};
|
|
330
|
-
keyManager.addCombination(config);
|
|
331
|
-
dispatchKeyEvent("A", { ctrlKey: true });
|
|
349
|
+
keyManager.addCombination(config).subscribe(mockCallback);
|
|
350
|
+
dispatchKeyEvent(document, "A", "keydown", { ctrlKey: true });
|
|
332
351
|
assert.strictEqual(mockCallback.calledCount, 1, "Ctrl+A did not trigger");
|
|
333
|
-
dispatchKeyEvent("Escape");
|
|
352
|
+
dispatchKeyEvent(document, "Escape");
|
|
334
353
|
assert.strictEqual(mockCallback.calledCount, 2, "Escape did not trigger");
|
|
335
|
-
dispatchKeyEvent("B", { shiftKey: true, altKey: true });
|
|
354
|
+
dispatchKeyEvent(document, "B", "keydown", { shiftKey: true, altKey: true });
|
|
336
355
|
assert.strictEqual(mockCallback.calledCount, 3, "Shift+Alt+B did not trigger");
|
|
337
|
-
dispatchKeyEvent("C"); // Should not trigger
|
|
356
|
+
dispatchKeyEvent(document, "C"); // Should not trigger
|
|
338
357
|
assert.strictEqual(mockCallback.calledCount, 3, "Unrelated key C triggered");
|
|
339
358
|
});
|
|
340
|
-
it(`should return
|
|
341
|
-
const config = { id: "emptyKeysArray", keys: []
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
assert.ok(consoleWarnMock.mock.calls.some(call => call.arguments[0].includes(`"keys"
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
359
|
+
it(`should return an empty observable and warn if "keys" array is empty`, () => {
|
|
360
|
+
const config = { id: "emptyKeysArray", keys: [] };
|
|
361
|
+
const combo$ = keyManager.addCombination(config);
|
|
362
|
+
combo$.subscribe(mockCallback);
|
|
363
|
+
assert.ok(consoleWarnMock.mock.calls.some(call => call.arguments[0].includes(`"keys" definition for combination shortcut "emptyKeysArray" is empty`)));
|
|
364
|
+
dispatchKeyEvent(document, "a");
|
|
365
|
+
assert.strictEqual(mockCallback.calledCount, 0);
|
|
366
|
+
});
|
|
367
|
+
it(`should return an empty observable and warn if a key in "keys" array is invalid (shorthand)`, () => {
|
|
368
|
+
const config = { id: "invalidInArrayShorthand", keys: [Keys.A, ""] };
|
|
369
|
+
const combo$ = keyManager.addCombination(config);
|
|
370
|
+
combo$.subscribe(mockCallback);
|
|
371
|
+
assert.ok(consoleWarnMock.mock.calls.some(call => call.arguments[0].includes(`Could not parse key: "" in shortcut "invalidInArrayShorthand"`)));
|
|
372
|
+
});
|
|
373
|
+
it(`should return an empty observable and warn if a key in "keys" array is invalid (object)`, () => {
|
|
374
|
+
const config = { id: "invalidInArrayObject", keys: [Keys.A, { key: "" }] };
|
|
375
|
+
const combo$ = keyManager.addCombination(config);
|
|
376
|
+
combo$.subscribe(mockCallback);
|
|
356
377
|
assert.ok(consoleWarnMock.mock.calls.some(call => call.arguments[0].includes(`Invalid "key" property in shortcut "invalidInArrayObject"`)));
|
|
357
378
|
});
|
|
358
379
|
it("should correctly log multiple key triggers in debug mode", () => {
|
|
359
380
|
const consoleLogMock = mock.method(console, "log");
|
|
360
381
|
keyManager.setDebugMode(true);
|
|
361
|
-
keyManager.addCombination({ id: "multiLog", keys: [Keys.F1, { key: Keys.F2, ctrlKey: true }]
|
|
382
|
+
keyManager.addCombination({ id: "multiLog", keys: [Keys.F1, { key: Keys.F2, ctrlKey: true }] });
|
|
362
383
|
assert.ok(consoleLogMock.mock.calls.some(call => call.arguments[0].includes(`Triggers: [ { key: "F1" (no mods) }, { key: "F2", ctrl: true } ]`)), "Multi-trigger log format incorrect");
|
|
363
384
|
consoleLogMock.mock.restore();
|
|
364
385
|
});
|
|
365
386
|
describe("addCombination - Shorthand Syntax", () => {
|
|
366
|
-
it("should
|
|
367
|
-
const config = { id: "shorthandX", keys: Keys.X
|
|
368
|
-
keyManager.addCombination(config);
|
|
369
|
-
dispatchKeyEvent(Keys.X.toLowerCase());
|
|
387
|
+
it("should emit for a simple key using shorthand (e.g., Keys.X)", () => {
|
|
388
|
+
const config = { id: "shorthandX", keys: Keys.X };
|
|
389
|
+
keyManager.addCombination(config).subscribe(mockCallback);
|
|
390
|
+
dispatchKeyEvent(document, Keys.X.toLowerCase());
|
|
370
391
|
assert.strictEqual(mockCallback.calledCount, 1, `Callback for "x" (shorthand) not called`);
|
|
371
392
|
mockCallback.mockClear();
|
|
372
|
-
dispatchKeyEvent(Keys.X);
|
|
393
|
+
dispatchKeyEvent(document, Keys.X);
|
|
373
394
|
assert.strictEqual(mockCallback.calledCount, 1, `Callback for "X" (shorthand) not called`);
|
|
374
395
|
});
|
|
375
|
-
it("should NOT
|
|
376
|
-
const config = { id: "shorthandY", keys: Keys.Y
|
|
377
|
-
keyManager.addCombination(config);
|
|
378
|
-
dispatchKeyEvent(Keys.Y, { ctrlKey: true });
|
|
396
|
+
it("should NOT emit for shorthand if modifier is pressed", () => {
|
|
397
|
+
const config = { id: "shorthandY", keys: Keys.Y };
|
|
398
|
+
keyManager.addCombination(config).subscribe(mockCallback);
|
|
399
|
+
dispatchKeyEvent(document, Keys.Y, "keydown", { ctrlKey: true });
|
|
379
400
|
assert.strictEqual(mockCallback.calledCount, 0, `Callback for "y" (shorthand) should not be called with Ctrl`);
|
|
380
401
|
mockCallback.mockClear();
|
|
381
|
-
dispatchKeyEvent(Keys.Y, { altKey: true });
|
|
402
|
+
dispatchKeyEvent(document, Keys.Y, "keydown", { altKey: true });
|
|
382
403
|
assert.strictEqual(mockCallback.calledCount, 0, `Callback for "y" (shorthand) should not be called with Alt`);
|
|
383
404
|
mockCallback.mockClear();
|
|
384
|
-
dispatchKeyEvent(Keys.Y, { shiftKey: true });
|
|
405
|
+
dispatchKeyEvent(document, Keys.Y, "keydown", { shiftKey: true });
|
|
385
406
|
assert.strictEqual(mockCallback.calledCount, 0, `Callback for "y" (shorthand) should not be called with Shift`);
|
|
386
407
|
mockCallback.mockClear();
|
|
387
|
-
dispatchKeyEvent(Keys.Y, { metaKey: true });
|
|
408
|
+
dispatchKeyEvent(document, Keys.Y, "keydown", { metaKey: true });
|
|
388
409
|
assert.strictEqual(mockCallback.calledCount, 0, `Callback for "y" (shorthand) should not be called with Meta`);
|
|
389
410
|
});
|
|
390
|
-
it("should
|
|
391
|
-
const config = { id: "shorthandZ", keys: Keys.Z
|
|
392
|
-
keyManager.addCombination(config);
|
|
393
|
-
dispatchKeyEvent(Keys.Z, { ctrlKey: false, altKey: false, shiftKey: false, metaKey: false });
|
|
411
|
+
it("should emit for shorthand if ONLY the key is pressed (no modifiers)", () => {
|
|
412
|
+
const config = { id: "shorthandZ", keys: Keys.Z };
|
|
413
|
+
keyManager.addCombination(config).subscribe(mockCallback);
|
|
414
|
+
dispatchKeyEvent(document, Keys.Z, "keydown", { ctrlKey: false, altKey: false, shiftKey: false, metaKey: false });
|
|
394
415
|
assert.strictEqual(mockCallback.calledCount, 1);
|
|
395
416
|
});
|
|
396
417
|
it("should handle preventDefault correctly for shorthand", () => {
|
|
397
|
-
const config = { id: "shorthandPrevent", keys: Keys.P,
|
|
398
|
-
keyManager.addCombination(config);
|
|
399
|
-
const event = dispatchKeyEvent(Keys.P.toLowerCase());
|
|
418
|
+
const config = { id: "shorthandPrevent", keys: Keys.P, preventDefault: true };
|
|
419
|
+
keyManager.addCombination(config).subscribe(mockCallback);
|
|
420
|
+
const event = dispatchKeyEvent(document, Keys.P.toLowerCase());
|
|
400
421
|
assert.strictEqual(mockCallback.calledCount, 1);
|
|
401
422
|
assert.strictEqual(event.defaultPrevented, true);
|
|
402
423
|
});
|
|
403
424
|
it("should respect context for shorthand", () => {
|
|
404
|
-
const config = { id: "shorthandContext", keys: Keys.C,
|
|
405
|
-
keyManager.addCombination(config);
|
|
425
|
+
const config = { id: "shorthandContext", keys: Keys.C, context: "editor" };
|
|
426
|
+
keyManager.addCombination(config).subscribe(mockCallback);
|
|
406
427
|
keyManager.setContext("other");
|
|
407
|
-
dispatchKeyEvent(Keys.C.toLowerCase());
|
|
428
|
+
dispatchKeyEvent(document, Keys.C.toLowerCase());
|
|
408
429
|
assert.strictEqual(mockCallback.calledCount, 0);
|
|
409
430
|
keyManager.setContext("editor");
|
|
410
|
-
dispatchKeyEvent(Keys.C.toLowerCase());
|
|
431
|
+
dispatchKeyEvent(document, Keys.C.toLowerCase());
|
|
411
432
|
assert.strictEqual(mockCallback.calledCount, 1);
|
|
412
433
|
});
|
|
413
|
-
it("should return
|
|
414
|
-
const config = { id: "emptyShorthand", keys: ""
|
|
415
|
-
const
|
|
416
|
-
|
|
434
|
+
it("should return an empty observable and warn if shorthand key is an empty string", () => {
|
|
435
|
+
const config = { id: "emptyShorthand", keys: "" };
|
|
436
|
+
const combo$ = keyManager.addCombination(config);
|
|
437
|
+
combo$.subscribe(mockCallback);
|
|
438
|
+
assert.strictEqual(consoleWarnMock.mock.calls.length, 1);
|
|
439
|
+
assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Could not parse key: "" in shortcut "emptyShorthand"`));
|
|
440
|
+
});
|
|
441
|
+
it("should return an empty observable and warn if shorthand key is an empty array", () => {
|
|
442
|
+
const config = { id: "emptyArrayShorthand", keys: [] };
|
|
443
|
+
const combo$ = keyManager.addCombination(config);
|
|
444
|
+
combo$.subscribe(mockCallback);
|
|
445
|
+
assert.strictEqual(consoleWarnMock.mock.calls.length, 1);
|
|
446
|
+
assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`"keys" definition for combination shortcut "emptyArrayShorthand" is empty or invalid. Shortcut not added.`));
|
|
447
|
+
});
|
|
448
|
+
it("should return an empty observable and warn if shorthand key is an empty string in array", () => {
|
|
449
|
+
const config = { id: "emptyStringShorthand", keys: [""] };
|
|
450
|
+
const combo$ = keyManager.addCombination(config);
|
|
451
|
+
combo$.subscribe(mockCallback);
|
|
417
452
|
assert.strictEqual(consoleWarnMock.mock.calls.length, 1);
|
|
418
|
-
assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`
|
|
453
|
+
assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Could not parse key: "" in shortcut "emptyStringShorthand"`));
|
|
454
|
+
});
|
|
455
|
+
it("should return an empty observable and warn if shorthand key is invalid", () => {
|
|
456
|
+
const config = { id: "invalidKeyPropertyShorthand", keys: { key: "" } };
|
|
457
|
+
const combo$ = keyManager.addCombination(config);
|
|
458
|
+
combo$.subscribe(mockCallback);
|
|
459
|
+
assert.strictEqual(consoleWarnMock.mock.calls.length, 1);
|
|
460
|
+
assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Invalid "key" property in shortcut "invalidKeyPropertyShorthand". Key must be a non-empty string value from Keys.`));
|
|
419
461
|
});
|
|
420
462
|
it("should correctly log shorthand key details in debug mode", () => {
|
|
421
463
|
const consoleLogMock = mock.method(console, "log");
|
|
422
464
|
keyManager.setDebugMode(true);
|
|
423
|
-
const config = { id: "debugShorthand", keys: Keys.D
|
|
465
|
+
const config = { id: "debugShorthand", keys: Keys.D };
|
|
424
466
|
keyManager.addCombination(config);
|
|
425
467
|
const logMessage = consoleLogMock.mock.calls.find(call => call.arguments[0].includes(`combination shortcut "debugShorthand" added.`));
|
|
426
468
|
assert.ok(logMessage, "Debug log for adding shortcut not found");
|
|
@@ -428,20 +470,153 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
428
470
|
consoleLogMock.mock.restore();
|
|
429
471
|
keyManager.setDebugMode(false);
|
|
430
472
|
});
|
|
431
|
-
it("should
|
|
432
|
-
const config = { id: "spaceShorthand", keys: Keys.Space
|
|
433
|
-
keyManager.addCombination(config);
|
|
434
|
-
dispatchKeyEvent(" "); // Event key for space is " "
|
|
473
|
+
it("should emit for Keys.Space using shorthand", () => {
|
|
474
|
+
const config = { id: "spaceShorthand", keys: Keys.Space };
|
|
475
|
+
keyManager.addCombination(config).subscribe(mockCallback);
|
|
476
|
+
dispatchKeyEvent(document, " "); // Event key for space is " "
|
|
435
477
|
assert.strictEqual(mockCallback.calledCount, 1, "Callback for Keys.Space (shorthand) not called");
|
|
436
478
|
});
|
|
437
|
-
it("should
|
|
438
|
-
const config = { id: "spaceObject", keys: { key: Keys.Space }
|
|
439
|
-
keyManager.addCombination(config);
|
|
440
|
-
dispatchKeyEvent(" ");
|
|
479
|
+
it("should emit for Keys.Space using object form", () => {
|
|
480
|
+
const config = { id: "spaceObject", keys: { key: Keys.Space } };
|
|
481
|
+
keyManager.addCombination(config).subscribe(mockCallback);
|
|
482
|
+
dispatchKeyEvent(document, " ");
|
|
441
483
|
assert.strictEqual(mockCallback.calledCount, 1, "Callback for Keys.Space (object form) not called");
|
|
442
484
|
});
|
|
443
485
|
});
|
|
444
486
|
});
|
|
487
|
+
describe("String-based Definitions", () => {
|
|
488
|
+
it(`should parse and trigger a simple string "ctrl+s"`, () => {
|
|
489
|
+
keyManager.addCombination({ id: "strSave", keys: "ctrl+s" }).subscribe(mockCallback);
|
|
490
|
+
dispatchKeyEvent(document, "s", "keydown", { ctrlKey: true });
|
|
491
|
+
assert.strictEqual(mockCallback.calledCount, 1);
|
|
492
|
+
dispatchKeyEvent(document, "s");
|
|
493
|
+
assert.strictEqual(mockCallback.calledCount, 1);
|
|
494
|
+
});
|
|
495
|
+
it(`should parse and trigger "shift+alt+k"`, () => {
|
|
496
|
+
keyManager.addCombination({ id: "strComplex", keys: "shift+alt+k" }).subscribe(mockCallback);
|
|
497
|
+
dispatchKeyEvent(document, "k", "keydown", { shiftKey: true, altKey: true });
|
|
498
|
+
assert.strictEqual(mockCallback.calledCount, 1);
|
|
499
|
+
});
|
|
500
|
+
it(`should parse aliases like "cmd+p"`, () => {
|
|
501
|
+
keyManager.addCombination({ id: "strAlias", keys: "cmd+p" }).subscribe(mockCallback);
|
|
502
|
+
dispatchKeyEvent(document, "p", "keydown", { metaKey: true });
|
|
503
|
+
assert.strictEqual(mockCallback.calledCount, 1);
|
|
504
|
+
});
|
|
505
|
+
it(`should parse special key strings like "escape"`, () => {
|
|
506
|
+
keyManager.addCombination({ id: "strSpecial", keys: "escape" }).subscribe(mockCallback);
|
|
507
|
+
dispatchKeyEvent(document, "Escape");
|
|
508
|
+
assert.strictEqual(mockCallback.calledCount, 1);
|
|
509
|
+
});
|
|
510
|
+
it(`should parse sequence string "g -> i"`, () => {
|
|
511
|
+
keyManager.addSequence({ id: "strSeq", sequence: "g -> i" }).subscribe(mockCallback);
|
|
512
|
+
dispatchKeyEvent(document, "g");
|
|
513
|
+
dispatchKeyEvent(document, "i");
|
|
514
|
+
assert.strictEqual(mockCallback.calledCount, 1);
|
|
515
|
+
});
|
|
516
|
+
it(`should parse sequence string with special keys "up -> down -> enter"`, () => {
|
|
517
|
+
keyManager.addSequence({ id: "strSeqSpecial", sequence: "up -> down -> enter" }).subscribe(mockCallback);
|
|
518
|
+
dispatchKeyEvent(document, "ArrowUp");
|
|
519
|
+
dispatchKeyEvent(document, "ArrowDown");
|
|
520
|
+
dispatchKeyEvent(document, "Enter");
|
|
521
|
+
assert.strictEqual(mockCallback.calledCount, 1);
|
|
522
|
+
});
|
|
523
|
+
it("should warn and return empty on invalid string", () => {
|
|
524
|
+
const combo$ = keyManager.addCombination({ id: "invalid", keys: "ctrl+badkey" });
|
|
525
|
+
combo$.subscribe(mockCallback);
|
|
526
|
+
assert.ok(consoleWarnMock.mock.calls.some(c => c.arguments[0].includes(`Could not parse key: "badkey"`)));
|
|
527
|
+
assert.strictEqual(mockCallback.calledCount, 0);
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
describe("Normalization and Edge Case Tests", () => {
|
|
531
|
+
it(`should handle the "+" key as a shorthand, not a combination`, () => {
|
|
532
|
+
keyManager.addCombination({ id: "plusKey", keys: Keys.KeypadAdd }).subscribe(mockCallback);
|
|
533
|
+
dispatchKeyEvent(document, "+");
|
|
534
|
+
assert.strictEqual(mockCallback.calledCount, 1, `Callback for "+" key not called`);
|
|
535
|
+
});
|
|
536
|
+
it(`should handle "escape" (lowercase) and normalize it to match the "Escape" event key`, () => {
|
|
537
|
+
keyManager.addCombination({ id: "lowerEscape", keys: "escape" }).subscribe(mockCallback);
|
|
538
|
+
dispatchKeyEvent(document, "Escape"); // Event key from browser is capitalized
|
|
539
|
+
assert.strictEqual(mockCallback.calledCount, 1, `Lowercase "escape" did not match event`);
|
|
540
|
+
});
|
|
541
|
+
it(`should handle "ESC" (uppercase) and normalize it`, () => {
|
|
542
|
+
keyManager.addCombination({ id: "upperEsc", keys: "ESC" }).subscribe(mockCallback);
|
|
543
|
+
dispatchKeyEvent(document, "Escape");
|
|
544
|
+
assert.strictEqual(mockCallback.calledCount, 1, `Uppercase "ESC" did not match event`);
|
|
545
|
+
});
|
|
546
|
+
it(`should handle a single-word modifier alias like "cmd" as a shorthand for the "Meta" key`, () => {
|
|
547
|
+
keyManager.addCombination({ id: "cmdKey", keys: "cmd" }).subscribe(mockCallback);
|
|
548
|
+
dispatchKeyEvent(document, "Meta"); // Event key for command key is "Meta"
|
|
549
|
+
assert.strictEqual(mockCallback.calledCount, 1, `Alias "cmd" did not match "Meta" key event`);
|
|
550
|
+
});
|
|
551
|
+
it(`should handle combination string with extra spaces like " ctrl + s "`, () => {
|
|
552
|
+
keyManager.addCombination({ id: "paddedCombo", keys: " ctrl + s " }).subscribe(mockCallback);
|
|
553
|
+
dispatchKeyEvent(document, "s", "keydown", { ctrlKey: true });
|
|
554
|
+
assert.strictEqual(mockCallback.calledCount, 1, "Padded combination string failed to parse");
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
describe("New Feature: Element-Scoped Listeners", () => {
|
|
558
|
+
let el1, el2;
|
|
559
|
+
beforeEach(() => {
|
|
560
|
+
el1 = document.createElement("div");
|
|
561
|
+
el2 = document.createElement("div");
|
|
562
|
+
document.body.append(el1, el2);
|
|
563
|
+
});
|
|
564
|
+
afterEach(() => {
|
|
565
|
+
el1.remove();
|
|
566
|
+
el2.remove();
|
|
567
|
+
});
|
|
568
|
+
it("should only trigger shortcut on the specified target element", () => {
|
|
569
|
+
keyManager.addCombination({ id: "scoped", keys: "a", target: el1 }).subscribe(mockCallback);
|
|
570
|
+
dispatchKeyEvent(el1, "a");
|
|
571
|
+
assert.strictEqual(mockCallback.calledCount, 1, "Should trigger on target element");
|
|
572
|
+
dispatchKeyEvent(el2, "a");
|
|
573
|
+
assert.strictEqual(mockCallback.calledCount, 1, "Should NOT trigger on another element");
|
|
574
|
+
dispatchKeyEvent(document, "a");
|
|
575
|
+
assert.strictEqual(mockCallback.calledCount, 1, "Should NOT trigger on document");
|
|
576
|
+
});
|
|
577
|
+
it("should trigger global shortcut if no target is specified", () => {
|
|
578
|
+
keyManager.addCombination({ id: "global", keys: "b" }).subscribe(mockCallback);
|
|
579
|
+
// Event bubbles up from el1 to document
|
|
580
|
+
dispatchKeyEvent(el1, "b");
|
|
581
|
+
assert.strictEqual(mockCallback.calledCount, 1, "Should trigger on event from child element");
|
|
582
|
+
dispatchKeyEvent(document, "b");
|
|
583
|
+
assert.strictEqual(mockCallback.calledCount, 2, "Should trigger on document directly");
|
|
584
|
+
});
|
|
585
|
+
it("should work for sequences on a specific target", () => {
|
|
586
|
+
keyManager.addSequence({ id: "scopedSeq", sequence: "a -> b", target: el1 }).subscribe(mockCallback);
|
|
587
|
+
dispatchKeyEvent(el1, "a");
|
|
588
|
+
dispatchKeyEvent(el1, "b");
|
|
589
|
+
assert.strictEqual(mockCallback.calledCount, 1, "Sequence should trigger on target");
|
|
590
|
+
dispatchKeyEvent(document, "a");
|
|
591
|
+
dispatchKeyEvent(document, "b");
|
|
592
|
+
assert.strictEqual(mockCallback.calledCount, 1, "Sequence should not trigger on document");
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
describe("New Feature: keyup Event Support", () => {
|
|
596
|
+
it("should trigger combination on keyup when specified", () => {
|
|
597
|
+
keyManager.addCombination({ id: "keyupCombo", keys: "a", event: "keyup" }).subscribe(mockCallback);
|
|
598
|
+
dispatchKeyEvent(document, "a", "keydown");
|
|
599
|
+
assert.strictEqual(mockCallback.calledCount, 0, "Should not trigger on keydown");
|
|
600
|
+
dispatchKeyEvent(document, "a", "keyup");
|
|
601
|
+
assert.strictEqual(mockCallback.calledCount, 1, "Should trigger on keyup");
|
|
602
|
+
});
|
|
603
|
+
it("should trigger sequence on keyup when specified", () => {
|
|
604
|
+
keyManager.addSequence({ id: "keyupSeq", sequence: "a -> b", event: "keyup" }).subscribe(mockCallback);
|
|
605
|
+
dispatchKeyEvent(document, "a", "keydown");
|
|
606
|
+
dispatchKeyEvent(document, "b", "keydown");
|
|
607
|
+
assert.strictEqual(mockCallback.calledCount, 0, "Sequence should not trigger on keydown events");
|
|
608
|
+
dispatchKeyEvent(document, "a", "keyup");
|
|
609
|
+
dispatchKeyEvent(document, "b", "keyup");
|
|
610
|
+
assert.strictEqual(mockCallback.calledCount, 1, "Sequence should trigger on keyup events");
|
|
611
|
+
});
|
|
612
|
+
it("should default to keydown if event type is not specified", () => {
|
|
613
|
+
keyManager.addCombination({ id: "keydownDefault", keys: "c" }).subscribe(mockCallback);
|
|
614
|
+
dispatchKeyEvent(document, "c", "keyup");
|
|
615
|
+
assert.strictEqual(mockCallback.calledCount, 0);
|
|
616
|
+
dispatchKeyEvent(document, "c", "keydown");
|
|
617
|
+
assert.strictEqual(mockCallback.calledCount, 1);
|
|
618
|
+
});
|
|
619
|
+
});
|
|
445
620
|
describe("Context Priority (Specific > Global)", () => {
|
|
446
621
|
let globalCallback;
|
|
447
622
|
let specificCallback;
|
|
@@ -452,31 +627,29 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
452
627
|
keyManager.addCombination({
|
|
453
628
|
id: "globalCtrlG",
|
|
454
629
|
keys: { key: Keys.G, ctrlKey: true },
|
|
455
|
-
callback: globalCallback,
|
|
456
630
|
context: null // Explicitly global
|
|
457
|
-
});
|
|
631
|
+
}).subscribe(globalCallback);
|
|
458
632
|
// Specific context shortcut: Ctrl+G in "editor" context
|
|
459
633
|
keyManager.addCombination({
|
|
460
634
|
id: "editorCtrlG",
|
|
461
635
|
keys: { key: Keys.G, ctrlKey: true },
|
|
462
|
-
callback: specificCallback,
|
|
463
636
|
context: "editor"
|
|
464
|
-
});
|
|
637
|
+
}).subscribe(specificCallback);
|
|
465
638
|
});
|
|
466
639
|
it("should only trigger specific context callback when specific context is active", () => {
|
|
467
640
|
keyManager.setContext("editor");
|
|
468
|
-
dispatchKeyEvent(Keys.G, { ctrlKey: true });
|
|
641
|
+
dispatchKeyEvent(document, Keys.G, "keydown", { ctrlKey: true });
|
|
469
642
|
assert.strictEqual(specificCallback.calledCount, 1, "Specific callback should have been called");
|
|
470
643
|
assert.strictEqual(globalCallback.calledCount, 0, "Global callback should NOT have been called");
|
|
471
644
|
});
|
|
472
645
|
it("should only trigger global callback when no specific context is active (or context doesn't match)", () => {
|
|
473
646
|
keyManager.setContext(null); // No specific context
|
|
474
|
-
dispatchKeyEvent(Keys.G, { ctrlKey: true });
|
|
647
|
+
dispatchKeyEvent(document, Keys.G, "keydown", { ctrlKey: true });
|
|
475
648
|
assert.strictEqual(specificCallback.calledCount, 0, "Specific callback should NOT have been called");
|
|
476
649
|
assert.strictEqual(globalCallback.calledCount, 1, "Global callback should have been called");
|
|
477
650
|
globalCallback.mockClear();
|
|
478
651
|
keyManager.setContext("anotherContext"); // Different specific context
|
|
479
|
-
dispatchKeyEvent(Keys.G, { ctrlKey: true });
|
|
652
|
+
dispatchKeyEvent(document, Keys.G, "keydown", { ctrlKey: true });
|
|
480
653
|
assert.strictEqual(specificCallback.calledCount, 0, `Specific callback should NOT have been called for "anotherContext"`);
|
|
481
654
|
assert.strictEqual(globalCallback.calledCount, 1, `Global callback should have been called when in "anotherContext"`);
|
|
482
655
|
});
|
|
@@ -491,31 +664,29 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
491
664
|
keyManager.addSequence({
|
|
492
665
|
id: "globalGI",
|
|
493
666
|
sequence: testSequence,
|
|
494
|
-
callback: globalSeqCallback,
|
|
495
667
|
context: null // Global
|
|
496
|
-
});
|
|
668
|
+
}).subscribe(globalSeqCallback);
|
|
497
669
|
keyManager.addSequence({
|
|
498
670
|
id: "editorGI",
|
|
499
671
|
sequence: testSequence,
|
|
500
|
-
callback: specificSeqCallback,
|
|
501
672
|
context: "editor" // Specific
|
|
502
|
-
});
|
|
673
|
+
}).subscribe(specificSeqCallback);
|
|
503
674
|
});
|
|
504
675
|
it("should only trigger specific context sequence callback when specific context is active", () => {
|
|
505
676
|
keyManager.setContext("editor");
|
|
506
|
-
testSequence.forEach(key => dispatchKeyEvent(key));
|
|
677
|
+
testSequence.forEach(key => dispatchKeyEvent(document, key));
|
|
507
678
|
assert.strictEqual(specificSeqCallback.calledCount, 1, "Specific sequence callback should have been called");
|
|
508
679
|
assert.strictEqual(globalSeqCallback.calledCount, 0, "Global sequence callback should NOT have been called");
|
|
509
680
|
});
|
|
510
681
|
it("should only trigger global sequence callback when no specific context is active (or context doesn't match)", () => {
|
|
511
682
|
keyManager.setContext(null); // No specific context
|
|
512
|
-
testSequence.forEach(key => dispatchKeyEvent(key));
|
|
683
|
+
testSequence.forEach(key => dispatchKeyEvent(document, key));
|
|
513
684
|
assert.strictEqual(specificSeqCallback.calledCount, 0, "Specific sequence callback should NOT have been called");
|
|
514
685
|
assert.strictEqual(globalSeqCallback.calledCount, 1, "Global sequence callback should have been called");
|
|
515
686
|
globalSeqCallback.mockClear();
|
|
516
687
|
specificSeqCallback.mockClear(); // Clear for next part of test
|
|
517
688
|
keyManager.setContext("anotherContext"); // Different specific context
|
|
518
|
-
testSequence.forEach(key => dispatchKeyEvent(key));
|
|
689
|
+
testSequence.forEach(key => dispatchKeyEvent(document, key));
|
|
519
690
|
assert.strictEqual(specificSeqCallback.calledCount, 0, `Specific sequence callback should NOT have been called for "anotherContext"`);
|
|
520
691
|
assert.strictEqual(globalSeqCallback.calledCount, 1, `Global sequence callback should have been called when in "anotherContext"`);
|
|
521
692
|
});
|
|
@@ -532,47 +703,43 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
532
703
|
keyManager.addCombination({
|
|
533
704
|
id: "editorSave",
|
|
534
705
|
keys: { key: Keys.S, ctrlKey: true },
|
|
535
|
-
callback: specificContextCallback,
|
|
536
706
|
context: "editor"
|
|
537
|
-
});
|
|
707
|
+
}).subscribe(specificContextCallback);
|
|
538
708
|
// 2. A "strictly global" shortcut, which only runs when context is null
|
|
539
709
|
keyManager.addCombination({
|
|
540
710
|
id: "strictGlobalOpen",
|
|
541
|
-
keys: { key: Keys.O, ctrlKey: true },
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
});
|
|
711
|
+
keys: { key: Keys.O, ctrlKey: true },
|
|
712
|
+
strict: true,
|
|
713
|
+
}).subscribe(strictGlobalCallback);
|
|
545
714
|
// 3. A default global shortcut, which runs in any context unless overridden
|
|
546
715
|
keyManager.addCombination({
|
|
547
716
|
id: "defaultGlobalSave",
|
|
548
717
|
keys: { key: Keys.S, ctrlKey: true },
|
|
549
|
-
|
|
550
|
-
// No `strict` flag here
|
|
551
|
-
});
|
|
718
|
+
}).subscribe(defaultGlobalCallback);
|
|
552
719
|
});
|
|
553
720
|
it("should trigger both strict and default global shortcuts when context is null", () => {
|
|
554
721
|
keyManager.setContext(null);
|
|
555
|
-
dispatchKeyEvent(Keys.O, { ctrlKey: true });
|
|
722
|
+
dispatchKeyEvent(document, Keys.O, "keydown", { ctrlKey: true });
|
|
556
723
|
assert.strictEqual(strictGlobalCallback.calledCount, 1, "Strictly global (Ctrl+O) should fire");
|
|
557
|
-
dispatchKeyEvent(Keys.S, { ctrlKey: true });
|
|
724
|
+
dispatchKeyEvent(document, Keys.S, "keydown", { ctrlKey: true });
|
|
558
725
|
assert.strictEqual(defaultGlobalCallback.calledCount, 1, "Default global (Ctrl+S) should fire");
|
|
559
726
|
assert.strictEqual(specificContextCallback.calledCount, 0, "Specific context callback should not fire");
|
|
560
727
|
});
|
|
561
728
|
it("should suppress strict global but allow default global (which is then suppressed by priority)", () => {
|
|
562
729
|
keyManager.setContext("editor");
|
|
563
|
-
dispatchKeyEvent(Keys.O, { ctrlKey: true });
|
|
564
|
-
assert.strictEqual(strictGlobalCallback.calledCount, 0,
|
|
565
|
-
dispatchKeyEvent(Keys.S, { ctrlKey: true });
|
|
730
|
+
dispatchKeyEvent(document, Keys.O, "keydown", { ctrlKey: true });
|
|
731
|
+
assert.strictEqual(strictGlobalCallback.calledCount, 0, `Strictly global (Ctrl+O) should NOT fire in "editor" context`);
|
|
732
|
+
dispatchKeyEvent(document, Keys.S, "keydown", { ctrlKey: true });
|
|
566
733
|
assert.strictEqual(defaultGlobalCallback.calledCount, 0, "Default global (Ctrl+S) should be suppressed by the specific one");
|
|
567
|
-
assert.strictEqual(specificContextCallback.calledCount, 1,
|
|
734
|
+
assert.strictEqual(specificContextCallback.calledCount, 1, `Specific "editor" callback (Ctrl+S) should fire and take priority`);
|
|
568
735
|
});
|
|
569
736
|
it("should suppress strict global but trigger default global in a non-conflicting context", () => {
|
|
570
737
|
keyManager.setContext("someOtherContext");
|
|
571
|
-
dispatchKeyEvent(Keys.O, { ctrlKey: true });
|
|
572
|
-
assert.strictEqual(strictGlobalCallback.calledCount, 0,
|
|
573
|
-
dispatchKeyEvent(Keys.S, { ctrlKey: true });
|
|
738
|
+
dispatchKeyEvent(document, Keys.O, "keydown", { ctrlKey: true });
|
|
739
|
+
assert.strictEqual(strictGlobalCallback.calledCount, 0, `Strictly global (Ctrl+O) should NOT fire in "someOtherContext"`);
|
|
740
|
+
dispatchKeyEvent(document, Keys.S, "keydown", { ctrlKey: true });
|
|
574
741
|
assert.strictEqual(defaultGlobalCallback.calledCount, 1, "Default global (Ctrl+S) should fire since no override exists for this context");
|
|
575
|
-
assert.strictEqual(specificContextCallback.calledCount, 0,
|
|
742
|
+
assert.strictEqual(specificContextCallback.calledCount, 0, `Specific "editor" callback should not fire`);
|
|
576
743
|
});
|
|
577
744
|
});
|
|
578
745
|
describe("Sequence Context Behavior (`strict` flag)", () => {
|
|
@@ -585,116 +752,107 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
585
752
|
strictSeqCallback = createMockFn();
|
|
586
753
|
defaultSeqCallback = createMockFn();
|
|
587
754
|
specificSeqCallback = createMockFn();
|
|
588
|
-
keyManager.addSequence({ id: "strictSeq", sequence: [Keys.G, Keys.O],
|
|
589
|
-
keyManager.addSequence({ id: "defaultSeq", sequence: testSequence
|
|
590
|
-
keyManager.addSequence({ id: "specificSeq", sequence: testSequence,
|
|
755
|
+
keyManager.addSequence({ id: "strictSeq", sequence: [Keys.G, Keys.O], strict: true }).subscribe(strictSeqCallback);
|
|
756
|
+
keyManager.addSequence({ id: "defaultSeq", sequence: testSequence }).subscribe(defaultSeqCallback);
|
|
757
|
+
keyManager.addSequence({ id: "specificSeq", sequence: testSequence, context: "editor" }).subscribe(specificSeqCallback);
|
|
591
758
|
});
|
|
592
759
|
it("should trigger both strict and default global sequences when context is null", () => {
|
|
593
760
|
keyManager.setContext(null);
|
|
594
|
-
[Keys.G, Keys.O].forEach(key => dispatchKeyEvent(key));
|
|
761
|
+
[Keys.G, Keys.O].forEach(key => dispatchKeyEvent(document, key));
|
|
595
762
|
assert.strictEqual(strictSeqCallback.calledCount, 1, "Strict sequence should fire");
|
|
596
|
-
testSequence.forEach(key => dispatchKeyEvent(key));
|
|
763
|
+
testSequence.forEach(key => dispatchKeyEvent(document, key));
|
|
597
764
|
assert.strictEqual(defaultSeqCallback.calledCount, 1, "Default sequence should fire");
|
|
598
765
|
assert.strictEqual(specificSeqCallback.calledCount, 0, "Specific sequence should not fire");
|
|
599
766
|
});
|
|
600
767
|
it("should suppress strict sequence and prioritize specific sequence in a matching context", () => {
|
|
601
768
|
keyManager.setContext("editor");
|
|
602
|
-
[Keys.G, Keys.O].forEach(key => dispatchKeyEvent(key));
|
|
603
|
-
assert.strictEqual(strictSeqCallback.calledCount, 0,
|
|
604
|
-
testSequence.forEach(key => dispatchKeyEvent(key));
|
|
769
|
+
[Keys.G, Keys.O].forEach(key => dispatchKeyEvent(document, key));
|
|
770
|
+
assert.strictEqual(strictSeqCallback.calledCount, 0, `Strict sequence should NOT fire in "editor" context`);
|
|
771
|
+
testSequence.forEach(key => dispatchKeyEvent(document, key));
|
|
605
772
|
assert.strictEqual(defaultSeqCallback.calledCount, 0, "Default global sequence should be suppressed");
|
|
606
|
-
assert.strictEqual(specificSeqCallback.calledCount, 1,
|
|
773
|
+
assert.strictEqual(specificSeqCallback.calledCount, 1, `Specific "editor" sequence should fire`);
|
|
607
774
|
});
|
|
608
775
|
it("should suppress strict sequence but trigger default global sequence in a non-conflicting context", () => {
|
|
609
776
|
keyManager.setContext("someOtherContext");
|
|
610
|
-
[Keys.G, Keys.O].forEach(key => dispatchKeyEvent(key));
|
|
777
|
+
[Keys.G, Keys.O].forEach(key => dispatchKeyEvent(document, key));
|
|
611
778
|
assert.strictEqual(strictSeqCallback.calledCount, 0, "Strict sequence should NOT fire");
|
|
612
|
-
testSequence.forEach(key => dispatchKeyEvent(key));
|
|
779
|
+
testSequence.forEach(key => dispatchKeyEvent(document, key));
|
|
613
780
|
assert.strictEqual(defaultSeqCallback.calledCount, 1, "Default global sequence should fire");
|
|
614
|
-
assert.strictEqual(specificSeqCallback.calledCount, 0,
|
|
781
|
+
assert.strictEqual(specificSeqCallback.calledCount, 0, `Specific "editor" sequence should not fire`);
|
|
615
782
|
});
|
|
616
783
|
});
|
|
617
784
|
describe("addSequence", () => {
|
|
618
|
-
it("should
|
|
619
|
-
const config = { id: "seqGI", sequence: [Keys.G, Keys.I]
|
|
620
|
-
const
|
|
621
|
-
assert
|
|
622
|
-
|
|
623
|
-
dispatchKeyEvent("
|
|
785
|
+
it("should return an Observable that emits on a simple key sequence", () => {
|
|
786
|
+
const config = { id: "seqGI", sequence: [Keys.G, Keys.I] };
|
|
787
|
+
const seq$ = keyManager.addSequence(config);
|
|
788
|
+
assert(seq$ instanceof Observable, "Did not return an Observable");
|
|
789
|
+
seq$.subscribe(mockCallback);
|
|
790
|
+
dispatchKeyEvent(document, "g");
|
|
791
|
+
dispatchKeyEvent(document, "i");
|
|
624
792
|
assert.strictEqual(mockCallback.calledCount, 1);
|
|
625
793
|
});
|
|
626
|
-
it("should
|
|
794
|
+
it("should emit on Konami code using Keys", () => {
|
|
627
795
|
const konamiSequence = [
|
|
628
796
|
Keys.ArrowUp, Keys.ArrowUp, Keys.ArrowDown, Keys.ArrowDown,
|
|
629
797
|
Keys.ArrowLeft, Keys.ArrowRight, Keys.ArrowLeft, Keys.ArrowRight,
|
|
630
|
-
Keys.B, Keys.A
|
|
798
|
+
Keys.B, Keys.A
|
|
631
799
|
];
|
|
632
|
-
const config = { id: "konami", sequence: konamiSequence
|
|
633
|
-
keyManager.addSequence(config);
|
|
634
|
-
|
|
635
|
-
["ArrowUp", "ArrowUp", "ArrowDown", "ArrowDown", "ArrowLeft", "ArrowRight", "ArrowLeft", "ArrowRight", "b", "a"].forEach(key => dispatchKeyEvent(key));
|
|
800
|
+
const config = { id: "konami", sequence: konamiSequence };
|
|
801
|
+
keyManager.addSequence(config).subscribe(mockCallback);
|
|
802
|
+
["ArrowUp", "ArrowUp", "ArrowDown", "ArrowDown", "ArrowLeft", "ArrowRight", "ArrowLeft", "ArrowRight", "b", "a"].forEach(key => dispatchKeyEvent(document, key));
|
|
636
803
|
assert.strictEqual(mockCallback.calledCount, 1, "Konami sequence callback not triggered");
|
|
637
804
|
});
|
|
638
|
-
it("should return
|
|
639
|
-
const config = { id: "emptySeq", sequence: []
|
|
640
|
-
const
|
|
641
|
-
|
|
805
|
+
it("should return an empty observable and warn if sequence is empty", () => {
|
|
806
|
+
const config = { id: "emptySeq", sequence: [] };
|
|
807
|
+
const seq$ = keyManager.addSequence(config);
|
|
808
|
+
seq$.subscribe(mockCallback);
|
|
642
809
|
assert.strictEqual(consoleWarnMock.mock.calls.length, 1);
|
|
643
810
|
assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Sequence for shortcut "emptySeq" is empty`));
|
|
811
|
+
dispatchKeyEvent(document, "a");
|
|
812
|
+
assert.strictEqual(mockCallback.calledCount, 0);
|
|
644
813
|
});
|
|
645
|
-
it(`should return
|
|
646
|
-
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
assert.strictEqual(result, undefined, "addSequence should return undefined for sequence with empty string");
|
|
814
|
+
it(`should return an empty observable and warn if sequence contains an invalid key`, () => {
|
|
815
|
+
const config = { id: "invalidKeyInSeq", sequence: [Keys.A, "", Keys.C] };
|
|
816
|
+
const seq$ = keyManager.addSequence(config);
|
|
817
|
+
seq$.subscribe(mockCallback);
|
|
650
818
|
assert.strictEqual(consoleWarnMock.mock.calls.length, 1, "console.warn was not called for invalid key in sequence");
|
|
651
819
|
assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Invalid key in sequence for shortcut "invalidKeyInSeq"`));
|
|
652
820
|
});
|
|
653
|
-
it("should pass the last KeyboardEvent of the sequence to the
|
|
654
|
-
const config = { id: "seqEventPass", sequence: [Keys.X, Keys.Y]
|
|
655
|
-
keyManager.addSequence(config);
|
|
656
|
-
dispatchKeyEvent("x");
|
|
657
|
-
const lastEvent = dispatchKeyEvent("y");
|
|
821
|
+
it("should pass the last KeyboardEvent of the sequence to the subscriber", () => {
|
|
822
|
+
const config = { id: "seqEventPass", sequence: [Keys.X, Keys.Y] };
|
|
823
|
+
keyManager.addSequence(config).subscribe(mockCallback);
|
|
824
|
+
dispatchKeyEvent(document, "x");
|
|
825
|
+
const lastEvent = dispatchKeyEvent(document, "y");
|
|
658
826
|
assert.strictEqual(mockCallback.calledCount, 1);
|
|
659
827
|
assert.deepStrictEqual(mockCallback.lastArgs, [lastEvent]);
|
|
660
828
|
});
|
|
661
829
|
it("should prevent default for the last key event in the sequence when preventDefault is true", () => {
|
|
662
|
-
const config = { id: "seqPrevent", sequence: [Keys.M, Keys.N],
|
|
663
|
-
keyManager.addSequence(config);
|
|
664
|
-
dispatchKeyEvent("m");
|
|
665
|
-
const eventN = dispatchKeyEvent("n");
|
|
830
|
+
const config = { id: "seqPrevent", sequence: [Keys.M, Keys.N], preventDefault: true };
|
|
831
|
+
keyManager.addSequence(config).subscribe(mockCallback);
|
|
832
|
+
dispatchKeyEvent(document, "m");
|
|
833
|
+
const eventN = dispatchKeyEvent(document, "n");
|
|
666
834
|
assert.strictEqual(mockCallback.calledCount, 1);
|
|
667
835
|
assert.strictEqual(eventN.defaultPrevented, true);
|
|
668
836
|
});
|
|
669
|
-
it("should
|
|
670
|
-
const errorCallback = () => { throw new Error("Test sequence callback error"); };
|
|
671
|
-
keyManager.addSequence({ id: "errorSeq", sequence: [Keys.E, Keys.S], callback: errorCallback });
|
|
672
|
-
dispatchKeyEvent("e");
|
|
673
|
-
dispatchKeyEvent("s");
|
|
674
|
-
assert.strictEqual(consoleErrorMock.mock.calls.length, 1);
|
|
675
|
-
assert.ok(consoleErrorMock.mock.calls[0].arguments[0].includes(`Error in user callback for sequence shortcut "errorSeq"`));
|
|
676
|
-
});
|
|
677
|
-
it("should trigger callback for a sequence including Keys.Space", () => {
|
|
837
|
+
it("should emit for a sequence including Keys.Space", () => {
|
|
678
838
|
const config = {
|
|
679
839
|
id: "seqWithSpace",
|
|
680
840
|
sequence: [Keys.G, Keys.Space, Keys.I],
|
|
681
|
-
callback: mockCallback
|
|
682
841
|
};
|
|
683
|
-
keyManager.addSequence(config);
|
|
684
|
-
dispatchKeyEvent(Keys.G);
|
|
685
|
-
dispatchKeyEvent(Keys.Space); // Dispatch " " for space
|
|
686
|
-
dispatchKeyEvent(Keys.I);
|
|
842
|
+
keyManager.addSequence(config).subscribe(mockCallback);
|
|
843
|
+
dispatchKeyEvent(document, Keys.G);
|
|
844
|
+
dispatchKeyEvent(document, Keys.Space); // Dispatch " " for space
|
|
845
|
+
dispatchKeyEvent(document, Keys.I);
|
|
687
846
|
assert.strictEqual(mockCallback.calledCount, 1, "Callback for sequence with Keys.Space not called");
|
|
688
847
|
});
|
|
689
|
-
it("should
|
|
848
|
+
it("should emit for a sequence starting or ending with Keys.Space", () => {
|
|
690
849
|
const config = {
|
|
691
850
|
id: "seqStartSpace",
|
|
692
851
|
sequence: [Keys.Space, Keys.A],
|
|
693
|
-
callback: mockCallback
|
|
694
852
|
};
|
|
695
|
-
keyManager.addSequence(config);
|
|
696
|
-
dispatchKeyEvent(Keys.Space);
|
|
697
|
-
dispatchKeyEvent(Keys.A);
|
|
853
|
+
keyManager.addSequence(config).subscribe(mockCallback);
|
|
854
|
+
dispatchKeyEvent(document, Keys.Space);
|
|
855
|
+
dispatchKeyEvent(document, Keys.A);
|
|
698
856
|
assert.strictEqual(mockCallback.calledCount, 1, "Callback for sequence starting with Keys.Space not called");
|
|
699
857
|
});
|
|
700
858
|
describe("Sequence Contextual Triggering", () => {
|
|
@@ -703,14 +861,13 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
703
861
|
editorSequenceConfig = {
|
|
704
862
|
id: "sequenceInEditor",
|
|
705
863
|
sequence: [Keys.C, Keys.O, Keys.D, Keys.E],
|
|
706
|
-
callback: mockCallback,
|
|
707
864
|
context: "editor",
|
|
708
865
|
};
|
|
709
866
|
});
|
|
710
|
-
it("should
|
|
711
|
-
keyManager.addSequence(editorSequenceConfig);
|
|
867
|
+
it("should emit sequence in matching context", () => {
|
|
868
|
+
keyManager.addSequence(editorSequenceConfig).subscribe(mockCallback);
|
|
712
869
|
keyManager.setContext("editor");
|
|
713
|
-
[Keys.C, Keys.O, Keys.D, Keys.E].forEach(k => dispatchKeyEvent(k));
|
|
870
|
+
[Keys.C, Keys.O, Keys.D, Keys.E].forEach(k => dispatchKeyEvent(document, k));
|
|
714
871
|
assert.strictEqual(mockCallback.calledCount, 1);
|
|
715
872
|
});
|
|
716
873
|
});
|
|
@@ -737,48 +894,54 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
737
894
|
}
|
|
738
895
|
mock.timers.reset();
|
|
739
896
|
});
|
|
740
|
-
it("should
|
|
741
|
-
const config = { id: "seqTimeoutOk", sequence: [Keys.T, Keys.O, Keys.K],
|
|
742
|
-
keyManager.addSequence(config);
|
|
743
|
-
dispatchKeyEvent(Keys.T);
|
|
897
|
+
it("should emit if keys are pressed within specified timeout", () => {
|
|
898
|
+
const config = { id: "seqTimeoutOk", sequence: [Keys.T, Keys.O, Keys.K], sequenceTimeoutMs: TIMEOUT_MS };
|
|
899
|
+
keyManager.addSequence(config).subscribe(mockCallback);
|
|
900
|
+
dispatchKeyEvent(document, Keys.T);
|
|
744
901
|
mock.timers.tick(TIMEOUT_MS / 2);
|
|
745
|
-
dispatchKeyEvent(Keys.O);
|
|
902
|
+
dispatchKeyEvent(document, Keys.O);
|
|
746
903
|
mock.timers.tick(TIMEOUT_MS / 2);
|
|
747
|
-
const lastEvent = dispatchKeyEvent(Keys.K);
|
|
904
|
+
const lastEvent = dispatchKeyEvent(document, Keys.K);
|
|
748
905
|
assert.strictEqual(mockCallback.calledCount, 1);
|
|
749
906
|
assert.deepStrictEqual(mockCallback.lastArgs, [lastEvent]);
|
|
750
907
|
});
|
|
751
|
-
it("should NOT
|
|
752
|
-
const config = { id: "seqTimeoutFail", sequence: [Keys.D, Keys.E, Keys.L],
|
|
753
|
-
keyManager.addSequence(config);
|
|
754
|
-
dispatchKeyEvent(Keys.D);
|
|
908
|
+
it("should NOT emit if a key press is delayed beyond timeout", () => {
|
|
909
|
+
const config = { id: "seqTimeoutFail", sequence: [Keys.D, Keys.E, Keys.L], sequenceTimeoutMs: TIMEOUT_MS };
|
|
910
|
+
keyManager.addSequence(config).subscribe(mockCallback);
|
|
911
|
+
dispatchKeyEvent(document, Keys.D);
|
|
755
912
|
mock.timers.tick(TIMEOUT_MS / 2);
|
|
756
|
-
dispatchKeyEvent(Keys.E);
|
|
913
|
+
dispatchKeyEvent(document, Keys.E);
|
|
757
914
|
mock.timers.tick(TIMEOUT_MS + 1);
|
|
758
|
-
dispatchKeyEvent(Keys.L);
|
|
915
|
+
dispatchKeyEvent(document, Keys.L);
|
|
759
916
|
assert.strictEqual(mockCallback.calledCount, 0);
|
|
760
917
|
});
|
|
761
918
|
});
|
|
762
919
|
});
|
|
763
920
|
describe("remove", () => {
|
|
764
|
-
it("should remove a combination shortcut", () => {
|
|
765
|
-
|
|
921
|
+
it("should remove a combination shortcut and complete its observable", () => {
|
|
922
|
+
const completeCallback = createMockFn();
|
|
923
|
+
const combo$ = keyManager.addCombination({ id: "remA", keys: { key: Keys.A } });
|
|
924
|
+
combo$.subscribe({ next: mockCallback, complete: completeCallback });
|
|
766
925
|
assert.strictEqual(keyManager.remove("remA"), true);
|
|
767
|
-
|
|
926
|
+
assert.strictEqual(completeCallback.calledCount, 1, "Observable should have completed");
|
|
927
|
+
dispatchKeyEvent(document, Keys.A);
|
|
768
928
|
assert.strictEqual(mockCallback.calledCount, 0);
|
|
769
929
|
});
|
|
770
|
-
it("should remove a sequence shortcut", () => {
|
|
771
|
-
|
|
930
|
+
it("should remove a sequence shortcut and complete its observable", () => {
|
|
931
|
+
const completeCallback = createMockFn();
|
|
932
|
+
const seq$ = keyManager.addSequence({ id: "remSeq", sequence: [Keys.A, Keys.B] });
|
|
933
|
+
seq$.subscribe({ next: mockCallback, complete: completeCallback });
|
|
772
934
|
assert.strictEqual(keyManager.remove("remSeq"), true);
|
|
773
|
-
|
|
774
|
-
dispatchKeyEvent(Keys.
|
|
935
|
+
assert.strictEqual(completeCallback.calledCount, 1, "Observable should have completed");
|
|
936
|
+
dispatchKeyEvent(document, Keys.A);
|
|
937
|
+
dispatchKeyEvent(document, Keys.B);
|
|
775
938
|
assert.strictEqual(mockCallback.calledCount, 0);
|
|
776
939
|
});
|
|
777
940
|
});
|
|
778
941
|
describe("getActiveShortcuts", () => {
|
|
779
942
|
it("should return active combination and sequence shortcuts with enum types", () => {
|
|
780
|
-
keyManager.addCombination({ id: "combo1", keys: { key: Keys.A },
|
|
781
|
-
keyManager.addSequence({ id: "seq1", sequence: [Keys.B, Keys.C],
|
|
943
|
+
keyManager.addCombination({ id: "combo1", keys: { key: Keys.A }, description: "Test A" });
|
|
944
|
+
keyManager.addSequence({ id: "seq1", sequence: [Keys.B, Keys.C], context: "modal", description: "Test BC" });
|
|
782
945
|
const active = keyManager.getActiveShortcuts();
|
|
783
946
|
assert.strictEqual(active.length, 2);
|
|
784
947
|
const combo = active.find(s => s.id === "combo1");
|
|
@@ -791,29 +954,30 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
791
954
|
});
|
|
792
955
|
describe("hasShortcut", () => {
|
|
793
956
|
it("should return true for an existing combination shortcut", () => {
|
|
794
|
-
keyManager.addCombination({ id: "existsCombo", keys: { key: Keys.E }
|
|
957
|
+
keyManager.addCombination({ id: "existsCombo", keys: { key: Keys.E } });
|
|
795
958
|
assert.strictEqual(keyManager.hasShortcut("existsCombo"), true);
|
|
796
959
|
});
|
|
797
960
|
it("should return false for a shortcut that failed to add (e.g. invalid key object)", () => {
|
|
798
|
-
|
|
799
|
-
const config = { id: "invalidKeyCombo", keys: { key: null }, callback: mockCallback };
|
|
961
|
+
const config = { id: "invalidKeyCombo", keys: { key: null } };
|
|
800
962
|
keyManager.addCombination(config);
|
|
801
963
|
assert.strictEqual(keyManager.hasShortcut("invalidKeyCombo"), false);
|
|
802
964
|
});
|
|
803
965
|
});
|
|
804
966
|
describe("destroy", () => {
|
|
805
|
-
it("should clear active shortcuts and
|
|
806
|
-
|
|
807
|
-
|
|
967
|
+
it("should clear active shortcuts and complete all observables", () => {
|
|
968
|
+
const comboComplete = createMockFn();
|
|
969
|
+
const seqComplete = createMockFn();
|
|
970
|
+
const combo$ = keyManager.addCombination({ id: "destroyTestCombo", keys: { key: Keys.D } });
|
|
971
|
+
combo$.subscribe({ complete: comboComplete });
|
|
972
|
+
const seq$ = keyManager.addSequence({ id: "destroyTestSeq", sequence: [Keys.X, Keys.Y] });
|
|
973
|
+
seq$.subscribe({ complete: seqComplete });
|
|
808
974
|
// @ts-ignore
|
|
809
975
|
assert.strictEqual(keyManager["activeShortcuts"].size, 2);
|
|
810
976
|
keyManager.destroy();
|
|
811
977
|
// @ts-ignore
|
|
812
978
|
assert.strictEqual(keyManager["activeShortcuts"].size, 0);
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
dispatchKeyEvent(Keys.Y);
|
|
816
|
-
assert.strictEqual(mockCallback.calledCount, 0);
|
|
979
|
+
assert.strictEqual(comboComplete.calledCount, 1, "Combination observable should have completed");
|
|
980
|
+
assert.strictEqual(seqComplete.calledCount, 1, "Sequence observable should have completed");
|
|
817
981
|
});
|
|
818
982
|
});
|
|
819
983
|
});
|