const http = require('http'); const WebSocket = require('ws'); const initInputWS = require('../server/InputWS'); const GCode = require('../robot/GCode'); const createDummyRobot = require('./helpers/createDummyRobot'); /* ---------- helpers ---------- */ function listen(server) { return new Promise((resolve, reject) => { server.listen(0, () => { const address = server.address(); if (address && address.port) resolve(address.port); else reject(new Error('Failed to get server port')); }); server.on('error', reject); }); } function connect(port) { return new Promise((resolve, reject) => { const ws = new WebSocket(`ws://127.0.0.1:${port}`); ws.on('open', () => resolve(ws)); ws.on('error', reject); }); } function nextMessage(ws) { return new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error('Timeout waiting for message')), 2000); ws.once('message', (data) => { clearTimeout(timer); resolve(data.toString()); }); }); } /** Resolves to true if NO message arrives within `ms`, false otherwise. */ function expectSilence(ws, ms = 200) { return new Promise((resolve) => { let gotMessage = false; const onMessage = () => { gotMessage = true; }; ws.on('message', onMessage); setTimeout(() => { ws.off('message', onMessage); resolve(!gotMessage); }, ms); }); } /* ---------- tests ---------- */ describe('InputWS API response routing', () => { let server; function startServer(robot, gcode = GCode) { server = http.createServer(); const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] }; initInputWS(server, robot, gcode, sharedState); return { sharedState }; } afterEach(async () => { if (server) { await new Promise((resolve) => server.close(resolve)); server = null; } }); test('Ping is answered to the requester only (not broadcast)', async () => { startServer(createDummyRobot()); const port = await listen(server); const a = await connect(port); const b = await connect(port); const aReply = nextMessage(a); const bSilent = expectSilence(b); a.send('Ping'); expect(await aReply).toBe('Ping'); expect(await bSilent).toBe(true); a.close(); b.close(); }); test('M114 status query is answered to the requester only', async () => { const robot = createDummyRobot(); robot.x = 5; robot.y = 6; robot.z = 7; startServer(robot); const port = await listen(server); const a = await connect(port); const b = await connect(port); const aReply = nextMessage(a); const bSilent = expectSilence(b); a.send('M114'); const parsed = JSON.parse(await aReply); expect(parsed.position).toEqual({ x: 5, y: 6, z: 7, a: 0, b: 0, c: 0 }); expect(await bSilent).toBe(true); a.close(); b.close(); }); test('G-code motion command broadcasts the new position to all clients', async () => { const robot = createDummyRobot(); startServer(robot); const port = await listen(server); const a = await connect(port); const b = await connect(port); const aReply = nextMessage(a); const bReply = nextMessage(b); a.send('G1 X1 Y2 Z3'); const aParsed = JSON.parse(await aReply); const bParsed = JSON.parse(await bReply); expect(aParsed.position).toEqual({ x: 1, y: 2, z: 3, a: 0, b: 0, c: 0 }); expect(bParsed.position).toEqual({ x: 1, y: 2, z: 3, a: 0, b: 0, c: 0 }); expect(robot.sendCommand).toHaveBeenCalled(); a.close(); b.close(); }); test('unknown input returns a machine-readable error to the sender only', async () => { startServer(createDummyRobot()); const port = await listen(server); const a = await connect(port); const b = await connect(port); const aReply = nextMessage(a); const bSilent = expectSilence(b); a.send('TOTALLY_UNKNOWN'); const err = JSON.parse(await aReply); expect(err).toEqual({ type: 'error', code: 'UNKNOWN_COMMAND', message: 'Unrecognized input', input: 'TOTALLY_UNKNOWN' }); expect(await bSilent).toBe(true); a.close(); b.close(); }); test('G-code processing errors are reported as a machine-readable error', async () => { const throwingGCode = { containsCommand: () => true, ContainsFilesCommand: () => false, receiveGCode: () => { throw new Error('boom'); }, getM114: () => '{}' }; startServer(createDummyRobot(), throwingGCode); const port = await listen(server); const a = await connect(port); const aReply = nextMessage(a); a.send('G1 X1'); const err = JSON.parse(await aReply); expect(err).toMatchObject({ type: 'error', code: 'GCODE_ERROR', message: 'boom', input: 'G1 X1' }); a.close(); }); });