32 tests covering registerChild, getChildren, deregistration, edge cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
361 lines
15 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|