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:
@@ -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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user