cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

fix/portal-dist-prebuild
goq / portal / controller-state.test.mjs
controller-state.test.mjs 8.7K
1import assert from 'node:assert/strict';2import test from 'node:test';34import {5 ControllerActivationGate,6 ControllerActionRepeater,7 GamepadEscapeHold,8 LatestControllerStatePublisher,9 STANDARD_AXIS_COUNT,10 STANDARD_BUTTON_COUNT,11 chooseDirectionalIndex,12 controllerStateSignature,13 disconnectedControllerState,14 maskGamepadEscapeChord,15 navigationDirection,16 neutralGamepadInputState,17 normalizeGamepad,18 selectPreferredController,19 toGamepadInputState,20} from './controller-state.mjs';2122function gamepad({ axes = [], pressed = [], values = [], index = 2 } = {}) {23 const buttons = Array.from({ length: 24 }, (_, buttonIndex) => ({24 pressed: pressed.includes(buttonIndex),25 value: values[buttonIndex] || 0,26 }));27 return { connected: true, index, id: 'test pad', mapping: 'standard', timestamp: 42, axes, buttons };28}2930test('normalization is immutable, clamped, and fixed-size', () => {31 const state = normalizeGamepad(gamepad({ axes: [-2, 0.25, 4, NaN, 0.8], values: { 0: 2 } }), 7);32 assert.equal(state.sequence, 7);33 assert.equal(state.axes.length, STANDARD_AXIS_COUNT);34 assert.equal(state.buttons.length, STANDARD_BUTTON_COUNT);35 assert.deepEqual(state.axes, [-1, 0.25, 1, 0]);36 assert.equal(state.buttons[0], 1);37 assert.ok(Object.isFrozen(state));38 assert.ok(Object.isFrozen(state.axes));39});4041test('navigation prefers dpad then the dominant left-stick axis', () => {42 assert.equal(navigationDirection(normalizeGamepad(gamepad({ pressed: [15], axes: [0, -1] }))), 'right');43 assert.equal(navigationDirection(normalizeGamepad(gamepad({ axes: [-0.8, 0.6] }))), 'left');44 assert.equal(navigationDirection(normalizeGamepad(gamepad({ axes: [0.2, 0.54] }))), null);45});4647test('actions are edge-triggered with deterministic directional repeat', () => {48 const repeater = new ControllerActionRepeater({ repeatDelayMs: 300, repeatIntervalMs: 100 });49 const held = normalizeGamepad(gamepad({ pressed: [0, 12] }));50 assert.deepEqual(repeater.update(held, 0), [51 { type: 'navigate', direction: 'up' },52 { type: 'activate' },53 ]);54 assert.deepEqual(repeater.update(held, 299), []);55 assert.deepEqual(repeater.update(held, 300), [{ type: 'navigate', direction: 'up' }]);56 assert.deepEqual(repeater.update(held, 399), []);57 assert.deepEqual(repeater.update(held, 400), [{ type: 'navigate', direction: 'up' }]);58 const released = normalizeGamepad(gamepad());59 assert.deepEqual(repeater.update(released, 401), []);60 assert.deepEqual(repeater.update(normalizeGamepad(gamepad({ pressed: [0, 1] })), 402), [61 { type: 'activate' },62 { type: 'back' },63 ]);64});6566test('directional selection follows geometry and never wraps unexpectedly', () => {67 const rects = [68 { left: 0, top: 0, width: 20, height: 20 },69 { left: 100, top: 0, width: 20, height: 20 },70 { left: 0, top: 100, width: 20, height: 20 },71 { left: 100, top: 100, width: 20, height: 20 },72 ];73 assert.equal(chooseDirectionalIndex(rects, 0, 'right'), 1);74 assert.equal(chooseDirectionalIndex(rects, 0, 'down'), 2);75 assert.equal(chooseDirectionalIndex(rects, 0, 'left'), 0);76 assert.equal(chooseDirectionalIndex(rects, -1, 'right'), 0);77});7879test('state signature ignores sequence and browser timestamp', () => {80 const one = normalizeGamepad(gamepad(), 1);81 const two = normalizeGamepad({ ...gamepad(), timestamp: 999 }, 2);82 assert.equal(controllerStateSignature(one), controllerStateSignature(two));83 assert.notEqual(controllerStateSignature(one), controllerStateSignature(disconnectedControllerState()));84});8586test('standard gamepad maps exactly to bounded protocol state', () => {87 const state = normalizeGamepad(gamepad({88 axes: [-1, 0.5, 2, -2],89 pressed: [0, 4, 8, 12, 13, 14],90 values: { 6: 0.5, 7: 1 },91 }));92 const mapped = toGamepadInputState(state);93 assert.deepEqual(mapped, {94 ...neutralGamepadInputState(),95 a: true,96 left_shoulder: true,97 back: true,98 dpad_left: true,99 left_x: -32767,100 left_y: 16384,101 right_x: 32767,102 right_y: -32767,103 left_trigger: 16384,104 right_trigger: 32767,105 });106 assert.equal(mapped.dpad_up, false);107 assert.equal(mapped.dpad_down, false);108});109110test('controller activation gate suppresses only the crossing A press', () => {111 const gate = new ControllerActivationGate();112 const held = { ...neutralGamepadInputState(), a: true };113 const releasedWithDrift = { ...neutralGamepadInputState(), left_x: 33 };114115 assert.equal(gate.accepts(held), true);116 gate.arm();117 assert.equal(gate.active, true);118 assert.equal(gate.accepts(held), false);119 assert.equal(gate.accepts(releasedWithDrift), true);120 assert.equal(gate.active, false);121 assert.equal(gate.accepts(held), true);122});123124test('controller activation gate reset cannot leak into another control session', () => {125 const gate = new ControllerActivationGate();126 const held = { ...neutralGamepadInputState(), a: true };127128 gate.arm();129 assert.equal(gate.accepts(held), false);130 gate.reset();131132 assert.equal(gate.active, false);133 assert.equal(gate.accepts(held), true);134});135136test('controller activation gate accepts an A release already current after acquisition', () => {137 const gate = new ControllerActivationGate();138 const releasedWhileAcquiring = { ...neutralGamepadInputState(), left_x: 33 };139140 gate.arm();141142 assert.equal(gate.accepts(releasedWhileAcquiring), true);143 assert.equal(gate.active, false);144});145146test('disconnected and non-standard pads map to neutral', () => {147 assert.deepEqual(toGamepadInputState(disconnectedControllerState()), neutralGamepadInputState());148 assert.deepEqual(149 toGamepadInputState(normalizeGamepad({ ...gamepad({ pressed: [0] }), mapping: '' })),150 neutralGamepadInputState(),151 );152});153154test('Back+Start escape chord is reserved without consuming unrelated controls', () => {155 const input = {156 ...neutralGamepadInputState(),157 a: true,158 back: true,159 start: true,160 left_x: 1234,161 };162 const masked = maskGamepadEscapeChord(input);163164 assert.deepEqual(masked, {165 ...input,166 back: false,167 start: false,168 });169 assert.equal(input.back, true);170 assert.equal(input.start, true);171 assert.equal(maskGamepadEscapeChord({ ...input, start: false }).back, true);172 assert.equal(maskGamepadEscapeChord({ ...input, back: false }).start, true);173});174175test('controller selection prefers standard mappings and retains a selected standard pad', () => {176 const nonStandard = { connected: true, index: 4, mapping: '' };177 const firstStandard = { connected: true, index: 1, mapping: 'standard' };178 const selectedStandard = { connected: true, index: 2, mapping: 'standard' };179180 assert.equal(181 selectPreferredController([nonStandard, firstStandard, selectedStandard], nonStandard.index),182 firstStandard,183 );184 assert.equal(185 selectPreferredController(186 [nonStandard, firstStandard, selectedStandard],187 selectedStandard.index,188 ),189 selectedStandard,190 );191});192193test('controller selection falls back deterministically without a standard mapping', () => {194 const first = { connected: true, index: 3, mapping: '' };195 const selected = { connected: true, index: 7, mapping: 'custom' };196 const disconnected = { connected: false, index: 9, mapping: 'standard' };197198 assert.equal(selectPreferredController([first, selected], selected.index), selected);199 assert.equal(selectPreferredController([first, selected], 99), first);200 assert.equal(selectPreferredController([disconnected, first], disconnected.index), first);201 assert.equal(selectPreferredController([], null), null);202 assert.equal(selectPreferredController(null, null), null);203});204205test('escape chord requires an uninterrupted one-second hold and fires once', () => {206 const escape = new GamepadEscapeHold(1000);207 const held = normalizeGamepad(gamepad({ pressed: [8, 9] }));208 assert.equal(escape.update(held, 100), false);209 assert.equal(escape.update(held, 1099), false);210 assert.equal(escape.update(held, 1100), true);211 assert.equal(escape.update(held, 2000), false);212 assert.equal(escape.update(normalizeGamepad(gamepad()), 2001), false);213 assert.equal(escape.update(held, 2002), false);214});215216test('slow publisher drops intermediate states and delivers the latest one', async () => {217 const publisher = new LatestControllerStatePublisher();218 const delivered = [];219 let releaseFirst;220 publisher.setHandler(async (state) => {221 delivered.push(state.sequence);222 if (state.sequence === 1) await new Promise((resolve) => { releaseFirst = resolve; });223 });224 publisher.publish(normalizeGamepad(gamepad(), 1));225 await Promise.resolve();226 publisher.publish(normalizeGamepad(gamepad({ axes: [0.1] }), 2));227 publisher.publish(normalizeGamepad(gamepad({ axes: [0.2] }), 3));228 assert.deepEqual(delivered, [1]);229 releaseFirst();230 await new Promise((resolve) => setTimeout(resolve, 0));231 assert.deepEqual(delivered, [1, 3]);232 assert.equal(publisher.latest.sequence, 3);233});