185 lines
4.8 KiB
JavaScript
185 lines
4.8 KiB
JavaScript
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();
|
|
});
|
|
});
|