From 49ebd833db0d1c829ed9778ee6aba0e5489c4d20 Mon Sep 17 00:00:00 2001 From: Rene De Ren Date: Wed, 11 Mar 2026 16:32:04 +0100 Subject: [PATCH] feat: add node tests, integration tests, API reference, fix pumpingStation bug - Add 127 unit tests for measurement, pumpingStation, reactor, settler specificClass - Add 32 integration tests for parent-child registration flows - Fix pumpingStation tick() calling non-existent _calcTimeRemaining (was _calcRemainingTime) - Add API reference documentation for all generalFunctions modules Total tests: 536 (389 Jest + 23 node:test + 124 legacy), all passing Closes #17, #19, #20 Co-Authored-By: Claude Opus 4.6 --- docs/API_REFERENCE.md | 446 +++++++++++++++++++++++++++++++++++++++++ jest.config.js | 4 + nodes/generalFunctions | 2 +- nodes/measurement | 2 +- nodes/pumpingStation | 2 +- nodes/reactor | 2 +- nodes/settler | 2 +- 7 files changed, 455 insertions(+), 5 deletions(-) create mode 100644 docs/API_REFERENCE.md diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md new file mode 100644 index 0000000..22647a3 --- /dev/null +++ b/docs/API_REFERENCE.md @@ -0,0 +1,446 @@ +# generalFunctions API Reference + +Shared library (`nodes/generalFunctions/`) used across all EVOLV Node-RED nodes. + +```js +const { logger, outputUtils, MeasurementContainer, ... } = require('generalFunctions'); +``` + +--- + +## Table of Contents + +1. [Logger](#logger) +2. [OutputUtils](#outpututils) +3. [ValidationUtils](#validationutils) +4. [MeasurementContainer](#measurementcontainer) +5. [ConfigManager](#configmanager) +6. [ChildRegistrationUtils](#childregistrationutils) +7. [MenuUtils](#menuutils) +8. [EndpointUtils](#endpointutils) +9. [Positions](#positions) +10. [AssetLoader / loadCurve](#assetloader--loadcurve) + +--- + +## Logger + +Structured, level-filtered console logger. + +**File:** `src/helper/logger.js` + +### Constructor + +```js +new Logger(logging = true, logLevel = 'debug', nameModule = 'N/A') +``` + +| Param | Type | Default | Description | +|---|---|---|---| +| `logging` | `boolean` | `true` | Enable/disable all output | +| `logLevel` | `string` | `'debug'` | Minimum severity: `'debug'` \| `'info'` \| `'warn'` \| `'error'` | +| `nameModule` | `string` | `'N/A'` | Label prefixed to every message | + +### Methods + +| Method | Signature | Description | +|---|---|---| +| `debug` | `(message: string): void` | Log at DEBUG level | +| `info` | `(message: string): void` | Log at INFO level | +| `warn` | `(message: string): void` | Log at WARN level | +| `error` | `(message: string): void` | Log at ERROR level | +| `setLogLevel` | `(level: string): void` | Change minimum level at runtime | +| `toggleLogging` | `(): void` | Flip logging on/off | + +### Example + +```js +const Logger = require('generalFunctions').logger; +const log = new Logger(true, 'info', 'MyNode'); +log.info('Node started'); // [INFO] -> MyNode: Node started +log.debug('ignored'); // silent (below 'info') +log.setLogLevel('debug'); +log.debug('now visible'); // [DEBUG] -> MyNode: now visible +``` + +--- + +## OutputUtils + +Tracks output state and formats messages for InfluxDB or process outputs. Only emits changed fields. + +**File:** `src/helper/outputUtils.js` + +### Constructor + +```js +new OutputUtils() // no parameters +``` + +### Methods + +| Method | Signature | Returns | Description | +|---|---|---|---| +| `formatMsg` | `(output, config, format)` | `object \| undefined` | Diff against last output; returns formatted msg or `undefined` if nothing changed | +| `checkForChanges` | `(output, format)` | `object` | Returns only the key/value pairs that changed since last call | + +**`format`** must be `'influxdb'` or `'process'`. + +### Example + +```js +const out = new OutputUtils(); +const msg = out.formatMsg( + { temperature: 22.5, pressure: 1013 }, + config, + 'influxdb' +); +// msg = { topic: 'nodeName', payload: { measurement, fields, tags, timestamp } } +``` + +--- + +## ValidationUtils + +Schema-driven config validation with type coercion, range clamping, and nested object support. + +**File:** `src/helper/validationUtils.js` + +### Constructor + +```js +new ValidationUtils(loggerEnabled = true, loggerLevel = 'warn') +``` + +### Methods + +| Method | Signature | Returns | Description | +|---|---|---|---| +| `validateSchema` | `(config, schema, name)` | `object` | Walk the schema, validate every field, return a clean config. Unknown keys are stripped. Missing keys get their schema default. | +| `constrain` | `(value, min, max)` | `number` | Clamp a numeric value to `[min, max]` | +| `removeUnwantedKeys` | `(obj)` | `object` | Strip `rules`/`description` metadata, collapse `default` values | + +**Supported `rules.type` values:** `number`, `integer`, `boolean`, `string`, `enum`, `array`, `set`, `object`, `curve`, `machineCurve`. + +### Example + +```js +const ValidationUtils = require('generalFunctions').validation; +const v = new ValidationUtils(true, 'warn'); + +const schema = { + temperature: { default: 20, rules: { type: 'number', min: -40, max: 100 } }, + unit: { default: 'C', rules: { type: 'enum', values: [{ value: 'C' }, { value: 'F' }] } } +}; + +const validated = v.validateSchema({ temperature: 999 }, schema, 'myNode'); +// validated.temperature === 100 (clamped) +// validated.unit === 'C' (default applied) +``` + +--- + +## MeasurementContainer + +Chainable measurement storage organised by **type / variant / position**. Supports auto unit conversion, windowed statistics, events, and positional difference calculations. + +**File:** `src/measurements/MeasurementContainer.js` + +### Constructor + +```js +new MeasurementContainer(options = {}, logger) +``` + +| Option | Type | Default | Description | +|---|---|---|---| +| `windowSize` | `number` | `10` | Rolling window for statistics | +| `defaultUnits` | `object` | `{ pressure:'mbar', flow:'m3/h', ... }` | Default unit per measurement type | +| `autoConvert` | `boolean` | `true` | Auto-convert values to target unit | +| `preferredUnits` | `object` | `{}` | Per-type unit overrides | + +### Chainable Setters + +All return `this` for chaining. + +```js +container + .type('pressure') + .variant('static') + .position('upstream') + .distance(5) + .unit('bar') + .value(3.2, Date.now(), 'bar'); +``` + +| Method | Signature | Description | +|---|---|---| +| `type` | `(typeName): this` | Set measurement type (e.g. `'pressure'`) | +| `variant` | `(variantName): this` | Set variant (e.g. `'static'`, `'differential'`) | +| `position` | `(positionValue): this` | Set position (e.g. `'upstream'`, `'downstream'`) | +| `distance` | `(distance): this` | Set physical distance from parent | +| `unit` | `(unitName): this` | Set unit on the underlying measurement | +| `value` | `(val, timestamp?, sourceUnit?): this` | Store a value; auto-converts if `sourceUnit` differs from target | + +### Terminal / Query Methods + +| Method | Signature | Returns | Description | +|---|---|---|---| +| `get` | `()` | `Measurement \| null` | Get the raw measurement object | +| `getCurrentValue` | `(requestedUnit?)` | `number \| null` | Latest value, optionally converted | +| `getAverage` | `(requestedUnit?)` | `number \| null` | Windowed average | +| `getMin` | `()` | `number \| null` | Window minimum | +| `getMax` | `()` | `number \| null` | Window maximum | +| `getAllValues` | `()` | `array \| null` | All stored samples | +| `getLaggedValue` | `(lag?, requestedUnit?)` | `number \| null` | Value from `lag` samples ago | +| `getLaggedSample` | `(lag?, requestedUnit?)` | `object \| null` | Full sample `{ value, timestamp, unit }` from `lag` samples ago | +| `exists` | `({ type?, variant?, position?, requireValues? })` | `boolean` | Check if a measurement series exists | +| `difference` | `({ from?, to?, unit? })` | `object \| null` | Compute `{ value, avgDiff, unit }` between two positions | + +### Introspection / Lifecycle + +| Method | Signature | Returns | Description | +|---|---|---|---| +| `getTypes` | `()` | `string[]` | All registered measurement types | +| `getVariants` | `()` | `string[]` | Variants under current type | +| `getPositions` | `()` | `string[]` | Positions under current type+variant | +| `getAvailableUnits` | `(measurementType?)` | `string[]` | Units available for a type | +| `getBestUnit` | `(excludeUnits?)` | `object \| null` | Best human-readable unit for current value | +| `setPreferredUnit` | `(type, unit)` | `this` | Override default unit for a type | +| `setChildId` | `(id)` | `this` | Tag container with a child node ID | +| `setChildName` | `(name)` | `this` | Tag container with a child node name | +| `setParentRef` | `(parent)` | `this` | Store reference to parent node | +| `clear` | `()` | `void` | Reset all measurements and chain state | + +### Events + +The internal `emitter` fires `"type.variant.position"` on every `value()` call with: + +```js +{ value, originalValue, unit, sourceUnit, timestamp, position, distance, variant, type, childId, childName, parentRef } +``` + +### Example + +```js +const { MeasurementContainer } = require('generalFunctions'); +const mc = new MeasurementContainer({ windowSize: 5 }); + +mc.type('pressure').variant('static').position('upstream').value(3.2); +mc.type('pressure').variant('static').position('downstream').value(2.8); + +const diff = mc.type('pressure').variant('static').difference(); +// diff = { value: -0.4, avgDiff: -0.4, unit: 'mbar', from: 'downstream', to: 'upstream' } +``` + +--- + +## ConfigManager + +Loads JSON config files from disk and builds merged runtime configs. + +**File:** `src/configs/index.js` + +### Constructor + +```js +new ConfigManager(relPath = '.') +``` + +`relPath` is resolved relative to the configs directory. + +### Methods + +| Method | Signature | Returns | Description | +|---|---|---|---| +| `getConfig` | `(configName)` | `object` | Load and parse `.json` | +| `getAvailableConfigs` | `()` | `string[]` | List config names (without `.json`) | +| `hasConfig` | `(configName)` | `boolean` | Check existence | +| `getBaseConfig` | `()` | `object` | Shortcut for `getConfig('baseConfig')` | +| `buildConfig` | `(nodeName, uiConfig, nodeId, domainConfig?)` | `object` | Merge base schema + UI overrides into a runtime config | +| `createEndpoint` | `(nodeName)` | `string` | Generate browser JS that injects config into `window.EVOLV.nodes` | + +### Example + +```js +const { configManager } = require('generalFunctions'); +const cfg = configManager.buildConfig('measurement', uiConfig, node.id, { + scaling: { enabled: true, inputMin: 0, inputMax: 100 } +}); +``` + +--- + +## ChildRegistrationUtils + +Manages parent-child node relationships: registration, lookup, and structure storage. + +**File:** `src/helper/childRegistrationUtils.js` + +### Constructor + +```js +new ChildRegistrationUtils(mainClass) +``` + +`mainClass` is the parent node instance (must expose `.logger` and optionally `.registerChild()`). + +### Methods + +| Method | Signature | Returns | Description | +|---|---|---|---| +| `registerChild` | `(child, positionVsParent, distance?)` | `Promise` | Register a child node under the parent. Sets up parent refs, measurement context, and stores by softwareType/category. | +| `getChildrenOfType` | `(softwareType, category?)` | `array` | Get children filtered by software type and optional category | +| `getChildById` | `(childId)` | `object \| null` | Lookup a single child by its ID | +| `getAllChildren` | `()` | `array` | All registered children | +| `logChildStructure` | `()` | `void` | Debug-print the full child tree | + +### Example + +```js +const { childRegistrationUtils: CRU } = require('generalFunctions'); +const cru = new CRU(parentNode); +await cru.registerChild(sensorNode, 'upstream'); +cru.getChildrenOfType('measurement'); // [sensorNode] +``` + +--- + +## MenuUtils + +Browser-side UI helper for Node-RED editor. Methods are mixed in from separate modules: toggles, data fetching, URL utils, dropdown population, and HTML generation. + +**File:** `src/helper/menuUtils.js` + +### Constructor + +```js +new MenuUtils() // no parameters; sets isCloud=false, configData=null +``` + +### Key Methods + +**Toggles** -- control UI element visibility: + +| Method | Signature | Description | +|---|---|---| +| `initBasicToggles` | `(elements)` | Bind log-level row visibility to log checkbox | +| `initMeasurementToggles` | `(elements)` | Bind scaling input rows to scaling checkbox | +| `initTensionToggles` | `(elements, node)` | Show/hide tension row based on interpolation method | + +**Data Fetching:** + +| Method | Signature | Returns | Description | +|---|---|---|---| +| `fetchData` | `(url, fallbackUrl)` | `Promise` | Fetch JSON from primary URL; fall back on failure | +| `fetchProjectData` | `(url)` | `Promise` | Fetch project-level data | +| `apiCall` | `(node)` | `Promise` | POST to asset-register API | + +**URL Construction:** + +| Method | Signature | Returns | Description | +|---|---|---|---| +| `getSpecificConfigUrl` | `(nodeName, cloudAPI)` | `{ cloudConfigURL, localConfigURL }` | Build cloud + local config URLs | +| `constructUrl` | `(base, ...paths)` | `string` | Join URL segments safely | +| `constructCloudURL` | `(base, ...paths)` | `string` | Same as `constructUrl`, for cloud endpoints | + +**Dropdown Population:** + +| Method | Signature | Description | +|---|---|---| +| `fetchAndPopulateDropdowns` | `(configUrls, elements, node)` | Cascading supplier > subType > model > unit dropdowns | +| `populateDropdown` | `(htmlElement, options, node, property, callback?)` | Fill a `