const assert = require('node:assert/strict'); const fs = require('node:fs'); const vm = require('node:vm'); const path = require('node:path'); const html = fs.readFileSync(path.join(__dirname, '../src/renderer/public/slam-viewer/index.html'), 'utf8'); const code = html.slice(html.indexOf('let maxLinear ='), html.indexOf('// A static Gazebo model')); function create(real = true, yahboom = true, mkmini = !yahboom) { const handlers = {}, messages = [], intervals = new Map(); let id = 0; const context = vm.createContext({ REAL_RUNTIME: real, USE_YAHBOOM_MODEL: yahboom, REAL_TELEOP: real && (yahboom || mkmini), REAL_MKMINI_TELEOP: real && mkmini, EDITOR_MODE: false, USE_MKMINI_MODEL: mkmini, DIRECT_BRIDGE: true, latestSlamState: {teleop_deadman: true}, latestSlamStateReceivedAt: Date.now(), controlSocket: {readyState: 1, bufferedAmount: 0, send: data => messages.push(JSON.parse(data)), close() {this.readyState = 3;}}, WebSocket: {OPEN: 1}, document: {hidden: false, hasFocus: () => true, addEventListener: (name, fn) => handlers[name] = fn}, window: {addEventListener: (name, fn) => handlers[name] = fn, setInterval: fn => {intervals.set(++id, fn); return id;}, clearInterval: id => intervals.delete(id), setTimeout: () => ++id, clearTimeout: () => {}}, console, Date, }); vm.runInContext(code, context); const key = (type, value, extra = {}) => handlers[type]({key: value, target: {tagName: 'CANVAS'}, preventDefault() {}, ...extra}); return {context, handlers, messages, intervals, key}; } let t = create(); t.key('keydown', 'i'); assert.equal(t.messages.at(-1).linear, 0.1); assert.equal(t.messages.at(-1).deadman, true); t.key('keyup', 'i', {target: {tagName: 'INPUT'}}); assert.equal(t.messages.at(-1).linear, 0); assert.equal(t.intervals.size, 0); for (const key of ['j', 'l', ',', 'u', 'o', 'm', '.']) { t.key('keydown', key); t.key('keyup', key); assert.equal(t.messages.at(-1).linear, 0); assert.equal(t.messages.at(-1).angular, 0); } t.key('keydown', 'i'); t.handlers.blur(); assert.equal(t.messages.at(-1).linear, 0); t.key('keydown', 'i'); t.context.document.hidden = true; t.handlers.visibilitychange(); assert.equal(t.messages.at(-1).linear, 0); t = create(); t.key('keydown', 'i'); t.context.latestSlamStateReceivedAt = 0; for (const tick of t.intervals.values()) tick(); assert.equal(t.messages.at(-1).linear, 0); assert.equal(t.intervals.size, 0); t = create(); t.context.latestSlamState.teleop_deadman = false; t.key('keydown', 'i'); assert.equal(t.messages.length, 0); t = create(true, false, false); t.key('keydown', 'i'); assert.equal(t.messages.length, 0); for (const [forward, left, reverse] of [['i', 'j', ','], ['ArrowUp', 'ArrowLeft', 'ArrowDown']]) { t = create(true, false); t.key('keydown', left); assert.equal(t.messages.at(-1).linear, 0); assert.equal(t.messages.at(-1).angular, 0); t.key('keydown', forward); assert.equal(t.messages.at(-1).linear, 0.1); assert.equal(t.messages.at(-1).angular, 0.08); assert.equal(t.messages.at(-1).deadman, true); t.key('keyup', left, {target: {tagName: 'INPUT'}}); assert.equal(t.messages.at(-1).linear, 0); assert.equal(t.intervals.size, 0); t.key('keydown', reverse); t.key('keydown', 'l'); assert.equal(t.messages.at(-1).linear, -0.1); assert.equal(t.messages.at(-1).angular, -0.08); t.key('keydown', ' '); assert.equal(t.messages.at(-1).linear, 0); assert.equal(t.messages.at(-1).angular, 0); } for (const event of ['blur', 'pagehide', 'visibilitychange']) { t = create(true, false); t.key('keydown', 'i'); if (event === 'visibilitychange') t.context.document.hidden = true; t.handlers[event](); assert.equal(t.messages.at(-1).linear, 0); assert.equal(t.intervals.size, 0); } t = create(true, false); t.key('keydown', 'q'); t.key('keydown', 'i'); assert.equal(t.messages.at(-1).linear, 0.1); t.context.latestSlamStateReceivedAt = 0; for (const tick of t.intervals.values()) tick(); assert.equal(t.messages.at(-1).linear, 0); t = create(true, false); t.context.latestSlamState.teleop_deadman = false; t.key('keydown', 'i'); assert.equal(t.messages.length, 0); t = create(true, false); t.context.controlSocket.bufferedAmount = 1; t.key('keydown', 'i'); assert.equal(t.messages.length, 0); assert.equal(t.context.controlSocket.readyState, 3); t = create(true, false); t.context.EDITOR_MODE = true; t.key('keydown', 'i'); assert.equal(t.messages.length, 0); t = create(false); t.key('keydown', 'i'); t.key('keyup', 'i'); assert.equal(t.messages.at(-1).linear, 0.22); assert.equal(t.intervals.size, 1); console.log('PASS: Yahboom/MK-mini deadman, combined steering, arrows/reverse, release/focus/stale-state stops, speed cap, legacy/read-only blocking, simulation unchanged');