Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
"type": "module",
"private": true,
"scripts": {
"postinstall": "node ./scripts/copy-uplot.js",
"test": "mkdir -p reports coverage && NODE_V8_COVERAGE=coverage node --test --experimental-test-coverage --test-reporter=spec --test-reporter-destination=stdout --test-reporter=junit --test-reporter-destination=reports/javascript-junit.xml && node ./scripts/export-coverage.js"
},
"dependencies": {
"uplot": "^1.6.30"
},
"devDependencies": {
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
Expand Down
24 changes: 15 additions & 9 deletions web/public/assets/js/app/__tests__/charts-page.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,19 @@ test('initializeChartsPage renders the telemetry charts when snapshots are avail
},
]);
let receivedOptions = null;
const renderCharts = (node, options) => {
let mountedModels = null;
const createCharts = (node, options) => {
receivedOptions = options;
return '<section class="node-detail__charts">Charts</section>';
return { chartsHtml: '<section class="node-detail__charts">Charts</section>', chartModels: [{ id: 'power' }] };
};
const result = await initializeChartsPage({ document: documentStub, fetchImpl, renderCharts });
const mountCharts = (chartModels, options) => {
mountedModels = { chartModels, options };
return [];
};
const result = await initializeChartsPage({ document: documentStub, fetchImpl, createCharts, mountCharts });
assert.equal(result, true);
assert.equal(container.innerHTML.includes('node-detail__charts'), true);
assert.equal(mountedModels.chartModels.length, 1);
assert.ok(receivedOptions);
assert.equal(receivedOptions.chartOptions.windowMs, 604_800_000);
assert.equal(typeof receivedOptions.chartOptions.lineReducer, 'function');
Expand Down Expand Up @@ -118,8 +124,8 @@ test('initializeChartsPage shows an error message when fetching fails', async ()
const fetchImpl = async () => {
throw new Error('network');
};
const renderCharts = () => '<section>unused</section>';
const result = await initializeChartsPage({ document: documentStub, fetchImpl, renderCharts });
const createCharts = () => ({ chartsHtml: '<section>unused</section>', chartModels: [] });
const result = await initializeChartsPage({ document: documentStub, fetchImpl, createCharts });
assert.equal(result, false);
assert.equal(container.innerHTML.includes('Failed to load telemetry charts.'), true);
});
Expand All @@ -136,8 +142,8 @@ test('initializeChartsPage handles missing containers and empty telemetry snapsh
},
};
const fetchImpl = async () => createResponse(200, []);
const renderCharts = () => '';
const result = await initializeChartsPage({ document: documentStub, fetchImpl, renderCharts });
const createCharts = () => ({ chartsHtml: '', chartModels: [] });
const result = await initializeChartsPage({ document: documentStub, fetchImpl, createCharts });
assert.equal(result, true);
assert.equal(container.innerHTML.includes('Telemetry snapshots are unavailable.'), true);
});
Expand All @@ -155,8 +161,8 @@ test('initializeChartsPage shows a status when rendering produces no markup', as
aggregates: { voltage: { avg: 3.9 } },
},
]);
const renderCharts = () => '';
const result = await initializeChartsPage({ document: documentStub, fetchImpl, renderCharts });
const createCharts = () => ({ chartsHtml: '', chartModels: [] });
const result = await initializeChartsPage({ document: documentStub, fetchImpl, createCharts });
assert.equal(result, true);
assert.equal(container.innerHTML.includes('Telemetry snapshots are unavailable.'), true);
});
Expand Down
20 changes: 20 additions & 0 deletions web/public/assets/js/app/__tests__/node-detail-overlay.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,26 @@ test('createNodeDetailOverlayManager renders fetched markup and restores focus',
assert.equal(focusTarget.focusCalled, true);
});

test('createNodeDetailOverlayManager mounts telemetry charts for overlay content', async () => {
const { document, content } = createOverlayHarness();
const chartModels = [{ id: 'power' }];
let mountCall = null;
const manager = createNodeDetailOverlayManager({
document,
fetchNodeDetail: async () => ({ html: '<section class="node-detail">Charts</section>', chartModels }),
mountCharts: (models, options) => {
mountCall = { models, options };
return [];
},
});
assert.ok(manager);
await manager.open({ nodeId: '!alpha' });
assert.equal(content.innerHTML.includes('Charts'), true);
assert.ok(mountCall);
assert.equal(mountCall.models, chartModels);
assert.equal(mountCall.options.root, content);
});

test('createNodeDetailOverlayManager surfaces errors and supports escape closing', async () => {
const { document, overlay, content } = createOverlayHarness();
const errors = [];
Expand Down
69 changes: 36 additions & 33 deletions web/public/assets/js/app/__tests__/node-page.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ const {
categoriseNeighbors,
renderNeighborGroups,
renderSingleNodeTable,
createTelemetryCharts,
renderTelemetryCharts,
buildUPlotChartConfig,
renderMessages,
renderTraceroutes,
renderTracePath,
Expand Down Expand Up @@ -386,23 +388,10 @@ test('renderTelemetryCharts renders condensed scatter charts when telemetry exis
},
};
const html = renderTelemetryCharts(node, { nowMs });
const fmt = new Date(nowMs);
const expectedDate = String(fmt.getDate()).padStart(2, '0');
assert.equal(html.includes('node-detail__charts'), true);
assert.equal(html.includes('Power metrics'), true);
assert.equal(html.includes('Environmental telemetry'), true);
assert.equal(html.includes('Battery (%)'), true);
assert.equal(html.includes('Voltage (V)'), true);
assert.equal(html.includes('Current (A)'), true);
assert.equal(html.includes('Channel utilization (%)'), true);
assert.equal(html.includes('Air util TX (%)'), true);
assert.equal(html.includes('Utilization (%)'), true);
assert.equal(html.includes('Gas resistance (\u03a9)'), true);
assert.equal(html.includes('Air quality'), true);
assert.equal(html.includes('IAQ index'), true);
assert.equal(html.includes('Temperature (\u00b0C)'), true);
assert.equal(html.includes(expectedDate), true);
assert.equal(html.includes('node-detail__chart-point'), true);
assert.equal(html.includes('node-detail__chart-plot'), true);
});

