/** * Tests des Liniendiagramms. * * Sie pruefen die eine Eigenschaft, wegen der dieses Diagramm selbst geschrieben * wurde: **Eine Luecke wird nicht ueberbrueckt und nicht auf null gezogen.** * Beides ergaebe eine Kurve, die etwas anderes behauptet als die Daten hergeben. */ import { render, screen } from '@testing-library/react'; import { describe, expect, it } from 'vitest'; import { LineChart } from './LineChart'; import type { DataPoint, Series } from './metricsApi'; /** Baut einen Punkt mit Wert. */ function pointWithValue(minuteOffset: number, dataValue: number): DataPoint { return { timestamp: new Date(Date.UTC(2026, 7, 12, 0, minuteOffset)).toISOString(), value: dataValue, has_value: true, sample_count: 1, }; } /** Baut einen Punkt ohne Messung. */ function pointWithoutValue(minuteOffset: number): DataPoint { return { timestamp: new Date(Date.UTC(2026, 7, 12, 0, minuteOffset)).toISOString(), has_value: false, sample_count: 0, }; } /** Baut eine Reihe aus den uebergebenen Punkten. */ function seriesOf(points: readonly DataPoint[]): Series { return { name: 'test', label: 'Testreihe', unit: 'count', points }; } /** Liest alle gezeichneten Linienzuege aus dem Dokument. */ function readPathDefinitions(container: HTMLElement): string[] { return Array.from(container.querySelectorAll('path')).map( (pathElement) => pathElement.getAttribute('d') ?? '', ); } describe('Liniendiagramm', () => { it('unterbricht die Linie an einer Luecke, statt sie zu ueberbruecken', () => { // Zwei Messungen, dazwischen ein Zeitfenster ohne Lauf. const { container } = render( , ); const pathDefinitions = readPathDefinitions(container); // Zwei getrennte Linienzuege — nicht einer, der die Luecke ueberspringt. expect(pathDefinitions).toHaveLength(2); for (const pathDefinition of pathDefinitions) { // Jeder Abschnitt beginnt mit einem eigenen Move-Befehl. Ein einziger // Linienzug mit zwei M-Befehlen waere zwar optisch gleich, liesse sich // aber nicht mehr als getrennt erkennen. expect(pathDefinition.startsWith('M')).toBe(true); expect(pathDefinition.split('M').length - 1).toBe(1); } }); it('zieht eine Luecke nicht auf den Nullpunkt', () => { const { container } = render( , ); const pathDefinitions = readPathDefinitions(container); const allCoordinates = pathDefinitions.join(' '); // Bei einem Maximum von 100 liegt der Nullpunkt am unteren Rand der // Zeichenflaeche. Taucht er auf, wurde die Luecke als Null gezeichnet — und // die Kurve behauptet einen Einbruch, den es nicht gab. const lowestDrawnPosition = Math.max( ...Array.from(allCoordinates.matchAll(/,(\d+\.\d)/g)).map((match) => Number(match[1])), ); expect(lowestDrawnPosition).toBeLessThan(190); }); it('zeichnet eine einzelne Messung als Punkt', () => { // Eine einzelne Messung ergaebe eine Linie der Laenge null und waere // unsichtbar — die Kurve saehe aus wie „nichts gemessen". const { container } = render( , ); expect(container.querySelectorAll('circle')).toHaveLength(1); }); it('sagt bei leerer Reihe, dass nichts gemessen wurde', () => { render( , ); // Kein leeres Achsenkreuz: Das saehe aus wie eine Kurve auf null. expect(screen.getByText(/keine Daten/)).toBeInTheDocument(); expect(screen.getByText(/nicht/)).toBeInTheDocument(); }); it('beginnt die Werteachse bei null', () => { // Eine abgeschnittene Achse laesst kleine Schwankungen wie Einbrueche // aussehen — der haeufigste Weg, mit korrekten Zahlen etwas Falsches zu // zeigen. render( , ); // Die Achse laeuft von null bis zum groessten Wert der Reihe. expect(screen.getByText('0')).toBeInTheDocument(); expect(screen.getByText('102')).toBeInTheDocument(); }); });