const test = require('node:test'); const assert = require('node:assert/strict'); const { makeMeasurementInstance } = require('../helpers/factories'); /** * Covers the scaling / offset / interpolation primitives and the min/max * tracking side effects that are not exercised by the existing * scaling-and-output test. */ test("applyOffset adds configured offset to the input", () => { const m = makeMeasurementInstance({ scaling: { enabled: false, inputMin: 0, inputMax: 1, absMin: 0, absMax: 100, offset: 7 }, }); assert.equal(m.applyOffset(10), 17); assert.equal(m.applyOffset(-3), 4); }); test("interpolateLinear maps within range", () => { const m = makeMeasurementInstance(); assert.equal(m.interpolateLinear(50, 0, 100, 0, 10), 5); assert.equal(m.interpolateLinear(0, 0, 100, 0, 10), 0); assert.equal(m.interpolateLinear(100, 0, 100, 0, 10), 10); }); test("interpolateLinear warns and returns input when ranges collapse", () => { const m = makeMeasurementInstance(); // iMin == iMax -> invalid assert.equal(m.interpolateLinear(42, 0, 0, 0, 10), 42); // oMin > oMax -> invalid assert.equal(m.interpolateLinear(42, 0, 100, 10, 0), 42); }); test("constrain clamps below, inside, and above range", () => { const m = makeMeasurementInstance(); assert.equal(m.constrain(-5, 0, 10), 0); assert.equal(m.constrain(5, 0, 10), 5); assert.equal(m.constrain(15, 0, 10), 10); }); test("handleScaling falls back when inputRange is invalid", () => { const m = makeMeasurementInstance({ scaling: { enabled: true, inputMin: 5, inputMax: 5, absMin: 0, absMax: 10, offset: 0 }, }); // Before the call, inputRange is 0 (5-5). handleScaling should reset // inputMin/inputMax to defaults [0, 1] and still return a finite number. const result = m.handleScaling(0.5); assert.ok(Number.isFinite(result), `expected finite result, got ${result}`); assert.equal(m.config.scaling.inputMin, 0); assert.equal(m.config.scaling.inputMax, 1); }); test("handleScaling constrains out-of-range inputs before interpolating", () => { const m = makeMeasurementInstance({ scaling: { enabled: true, inputMin: 0, inputMax: 100, absMin: 0, absMax: 10, offset: 0 }, }); // Input above inputMax is constrained to inputMax then mapped to absMax. assert.equal(m.handleScaling(150), 10); // Input below inputMin is constrained to inputMin then mapped to absMin. assert.equal(m.handleScaling(-20), 0); }); test("calculateInput updates raw min/max from the unfiltered input", () => { const m = makeMeasurementInstance({ scaling: { enabled: false, inputMin: 0, inputMax: 1, absMin: 0, absMax: 1000, offset: 0 }, smoothing: { smoothWindow: 1, smoothMethod: 'none' }, }); m.calculateInput(10); m.calculateInput(30); m.calculateInput(5); assert.equal(m.totalMinValue, 5); assert.equal(m.totalMaxValue, 30); }); test("updateOutputPercent falls back to observed min/max when processRange <= 0", () => { const m = makeMeasurementInstance({ scaling: { enabled: false, inputMin: 0, inputMax: 1, absMin: 5, absMax: 5, offset: 0 }, smoothing: { smoothWindow: 1, smoothMethod: 'none' }, }); // processRange starts at 0 so updateOutputPercent uses totalMinValue/Max. m.totalMinValue = 0; m.totalMaxValue = 100; const pct = m.updateOutputPercent(50); // Linear interp: (50 - 0) / (100 - 0) * 100 = 50. assert.ok(Math.abs(pct - 50) < 0.01, `expected ~50, got ${pct}`); }); test("updateOutputAbs only emits MeasurementContainer update when value changes", async () => { const m = makeMeasurementInstance({ scaling: { enabled: false, inputMin: 0, inputMax: 1, absMin: 0, absMax: 100, offset: 0 }, smoothing: { smoothWindow: 1, smoothMethod: 'none' }, }); let emitCount = 0; // MeasurementContainer normalizes positions to lowercase, so the // event name uses 'atequipment' not the camelCase config value. m.measurements.emitter.on('pressure.measured.atequipment', () => { emitCount += 1; }); m.calculateInput(10); await new Promise((r) => setImmediate(r)); m.calculateInput(10); // same value -> no emit await new Promise((r) => setImmediate(r)); m.calculateInput(20); // new value -> emit await new Promise((r) => setImmediate(r)); assert.equal(emitCount, 2, `expected 2 emits (two distinct values), got ${emitCount}`); }); test("getOutput returns the full tracked state object", () => { const m = makeMeasurementInstance({ scaling: { enabled: false, inputMin: 0, inputMax: 1, absMin: 0, absMax: 100, offset: 0 }, smoothing: { smoothWindow: 1, smoothMethod: 'none' }, }); m.calculateInput(15); const out = m.getOutput(); assert.equal(typeof out.mAbs, 'number'); assert.equal(typeof out.mPercent, 'number'); assert.equal(typeof out.totalMinValue, 'number'); assert.equal(typeof out.totalMaxValue, 'number'); assert.equal(typeof out.totalMinSmooth, 'number'); assert.equal(typeof out.totalMaxSmooth, 'number'); });