test('renderTelemetryCharts expands upper bounds when overflow metrics exceed defaults', () => {
Expand Down Expand Up @@ -433,12 +422,18 @@ test('renderTelemetryCharts expands upper bounds when overflow metrics exceed de
},
},
};
const html = renderTelemetryCharts(node, { nowMs });
assert.match(html, />7\.2<\/text>/);
assert.match(html, />3\.6<\/text>/);
assert.match(html, />45<\/text>/);
assert.match(html, />650<\/text>/);
assert.match(html, />1100<\/text>/);
const { chartModels } = createTelemetryCharts(node, { nowMs });
const powerChart = chartModels.find(model => model.id === 'power');
const environmentChart = chartModels.find(model => model.id === 'environment');
const airChart = chartModels.find(model => model.id === 'airQuality');
const powerConfig = buildUPlotChartConfig(powerChart);
const envConfig = buildUPlotChartConfig(environmentChart);
const airConfig = buildUPlotChartConfig(airChart);
assert.equal(powerConfig.options.scales.voltage.range()[1], 7.2);
assert.equal(powerConfig.options.scales.current.range()[1], 3.6);
assert.equal(envConfig.options.scales.temperature.range()[1], 45);
assert.equal(airConfig.options.scales.iaq.range()[1], 650);
assert.equal(airConfig.options.scales.pressure.range()[1], 1100);
});

test('renderTelemetryCharts keeps default bounds when metrics stay within limits', () => {
Expand Down Expand Up @@ -469,11 +464,17 @@ test('renderTelemetryCharts keeps default bounds when metrics stay within limits
},
},
};
const html = renderTelemetryCharts(node, { nowMs });
assert.match(html, />6\.0<\/text>/);
assert.match(html, />3\.0<\/text>/);
assert.match(html, />40<\/text>/);
assert.match(html, />500<\/text>/);
const { chartModels } = createTelemetryCharts(node, { nowMs });
const powerChart = chartModels.find(model => model.id === 'power');
const environmentChart = chartModels.find(model => model.id === 'environment');
const airChart = chartModels.find(model => model.id === 'airQuality');
const powerConfig = buildUPlotChartConfig(powerChart);
const envConfig = buildUPlotChartConfig(environmentChart);
const airConfig = buildUPlotChartConfig(airChart);
assert.equal(powerConfig.options.scales.voltage.range()[1], 6);
assert.equal(powerConfig.options.scales.current.range()[1], 3);
assert.equal(envConfig.options.scales.temperature.range()[1], 40);
assert.equal(airConfig.options.scales.iaq.range()[1], 500);
});

test('renderNodeDetailHtml composes the table, neighbors, and messages', () => {
Expand Down Expand Up @@ -589,17 +590,18 @@ test('fetchNodeDetailHtml renders the node layout for overlays', async () => {
neighbors: [],
rawSources: { node: { node_id: '!alpha', role: 'CLIENT', short_name: 'ALPH' } },
});
const html = await fetchNodeDetailHtml(reference, {
const result = await fetchNodeDetailHtml(reference, {
refreshImpl,
fetchImpl,
renderShortHtml: short => `<span class="short-name">${short}</span>`,
returnState: true,
});
assert.equal(calledUrls.some(url => url.includes('/api/messages/!alpha')), true);
assert.equal(calledUrls.some(url => url.includes('/api/traces/!alpha')), true);
assert.equal(html.includes('Example Alpha'), true);
assert.equal(html.includes('Overlay hello'), true);
assert.equal(html.includes('Traceroutes'), true);
assert.equal(html.includes('node-detail__table'), true);
assert.equal(result.html.includes('Example Alpha'), true);
assert.equal(result.html.includes('Overlay hello'), true);
assert.equal(result.html.includes('Traceroutes'), true);
assert.equal(result.html.includes('node-detail__table'), true);
});

test('fetchNodeDetailHtml hydrates traceroute nodes with API metadata', async () => {
Expand Down Expand Up @@ -637,16 +639,17 @@ test('fetchNodeDetailHtml hydrates traceroute nodes with API metadata', async ()
rawSources: { node: { node_id: '!origin', role: 'CLIENT', short_name: 'ORIG' } },
});

const html = await fetchNodeDetailHtml(reference, {
const result = await fetchNodeDetailHtml(reference, {
refreshImpl,
fetchImpl,
renderShortHtml: short => `<span class="short-name">${short}</span>`,
returnState: true,
});

assert.equal(calledUrls.some(url => url.includes('/api/nodes/!relay')), true);
assert.equal(calledUrls.some(url => url.includes('/api/nodes/!target')), true);
assert.equal(html.includes('RLY1'), true);
assert.equal(html.includes('TGT1'), true);
assert.equal(result.html.includes('RLY1'), true);
assert.equal(result.html.includes('TGT1'), true);
});

test('fetchNodeDetailHtml requires a node identifier reference', async () => {
Expand Down
Loading
Loading