From d37fa1584c8747343588148b35315adb4da83826 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Tue, 30 Jun 2026 13:50:55 -0400 Subject: [PATCH] test: add suite for utils/keyboard handlers (+4) Covers onTabPress (Tab-only), preventScrollWithArrowKey (arrows-only), onEnterOrSpace (Enter/Space gate the callback), and stopPropagation's editable-element check (does not swallow keys when an input/textarea/ contenteditable is focused) via mock events + a document.activeElement stub. Full suite now 133 tests. Co-Authored-By: Claude Opus 4.8 --- src/app/utils/keyboard.test.ts | 117 +++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 src/app/utils/keyboard.test.ts diff --git a/src/app/utils/keyboard.test.ts b/src/app/utils/keyboard.test.ts new file mode 100644 index 000000000..8e16059a2 --- /dev/null +++ b/src/app/utils/keyboard.test.ts @@ -0,0 +1,117 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { onTabPress, preventScrollWithArrowKey, onEnterOrSpace, stopPropagation } from './keyboard'; + +type Evt = { + key: string; + which: number; + altKey: boolean; + ctrlKey: boolean; + metaKey: boolean; + shiftKey: boolean; + prevented: boolean; + preventDefault(): void; +}; +const evt = (key: string, which: number): Evt => ({ + key, + which, + altKey: false, + ctrlKey: false, + metaKey: false, + shiftKey: false, + prevented: false, + preventDefault() { + this.prevented = true; + }, +}); + +test('onTabPress fires only on Tab', () => { + let called = 0; + const tab = evt('Tab', 9); + onTabPress(tab, () => { + called += 1; + }); + assert.equal(called, 1); + assert.equal(tab.prevented, true); + + const a = evt('a', 65); + onTabPress(a, () => { + called += 1; + }); + assert.equal(called, 1); // unchanged + assert.equal(a.prevented, false); +}); + +test('preventScrollWithArrowKey prevents default only on arrows', () => { + const up = evt('ArrowUp', 38); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + preventScrollWithArrowKey(up as any); + assert.equal(up.prevented, true); + + const a = evt('a', 65); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + preventScrollWithArrowKey(a as any); + assert.equal(a.prevented, false); +}); + +test('onEnterOrSpace fires on Enter or Space, not others', () => { + let count = 0; + const handler = onEnterOrSpace(() => { + count += 1; + }); + + const enter = evt('Enter', 13); + handler(enter); + assert.equal(count, 1); + assert.equal(enter.prevented, true); + + const space = evt(' ', 32); + handler(space); + assert.equal(count, 2); + assert.equal(space.prevented, true); + + const other = evt('a', 65); + handler(other); + assert.equal(count, 2); // unchanged + assert.equal(other.prevented, false); +}); + +test('stopPropagation: stops unless an editable element is focused', () => { + const makeKeyEvt = () => { + let stopped = false; + return { + ev: { + stopPropagation() { + stopped = true; + }, + }, + wasStopped: () => stopped, + }; + }; + const withActive = (activeElement: unknown) => { + (globalThis as { document?: unknown }).document = { activeElement }; + }; + + // nothing focused → stops, returns true + withActive(null); + let k = makeKeyEvt(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + assert.equal(stopPropagation(k.ev as any), true); + assert.equal(k.wasStopped(), true); + + // input focused → does not stop, returns false + withActive({ nodeName: 'INPUT', getAttribute: () => null }); + k = makeKeyEvt(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + assert.equal(stopPropagation(k.ev as any), false); + assert.equal(k.wasStopped(), false); + + // contenteditable focused → does not stop + withActive({ + nodeName: 'DIV', + getAttribute: (a: string) => (a === 'contenteditable' ? 'true' : null), + }); + k = makeKeyEvt(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + assert.equal(stopPropagation(k.ev as any), false); +});