Something went wrong. Try again.
A WebGPU retro game engine for TypeScript, inspired by RetroBlit. Build pixel-perfect 2D games with a clean, fantasy-console-style API.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369// @vitest-environment happy-dom
/** * Unit tests for {@link KeyboardInput}. */
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { DEFAULT_KEYBOARD_PLAYER1 } from './defaultKeyboardMap';import { KeyboardInput } from './KeyboardInput';
const createCanvas = (): HTMLCanvasElement => { const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
return canvas;};
describe('KeyboardInput', () => { let tick = 0;
afterEach(() => { document.body.replaceChildren(); tick = 0; });
beforeEach(() => { tick = 0; });
it('attach / detach removes listeners without throwing', () => { const canvas = createCanvas(); const kb = new KeyboardInput();
kb.attach(canvas, { getTicks: () => tick }); kb.detach(); kb.detach(); });
it('keydown adds key; keyUp removes', () => { const canvas = createCanvas(); const kb = new KeyboardInput();
kb.attach(canvas, { getTicks: () => tick });
canvas.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyQ', bubbles: true }));
expect(kb.isKeyDown('KeyQ')).toBe(true);
canvas.dispatchEvent(new KeyboardEvent('keyup', { code: 'KeyQ', bubbles: true }));
expect(kb.isKeyDown('KeyQ')).toBe(false);
kb.detach(); });
it('ignores duplicate keydown while held', () => { const canvas = createCanvas(); const kb = new KeyboardInput();
kb.attach(canvas, { getTicks: () => tick });
canvas.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyA', bubbles: true })); canvas.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyA', bubbles: true }));
expect(kb.isKeyDown('KeyA')).toBe(true);
kb.detach(); });
it('endFrame snapshots for pressed / released edges', () => { const canvas = createCanvas(); const kb = new KeyboardInput();
kb.attach(canvas, { getTicks: () => tick });
kb.endFrame(0);
canvas.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyZ', bubbles: true }));
expect(kb.isKeyPressed('KeyZ', undefined, 0)).toBe(true);
kb.endFrame(1);
canvas.dispatchEvent(new KeyboardEvent('keyup', { code: 'KeyZ', bubbles: true }));
expect(kb.isKeyReleased('KeyZ')).toBe(true);
kb.detach(); });
it('blur clears held keys and buffers', () => { const canvas = createCanvas(); const kb = new KeyboardInput();
kb.attach(canvas, { getTicks: () => tick });
canvas.dispatchEvent(new KeyboardEvent('keydown', { code: 'Space', bubbles: true }));
expect(kb.isKeyDown('Space')).toBe(true);
window.dispatchEvent(new Event('blur'));
expect(kb.isKeyDown('Space')).toBe(false);
kb.detach(); });
it('repeatRate > 0 repeats on schedule while held', () => { const canvas = createCanvas(); const kb = new KeyboardInput();
kb.attach(canvas, { getTicks: () => tick });
kb.endFrame(0);
tick = 10; canvas.dispatchEvent(new KeyboardEvent('keydown', { code: 'ArrowDown', bubbles: true }));
expect(kb.isKeyPressed('ArrowDown', 5, 10)).toBe(true);
kb.endFrame(10);
expect(kb.isKeyPressed('ArrowDown', 5, 11)).toBe(false);
expect(kb.isKeyPressed('ArrowDown', 5, 15)).toBe(true);
kb.detach(); });
it('repeatRate 0 uses edge only', () => { const canvas = createCanvas(); const kb = new KeyboardInput();
kb.attach(canvas, { getTicks: () => tick });
kb.endFrame(0);
canvas.dispatchEvent(new KeyboardEvent('keydown', { code: 'Digit1', bubbles: true }));
expect(kb.isKeyPressed('Digit1', 0, 0)).toBe(true);
kb.endFrame(1);
expect(kb.isKeyPressed('Digit1', 0, 2)).toBe(false);
kb.detach(); });
it('accumulates insertText into input string and clears on endFrame', () => { const canvas = createCanvas(); const kb = new KeyboardInput();
kb.attach(canvas, { getTicks: () => tick });
canvas.dispatchEvent( new InputEvent('beforeinput', { inputType: 'insertText', data: 'Hi', bubbles: true, }), );
expect(kb.getInputString()).toBe('Hi');
kb.endFrame(0);
expect(kb.getInputString()).toBe(''); });
it('maps insertLineBreak to CR', () => { const canvas = createCanvas(); const kb = new KeyboardInput();
kb.attach(canvas, { getTicks: () => tick });
canvas.dispatchEvent( new InputEvent('beforeinput', { inputType: 'insertLineBreak', bubbles: true, }), );
expect(kb.getInputString()).toBe('\x0d');
kb.detach(); });
it('maps insertParagraph to CR', () => { const canvas = createCanvas(); const kb = new KeyboardInput();
kb.attach(canvas, { getTicks: () => tick });
canvas.dispatchEvent( new InputEvent('beforeinput', { inputType: 'insertParagraph', bubbles: true, }), );
expect(kb.getInputString()).toBe('\x0d');
kb.detach(); });
it('insertCompositionText accumulates printable ASCII', () => { const canvas = createCanvas(); const kb = new KeyboardInput();
kb.attach(canvas, { getTicks: () => tick });
canvas.dispatchEvent( new InputEvent('beforeinput', { inputType: 'insertCompositionText', data: 'Z', bubbles: true, }), );
expect(kb.getInputString()).toBe('Z');
kb.detach(); });
it('ignores insertText when data is missing', () => { const canvas = createCanvas(); const kb = new KeyboardInput();
kb.attach(canvas, { getTicks: () => tick });
canvas.dispatchEvent( new InputEvent('beforeinput', { inputType: 'insertText', bubbles: true, }), );
expect(kb.getInputString()).toBe('');
kb.detach(); });
it('filters insertText to allowed code units only', () => { const canvas = createCanvas(); const kb = new KeyboardInput();
kb.attach(canvas, { getTicks: () => tick });
canvas.dispatchEvent( new InputEvent('beforeinput', { inputType: 'insertText', data: '\x07', bubbles: true, }), );
expect(kb.getInputString()).toBe('');
kb.detach(); });
it('maps deleteContentBackward to backspace in buffer', () => { const canvas = createCanvas(); const kb = new KeyboardInput();
kb.attach(canvas, { getTicks: () => tick });
canvas.dispatchEvent( new InputEvent('beforeinput', { inputType: 'deleteContentBackward', bubbles: true, }), );
expect(kb.getInputString()).toBe('\x08');
kb.detach(); });
it('OR button released when last mapped key is released', () => { const canvas = createCanvas(); const kb = new KeyboardInput();
kb.attach(canvas, { getTicks: () => tick });
const codes = DEFAULT_KEYBOARD_PLAYER1[1 << 0] ?? [];
kb.endFrame(0);
canvas.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyW', bubbles: true }));
kb.endFrame(1);
canvas.dispatchEvent(new KeyboardEvent('keyup', { code: 'KeyW', bubbles: true }));
expect(kb.isButtonReleased(codes)).toBe(true);
kb.detach(); });
it('OR button repeat uses repeatRate while held', () => { const canvas = createCanvas(); const kb = new KeyboardInput();
kb.attach(canvas, { getTicks: () => tick });
const codes = DEFAULT_KEYBOARD_PLAYER1[1 << 0] ?? [];
kb.endFrame(0);
tick = 10; canvas.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyW', bubbles: true }));
expect(kb.isButtonPressed(codes, 4, 10)).toBe(true);
kb.endFrame(10);
expect(kb.isButtonPressed(codes, 4, 14)).toBe(true);
kb.detach(); });
it('Tab and Escape append via keydown', () => { const canvas = createCanvas(); const kb = new KeyboardInput();
kb.attach(canvas, { getTicks: () => tick });
canvas.dispatchEvent(new KeyboardEvent('keydown', { code: 'Tab', bubbles: true })); canvas.dispatchEvent(new KeyboardEvent('keydown', { code: 'Escape', bubbles: true }));
expect(kb.getInputString()).toBe('\x09\x1b');
kb.detach(); });
it('OR button mapping: BTN_UP player1 reflects KeyW', () => { const canvas = createCanvas(); const kb = new KeyboardInput();
kb.attach(canvas, { getTicks: () => tick });
const codes = DEFAULT_KEYBOARD_PLAYER1[1 << 0] ?? [];
canvas.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyW', bubbles: true }));
expect(kb.isButtonDown(codes)).toBe(true);
kb.detach(); });
it('OR button: BTN_A down if either Space or KeyB', () => { const canvas = createCanvas(); const kb = new KeyboardInput();
kb.attach(canvas, { getTicks: () => tick });
const codes = DEFAULT_KEYBOARD_PLAYER1[1 << 4] ?? [];
canvas.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyB', bubbles: true }));
expect(kb.isButtonDown(codes)).toBe(true);
kb.detach(); });});