/** * 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); }); }); });