69 lines
4.2 KiB
JavaScript
69 lines
4.2 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
|
|
const source = await readFile(new URL('./forecast-chart.js', import.meta.url), 'utf8');
|
|
const { finiteValue, formatNumber, formatTime, chartDomain, seriesSegments, pointIndexAt, forecastSeries } = await import('data:text/javascript;base64,' + Buffer.from(source).toString('base64'));
|
|
const point = (minute, value, duration = 5) => ({ time: new Date(Date.UTC(2026, 9, 6, 10, minute)).toISOString(), validUntil: new Date(Date.UTC(2026, 9, 6, 10, minute + duration)).toISOString(), value });
|
|
const series = { value: row => row.value };
|
|
|
|
test('missing values are not fabricated as zero', () => {
|
|
for (const value of [null, undefined, '12', NaN, Infinity]) assert.equal(finiteValue(value), false);
|
|
assert.equal(finiteValue(0), true);
|
|
assert.equal(formatNumber(null), 'nicht vorhanden');
|
|
assert.equal(chartDomain([point(0, null)], [series]).valid, false);
|
|
});
|
|
test('domains include negative values and interval ends', () => {
|
|
const result = chartDomain([point(0, -10), point(5, 20)], [series]);
|
|
assert.equal(result.low, -10); assert.equal(result.high, 20);
|
|
assert.equal(result.end - result.start, 600000);
|
|
assert.equal(chartDomain([], [series]), null);
|
|
assert.equal(chartDomain([{ time: 'invalid', value: 5 }], [series]), null);
|
|
});
|
|
test('power curves cover entire intervals and split missing values', () => {
|
|
const rows = [point(0, 5), point(5, null), point(10, 7)];
|
|
const segments = seriesSegments(rows, series, value => value, value => value);
|
|
assert.equal(segments.length, 2); assert.equal(segments[0].length, 2);
|
|
assert.equal(segments[0][1][0], Date.parse(rows[0].validUntil));
|
|
});
|
|
test('time gaps and reversed intervals do not join curves', () => {
|
|
assert.equal(seriesSegments([point(0, 5), point(10, 7)], series, x => x, y => y).length, 2);
|
|
assert.equal(seriesSegments([point(0, 5, -1)], series, x => x, y => y).length, 0);
|
|
});
|
|
test('cumulative and SOC samples use the interval end', () => {
|
|
const row = point(0, 7);
|
|
assert.deepEqual(seriesSegments([row], { ...series, position: 'end' }, x => x, y => y), [[[Date.parse(row.validUntil), 7]]]);
|
|
});
|
|
test('pointer selection identifies the containing interval and clamps endpoints', () => {
|
|
const rows = [point(0, 1), point(5, 2), point(10, 3)];
|
|
assert.equal(pointIndexAt(rows, Date.parse(rows[0].time) - 100), 0);
|
|
assert.equal(pointIndexAt(rows, Date.parse(rows[1].time)), 1);
|
|
assert.equal(pointIndexAt(rows, Date.parse(rows[1].time) + 1000), 1);
|
|
assert.equal(pointIndexAt(rows, Date.parse(rows[2].validUntil) + 100), 2);
|
|
});
|
|
test('time labels use Zurich and reject invalid timestamps', () => {
|
|
assert.match(formatTime('2026-10-06T10:00:00Z'), /12:00/);
|
|
assert.equal(formatTime('invalid'), 'nicht vorhanden');
|
|
});
|
|
test('full forecast horizon takes precedence without extending optimized plan points', () => {
|
|
const optimized = [point(0, 1)], full = [point(0, 1), point(5, 2)];
|
|
const plan = { points: optimized, forecastPoints: full };
|
|
assert.deepEqual(forecastSeries(plan), { fullHorizon: true, points: full });
|
|
assert.equal(plan.points.length, 1);
|
|
});
|
|
test('older or empty forecasts fall back to explicitly limited optimized points', () => {
|
|
const optimized = [point(0, 1)];
|
|
for (const forecastPoints of [undefined, null, [], {}, 'invalid']) {
|
|
assert.deepEqual(forecastSeries({ points: optimized, forecastPoints }), { fullHorizon: false, points: optimized });
|
|
}
|
|
assert.deepEqual(forecastSeries({}), { fullHorizon: false, points: [] });
|
|
});
|
|
test('chart styles stay scoped to the planner, including grouped and mobile selectors', async () => {
|
|
const css = await readFile(new URL('./netplan-v4.css', import.meta.url), 'utf8');
|
|
const selectors = [...css.matchAll(/([^{}]+)\{/g)].flatMap(match => match[1].split(',').map(selector => selector.trim()));
|
|
const chartSelectors = selectors.filter(selector => selector.includes('.forecast-chart'));
|
|
assert.ok(chartSelectors.length > 20, 'chart selectors are present and checked');
|
|
for (const selector of chartSelectors) assert.match(selector, /^\.forecast-planner\s+/, `unscoped chart selector: ${selector}`);
|
|
assert.match(css, /\.forecast-planner \.forecast-chart\s*\{[^}]*height:\s*auto\s*;/, 'legacy SVG height must not constrain the new chart sections');
|
|
});
|