'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const DashboardApi = require('../../src/specificClass.js'); // Build an MGC root with N rotatingMachine children, compose the graph, and // return the MGC dashboard (results[0]). function composeMgcWith(pumpDefs) { const api = new DashboardApi({}); const entries = pumpDefs.map((p) => [p.id, { child: { config: { general: { id: p.id, name: p.name }, functionality: { softwareType: p.softwareType || 'machine', positionVsParent: 'downstream' } } }, position: 'downstream', }]); const root = { config: { general: { id: 'mgc-1', name: 'MGC' }, functionality: { softwareType: 'machineGroupControl', positionVsParent: 'atequipment' } }, childRegistrationUtils: { registeredChildren: new Map(entries) }, }; return { api, dash: api.generateDashboardsForGraph(root)[0].dashboard }; } const PUMPS = [ { id: 'pump-a', name: 'Pump A' }, { id: 'pump-b', name: 'Pump B' }, ]; test('MGC dashboard gains the three pump fan-out panels', () => { const { dash } = composeMgcWith(PUMPS); const titles = dash.panels.filter((p) => p.type === 'timeseries').map((p) => p.title); assert.ok(titles.includes('Pump % Control'), 'missing % control panel'); assert.ok(titles.includes('Pump Predicted Flow vs Demand'), 'missing flow panel'); assert.ok(titles.includes('Pump Predicted Power'), 'missing power panel'); }); test('static group-total panels are replaced by the richer fan-out panels', () => { const { dash } = composeMgcWith(PUMPS); const titles = dash.panels.map((p) => p.title); assert.ok(!titles.includes('Total Flow'), 'static Total Flow should be removed'); assert.ok(!titles.includes('Total Power'), 'static Total Power should be removed'); }); test('% control query targets every pump measurement; ctrl is already percent (no scaling)', () => { const { dash } = composeMgcWith(PUMPS); const panel = dash.panels.find((p) => p.title === 'Pump % Control'); const q = panel.targets[0].query; assert.match(q, /r\._measurement == "Pump A"/); assert.match(q, /r\._measurement == "Pump B"/); assert.match(q, /r\._field == "ctrl"/); assert.ok(!/_value \* 100/.test(q), 'ctrl is 0..100 already — must NOT be ×100 scaled'); assert.equal(panel.fieldConfig.defaults.unit, 'percent'); }); test('% control plots both realized position and commanded setpoint per pump', () => { const { dash } = composeMgcWith(PUMPS); const panel = dash.panels.find((p) => p.title === 'Pump % Control'); const realized = panel.targets.find((t) => /r\._field == "ctrl"/.test(t.query)); const setpoint = panel.targets.find((t) => /ctrl\\\.predicted\\\.atequipment/.test(t.query)); assert.ok(realized, 'missing realized-position (ctrl) series'); assert.ok(setpoint, 'missing commanded-setpoint (ctrl.predicted.atequipment) series'); // childId varies per pump → setpoint must be a regex (=~) prefix match. assert.match(setpoint.query, /r\._field =~ \/\^ctrl\\\.predicted\\\.atequipment\\\.\//); assert.ok(!/\.default/.test(setpoint.query), 'must not hardcode childId .default'); // Legend disambiguation: each series suffixes its _measurement. assert.match(realized.query, /\(realized\)/); assert.match(setpoint.query, /\(setpoint\)/); }); test('setpoint series is drawn dashed to distinguish it from realized', () => { const { dash } = composeMgcWith(PUMPS); const panel = dash.panels.find((p) => p.title === 'Pump % Control'); const ov = panel.fieldConfig.overrides.find((o) => /setpoint/.test(o.matcher.options)); assert.ok(ov, 'missing dashed override for setpoint series'); assert.equal(ov.matcher.id, 'byRegexp'); const lineStyle = ov.properties.find((p) => p.id === 'custom.lineStyle')?.value; assert.equal(lineStyle?.fill, 'dash', 'setpoint must be dashed'); }); test('per-pump flow/power match the position prefix (childId varies per pump)', () => { const { dash } = composeMgcWith(PUMPS); const flowQ = dash.panels.find((p) => p.title === 'Pump Predicted Flow vs Demand').targets[0].query; const powerQ = dash.panels.find((p) => p.title === 'Pump Predicted Power').targets[0].query; // Regex field match (=~), not an exact `.default` key, so it catches // `flow.predicted.atequipment.` whatever the childId is. assert.match(flowQ, /r\._field =~ \/\^flow\\\.predicted\\\.atequipment\\\.\//); assert.match(powerQ, /r\._field =~ \/\^power\\\.predicted\\\.atequipment\\\.\//); assert.ok(!/\.default/.test(flowQ), 'must not hardcode childId .default'); }); test('measurement name falls back to _ when name is unset', () => { const { dash } = composeMgcWith([{ id: 'p9', softwareType: 'rotatingmachine' }]); const panel = dash.panels.find((p) => p.title === 'Pump % Control'); assert.match(panel.targets[0].query, /r\._measurement == "rotatingmachine_p9"/); }); test('flow panel folds in total flow, demand setpoint, demand %, and per-pump flow', () => { const { dash } = composeMgcWith(PUMPS); const panel = dash.panels.find((p) => p.title === 'Pump Predicted Flow vs Demand'); const queries = panel.targets.map((t) => t.query).join('\n'); assert.match(queries, /flow\\\.predicted\\\.atequipment/, 'per-pump flow field'); assert.match(queries, /atEquipment_predicted_flow/, 'group total flow field'); assert.match(queries, /demandFlow/, 'resolved flow setpoint field'); assert.match(queries, /demandPct/, 'demand percent field'); }); test('flow capacity envelope is drawn as dashed min/max lines', () => { const { dash } = composeMgcWith(PUMPS); const panel = dash.panels.find((p) => p.title === 'Pump Predicted Flow vs Demand'); const byName = Object.fromEntries( panel.fieldConfig.overrides.map((o) => [o.matcher.options, o.properties])); for (const cap of ['flowCapacityMin', 'flowCapacityMax']) { const props = byName[cap]; assert.ok(props, `missing override for ${cap}`); const lineStyle = props.find((p) => p.id === 'custom.lineStyle')?.value; assert.equal(lineStyle?.fill, 'dash', `${cap} must be dashed`); } }); test('demand % is placed on a secondary (right) axis in percent', () => { const { dash } = composeMgcWith(PUMPS); const panel = dash.panels.find((p) => p.title === 'Pump Predicted Flow vs Demand'); const props = panel.fieldConfig.overrides.find((o) => o.matcher.options === 'demandPct')?.properties || []; assert.equal(props.find((p) => p.id === 'unit')?.value, 'percent'); assert.equal(props.find((p) => p.id === 'custom.axisPlacement')?.value, 'right'); }); test('power panel folds total power in with per-pump power', () => { const { dash } = composeMgcWith(PUMPS); const panel = dash.panels.find((p) => p.title === 'Pump Predicted Power'); const queries = panel.targets.map((t) => t.query).join('\n'); assert.match(queries, /power\\\.predicted\\\.atequipment/, 'per-pump power field'); assert.match(queries, /atEquipment_predicted_power/, 'group total power field'); }); test('injected panels are exempt from the no-duplication dedup (empty emittedFields)', () => { const { dash } = composeMgcWith(PUMPS); const dynamic = dash.panels.filter((p) => p?.meta?.dynamic === 'mgc-pump-fanout'); assert.equal(dynamic.length, 3); for (const p of dynamic) assert.deepEqual(p.meta.emittedFields, []); }); test('a machineGroup with no pump children keeps the static template panels', () => { const { dash } = composeMgcWith([ { id: 'm1', name: 'Meter', softwareType: 'measurement' }, ]); const titles = dash.panels.map((p) => p.title); assert.ok(titles.includes('Total Flow'), 'static totals must remain when no pumps'); assert.ok(!titles.includes('Pump % Control'), 'no fan-out panels without pumps'); }); test('injected panels reuse the dashboard influxdb datasource uid', () => { const { dash } = composeMgcWith(PUMPS); const panel = dash.panels.find((p) => p.title === 'Pump % Control'); assert.equal(panel.datasource.type, 'influxdb'); assert.equal(panel.datasource.uid, 'cdzg44tv250jkd'); });