diff --git a/src/specificClass.js b/src/specificClass.js index 6ef949c..8366ddd 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -211,7 +211,7 @@ class pumpingStation { this._updateVolumePrediction("in"); // check for changes in incomming flow //calc the most important values back to determine state and net up or downstream flow this._calcNetFlow(); - this._calcTimeRemaining(); + this._calcRemainingTime(); } diff --git a/test/specificClass.test.js b/test/specificClass.test.js new file mode 100644 index 0000000..b4a477e --- /dev/null +++ b/test/specificClass.test.js @@ -0,0 +1,260 @@ +/** + * Tests for pumpingStation specificClass (domain logic). + * + * The pumpingStation class manages a basin (wet well): + * - initBasinProperties: derives surface area, volumes from config + * - _calcVolumeFromLevel / _calcLevelFromVolume: linear geometry + * - _calcDirection: filling / draining / stable from flow diff + * - _callMeasurementHandler: dispatches to type-specific handlers + * - getOutput: builds an output snapshot + */ + +const PumpingStation = require('../src/specificClass'); + +// --------------- helpers --------------- + +function makeConfig(overrides = {}) { + const base = { + general: { + name: 'TestStation', + id: 'ps-test-1', + unit: 'm3/h', + logging: { enabled: false, logLevel: 'error' }, + }, + functionality: { + softwareType: 'pumpingStation', + role: 'stationcontroller', + positionVsParent: 'atEquipment', + }, + basin: { + volume: 50, // m3 (empty basin volume) + height: 5, // m + heightInlet: 0.3, // m + heightOutlet: 0.2, // m + heightOverflow: 4.0, // m + }, + hydraulics: { + refHeight: 'NAP', + basinBottomRef: 0, + }, + }; + + for (const key of Object.keys(overrides)) { + if (typeof overrides[key] === 'object' && !Array.isArray(overrides[key]) && base[key]) { + base[key] = { ...base[key], ...overrides[key] }; + } else { + base[key] = overrides[key]; + } + } + return base; +} + +// --------------- tests --------------- + +describe('pumpingStation specificClass', () => { + + describe('constructor / initialization', () => { + it('should create an instance with the given config', () => { + const ps = new PumpingStation(makeConfig()); + expect(ps).toBeDefined(); + expect(ps.config.general.name).toBe('teststation'); + }); + + it('should initialize state object with default values', () => { + const ps = new PumpingStation(makeConfig()); + expect(ps.state).toEqual({ direction: '', netDownstream: 0, netUpstream: 0, seconds: 0 }); + }); + + it('should initialize empty machines, stations, child, parent objects', () => { + const ps = new PumpingStation(makeConfig()); + expect(ps.machines).toEqual({}); + expect(ps.stations).toEqual({}); + expect(ps.child).toEqual({}); + expect(ps.parent).toEqual({}); + }); + }); + + describe('initBasinProperties()', () => { + it('should calculate surfaceArea = volume / height', () => { + const ps = new PumpingStation(makeConfig()); + // 50 / 5 = 10 m2 + expect(ps.basin.surfaceArea).toBe(10); + }); + + it('should calculate maxVol = height * surfaceArea', () => { + const ps = new PumpingStation(makeConfig()); + // 5 * 10 = 50 + expect(ps.basin.maxVol).toBe(50); + }); + + it('should calculate maxVolOverflow = heightOverflow * surfaceArea', () => { + const ps = new PumpingStation(makeConfig()); + // 4.0 * 10 = 40 + expect(ps.basin.maxVolOverflow).toBe(40); + }); + + it('should calculate minVol = heightOutlet * surfaceArea', () => { + const ps = new PumpingStation(makeConfig()); + // 0.2 * 10 = 2 + expect(ps.basin.minVol).toBeCloseTo(2, 5); + }); + + it('should calculate minVolOut = heightInlet * surfaceArea', () => { + const ps = new PumpingStation(makeConfig()); + // 0.3 * 10 = 3 + expect(ps.basin.minVolOut).toBeCloseTo(3, 5); + }); + + it('should store the raw config values on basin', () => { + const ps = new PumpingStation(makeConfig()); + expect(ps.basin.volEmptyBasin).toBe(50); + expect(ps.basin.heightBasin).toBe(5); + expect(ps.basin.heightInlet).toBe(0.3); + expect(ps.basin.heightOutlet).toBe(0.2); + expect(ps.basin.heightOverflow).toBe(4.0); + }); + }); + + describe('_calcVolumeFromLevel()', () => { + let ps; + beforeAll(() => { ps = new PumpingStation(makeConfig()); }); + + it('should return level * surfaceArea', () => { + // surfaceArea = 10, level = 2 => 20 + expect(ps._calcVolumeFromLevel(2)).toBe(20); + }); + + it('should return 0 for level = 0', () => { + expect(ps._calcVolumeFromLevel(0)).toBe(0); + }); + + it('should clamp negative levels to 0', () => { + expect(ps._calcVolumeFromLevel(-3)).toBe(0); + }); + }); + + describe('_calcLevelFromVolume()', () => { + let ps; + beforeAll(() => { ps = new PumpingStation(makeConfig()); }); + + it('should return volume / surfaceArea', () => { + // surfaceArea = 10, vol = 20 => 2 + expect(ps._calcLevelFromVolume(20)).toBe(2); + }); + + it('should return 0 for volume = 0', () => { + expect(ps._calcLevelFromVolume(0)).toBe(0); + }); + + it('should clamp negative volumes to 0', () => { + expect(ps._calcLevelFromVolume(-10)).toBe(0); + }); + }); + + describe('volume/level roundtrip', () => { + it('should roundtrip level -> volume -> level', () => { + const ps = new PumpingStation(makeConfig()); + const level = 2.7; + const vol = ps._calcVolumeFromLevel(level); + const levelBack = ps._calcLevelFromVolume(vol); + expect(levelBack).toBeCloseTo(level, 10); + }); + }); + + describe('_calcDirection()', () => { + let ps; + beforeAll(() => { ps = new PumpingStation(makeConfig()); }); + + it('should return "filling" for positive flow above threshold', () => { + expect(ps._calcDirection(0.01)).toBe('filling'); + }); + + it('should return "draining" for negative flow below negative threshold', () => { + expect(ps._calcDirection(-0.01)).toBe('draining'); + }); + + it('should return "stable" for flow near zero (within threshold)', () => { + expect(ps._calcDirection(0.0005)).toBe('stable'); + expect(ps._calcDirection(-0.0005)).toBe('stable'); + expect(ps._calcDirection(0)).toBe('stable'); + }); + }); + + describe('_callMeasurementHandler()', () => { + it('should not throw for flow and temperature measurement types', () => { + const ps = new PumpingStation(makeConfig()); + // flow and temperature handlers are empty stubs, safe to call + expect(() => ps._callMeasurementHandler('flow', 0.5, 'downstream', {})).not.toThrow(); + expect(() => ps._callMeasurementHandler('temperature', 15, 'atEquipment', {})).not.toThrow(); + }); + + it('should dispatch to the correct handler based on measurement type', () => { + const ps = new PumpingStation(makeConfig()); + // Verify the switch dispatches by checking it does not warn for known types + // pressure handler stores values and attempts coolprop calculation + // level handler stores values and computes volume + // We verify the dispatch logic by calling with type and checking no unhandled error + const spy = jest.spyOn(ps, 'updateMeasuredFlow'); + ps._callMeasurementHandler('flow', 0.5, 'downstream', {}); + expect(spy).toHaveBeenCalledWith(0.5, 'downstream', {}); + spy.mockRestore(); + }); + }); + + describe('getOutput()', () => { + it('should return an object containing state and basin', () => { + const ps = new PumpingStation(makeConfig()); + const out = ps.getOutput(); + expect(out).toHaveProperty('state'); + expect(out).toHaveProperty('basin'); + expect(out.state).toBe(ps.state); + expect(out.basin).toBe(ps.basin); + }); + + it('should include measurement keys in the output', () => { + const ps = new PumpingStation(makeConfig()); + const out = ps.getOutput(); + // After initialization the predicted volume is set + expect(typeof out).toBe('object'); + }); + }); + + describe('_calcRemainingTime()', () => { + it('should not throw when called with a level and variant', () => { + const ps = new PumpingStation(makeConfig()); + // Should not throw even with no measurement data; it will just find null diffs + expect(() => ps._calcRemainingTime(2, 'predicted')).not.toThrow(); + }); + }); + + describe('tick()', () => { + it('should call _updateVolumePrediction and _calcNetFlow', () => { + const ps = new PumpingStation(makeConfig()); + const spyVol = jest.spyOn(ps, '_updateVolumePrediction'); + const spyNet = jest.spyOn(ps, '_calcNetFlow'); + // stub _calcRemainingTime to avoid needing full measurement data + ps._calcRemainingTime = jest.fn(); + ps.tick(); + expect(spyVol).toHaveBeenCalledWith('out'); + expect(spyVol).toHaveBeenCalledWith('in'); + expect(spyNet).toHaveBeenCalled(); + spyVol.mockRestore(); + spyNet.mockRestore(); + }); + }); + + describe('edge cases', () => { + it('should handle basin with zero height gracefully', () => { + // surfaceArea = volume / height => division by 0 gives Infinity + const config = makeConfig({ basin: { volume: 50, height: 0, heightInlet: 0, heightOutlet: 0, heightOverflow: 0 } }); + const ps = new PumpingStation(config); + expect(ps.basin.surfaceArea).toBe(Infinity); + }); + + it('should handle basin with very small dimensions', () => { + const config = makeConfig({ basin: { volume: 0.001, height: 0.001, heightInlet: 0, heightOutlet: 0, heightOverflow: 0.0005 } }); + const ps = new PumpingStation(config); + expect(ps.basin.surfaceArea).toBeCloseTo(1, 5); + }); + }); +});