Punkt 2 implementieren GitHub CoPilot
This commit is contained in:
@@ -79,8 +79,73 @@ describe('InfoServer', () => {
|
||||
expect(status.lastCommands).toEqual(['G1 X10 Y10']);
|
||||
expect(status.lastPings).toEqual(['Ping']);
|
||||
expect(status.senders).toEqual([
|
||||
{ name: 'Base', status: 'connected' },
|
||||
{ name: 'Hand', status: 'disconnected' }
|
||||
{
|
||||
name: 'Base',
|
||||
state: 'connected',
|
||||
url: null,
|
||||
isTestMode: false,
|
||||
error: null,
|
||||
reconnectAttempt: 0,
|
||||
reconnectTimer: false,
|
||||
health: 'ok',
|
||||
reason: undefined
|
||||
},
|
||||
{
|
||||
name: 'Hand',
|
||||
state: 'disconnected',
|
||||
url: null,
|
||||
isTestMode: false,
|
||||
error: null,
|
||||
reconnectAttempt: 0,
|
||||
reconnectTimer: false,
|
||||
health: 'disconnected',
|
||||
reason: 'no active socket connection'
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
test('returns sender health details from instance.getStatus()', async () => {
|
||||
const key = fs.readFileSync('https/localhost.key');
|
||||
const cert = fs.readFileSync('https/localhost.pem');
|
||||
const httpsOptions = { key, cert, passphrase: 'abcd' };
|
||||
|
||||
const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] };
|
||||
const robot = { x: 0, y: 0, z: 0, phi: 0, theta: 0, psi: 0 };
|
||||
const senders = [
|
||||
{
|
||||
name: 'Reconnect',
|
||||
instance: {
|
||||
getStatus: () => ({
|
||||
state: 'reconnecting',
|
||||
url: 'reconnect.test',
|
||||
error: 'timeout',
|
||||
isTestMode: false,
|
||||
reconnectAttempt: 2,
|
||||
reconnectTimer: true
|
||||
})
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
server = createInfoServer(httpsOptions, sharedState, robot, GCode, senders);
|
||||
port = await listen(server);
|
||||
|
||||
const { statusCode, body } = await request(`https://127.0.0.1:${port}/api/status`);
|
||||
expect(statusCode).toBe(200);
|
||||
|
||||
const status = JSON.parse(body);
|
||||
expect(status.senders).toEqual([
|
||||
{
|
||||
name: 'Reconnect',
|
||||
state: 'reconnecting',
|
||||
url: 'reconnect.test',
|
||||
isTestMode: false,
|
||||
error: 'timeout',
|
||||
reconnectAttempt: 2,
|
||||
reconnectTimer: true,
|
||||
health: 'warning',
|
||||
reason: undefined
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -93,4 +93,24 @@ describe('InputWS', () => {
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
test('receives GCode text and broadcasts updated position', async () => {
|
||||
server = http.createServer();
|
||||
const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] };
|
||||
const robot = createDummyRobot();
|
||||
|
||||
wss = initInputWS(server, robot, GCode, sharedState);
|
||||
port = await listen(server);
|
||||
const client = await connectWebSocket(port);
|
||||
|
||||
const messagePromise = waitForMessage(client);
|
||||
client.send('G1 X1 Y2 Z3');
|
||||
|
||||
const message = await messagePromise;
|
||||
const parsed = JSON.parse(message);
|
||||
expect(parsed.position).toEqual({ x: 1, y: 2, z: 3, a: 0, b: 0, c: 0 });
|
||||
expect(robot.sendCommand).toHaveBeenCalled();
|
||||
|
||||
client.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,76 +1,72 @@
|
||||
var Sender = require('../robot/WSSenderGrbl.js')
|
||||
|
||||
|
||||
describe("WS-SenderGRBL.execCommand", () => {
|
||||
|
||||
test("writes correct G-code to mocked WS tSocket", () => {
|
||||
|
||||
// Create instance (will try real connection, but we override tSocket immediately)
|
||||
const sender = new Sender(urlGRBL = "test.test", maxSpeedF = 2300, xAxisGrbl = "x", yAxisGrbl = "y", zAxisGrbl = "z");
|
||||
|
||||
// Mock tSocket.write
|
||||
sender.tSocket = {
|
||||
written: "",
|
||||
write: function(txt) {
|
||||
this.written = txt; // store what was written
|
||||
}
|
||||
};
|
||||
|
||||
// Provide some sample motion data
|
||||
const mOld = { x: 0, y: 0, z: 0, a:0, b:0, c:0, e:0 }; // not used in your code
|
||||
const mNew = { x: 12.34, y: 1.0, z: 2.0, a:0, b:0, c:0, e:0 };
|
||||
|
||||
sender.execCommand("G1", mOld, mNew);
|
||||
|
||||
// ✅ verify output
|
||||
expect(sender.tSocket.written).toBe("G90 G1 x12.34 y57.30 z57.30 f2300.00\r\n");
|
||||
});
|
||||
|
||||
|
||||
test("writes correct G-code to mocked tSocket Ellbow", () => {
|
||||
|
||||
// Create instance (will try real connection, but we override tSocket immediately)
|
||||
const sender = new Sender(urlGRBL = "test.test", maxSpeedF = 2300, xAxisGrbl = "a", yAxisGrbl = null, zAxisGrbl = null );
|
||||
|
||||
// Mock tSocket.write
|
||||
sender.tSocket = {
|
||||
written: "",
|
||||
write: function(txt) {
|
||||
this.written = txt; // store what was written
|
||||
}
|
||||
};
|
||||
|
||||
// Provide some sample motion data
|
||||
const mOld = { x: 0, y: 0, z: 0, a:Math.PI, b:0, c:0, e:0 }; // not used in your code
|
||||
const mNew = { x: 12.34, y: Math.PI/2, z: 0, a:Math.PI/8, b:0, c:0, e:0 };
|
||||
|
||||
sender.execCommand("G1", mOld, mNew);
|
||||
|
||||
// ✅ verify output
|
||||
expect(sender.tSocket.written).toBe("G90 G1 x22.50 f2300.00\r\n");
|
||||
});
|
||||
|
||||
test("writes correct G-code G92 to mocked WS tSocket", () => {
|
||||
|
||||
// Create instance (will try real connection, but we override tSocket immediately)
|
||||
const sender = new Sender(urlGRBL = "test.test", maxSpeedF = 2300, xAxisGrbl = "x", yAxisGrbl = "y", zAxisGrbl = "z");
|
||||
|
||||
// Mock tSocket.write
|
||||
sender.tSocket = {
|
||||
written: "",
|
||||
write: function(txt) {
|
||||
this.written = txt; // store what was written
|
||||
}
|
||||
};
|
||||
|
||||
// Provide some sample motion data
|
||||
const mOld = { x: 0, y: 0, z: 0, a:0, b:0, c:0, e:0 }; // not used in your code
|
||||
const mNew = { x: 12.34, y: 1.0, z: 2.0, a:0, b:0, c:0, e:0 };
|
||||
|
||||
sender.execCommand("G92", mOld, mNew);
|
||||
|
||||
// ✅ verify output
|
||||
expect(sender.tSocket.written.replace("G90 ","")).toBe("G92 x12.34 y57.30 z57.30 f2300.00\r\n");
|
||||
});
|
||||
|
||||
});
|
||||
const WSSenderGrbl = require('../robot/WSSenderGrbl.js');
|
||||
|
||||
describe("WSSenderGrbl implements SenderInterface", () => {
|
||||
|
||||
test("is an instance of SenderInterface and exposes required methods", () => {
|
||||
const SenderInterface = require('../robot/SenderInterface');
|
||||
const sender = new WSSenderGrbl("test.test", 2300, "x", "y", "z");
|
||||
|
||||
expect(sender).toBeInstanceOf(SenderInterface);
|
||||
expect(typeof sender.connect).toBe('function');
|
||||
expect(typeof sender.send).toBe('function');
|
||||
expect(typeof sender.getStatus).toBe('function');
|
||||
expect(typeof sender.disconnect).toBe('function');
|
||||
});
|
||||
|
||||
test("test mode has connected status", () => {
|
||||
const sender = new WSSenderGrbl("test.test", 2300, "x", "y", "z");
|
||||
expect(sender.getStatus()).toMatchObject({ state: 'connected', isTestMode: true });
|
||||
});
|
||||
|
||||
test("send() returns false when not connected, true in test mode", () => {
|
||||
const sender = new WSSenderGrbl("test.test", 2300, "x", "y", "z");
|
||||
expect(sender.send("G1 x1")).toBe(true);
|
||||
|
||||
sender.ws = null;
|
||||
expect(sender.send("G1 x1")).toBe(false);
|
||||
});
|
||||
|
||||
test("disconnect() transitions to disconnected state", () => {
|
||||
const sender = new WSSenderGrbl("test.test", 2300, "x", "y", "z");
|
||||
sender.disconnect();
|
||||
expect(sender.getStatus()).toMatchObject({ state: 'disconnected' });
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("WSSenderGrbl.execCommand writes correct G-code via send()", () => {
|
||||
|
||||
test("writes correct G-code for xyz axes", () => {
|
||||
const sender = new WSSenderGrbl("test.test", 2300, "x", "y", "z");
|
||||
|
||||
const mOld = { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, e: 0 };
|
||||
const mNew = { x: 12.34, y: 1.0, z: 2.0, a: 0, b: 0, c: 0, e: 0 };
|
||||
|
||||
sender.execCommand("G1", mOld, mNew);
|
||||
|
||||
expect(sender.ws.written).toBe("G90 G1 x12.34 y57.30 z57.30 f2300.00\n");
|
||||
});
|
||||
|
||||
test("writes correct G-code for elbow (a axis)", () => {
|
||||
const sender = new WSSenderGrbl("test.test", 2300, "a", null, null);
|
||||
|
||||
const mOld = { x: 0, y: 0, z: 0, a: Math.PI, b: 0, c: 0, e: 0 };
|
||||
const mNew = { x: 12.34, y: Math.PI / 2, z: 0, a: Math.PI / 8, b: 0, c: 0, e: 0 };
|
||||
|
||||
sender.execCommand("G1", mOld, mNew);
|
||||
|
||||
expect(sender.ws.written).toBe("G90 G1 x22.50 f2300.00\n");
|
||||
});
|
||||
|
||||
test("G92 command is sent without extra G90 prefix", () => {
|
||||
const sender = new WSSenderGrbl("test.test", 2300, "x", "y", "z");
|
||||
|
||||
const mOld = { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, e: 0 };
|
||||
const mNew = { x: 12.34, y: 1.0, z: 2.0, a: 0, b: 0, c: 0, e: 0 };
|
||||
|
||||
sender.execCommand("G92", mOld, mNew);
|
||||
|
||||
expect(sender.ws.written.replace("G90 ", "")).toBe("G92 x12.34 y57.30 z57.30 f2300.00\n");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
103
test/SenderInterface.test.js
Normal file
103
test/SenderInterface.test.js
Normal file
@@ -0,0 +1,103 @@
|
||||
const EventEmitter = require('events');
|
||||
const SenderInterface = require('../robot/SenderInterface');
|
||||
const TelnetSender = require('../robot/TelnetSenderGRBL');
|
||||
|
||||
describe('Sender Interface and TelnetSenderGRBL implementation', () => {
|
||||
test('TelnetSenderGRBL implements the required sender interface methods', () => {
|
||||
const sender = new TelnetSender('test.test', 2300, 'x', 'y', 'z');
|
||||
|
||||
expect(sender).toBeInstanceOf(SenderInterface);
|
||||
expect(typeof sender.connect).toBe('function');
|
||||
expect(typeof sender.send).toBe('function');
|
||||
expect(typeof sender.getStatus).toBe('function');
|
||||
expect(typeof sender.disconnect).toBe('function');
|
||||
});
|
||||
|
||||
test('test mode sender rejects invalid send payloads and supports idempotent disconnect', () => {
|
||||
const sender = new TelnetSender('test.test', 2300, 'x', 'y', 'z');
|
||||
|
||||
expect(sender.getStatus()).toMatchObject({ state: 'connected' });
|
||||
|
||||
sender.tSocket = null;
|
||||
expect(sender.send('HELLO')).toBe(false);
|
||||
expect(sender.send('')).toBe(false);
|
||||
|
||||
sender.disconnect();
|
||||
expect(sender.getStatus()).toMatchObject({ state: 'disconnected' });
|
||||
|
||||
sender.disconnect();
|
||||
expect(sender.getStatus()).toMatchObject({ state: 'disconnected' });
|
||||
});
|
||||
|
||||
test('sender reconnects on connection failure and eventually connects', async () => {
|
||||
let connectAttempts = 0;
|
||||
const netMock = {
|
||||
createConnection: () => {
|
||||
const socket = new EventEmitter();
|
||||
socket.end = jest.fn();
|
||||
|
||||
process.nextTick(() => {
|
||||
connectAttempts += 1;
|
||||
if (connectAttempts === 1) {
|
||||
socket.emit('error', new Error('connection failed'));
|
||||
} else {
|
||||
socket.emit('connect');
|
||||
}
|
||||
});
|
||||
|
||||
return socket;
|
||||
}
|
||||
};
|
||||
|
||||
class DummyTelnetSocket {
|
||||
constructor(connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
on(event, listener) {
|
||||
this.connection.on(event, listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
write(txt) {
|
||||
this.connection.written = txt;
|
||||
}
|
||||
}
|
||||
|
||||
const sender = new TelnetSender('localhost', 2300, 'x', 'y', 'z', null, null, null, null, {
|
||||
netModule: netMock,
|
||||
TelnetSocketClass: DummyTelnetSocket,
|
||||
setTimeoutFn: (fn) => fn(),
|
||||
autoConnect: false,
|
||||
reconnectDelay: 1,
|
||||
maxReconnectDelay: 2
|
||||
});
|
||||
|
||||
const result = await sender.connect();
|
||||
expect(connectAttempts).toBe(2);
|
||||
expect(result.getStatus()).toMatchObject({
|
||||
state: 'connected',
|
||||
url: 'localhost'
|
||||
});
|
||||
expect(result.getStatus().reconnectTimer).toBe(false);
|
||||
});
|
||||
|
||||
test('test mode sender has connected status and can send/disconnect', async () => {
|
||||
const sender = new TelnetSender('test.test', 2300, 'x', 'y', 'z');
|
||||
|
||||
expect(sender.getStatus()).toMatchObject({
|
||||
state: 'connected',
|
||||
url: 'test.test',
|
||||
isTestMode: true
|
||||
});
|
||||
|
||||
expect(sender.send('HELLO')).toBe(true);
|
||||
expect(sender.tSocket.written).toBe('HELLO\r\n');
|
||||
|
||||
sender.disconnect();
|
||||
expect(sender.getStatus()).toMatchObject({ state: 'disconnected' });
|
||||
|
||||
await expect(sender.connect()).resolves.toBe(sender);
|
||||
expect(sender.getStatus()).toMatchObject({ state: 'connected' });
|
||||
});
|
||||
});
|
||||
114
test/StartRobot.test.js
Normal file
114
test/StartRobot.test.js
Normal file
@@ -0,0 +1,114 @@
|
||||
const { createApp } = require('../startRobot');
|
||||
|
||||
describe('startRobot orchestrator', () => {
|
||||
test('creates HTTPS and info servers and binds modules', () => {
|
||||
const readFileSync = jest.fn()
|
||||
.mockReturnValueOnce('fake-key')
|
||||
.mockReturnValueOnce('fake-cert');
|
||||
|
||||
const httpsServerMock = {
|
||||
listen: jest.fn()
|
||||
};
|
||||
|
||||
const httpsModuleMock = {
|
||||
createServer: jest.fn(() => httpsServerMock)
|
||||
};
|
||||
|
||||
const initInputWS = jest.fn();
|
||||
const infoServerMock = {
|
||||
listen: jest.fn()
|
||||
};
|
||||
const createInfoServer = jest.fn(() => infoServerMock);
|
||||
|
||||
const TelnetSenderClass = jest.fn(() => ({ tSocket: null }));
|
||||
|
||||
const robotInstances = [];
|
||||
class RobotClass {
|
||||
constructor() {
|
||||
this.cmdReceivers = [];
|
||||
robotInstances.push(this);
|
||||
}
|
||||
}
|
||||
|
||||
const result = createApp({
|
||||
fsModule: { readFileSync },
|
||||
httpsModule: httpsModuleMock,
|
||||
processEnv: {},
|
||||
RobotClass,
|
||||
GCodeModule: { dummy: true },
|
||||
TelnetSenderClass,
|
||||
initInputWSFn: initInputWS,
|
||||
createInfoServerFn: createInfoServer,
|
||||
setTimeoutFn: (fn) => fn(),
|
||||
consoleObj: { log: jest.fn(), warn: jest.fn(), error: jest.fn() }
|
||||
});
|
||||
|
||||
expect(readFileSync).toHaveBeenCalledTimes(2);
|
||||
expect(httpsModuleMock.createServer).toHaveBeenCalledWith({
|
||||
enable: true,
|
||||
key: 'fake-key',
|
||||
cert: 'fake-cert',
|
||||
passphrase: 'abcd'
|
||||
});
|
||||
|
||||
expect(initInputWS).toHaveBeenCalledWith(httpsServerMock, expect.any(RobotClass), { dummy: true }, expect.any(Object));
|
||||
expect(createInfoServer).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ key: 'fake-key', cert: 'fake-cert', passphrase: 'abcd' }),
|
||||
expect.any(Object),
|
||||
expect.any(RobotClass),
|
||||
{ dummy: true },
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ name: 'Base', instance: expect.any(Object) }),
|
||||
expect.objectContaining({ name: 'Elbow', instance: expect.any(Object) }),
|
||||
expect.objectContaining({ name: 'Hand', instance: expect.any(Object) })
|
||||
])
|
||||
);
|
||||
|
||||
expect(httpsServerMock.listen).toHaveBeenCalledWith(2095);
|
||||
expect(infoServerMock.listen).toHaveBeenCalledWith(2098);
|
||||
|
||||
expect(result).toHaveProperty('httpsServer', httpsServerMock);
|
||||
expect(result).toHaveProperty('infoServer', infoServerMock);
|
||||
expect(result).toHaveProperty('senders');
|
||||
expect(result.senders).toHaveLength(3);
|
||||
expect(result.startupStatus).toEqual({
|
||||
https: { ok: true },
|
||||
senders: [
|
||||
{ name: 'Base', status: 'disconnected', reason: 'no active socket connection' },
|
||||
{ name: 'Elbow', status: 'disconnected', reason: 'no active socket connection' },
|
||||
{ name: 'Hand', status: 'disconnected', reason: 'no active socket connection' }
|
||||
]
|
||||
});
|
||||
expect(result.sharedState.connectedClients).toEqual([]);
|
||||
});
|
||||
|
||||
test('reports missing HTTPS certificates on startup', () => {
|
||||
const readFileSync = jest.fn().mockImplementation(() => {
|
||||
throw new Error('ENOENT: no such file or directory');
|
||||
});
|
||||
|
||||
const httpsModuleMock = {
|
||||
createServer: jest.fn()
|
||||
};
|
||||
|
||||
const result = createApp({
|
||||
fsModule: { readFileSync },
|
||||
httpsModule: httpsModuleMock,
|
||||
processEnv: {},
|
||||
RobotClass: class {},
|
||||
GCodeModule: { dummy: true },
|
||||
TelnetSenderClass: jest.fn(),
|
||||
initInputWSFn: jest.fn(),
|
||||
createInfoServerFn: jest.fn(),
|
||||
setTimeoutFn: jest.fn(),
|
||||
consoleObj: { log: jest.fn(), error: jest.fn(), warn: jest.fn() }
|
||||
});
|
||||
|
||||
expect(result.startupStatus.https.ok).toBe(false);
|
||||
expect(result.startupStatus.https.error).toMatch(/Failed to load HTTPS certificate\/key/);
|
||||
expect(httpsModuleMock.createServer).not.toHaveBeenCalled();
|
||||
expect(result.httpsServer).toBeUndefined();
|
||||
expect(result.infoServer).toBeUndefined();
|
||||
expect(result.senders).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user