- specificClass updates for MGC per-pump panel sources. - Output manifest + slice47 basic test for the pump-panel outputs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
157 lines
7.8 KiB
JavaScript
157 lines
7.8 KiB
JavaScript
'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.<pumpId>` 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 <softwareType>_<id> 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');
|
||
});
|