cat / goq
clone

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

release/portal-packaging
goq / portal / input-state.test.mjs
input-state.test.mjs 12.9K
1import assert from 'node:assert/strict';2import test from 'node:test';34import {5 HeldInputState,6 MAX_HELD_KEYS,7 MAX_HELD_MOUSE_BUTTONS,8 MAX_RELATIVE_POINTER_DELTA,9 PointerMotionBuffer,10 RelativePointerAccumulator,11 advanceRemotePointerPosition,12 browserPointerLockLossRequiresControlExit,13 inputCapabilityLabel,14 mapCanvasPointToSurface,15 resolvePointerSurfaceSize,16 restoreRejectedPointerMotion,17 scaleRelativePointerDelta,18 validatePointerSurfaceDimensions,19 validatePointerPositionFeedback,20 browserMouseButtonCode,21} from './input-state.mjs';2223test('input capability labels group keyboard/mouse and rename gamepad for the UI', () => {24 assert.equal(inputCapabilityLabel({25 relativePointer: true,26 absolutePointer: false,27 keyboard: true,28 text: false,29 gamepad: true,30 }), 'kbm + controller');31 assert.equal(inputCapabilityLabel({32 absolutePointer: true,33 keyboard: true,34 }), 'kbm');35 assert.equal(inputCapabilityLabel({ gamepad: true }), 'controller');36 assert.equal(inputCapabilityLabel({ relativePointer: true }), 'mouse');37 assert.equal(inputCapabilityLabel({ keyboard: true }), 'keyboard');38 assert.equal(inputCapabilityLabel({ text: true }), 'text');39 assert.equal(inputCapabilityLabel({}), 'view only');40});4142test('relative motion forwards finite safe deltas without surface scaling', () => {43 assert.deepEqual(44 scaleRelativePointerDelta(10, -5, 1280, 800, 640, 400),45 { dx: 10, dy: -5 },46 );47 assert.deepEqual(48 scaleRelativePointerDelta(1, 1, 1280, 800, 0, 400),49 { dx: 1, dy: 1 },50 );51 assert.deepEqual(52 scaleRelativePointerDelta(Number.POSITIVE_INFINITY, 1, 1280, 800, 640, 400),53 { dx: 0, dy: 1 },54 );55 assert.deepEqual(56 scaleRelativePointerDelta(10.4, -5.6),57 { dx: 10, dy: -6 },58 );59 assert.deepEqual(60 scaleRelativePointerDelta(61 MAX_RELATIVE_POINTER_DELTA + 1_000,62 -MAX_RELATIVE_POINTER_DELTA - 1_000,63 ),64 {65 dx: MAX_RELATIVE_POINTER_DELTA + 1_000,66 dy: -MAX_RELATIVE_POINTER_DELTA - 1_000,67 },68 );69 assert.deepEqual(scaleRelativePointerDelta(Number.MAX_VALUE, 1), { dx: 0, dy: 1 });70});7172test('pointer surface metadata is bounded and old hosts fall back to frame size', () => {73 const native = validatePointerSurfaceDimensions({ width: 2560, height: 1600 });74 assert.deepEqual(native, { width: 2560, height: 1600 });75 assert.deepEqual(76 resolvePointerSurfaceSize(native, 1280, 800, true),77 { width: 2560, height: 1600 },78 );79 assert.deepEqual(80 resolvePointerSurfaceSize(null, 1280, 800, true),81 { width: 1280, height: 800 },82 );83 assert.deepEqual(84 resolvePointerSurfaceSize(native, 1280, 800, false),85 { width: 2560, height: 1600 },86 );87 assert.equal(resolvePointerSurfaceSize(null, 0, 800, true), null);88 assert.throws(89 () => validatePointerSurfaceDimensions({ width: 7681, height: 1600 }),90 /invalid pointer surface dimensions/,91 );92 assert.throws(93 () => validatePointerSurfaceDimensions({ width: 2560, height: 63 }),94 /invalid pointer surface dimensions/,95 );96});9798test('absolute pointer mapping always targets the negotiated native surface', () => {99 const rect = { left: 20, top: 10, width: 640, height: 400 };100 const surface = { width: 1280, height: 800 };101 assert.deepEqual(mapCanvasPointToSurface({ clientX: 340, clientY: 210, rect, surface }), {102 x: 640,103 y: 400,104 });105 assert.deepEqual(mapCanvasPointToSurface({ clientX: 700, clientY: -10, rect, surface }), {106 x: 1279,107 y: 0,108 });109});110111test('host pointer feedback is bounded and validated against the native surface', () => {112 const surface = { width: 2560, height: 1600 };113 assert.deepEqual(114 validatePointerPositionFeedback({ sequence: 7, position: { x: 1280, y: 800 } }, surface),115 { sequence: 7, position: { x: 1280, y: 800 }, pointer_visible: true },116 );117 assert.deepEqual(118 validatePointerPositionFeedback({ sequence: 8, position: null }, surface),119 { sequence: 8, position: null, pointer_visible: false },120 );121 assert.deepEqual(122 validatePointerPositionFeedback({123 sequence: 9,124 position: { x: 1280, y: 800 },125 pointer_visible: false,126 }, surface),127 { sequence: 9, position: { x: 1280, y: 800 }, pointer_visible: false },128 );129 const normalizedHidden = validatePointerPositionFeedback({130 sequence: 10,131 position: { x: 1280, y: 800 },132 pointer_visible: false,133 }, surface);134 assert.deepEqual(135 validatePointerPositionFeedback(normalizedHidden, surface),136 normalizedHidden,137 );138 assert.throws(139 () => validatePointerPositionFeedback({ sequence: -1, position: null }, surface),140 /invalid pointer feedback sequence/,141 );142 assert.throws(143 () => validatePointerPositionFeedback({ sequence: 11, position: { x: 2560, y: 0 } }, surface),144 /outside the negotiated surface/,145 );146 assert.throws(147 () => validatePointerPositionFeedback({148 sequence: 12,149 position: { x: 1, y: 1 },150 pointer_visible: 'yes',151 }, surface),152 /invalid pointer feedback visibility/,153 );154 assert.throws(155 () => validatePointerPositionFeedback({156 sequence: 13,157 position: null,158 pointer_visible: true,159 }, surface),160 /visible pointer feedback requires a position/,161 );162});163164test('software cursor follows remote deltas and clamps to the streamed surface', () => {165 assert.deepEqual(166 advanceRemotePointerPosition({ x: 640, y: 400 }, { dx: 20, dy: -10 }, 1280, 800),167 { x: 660, y: 390 },168 );169 assert.deepEqual(170 advanceRemotePointerPosition({ x: 2, y: 798 }, { dx: -20, dy: 10 }, 1280, 800),171 { x: 0, y: 799 },172 );173 assert.equal(advanceRemotePointerPosition(null, { dx: 1, dy: 1 }, 1280, 800), null);174});175176test('browser pointer lock loss only exits a browser-owned control session', () => {177 const canvas = {};178 const state = {179 browserPointerLockRequired: true,180 pointerLockElement: canvas,181 expectedElement: canvas,182 controlMode: true,183 controlTransitionInProgress: false,184 };185 assert.equal(browserPointerLockLossRequiresControlExit(state), false);186 assert.equal(browserPointerLockLossRequiresControlExit({187 ...state,188 pointerLockElement: null,189 }), true);190 assert.equal(browserPointerLockLossRequiresControlExit({191 ...state,192 pointerLockElement: null,193 controlMode: false,194 controlTransitionInProgress: true,195 }), true);196 assert.equal(browserPointerLockLossRequiresControlExit({197 ...state,198 browserPointerLockRequired: false,199 pointerLockElement: null,200 }), false);201 assert.equal(browserPointerLockLossRequiresControlExit({202 ...state,203 pointerLockElement: null,204 controlMode: false,205 }), false);206});207208test('browser mouse buttons map exactly and reject auxiliary buttons', () => {209 assert.equal(browserMouseButtonCode(0), 1);210 assert.equal(browserMouseButtonCode(2), 2);211 assert.equal(browserMouseButtonCode(1), 3);212 assert.equal(browserMouseButtonCode(3), null);213 assert.equal(browserMouseButtonCode(-1), null);214});215216test('held transitions produce one matching release and then clear', () => {217 const state = new HeldInputState();218 assert.equal(state.trackKey('KeyA', 'a'), 'tracked');219 assert.equal(state.trackKey('KeyA', 'a'), 'repeat');220 assert.equal(state.trackMouseButton(1), true);221 assert.equal(state.trackMouseButton(1), false);222 assert.deepEqual(state.releaseEvents(), [223 { t: 'ku', k: 'a' },224 { t: 'mu', b: 1 },225 ]);226 assert.equal(state.size, 2);227 state.clear();228 assert.equal(state.size, 0);229 assert.deepEqual(state.releaseEvents(), []);230});231232test('held transition tracking is strictly bounded', () => {233 const state = new HeldInputState();234 assert.equal(state.trackMouseButton(0), false);235 assert.equal(state.trackMouseButton(4), false);236 for (let index = 0; index < MAX_HELD_KEYS; index++) {237 assert.equal(state.trackKey(`Code${index}`, `key-${index}`), 'tracked');238 }239 assert.equal(state.trackKey('overflow', 'overflow'), 'full');240 for (let button = 1; button <= MAX_HELD_MOUSE_BUTTONS; button++) {241 assert.equal(state.trackMouseButton(button), true);242 }243 assert.equal(state.releaseEvents().length, MAX_HELD_KEYS + MAX_HELD_MOUSE_BUTTONS);244});245246test('ordinary releases retain the original forwarded key value', () => {247 const state = new HeldInputState();248 state.trackKey('Digit1', '!');249 assert.deepEqual(state.takeKeyRelease('Digit1'), { t: 'ku', k: '!' });250 assert.equal(state.takeKeyRelease('Digit1'), null);251});252253test('extended physical keys suppress repeats and retain exact neutralization tokens', () => {254 const state = new HeldInputState();255 assert.equal(state.trackKey('F12', 'F12'), 'tracked');256 assert.equal(state.trackKey('F12', 'F12'), 'repeat');257 assert.equal(state.trackKey('IntlBackslash', 'IntlBackslash'), 'tracked');258 assert.equal(state.trackKey('AltRight', 'AltRight'), 'tracked');259 assert.deepEqual(state.releaseEvents(), [260 { t: 'ku', k: 'F12' },261 { t: 'ku', k: 'IntlBackslash' },262 { t: 'ku', k: 'AltRight' },263 ]);264 assert.deepEqual(state.takeKeyRelease('F12'), { t: 'ku', k: 'F12' });265 assert.equal(state.takeKeyRelease('F12'), null);266});267268test('relative motion coalesces small displacement into one bounded event', () => {269 const motion = new RelativePointerAccumulator();270 assert.equal(motion.add(2.4, -3.6), true);271 assert.equal(motion.add(5, 2), true);272 assert.equal(motion.pending, true);273 assert.deepEqual(motion.take(), { t: 'mr', dx: 7, dy: -2 });274 assert.equal(motion.pending, false);275 assert.equal(motion.take(), null);276});277278test('relative motion emits bounded chunks without losing accumulated displacement', () => {279 const motion = new RelativePointerAccumulator();280 assert.equal(motion.add(Number.NaN, Number.POSITIVE_INFINITY), false);281 assert.equal(motion.add(MAX_RELATIVE_POINTER_DELTA, -MAX_RELATIVE_POINTER_DELTA), true);282 assert.equal(motion.add(500, -500), true);283 assert.equal(motion.chunkCount, 2);284 assert.deepEqual(motion.take(), {285 t: 'mr',286 dx: MAX_RELATIVE_POINTER_DELTA,287 dy: -MAX_RELATIVE_POINTER_DELTA,288 });289 assert.deepEqual(motion.take(), { t: 'mr', dx: 500, dy: -500 });290 assert.equal(motion.take(), null);291});292293test('one wide browser sample is preserved across protocol-sized chunks', () => {294 const motion = new RelativePointerAccumulator();295 assert.equal(motion.add(50_000, -50_000), true);296 assert.equal(motion.chunkCount, 2);297 assert.deepEqual(motion.take(), {298 t: 'mr',299 dx: MAX_RELATIVE_POINTER_DELTA,300 dy: -MAX_RELATIVE_POINTER_DELTA,301 });302 assert.deepEqual(motion.take(), { t: 'mr', dx: 17_233, dy: -17_233 });303 assert.equal(motion.take(), null);304});305306test('relative motion restore preserves a rejected in-flight displacement', () => {307 const motion = new RelativePointerAccumulator();308 assert.equal(motion.restore({ t: 'mm', x: 1, y: 2 }), false);309 assert.equal(motion.restore({ t: 'mr', dx: -9, dy: 4 }), true);310 motion.add(3, 2);311 assert.deepEqual(motion.take(), { t: 'mr', dx: -6, dy: 6 });312});313314test('rejected wide motion stays before a click queued during its invoke', () => {315 const motion = new PointerMotionBuffer();316 const reliable = [];317 motion.addRelative(50_000, 0);318 const rejected = motion.take();319 reliable.push(...motion.takeBarrierBefore({ t: 'md', b: 1 }));320321 assert.equal(322 restoreRejectedPointerMotion(reliable, motion, rejected, 128),323 'queued',324 );325 assert.deepEqual(reliable, [326 { t: 'mr', dx: MAX_RELATIVE_POINTER_DELTA, dy: 0 },327 { t: 'mr', dx: 17_233, dy: 0 },328 { t: 'md', b: 1 },329 ]);330});331332test('rejected older absolute motion cannot replace a newer latest position', () => {333 const motion = new PointerMotionBuffer();334 motion.setAbsolute({ t: 'mm', x: 30, y: 40 });335 assert.equal(336 restoreRejectedPointerMotion(337 [],338 motion,339 { t: 'mm', x: 10, y: 20 },340 128,341 ),342 'superseded',343 );344 assert.deepEqual(motion.take(), { t: 'mm', x: 30, y: 40 });345});346347test('pointer motion barrier stays immediately ahead of a button under a busy pump', () => {348 const motion = new PointerMotionBuffer();349 motion.addRelative(4, -2);350 motion.addRelative(3, 1);351 assert.equal(motion.barrierLength, 1);352 assert.deepEqual(motion.takeBarrierBefore({ t: 'md', b: 1 }), [353 { t: 'mr', dx: 7, dy: -1 },354 { t: 'md', b: 1 },355 ]);356 assert.equal(motion.pending, false);357});358359test('pointer motion barrier drains every bounded relative chunk before a button', () => {360 const motion = new PointerMotionBuffer();361 motion.addRelative(MAX_RELATIVE_POINTER_DELTA, -MAX_RELATIVE_POINTER_DELTA);362 motion.addRelative(9, -11);363 assert.equal(motion.barrierLength, 2);364 assert.deepEqual(motion.takeBarrierBefore({ t: 'md', b: 1 }), [365 {366 t: 'mr',367 dx: MAX_RELATIVE_POINTER_DELTA,368 dy: -MAX_RELATIVE_POINTER_DELTA,369 },370 { t: 'mr', dx: 9, dy: -11 },371 { t: 'md', b: 1 },372 ]);373 assert.equal(motion.pending, false);374});375376test('absolute fallback also forms a latest-position barrier before click', () => {377 const motion = new PointerMotionBuffer();378 motion.setAbsolute({ t: 'mm', x: 10, y: 20 });379 motion.setAbsolute({ t: 'mm', x: 30, y: 40 });380 assert.deepEqual(motion.takeBarrierBefore({ t: 'mc', b: 1 }), [381 { t: 'mm', x: 30, y: 40 },382 { t: 'mc', b: 1 },383 ]);384});