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'); });