From 2ed1ee3d4781a6665038a1ea08f05538889a7fe6 Mon Sep 17 00:00:00 2001 From: Arvin Liu Date: Sat, 22 Aug 2026 01:43:49 +0800 Subject: [PATCH] test(node-js): add unit tests for controller command logic; fix DriveValue.read rounding - Add controller/node-js/test/commands.test.js covering CommandHandler drive mapping, DriveCommandReducer de-duplication, and DriveValue bounds. - Fix DriveValue.read(): Math.round(value, 3) ignored the 2nd argument and returned an integer; use Math.round(value * 1000) / 1000 for 3-decimal precision. Export DriveValue so it is testable. - Add "test": "node --test" script (zero new dependencies, runs on Node 18+). --- controller/node-js/package.json | 3 +- controller/node-js/server/commands.js | 4 +- controller/node-js/test/commands.test.js | 97 ++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 controller/node-js/test/commands.test.js diff --git a/controller/node-js/package.json b/controller/node-js/package.json index 6911cc383..5857eb9a7 100644 --- a/controller/node-js/package.json +++ b/controller/node-js/package.json @@ -10,7 +10,8 @@ "dev:server": "npm run start --prefix server", "dev:start-client": "vite --port 8081 --host", "build": "vite build", - "lint": "eslint server/*.js --fix && eslint client/*.js --fix" + "lint": "eslint server/*.js --fix && eslint client/*.js --fix", + "test": "node --test" }, "author": "Ivo Zivkov", "devDependencies": { diff --git a/controller/node-js/server/commands.js b/controller/node-js/server/commands.js index eb047acc6..737385aa3 100644 --- a/controller/node-js/server/commands.js +++ b/controller/node-js/server/commands.js @@ -42,7 +42,7 @@ function DriveValue () { } this.read = () => { - return Math.round(value, 3) + return Math.round(value * 1000) / 1000 } } @@ -121,4 +121,4 @@ function DriveCommandReducer () { } } -module.exports = { Commands, CommandHandler } +module.exports = { Commands, CommandHandler, DriveValue } diff --git a/controller/node-js/test/commands.test.js b/controller/node-js/test/commands.test.js new file mode 100644 index 000000000..85d05e501 --- /dev/null +++ b/controller/node-js/test/commands.test.js @@ -0,0 +1,97 @@ +/* + * Unit tests for the OpenBot node-js controller command logic. + * + * These exercise the pure server-side command mapping that ultimately drives + * the robot's left/right motors, so a regression here could send wrong motor + * commands. Run with: `npm test` (uses Node's built-in test runner). + */ + +const test = require('node:test') +const assert = require('node:assert') + +const { Commands, DriveValue } = require('../server/commands.js') + +// Creates a CommandHandler and captures every command string it sends. +function makeHandler () { + const sent = [] + const commands = new Commands(msg => sent.push(msg)) + return { handler: commands.getCommandHandler(), sent } +} + +test('goForward sends full forward drive command', () => { + const { handler, sent } = makeHandler() + handler.goForward() + assert.deepStrictEqual(JSON.parse(sent[0]), { driveCmd: { l: 1, r: 1 } }) +}) + +test('goBackward sends full reverse drive command', () => { + const { handler, sent } = makeHandler() + handler.goBackward() + assert.deepStrictEqual(JSON.parse(sent[0]), { driveCmd: { l: -1, r: -1 } }) +}) + +test('forwardLeft mixes left/right correctly', () => { + const { handler, sent } = makeHandler() + handler.forwardLeft() + assert.deepStrictEqual(JSON.parse(sent[0]), { driveCmd: { l: -0.5, r: 1 } }) +}) + +test('forwardRight mixes right/left correctly', () => { + const { handler, sent } = makeHandler() + handler.forwardRight() + assert.deepStrictEqual(JSON.parse(sent[0]), { driveCmd: { l: 1, r: -0.5 } }) +}) + +test('rotateLeft / rotateRight send opposing wheel commands', () => { + const { handler, sent } = makeHandler() + handler.rotateLeft() + assert.deepStrictEqual(JSON.parse(sent[0]), { driveCmd: { l: -1, r: 1 } }) + + const { handler: handler2, sent: sent2 } = makeHandler() + handler2.rotateRight() + assert.deepStrictEqual(JSON.parse(sent2[0]), { driveCmd: { l: 1, r: -1 } }) +}) + +test('reset sends a zero drive command', () => { + const { handler, sent } = makeHandler() + handler.reset() + assert.deepStrictEqual(JSON.parse(sent[0]), { driveCmd: { l: 0, r: 0 } }) +}) + +test('consecutive identical drive commands are de-duplicated', () => { + const { handler, sent } = makeHandler() + handler.goForward() + handler.goForward() + handler.goForward() + assert.strictEqual(sent.length, 1) +}) + +test('different drive commands are each sent', () => { + const { handler, sent } = makeHandler() + handler.goForward() + handler.goBackward() + assert.strictEqual(sent.length, 2) +}) + +test('sendCommand forwards the raw command string', () => { + const { handler, sent } = makeHandler() + handler.sendCommand('NOISE') + assert.strictEqual(sent[0], '{command: NOISE }') +}) + +test('DriveValue clamps to [-1, 1] and read() rounds to 3 decimals', () => { + const dv = new DriveValue() + assert.strictEqual(dv.reset(), 0) + assert.strictEqual(dv.min(), -1) + assert.strictEqual(dv.max(), 1) + + // Before the fix this returned Math.round(0.12345) === 0 (2nd arg ignored). + dv.write(0.12345) + assert.strictEqual(dv.read(), 0.123) + + dv.write(0.9999) + assert.strictEqual(dv.read(), 1) + + dv.write(-0.4567) + assert.strictEqual(dv.read(), -0.457) +})