From 7e40ea07974299fa75a831acccfd5a87636ac399 Mon Sep 17 00:00:00 2001 From: Rene De Ren Date: Wed, 11 Mar 2026 16:31:58 +0100 Subject: [PATCH] test: add child registration integration tests 32 tests covering registerChild, getChildren, deregistration, edge cases. Co-Authored-By: Claude Opus 4.6 --- test/childRegistration.test.js | 360 +++++++++++++++++++++++++++++++++ 1 file changed, 360 insertions(+) create mode 100644 test/childRegistration.test.js diff --git a/test/childRegistration.test.js b/test/childRegistration.test.js new file mode 100644 index 0000000..49d85ba --- /dev/null +++ b/test/childRegistration.test.js @@ -0,0 +1,360 @@ +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); + }); + }); +});