Files
generalFunctions/test/childRegistration.test.js
Rene De Ren 7e40ea0797 test: add child registration integration tests
32 tests covering registerChild, getChildren, deregistration, edge cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 16:31:58 +01:00

361 lines
15 KiB
JavaScript

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