const ChildRegistrationUtils = require('../src/helper/childRegistrationUtils'); const { POSITIONS } = require('../src/constants/positions'); // ── Helpers ────────────────────────────────────────────────────────────────── /** Create a minimal mock parent (mainClass) that ChildRegistrationUtils expects. */ function createMockParent(opts = {}) { return { child: {}, logger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), }, // optionally provide a registerChild callback so the utils can delegate registerChild: opts.registerChild || undefined, ...opts, }; } /** Create a minimal mock child node with the given overrides. */ function createMockChild(overrides = {}) { const defaults = { config: { general: { id: overrides.id || 'child-1', name: overrides.name || 'TestChild', }, functionality: { softwareType: overrides.softwareType !== undefined ? overrides.softwareType : 'measurement', positionVsParent: overrides.position || POSITIONS.UPSTREAM, }, asset: { category: overrides.category || 'sensor', type: overrides.assetType || 'pressure', }, }, measurements: overrides.measurements || null, }; // allow caller to add extra top-level props return { ...defaults, ...(overrides.extra || {}) }; } // ── Tests ──────────────────────────────────────────────────────────────────── describe('ChildRegistrationUtils', () => { let parent; let utils; beforeEach(() => { parent = createMockParent(); utils = new ChildRegistrationUtils(parent); }); // ── Construction ───────────────────────────────────────────────────────── describe('constructor', () => { it('should store a reference to the mainClass', () => { expect(utils.mainClass).toBe(parent); }); it('should initialise with an empty registeredChildren map', () => { expect(utils.registeredChildren.size).toBe(0); }); it('should use the parent logger', () => { expect(utils.logger).toBe(parent.logger); }); }); // ── registerChild ──────────────────────────────────────────────────────── describe('registerChild()', () => { it('should register a child and store it in the internal map', async () => { const child = createMockChild(); await utils.registerChild(child, POSITIONS.UPSTREAM); expect(utils.registeredChildren.size).toBe(1); expect(utils.registeredChildren.has('child-1')).toBe(true); }); it('should store softwareType, position and timestamp in the registry entry', async () => { const child = createMockChild({ softwareType: 'machine' }); const before = Date.now(); await utils.registerChild(child, POSITIONS.DOWNSTREAM); const after = Date.now(); const entry = utils.registeredChildren.get('child-1'); expect(entry.softwareType).toBe('machine'); expect(entry.position).toBe(POSITIONS.DOWNSTREAM); expect(entry.registeredAt).toBeGreaterThanOrEqual(before); expect(entry.registeredAt).toBeLessThanOrEqual(after); }); it('should store the child in mainClass.child[softwareType][category]', async () => { const child = createMockChild({ softwareType: 'measurement', category: 'sensor' }); await utils.registerChild(child, POSITIONS.UPSTREAM); expect(parent.child.measurement).toBeDefined(); expect(parent.child.measurement.sensor).toBeInstanceOf(Array); expect(parent.child.measurement.sensor).toContain(child); }); it('should set the parent reference on the child', async () => { const child = createMockChild(); await utils.registerChild(child, POSITIONS.UPSTREAM); expect(child.parent).toEqual([parent]); }); it('should set positionVsParent on the child', async () => { const child = createMockChild(); await utils.registerChild(child, POSITIONS.DOWNSTREAM); expect(child.positionVsParent).toBe(POSITIONS.DOWNSTREAM); }); it('should lowercase the softwareType before storing', async () => { const child = createMockChild({ softwareType: 'Measurement' }); await utils.registerChild(child, POSITIONS.UPSTREAM); const entry = utils.registeredChildren.get('child-1'); expect(entry.softwareType).toBe('measurement'); expect(parent.child.measurement).toBeDefined(); }); it('should delegate to mainClass.registerChild when it is a function', async () => { const registerSpy = jest.fn(); parent.registerChild = registerSpy; const child = createMockChild({ softwareType: 'measurement' }); await utils.registerChild(child, POSITIONS.UPSTREAM); expect(registerSpy).toHaveBeenCalledWith(child, 'measurement'); }); it('should NOT throw when mainClass has no registerChild method', async () => { delete parent.registerChild; const child = createMockChild(); await expect(utils.registerChild(child, POSITIONS.UPSTREAM)).resolves.not.toThrow(); }); it('should log a debug message on registration', async () => { const child = createMockChild({ name: 'Pump1', id: 'p1' }); await utils.registerChild(child, POSITIONS.UPSTREAM); expect(parent.logger.debug).toHaveBeenCalledWith( expect.stringContaining('Registering child: Pump1') ); }); it('should handle empty softwareType gracefully', async () => { const child = createMockChild({ softwareType: '' }); await utils.registerChild(child, POSITIONS.UPSTREAM); const entry = utils.registeredChildren.get('child-1'); expect(entry.softwareType).toBe(''); }); }); // ── Multiple children ──────────────────────────────────────────────────── describe('multiple children registration', () => { it('should register multiple children of the same softwareType', async () => { const c1 = createMockChild({ id: 'c1', name: 'Sensor1', softwareType: 'measurement' }); const c2 = createMockChild({ id: 'c2', name: 'Sensor2', softwareType: 'measurement' }); await utils.registerChild(c1, POSITIONS.UPSTREAM); await utils.registerChild(c2, POSITIONS.DOWNSTREAM); expect(utils.registeredChildren.size).toBe(2); expect(parent.child.measurement.sensor).toHaveLength(2); }); it('should register children of different softwareTypes', async () => { const sensor = createMockChild({ id: 's1', softwareType: 'measurement' }); const machine = createMockChild({ id: 'm1', softwareType: 'machine', category: 'pump' }); await utils.registerChild(sensor, POSITIONS.UPSTREAM); await utils.registerChild(machine, POSITIONS.AT_EQUIPMENT); expect(parent.child.measurement).toBeDefined(); expect(parent.child.machine).toBeDefined(); expect(parent.child.machine.pump).toContain(machine); }); it('should register children of different categories under the same softwareType', async () => { const sensor = createMockChild({ id: 's1', softwareType: 'measurement', category: 'sensor' }); const analyser = createMockChild({ id: 'a1', softwareType: 'measurement', category: 'analyser' }); await utils.registerChild(sensor, POSITIONS.UPSTREAM); await utils.registerChild(analyser, POSITIONS.DOWNSTREAM); expect(parent.child.measurement.sensor).toHaveLength(1); expect(parent.child.measurement.analyser).toHaveLength(1); }); it('should support multiple parents on a child (array append)', async () => { const parent2 = createMockParent(); const utils2 = new ChildRegistrationUtils(parent2); const child = createMockChild(); await utils.registerChild(child, POSITIONS.UPSTREAM); await utils2.registerChild(child, POSITIONS.DOWNSTREAM); expect(child.parent).toEqual([parent, parent2]); }); }); // ── Duplicate registration ─────────────────────────────────────────────── describe('duplicate registration', () => { it('should overwrite the registry entry when the same child id is registered twice', async () => { const child = createMockChild({ id: 'dup-1' }); await utils.registerChild(child, POSITIONS.UPSTREAM); await utils.registerChild(child, POSITIONS.DOWNSTREAM); // Map.set overwrites, so still size 1 expect(utils.registeredChildren.size).toBe(1); const entry = utils.registeredChildren.get('dup-1'); expect(entry.position).toBe(POSITIONS.DOWNSTREAM); }); it('should push the child into the category array again on duplicate registration', async () => { const child = createMockChild({ id: 'dup-1' }); await utils.registerChild(child, POSITIONS.UPSTREAM); await utils.registerChild(child, POSITIONS.UPSTREAM); // _storeChild does a push each time expect(parent.child.measurement.sensor).toHaveLength(2); }); }); // ── Measurement context setup ──────────────────────────────────────────── describe('measurement context on child', () => { it('should call setChildId, setChildName, setParentRef when child has measurements', async () => { const measurements = { setChildId: jest.fn(), setChildName: jest.fn(), setParentRef: jest.fn(), }; const child = createMockChild({ id: 'mc-1', name: 'Sensor1', measurements }); await utils.registerChild(child, POSITIONS.UPSTREAM); expect(measurements.setChildId).toHaveBeenCalledWith('mc-1'); expect(measurements.setChildName).toHaveBeenCalledWith('Sensor1'); expect(measurements.setParentRef).toHaveBeenCalledWith(parent); }); it('should skip measurement setup when child has no measurements object', async () => { const child = createMockChild({ measurements: null }); // Should not throw await expect(utils.registerChild(child, POSITIONS.UPSTREAM)).resolves.not.toThrow(); }); }); // ── getChildrenOfType ──────────────────────────────────────────────────── describe('getChildrenOfType()', () => { beforeEach(async () => { const s1 = createMockChild({ id: 's1', softwareType: 'measurement', category: 'sensor' }); const s2 = createMockChild({ id: 's2', softwareType: 'measurement', category: 'sensor' }); const a1 = createMockChild({ id: 'a1', softwareType: 'measurement', category: 'analyser' }); const m1 = createMockChild({ id: 'm1', softwareType: 'machine', category: 'pump' }); await utils.registerChild(s1, POSITIONS.UPSTREAM); await utils.registerChild(s2, POSITIONS.DOWNSTREAM); await utils.registerChild(a1, POSITIONS.UPSTREAM); await utils.registerChild(m1, POSITIONS.AT_EQUIPMENT); }); it('should return all children of a given softwareType', () => { const measurements = utils.getChildrenOfType('measurement'); expect(measurements).toHaveLength(3); }); it('should return children filtered by category', () => { const sensors = utils.getChildrenOfType('measurement', 'sensor'); expect(sensors).toHaveLength(2); }); it('should return empty array for unknown softwareType', () => { expect(utils.getChildrenOfType('nonexistent')).toEqual([]); }); it('should return empty array for unknown category', () => { expect(utils.getChildrenOfType('measurement', 'nonexistent')).toEqual([]); }); }); // ── getChildById ───────────────────────────────────────────────────────── describe('getChildById()', () => { it('should return the child by its id', async () => { const child = createMockChild({ id: 'find-me' }); await utils.registerChild(child, POSITIONS.UPSTREAM); expect(utils.getChildById('find-me')).toBe(child); }); it('should return null for unknown id', () => { expect(utils.getChildById('does-not-exist')).toBeNull(); }); }); // ── getAllChildren ─────────────────────────────────────────────────────── describe('getAllChildren()', () => { it('should return an empty array when no children registered', () => { expect(utils.getAllChildren()).toEqual([]); }); it('should return all registered child objects', async () => { const c1 = createMockChild({ id: 'c1' }); const c2 = createMockChild({ id: 'c2' }); await utils.registerChild(c1, POSITIONS.UPSTREAM); await utils.registerChild(c2, POSITIONS.DOWNSTREAM); const all = utils.getAllChildren(); expect(all).toHaveLength(2); expect(all).toContain(c1); expect(all).toContain(c2); }); }); // ── logChildStructure ─────────────────────────────────────────────────── describe('logChildStructure()', () => { it('should log the child structure via debug', async () => { const child = createMockChild({ id: 'log-1', name: 'LogChild' }); await utils.registerChild(child, POSITIONS.UPSTREAM); utils.logChildStructure(); expect(parent.logger.debug).toHaveBeenCalledWith( 'Current child structure:', expect.any(String) ); }); }); // ── _storeChild (internal) ────────────────────────────────────────────── describe('_storeChild() internal behaviour', () => { it('should create the child object on parent if it does not exist', async () => { delete parent.child; const child = createMockChild(); await utils.registerChild(child, POSITIONS.UPSTREAM); expect(parent.child).toBeDefined(); expect(parent.child.measurement.sensor).toContain(child); }); it('should use "sensor" as default category when asset.category is absent', async () => { const child = createMockChild(); // remove asset.category to trigger default delete child.config.asset.category; await utils.registerChild(child, POSITIONS.UPSTREAM); expect(parent.child.measurement.sensor).toContain(child); }); }); });