'use strict'; // Tests für ToDo_9 Paket 1/3: // Paket 1 — eingehende GRBL/FluidNC-Antworten lesen und klassifizieren // Paket 3 — opt-in Auto-Report-Konfiguration ($10=3, $Report/Interval) const { EventEmitter } = require('events'); const TelnetSenderGRBL = require('../robot/TelnetSenderGRBL'); // ── Hilfsfunktionen ────────────────────────────────────────────────────────── function makeRawSocket() { const em = new EventEmitter(); return Object.assign(em, { write: jest.fn(), end: jest.fn(), destroy: jest.fn(), setKeepAlive: jest.fn(), }); } function makeTelnetSocket() { const em = new EventEmitter(); return Object.assign(em, { write: jest.fn(), end: jest.fn(), destroy: jest.fn(), }); } /** Sender mit gemockten Abhängigkeiten; simuliert erfolgreichen Verbindungsaufbau. */ function setup(extraOptions = {}) { const rawSocket = makeRawSocket(); const telnetSock = makeTelnetSocket(); const sender = new TelnetSenderGRBL( 'robot.local', 5000, 'x', 'y', 'z', null, null, null, null, { netModule: { createConnection: jest.fn(() => rawSocket) }, TelnetSocketClass: jest.fn(() => telnetSock), setIntervalFn: jest.fn(() => 1), clearIntervalFn: jest.fn(), setTimeoutFn: jest.fn(() => 99), clearTimeoutFn: jest.fn(), autoConnect: false, ...extraOptions, } ); sender.connect(); rawSocket.emit('connect'); /** Simuliert vom Controller eingehende Bytes. */ const recv = (str) => telnetSock.emit('data', Buffer.from(str, 'utf8')); return { sender, rawSocket, telnetSock, recv }; } // ── Tests ──────────────────────────────────────────────────────────────────── describe('TelnetSenderGRBL — Antworten lesen (ToDo_9 Paket 1)', () => { beforeAll(() => { jest.spyOn(console, 'log').mockImplementation(() => {}); }); afterAll(() => { jest.restoreAllMocks(); }); test('parst Statusreport: State, MPos und Bf', () => { const { sender, recv } = setup(); recv('\r\n'); expect(sender.grblState).toBe('Idle'); expect(sender.machinePosition).toEqual([151.0, 149.0, -1.0]); expect(sender.machinePositionType).toBe('MPos'); expect(sender.plannerBlocksFree).toBe(15); expect(sender.rxBytesFree).toBe(128); }); test('parst Run-Zustand', () => { const { sender, recv } = setup(); recv('\r\n'); expect(sender.grblState).toBe('Run'); expect(sender.machinePosition).toEqual([1, 2, 3]); }); test('nutzt WPos, wenn kein MPos vorhanden', () => { const { sender, recv } = setup(); recv('\r\n'); expect(sender.machinePosition).toEqual([10.5, 20.5, 30.5]); expect(sender.machinePositionType).toBe('WPos'); }); test('puffert über fragmentierte data-Events', () => { const { sender, recv } = setup(); recv('\r\n'); expect(sender.machinePosition).toEqual([1.0, 2.0, 3.0]); expect(sender.plannerBlocksFree).toBe(10); }); test('verarbeitet mehrere Zeilen in einem Chunk', () => { const { sender, recv } = setup(); recv('ok\r\nok\r\n\r\n'); expect(sender.grblState).toBe('Idle'); expect(sender.lastResponse).toBe(''); expect(sender.lastOk).toBeGreaterThan(0); }); test('erkennt "ok" und setzt lastOk', () => { const { sender, recv } = setup(); expect(sender.lastOk).toBe(0); recv('ok\r\n'); expect(sender.lastOk).toBeGreaterThan(0); }); test('erkennt error:-Zeile und legt sie in lastError ab', () => { const { sender, recv } = setup(); recv('error:9\r\n'); expect(sender.lastError).toBe('error:9'); }); test('erkennt ALARM-Zeile', () => { const { sender, recv } = setup(); recv('ALARM:1\r\n'); expect(sender.lastError).toBe('ALARM:1'); }); test('ignoriert fremde/unbekannte Zeilen ohne zu werfen (Cross-Channel)', () => { const { sender, recv } = setup(); expect(() => recv('[MSG:some banner]\r\n')).not.toThrow(); expect(sender.grblState).toBeNull(); expect(sender.lastError).toBeNull(); // lastResponse wird trotzdem gesetzt expect(sender.lastResponse).toBe('[MSG:some banner]'); }); test('zerstörter Report wirft nicht und lässt alten Zustand bestehen', () => { const { sender, recv } = setup(); recv('\r\n'); expect(() => recv('\r\n')).not.toThrow(); // State wird übernommen, kaputte Position aber nicht expect(sender.grblState).toBe('Run'); expect(sender.machinePosition).toEqual([1, 2, 3]); }); test('Zeilen-Puffer wird bei Reconnect zurückgesetzt', () => { const { sender, rawSocket, telnetSock, recv } = setup(); recv(' { const { sender, recv } = setup(); recv('\r\n'); const st = sender.getStatus(); expect(st.grblState).toBe('Hold'); expect(st.machinePosition).toEqual([5, 6, 7]); expect(st.plannerBlocksFree).toBe(12); expect(st.rxBytesFree).toBe(100); expect(st.autoReport).toBe(false); }); }); describe('TelnetSenderGRBL — Auto-Report Opt-in (ToDo_9 Paket 3)', () => { beforeAll(() => { jest.spyOn(console, 'log').mockImplementation(() => {}); }); afterAll(() => { jest.restoreAllMocks(); }); test('Default AUS: sendet beim Connect keine Settings-Kommandos', () => { const { telnetSock } = setup(); // autoReport nicht gesetzt expect(telnetSock.write).not.toHaveBeenCalled(); }); test('Opt-in EIN: sendet $10=3 und $Report/Interval beim Connect', () => { const { telnetSock } = setup({ autoReport: true, reportInterval: 150 }); expect(telnetSock.write).toHaveBeenCalledWith('$10=3\r\n'); expect(telnetSock.write).toHaveBeenCalledWith('$Report/Interval=150\r\n'); }); test('Opt-in EIN: nutzt Default-Intervall 200, wenn nicht angegeben', () => { const { telnetSock } = setup({ autoReport: true }); expect(telnetSock.write).toHaveBeenCalledWith('$Report/Interval=200\r\n'); }); }); describe('TelnetSenderGRBL — requestStatusReport (ToDo_9 Paket 4)', () => { beforeAll(() => { jest.spyOn(console, 'log').mockImplementation(() => {}); }); afterAll(() => { jest.restoreAllMocks(); }); // Setup mit erfassbarem Timeout-Callback (für den Timeout-Pfad). function setupTimed() { const rawSocket = makeRawSocket(); const telnetSock = makeTelnetSocket(); let timeoutCb = null; const sender = new TelnetSenderGRBL( 'robot.local', 5000, 'x', 'y', 'z', null, null, null, null, { netModule: { createConnection: jest.fn(() => rawSocket) }, TelnetSocketClass: jest.fn(() => telnetSock), setIntervalFn: jest.fn(() => 1), clearIntervalFn: jest.fn(), setTimeoutFn: jest.fn((cb) => { timeoutCb = cb; return 42; }), clearTimeoutFn: jest.fn(), autoConnect: false, } ); sender.connect(); rawSocket.emit('connect'); const recv = (str) => telnetSock.emit('data', Buffer.from(str, 'utf8')); return { sender, rawSocket, telnetSock, recv, fireTimeout: () => timeoutCb && timeoutCb() }; } test('sendet ? und löst beim nächsten Report auf', async () => { const { sender, telnetSock, recv } = setupTimed(); const p = sender.requestStatusReport(1000); expect(telnetSock.write).toHaveBeenCalledWith('?'); recv('\r\n'); const snap = await p; expect(snap.grblState).toBe('Idle'); expect(snap.machinePosition).toEqual([1, 2, 3]); }); test('wirft bei Timeout ohne Report', async () => { const { sender, fireTimeout } = setupTimed(); const p = sender.requestStatusReport(1000); fireTimeout(); await expect(p).rejects.toThrow(/Timeout/); }); test('wirft, wenn nicht verbunden', async () => { const { sender } = setupTimed(); sender.tSocket = null; await expect(sender.requestStatusReport(1000)).rejects.toThrow(/not connected/); }); test('bricht offene Anfrage bei Verbindungsverlust ab', async () => { const { sender, telnetSock } = setupTimed(); const p = sender.requestStatusReport(1000); telnetSock.emit('close'); await expect(p).rejects.toThrow(/geschlossen/); }); test('mehrere gleichzeitige Anfragen werden alle vom selben Report aufgelöst', async () => { const { sender, recv } = setupTimed(); const p1 = sender.requestStatusReport(1000); const p2 = sender.requestStatusReport(1000); recv('\r\n'); const [s1, s2] = await Promise.all([p1, p2]); expect(s1.machinePosition).toEqual([4, 5, 6]); expect(s2.machinePosition).toEqual([4, 5, 6]); }); });