196 lines
7.1 KiB
JavaScript
196 lines
7.1 KiB
JavaScript
jest.mock('../src/driverClient');
|
|
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const fsp = require('fs/promises');
|
|
const cfg = require('../src/config');
|
|
const store = require('../src/store/fileStore');
|
|
const units = require('../src/gcode/units');
|
|
const driverClient = require('../src/driverClient');
|
|
const { ActiveState } = require('../src/active/activeState');
|
|
|
|
let tmp;
|
|
beforeEach(async () => {
|
|
tmp = await fsp.mkdtemp(path.join(os.tmpdir(), 'fsvc-act-'));
|
|
cfg.storageDir = tmp;
|
|
});
|
|
afterEach(async () => {
|
|
await fsp.rm(tmp, { recursive: true, force: true });
|
|
});
|
|
|
|
test('Teaching: load(leer) → appendPoint → reload findet Zeile + Cursor', async () => {
|
|
const a = new ActiveState();
|
|
await a.load('teach_1'); // existiert nicht → leer angelegt
|
|
expect(a.getState().lineCount).toBe(0);
|
|
|
|
const pose = { x: 0, y: 300, z: 0, a: Math.PI / 2, b: -Math.PI / 2, c: 0, e: 0 };
|
|
const r = await a.appendPoint(pose, 1000);
|
|
expect(r.index).toBe(0);
|
|
expect(r.line).toContain('a90.00'); // in Grad gespeichert
|
|
|
|
const b = new ActiveState();
|
|
await b.load('teach_1');
|
|
expect(b.getState().lineCount).toBe(1);
|
|
expect(b.getState().cursor).toBe(0);
|
|
});
|
|
|
|
test('Playback: stepping liefert driver-native (Radian) Zeilen, Grenzen werfen', async () => {
|
|
await store.write('play_1', {
|
|
name: 'Play 1',
|
|
lines: [
|
|
'G90 G1 x0 y300 z0 a90.00 b-90.00 c0.00 e0.00 f1000 ;1',
|
|
'G90 G1 x10 y300 z0 a0.00 b-90.00 c0.00 e0.00 f1000 ;2',
|
|
],
|
|
});
|
|
const a = new ActiveState();
|
|
await a.load('play_1');
|
|
|
|
const first = await a.first();
|
|
expect(first.cursor).toBe(0);
|
|
expect(first.line).not.toMatch(/;/);
|
|
const aVal = Number(first.line.split(/\s+/).find((t) => t.startsWith('a')).slice(1));
|
|
expect(aVal).toBeCloseTo(Math.PI / 2, 4);
|
|
|
|
expect((await a.next()).cursor).toBe(1);
|
|
await expect(a.next()).rejects.toThrow(); // über das Ende → CURSOR_OUT_OF_RANGE
|
|
});
|
|
|
|
test('Cursor liegt als ;! in der .gcode-Datei, store.read() liefert saubere Zeilen', async () => {
|
|
await store.write('cur_1', {
|
|
name: 'Cur',
|
|
lines: [
|
|
'G90 G1 x0 y0 z0 a0 b0 c0 e0 f1000 ;1',
|
|
'G90 G1 x1 y0 z0 a0 b0 c0 e0 f1000 ;2',
|
|
],
|
|
});
|
|
const a = new ActiveState();
|
|
await a.load('cur_1');
|
|
await a.next(); // cursor → 1, persistiert als ;! im .gcode
|
|
await a.appendLine('G4 P0.1'); // cursor → 2, persistiert
|
|
|
|
// Rohe .gcode-Datei: genau eine Zeile mit '!', und zwar an Index 2
|
|
const rawText = await fsp.readFile(path.join(tmp, 'cur_1.gcode'), 'utf8');
|
|
const rawLines = rawText.split('\n').filter(Boolean);
|
|
const markedLines = rawLines.filter(units.hasCursorMarker);
|
|
expect(markedLines).toHaveLength(1);
|
|
expect(rawLines.indexOf(markedLines[0])).toBe(2);
|
|
|
|
// store.read() liefert saubere Zeilen (kein '!') und korrekten Cursor
|
|
const prog = await store.read('cur_1');
|
|
expect(prog.cursor).toBe(2);
|
|
expect(prog.lines.filter(units.hasCursorMarker)).toHaveLength(0);
|
|
expect(units.splitComment(prog.lines[2]).code).toBe('G4 P0.1');
|
|
});
|
|
|
|
test('Stepping ohne aktives Programm → auto-lädt Default (leer → EMPTY_PROGRAM)', async () => {
|
|
const a = new ActiveState();
|
|
// next() lädt automatisch das Default-Programm; da es (im tmp) leer ist → EMPTY_PROGRAM
|
|
await expect(a.next()).rejects.toMatchObject({ code: 'EMPTY_PROGRAM' });
|
|
expect(a.programId).toBe(cfg.defaultProgramId); // Default wurde geladen
|
|
});
|
|
|
|
test('FShow ohne FLoad → auto-lädt Default und liefert volle Zeilenliste', async () => {
|
|
await store.write(cfg.defaultProgramId, {
|
|
name: 'log',
|
|
lines: [
|
|
'G90 G1 x0 y300 z0 a90.00 b-90.00 c0.00 e0.00 f1000 ;1',
|
|
'G90 G1 x10 y300 z0 a0.00 b-90.00 c0.00 e0.00 f1000 ;2',
|
|
],
|
|
});
|
|
const a = new ActiveState();
|
|
const state = await a.show(); // frisch, ohne vorheriges FLoad/FPoint
|
|
expect(state.programId).toBe(cfg.defaultProgramId);
|
|
expect(state.lineCount).toBe(2);
|
|
expect(state.lines).toHaveLength(2); // volle Liste für die Anzeige
|
|
expect(state.lines[0]).toContain('a90.00'); // in Grad, wie gespeichert
|
|
});
|
|
|
|
test('Stepping nach Neustart liest Default-Programm von Disk (FFirst ohne FLoad)', async () => {
|
|
// Simuliert: log.gcode liegt auf Disk, frischer ActiveState (wie nach Container-Neustart)
|
|
await store.write(cfg.defaultProgramId, {
|
|
name: 'log',
|
|
lines: [
|
|
'G90 G1 x0 y300 z0 a90.00 b-90.00 c0.00 e0.00 f1000 ;1',
|
|
'G90 G1 x10 y300 z0 a0.00 b-90.00 c0.00 e0.00 f1000 ;2',
|
|
],
|
|
cursor: 1,
|
|
});
|
|
const a = new ActiveState();
|
|
const r = await a.first(); // ohne vorheriges FLoad → Default wird geladen
|
|
expect(a.programId).toBe(cfg.defaultProgramId);
|
|
expect(r.cursor).toBe(0);
|
|
expect(a.getState().lineCount).toBe(2);
|
|
});
|
|
|
|
test('FPoint ohne aktives Programm → auto-lädt Default-Programm (log)', async () => {
|
|
const a = new ActiveState();
|
|
// appendPoint lädt automatisch das Default-Programm und legt es leer an
|
|
const r = await a.appendPoint({ x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, e: 0 });
|
|
expect(r.index).toBe(0);
|
|
expect(a.programId).toBeTruthy(); // Default-Programm wurde geladen
|
|
});
|
|
|
|
// ─── Senden-Modus (execute=true) ─────────────────────────────────────────────
|
|
|
|
describe('Senden-Modus (execute=true)', () => {
|
|
const M114 = { position: { x: 10, y: 300, z: 0, a: 0 }, motorCounts: {} };
|
|
|
|
beforeEach(() => {
|
|
driverClient.send.mockReset();
|
|
driverClient.send.mockResolvedValue(M114);
|
|
});
|
|
|
|
async function makeActive() {
|
|
await store.write('step_1', {
|
|
name: 'Step',
|
|
lines: [
|
|
'G90 G1 x0 y300 z0 a90.00 b-90.00 c0.00 e0.00 f1000 ;1',
|
|
'G90 G1 x10 y300 z0 a0.00 b-90.00 c0.00 e0.00 f1000 ;2',
|
|
],
|
|
});
|
|
const a = new ActiveState();
|
|
await a.load('step_1');
|
|
return a;
|
|
}
|
|
|
|
test('next(true) ruft driverClient.send mit der G-Code-Zeile auf', async () => {
|
|
const a = await makeActive();
|
|
await a.first(); // cursor → 0
|
|
await a.next(true); // cursor → 1, sendet Zeile 1
|
|
|
|
expect(driverClient.send).toHaveBeenCalledTimes(1);
|
|
const sentLine = driverClient.send.mock.calls[0][0];
|
|
// Zeile muss Radian-Werte enthalten (a ≈ 0) und kein Semikolon (kein Kommentar)
|
|
expect(sentLine).toContain('G90 G1');
|
|
expect(sentLine).not.toContain(';');
|
|
});
|
|
|
|
test('next(true) gibt { cursor, line, driverPos } zurück', async () => {
|
|
const a = await makeActive();
|
|
await a.first();
|
|
const result = await a.next(true);
|
|
|
|
expect(result.cursor).toBe(1);
|
|
expect(result.line).toContain('G90 G1');
|
|
expect(result.driverPos).toEqual(M114);
|
|
});
|
|
|
|
test('next(true) wirft Fehler mit driverCode wenn driverClient.send ablehnt', async () => {
|
|
const driverErr = Object.assign(new Error('inverse kinematics failed'), { driverCode: 'GCODE_ERROR' });
|
|
driverClient.send.mockRejectedValueOnce(driverErr);
|
|
|
|
const a = await makeActive();
|
|
await a.first();
|
|
|
|
await expect(a.next(true)).rejects.toMatchObject({ driverCode: 'GCODE_ERROR' });
|
|
});
|
|
|
|
test('next(false) ruft driverClient.send NICHT auf', async () => {
|
|
const a = await makeActive();
|
|
await a.first();
|
|
await a.next(false); // Navigieren-Modus
|
|
|
|
expect(driverClient.send).not.toHaveBeenCalled();
|
|
});
|
|
});
|