clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

main
miau-is-true / tests / input.test.js
input.test.js 6.8K
1import test from 'node:test';2import assert from 'node:assert/strict';3import { createInput } from '../src/input.js';45class FakeTarget {6 constructor() { this.listeners = new Map(); }7 addEventListener(type, handler) { this.listeners.set(`${type}:${handler.name}:${this.listeners.size}`, handler); }8 removeEventListener(type, handler) {9 for (const [key, value] of this.listeners) if (key.startsWith(`${type}:`) && value === handler) this.listeners.delete(key);10 }11 dispatch(type, event = {}) {12 for (const [key, handler] of this.listeners) if (key.startsWith(`${type}:`)) handler({ preventDefault() {}, target: {}, ...event });13 }14}1516function harness() {17 const target = new FakeTarget();18 const intents = [];19 let time = 0;20 const input = createInput({ target, autoTick: false, now: () => time, getGamepads: () => [], onIntent: intent => intents.push({ ...intent }) });21 input.attach();22 return { target, input, intents, setTime(value) { time = value; }, tick(value) { time = value; input.tick(value); } };23}2425test('keyboard presses act immediately and browser repeat is ignored', () => {26 const h = harness();27 h.target.dispatch('keydown', { key: 'ArrowRight', repeat: false });28 h.target.dispatch('keydown', { key: 'ArrowRight', repeat: true });29 assert.deepEqual(h.intents.map(intent => intent.type), ['move']);30 assert.equal(h.intents[0].dir, 'right');31 h.tick(174); assert.equal(h.intents.length, 1);32 h.tick(175); assert.equal(h.intents.length, 2);33 h.target.dispatch('keyup', { key: 'ArrowRight' });34 h.tick(500); assert.equal(h.intents.length, 2);35});3637test('only the most recent held direction repeats, then older direction resumes', () => {38 const h = harness();39 h.target.dispatch('keydown', { key: 'ArrowLeft', repeat: false });40 h.setTime(10); h.target.dispatch('keydown', { key: 'ArrowDown', repeat: false });41 h.tick(184); assert.deepEqual(h.intents.map(intent => intent.dir), ['left', 'down']);42 h.tick(185); assert.deepEqual(h.intents.map(intent => intent.dir), ['left', 'down', 'down']);43 h.setTime(200); h.target.dispatch('keyup', { key: 'ArrowDown' });44 h.tick(374); assert.equal(h.intents.length, 3);45 h.tick(375); assert.deepEqual(h.intents.map(intent => intent.dir), ['left', 'down', 'down', 'left']);46});4748test('undo repeats after its delay and accelerates to the configured floor', () => {49 const h = harness();50 h.target.dispatch('keydown', { key: 'z', repeat: false });51 h.tick(239); assert.equal(h.intents.length, 1);52 h.tick(240); assert.equal(h.intents.length, 2);53 h.tick(389); assert.equal(h.intents.length, 2);54 h.tick(390); assert.equal(h.intents.length, 3);55 h.tick(521); assert.equal(h.intents.length, 3);56 h.tick(522); assert.equal(h.intents.length, 4);57 h.tick(800); assert.equal(h.intents.length, 5);58 h.target.dispatch('keyup', { key: 'z' });59 h.tick(2_000); assert.equal(h.intents.length, 5);60 assert.ok(h.intents.every(intent => intent.type === 'undo'));61});6263test('standard gamepad d-pad, stick, and action buttons share the input contract', () => {64 const intents = [];65 let time = 0;66 const buttons = Array.from({ length: 16 }, () => ({ pressed: false }));67 const pad = { connected: true, buttons, axes: [0, 0] };68 const input = createInput({ autoTick: false, now: () => time, getGamepads: () => [pad], onIntent: intent => intents.push({ ...intent }) });69 buttons[12].pressed = true; buttons[0].pressed = true;70 input.tick(0);71 assert.deepEqual(intents.map(intent => intent.type), ['move', 'confirm']);72 assert.equal(intents[0].dir, 'up');73 buttons[12].pressed = false; buttons[0].pressed = false; pad.axes[0] = 0.8;74 time = 1; input.tick(time);75 assert.equal(intents.at(-1).dir, 'right');76 buttons[1].pressed = true; time = 2; input.tick(time);77 assert.equal(intents.at(-1).type, 'undo');78 buttons[1].pressed = false; pad.axes[0] = 0; time = 3; input.tick(time);79 time = 242; input.tick(time);80 assert.equal(intents.at(-1).type, 'undo');81});8283test('gamepad bumpers request adjacent INDEX worlds once per press', () => {84 const intents = [];85 const buttons = Array.from({ length: 16 }, () => ({ pressed: false }));86 const pad = { connected: true, buttons, axes: [0, 0] };87 const input = createInput({ autoTick: false, getGamepads: () => [pad], onIntent: intent => intents.push({ ...intent }) });88 buttons[4].pressed = true;89 input.tick(0); input.tick(1);90 buttons[4].pressed = false; buttons[5].pressed = true;91 input.tick(2);92 assert.deepEqual(intents.map(intent => [intent.type, intent.delta]), [['world', -1], ['world', 1]]);93});9495test('touch swipes move while taps confirm', () => {96 const h = harness();97 h.input.pointerDown({ pointerId: 1, pointerType: 'touch', clientX: 10, clientY: 20, preventDefault() {} });98 h.input.pointerUp({ pointerId: 1, pointerType: 'touch', clientX: 20, clientY: 25, preventDefault() {} });99 assert.equal(h.intents.at(-1).type, 'confirm');100 h.input.pointerDown({ pointerId: 2, pointerType: 'touch', clientX: 10, clientY: 20, preventDefault() {} });101 h.input.pointerUp({ pointerId: 2, pointerType: 'touch', clientX: 42, clientY: 21, preventDefault() {} });102 assert.equal(h.intents.at(-1).type, 'move');103 assert.equal(h.intents.at(-1).dir, 'right');104 h.input.pointerDown({ pointerId: 3, pointerType: 'touch', clientX: 30, clientY: 40, preventDefault() {} });105 h.input.pointerUp({ pointerId: 3, pointerType: 'touch', clientX: 30, clientY: 67, preventDefault() {} });106 assert.equal(h.intents.at(-1).dir, 'down');107});108109test('two-finger horizontal swipe requests one world switch per gesture', () => {110 const h = harness();111 const event = (id, x, y) => ({ pointerId: id, pointerType: 'touch', clientX: x, clientY: y, preventDefault() {} });112 h.input.pointerDown(event(1, 10, 20));113 h.input.pointerDown(event(2, 10, 60));114 h.input.pointerUp(event(1, 55, 20));115 h.input.pointerUp(event(2, 55, 60));116 assert.deepEqual(h.intents.map(intent => [intent.type, intent.delta]), [['world', 1]]);117 h.input.pointerDown(event(3, 80, 20));118 h.input.pointerDown(event(4, 80, 60));119 h.input.pointerUp(event(3, 40, 20));120 h.input.pointerUp(event(4, 40, 60));121 assert.deepEqual(h.intents.map(intent => [intent.type, intent.delta]), [['world', 1], ['world', -1]]);122});123124test('T requests an accessible board summary without moving the game', () => {125 const h = harness();126 h.target.dispatch('keydown', { key: 't', repeat: false });127 assert.equal(h.intents.at(-1).type, 'summary');128 h.target.dispatch('keydown', { key: 'T', repeat: false });129 assert.equal(h.intents.at(-1).type, 'summary');130});131132test('Space and gamepad X request a single wait turn', () => {133 const h = harness();134 h.target.dispatch('keydown', { key: ' ', repeat: false });135 h.target.dispatch('keydown', { key: ' ', repeat: true });136 assert.deepEqual(h.intents.map(intent => intent.type), ['wait']);137 h.input.pressAction('wait', 'gamepad');138 assert.deepEqual(h.intents.map(intent => intent.type), ['wait', 'wait']);139});