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 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 13:50:55 -04:00
parent e17cb09269
commit d37fa1584c
+117
View File
@@ -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);
});