import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ocideck/models/chart.dart'; import 'package:ocideck/models/slide.dart'; import 'package:ocideck/widgets/slides/slide_preview.dart'; Widget _host(ChartSpec spec, {bool presentationMode = false}) { return MaterialApp( home: Scaffold( body: Center( child: SizedBox( width: 800, height: 450, child: SlidePreviewWidget( slide: Slide.create( SlideType.chart, ).copyWith(customMarkdown: spec.toBlock()), presentationMode: presentationMode, ), ), ), ), ); } void main() { testWidgets('chart title stays above the plot area', (tester) async { const spec = ChartSpec( type: ChartType.bar, title: 'Omzet per kwartaal', x: ['Q1', 'Q2'], series: [ ChartSeries(name: '2026', data: [10, 14]), ], ); await tester.pumpWidget(_host(spec)); await tester.pump(); final titleBottom = tester.getBottomLeft(find.text(spec.title)).dy; final plotTop = tester.getTopLeft(find.byType(BarChart)).dy; expect(titleBottom, lessThan(plotTop)); expect(tester.takeException(), isNull); }); testWidgets('pie renders one chart per series with labels as slices', ( tester, ) async { const spec = ChartSpec( type: ChartType.pie, title: 'Verdeling', x: ['Team A', 'Team B'], series: [ ChartSeries(name: 'Gereed', data: [70, 40], color: '#10B981'), ChartSeries(name: 'Open', data: [30, 60], color: '#EF4444'), ], ); await tester.pumpWidget(_host(spec)); await tester.pump(); expect(find.byType(PieChart), findsNWidgets(2)); expect(find.text('Team A'), findsOneWidget); expect(find.text('Team B'), findsOneWidget); expect(find.text('Gereed'), findsOneWidget); expect(find.text('Open'), findsOneWidget); final pieRect = tester.getRect(find.byType(PieChart).first); final titleRect = tester.getRect(find.text('Gereed')); expect(titleRect.left, greaterThan(pieRect.center.dx)); expect(tester.takeException(), isNull); }); testWidgets('bar chart uses most of the available vertical plot area', ( tester, ) async { const spec = ChartSpec( type: ChartType.bar, title: 'Compacte titel', x: ['A', 'B', 'C'], series: [ ChartSeries(name: 'Waarde', data: [10, 20, 15]), ], ); await tester.pumpWidget(_host(spec)); await tester.pump(); expect(tester.getSize(find.byType(BarChart)).height, greaterThan(260)); expect(tester.takeException(), isNull); }); testWidgets('chart surface fills the remaining slide height', (tester) async { const spec = ChartSpec( type: ChartType.bar, title: 'Titel', x: ['A', 'B'], series: [ ChartSeries(name: 'Waarde', data: [10, 20]), ], ); await tester.pumpWidget(_host(spec)); await tester.pump(); final slide = tester.getRect(find.byType(SlidePreviewWidget)); final surface = tester.getRect(find.byKey(const ValueKey('chart-surface'))); expect(surface.height, greaterThan(slide.height * 0.72)); expect(slide.bottom - surface.bottom, lessThan(slide.height * 0.04)); expect(tester.takeException(), isNull); }); testWidgets('bar and line hover tooltips show labels and values', ( tester, ) async { const barSpec = ChartSpec( type: ChartType.bar, x: ['Januari'], series: [ ChartSeries(name: 'Omzet', data: [42]), ], ); await tester.pumpWidget(_host(barSpec)); final bar = tester.widget(find.byType(BarChart)); final barItem = bar.data.barTouchData.touchTooltipData.getTooltipItem( bar.data.barGroups.single, 0, bar.data.barGroups.single.barRods.single, 0, ); expect(barItem?.text, 'Januari\nOmzet: 42'); const lineSpec = ChartSpec( type: ChartType.line, x: ['Februari'], series: [ ChartSeries(name: 'Bezoekers', data: [17.5]), ], ); await tester.pumpWidget(_host(lineSpec)); final line = tester.widget(find.byType(LineChart)); final spot = LineBarSpot( line.data.lineBarsData.single, 0, line.data.lineBarsData.single.spots.single, ); final lineItems = line.data.lineTouchData.touchTooltipData.getTooltipItems([ spot, ]); expect(lineItems.single?.text, 'Februari\nBezoekers: 17.5'); }); testWidgets('pie hover shows the underlying category value', (tester) async { const spec = ChartSpec( type: ChartType.pie, x: ['Gereed', 'Open'], series: [ ChartSeries(name: 'Status', data: [70, 30]), ], ); await tester.pumpWidget(_host(spec)); await tester.pump(); final pie = tester.widget(find.byType(PieChart)); final section = pie.data.sections.first; pie.data.pieTouchData.touchCallback!( const FlPointerHoverEvent(PointerHoverEvent()), PieTouchResponse( touchLocation: Offset.zero, touchedSection: PieTouchedSection(section, 0, 0, section.radius), ), ); await tester.pump(); expect(find.byKey(const ValueKey('pie-hover-tooltip')), findsOneWidget); expect(find.text('Gereed: 70'), findsOneWidget); }); testWidgets('bar chart draws the configured min/max bound lines', ( tester, ) async { const spec = ChartSpec( type: ChartType.bar, x: ['Q1'], series: [ ChartSeries(name: 'Omzet', data: [10]), ], minBound: 5, maxBound: 20, ); await tester.pumpWidget(_host(spec)); await tester.pump(); final bar = tester.widget(find.byType(BarChart)); final ys = bar.data.extraLinesData.horizontalLines.map((l) => l.y).toList(); expect(ys, containsAll([5, 20])); // The max bound widens the axis so the line stays inside the plot. expect(bar.data.maxY, greaterThanOrEqualTo(20)); expect(tester.takeException(), isNull); }); testWidgets('hovering a legend entry fades the other series', (tester) async { const spec = ChartSpec( type: ChartType.line, x: ['Q1', 'Q2'], series: [ ChartSeries(name: 'Alpha', data: [10, 12], color: '#2563EB'), ChartSeries(name: 'Beta', data: [8, 9], color: '#EF4444'), ], ); await tester.pumpWidget(_host(spec)); await tester.pump(); var line = tester.widget(find.byType(LineChart)); expect(line.data.lineBarsData[0].color!.a, 1.0); expect(line.data.lineBarsData[1].color!.a, 1.0); final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(location: Offset.zero); addTearDown(gesture.removePointer); await gesture.moveTo(tester.getCenter(find.text('Alpha'))); await tester.pumpAndSettle(); line = tester.widget(find.byType(LineChart)); expect(line.data.lineBarsData[0].color!.a, 1.0); // hovered stays solid expect(line.data.lineBarsData[1].color!.a, lessThan(1.0)); // other fades expect(tester.takeException(), isNull); }); testWidgets('presentation mode enlarges chart labels', (tester) async { const spec = ChartSpec( type: ChartType.bar, x: ['Categorie'], series: [ ChartSeries(name: 'Waarde', data: [10]), ], ); await tester.pumpWidget(_host(spec)); final normal = tester.widget(find.text('Categorie').first); final normalSize = normal.style!.fontSize!; expect(normalSize, lessThanOrEqualTo(12)); await tester.pumpWidget(_host(spec, presentationMode: true)); final presented = tester.widget(find.text('Categorie').first); expect(presented.style!.fontSize!, greaterThan(normalSize)); }); testWidgets('dense axis labels are thinned and stay inside the slide', ( tester, ) async { const labels = [ 'Januari bijzonder lang', 'Februari bijzonder lang', 'Maart bijzonder lang', 'April bijzonder lang', 'Mei bijzonder lang', 'Juni bijzonder lang', 'Juli bijzonder lang', 'Augustus bijzonder lang', 'September bijzonder lang', 'Oktober bijzonder lang', 'November bijzonder lang', 'December bijzonder lang', ]; const spec = ChartSpec( type: ChartType.line, x: labels, series: [ ChartSeries( name: 'Waarde', data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], ), ], ); await tester.pumpWidget(_host(spec)); await tester.pump(); final visibleLabels = [ for (final label in labels) if (find.text(label).evaluate().isNotEmpty) label, ]; expect(visibleLabels.length, lessThanOrEqualTo(8)); final slideRect = tester.getRect(find.byType(SlidePreviewWidget)); for (final label in visibleLabels) { final rect = tester.getRect(find.text(label).first); expect(slideRect.contains(rect.topLeft), isTrue); expect(slideRect.contains(rect.bottomRight), isTrue); } expect(tester.takeException(), isNull); }); testWidgets( 'pie shows at most two series and keeps labels inside the slide', (tester) async { const spec = ChartSpec( type: ChartType.pie, title: 'Veel gegevens', x: [ 'Een uitzonderlijk lang eerste label', 'Een uitzonderlijk lang tweede label', 'Een uitzonderlijk lang derde label', 'Een uitzonderlijk lang vierde label', 'Een uitzonderlijk lang vijfde label', 'Een uitzonderlijk lang zesde label', ], series: [ ChartSeries(name: 'Een', data: [1, 2, 3, 4, 5, 6]), ChartSeries(name: 'Twee', data: [2, 3, 4, 5, 6, 7]), ChartSeries(name: 'Drie', data: [3, 4, 5, 6, 7, 8]), ChartSeries(name: 'Vier', data: [4, 5, 6, 7, 8, 9]), ChartSeries(name: 'Vijf', data: [5, 6, 7, 8, 9, 10]), ChartSeries(name: 'Zes', data: [6, 7, 8, 9, 10, 11]), ], ); await tester.pumpWidget(_host(spec)); await tester.pump(); expect(find.byType(PieChart), findsNWidgets(2)); expect(find.text('Drie'), findsNothing); final legendTop = tester.getTopLeft(find.text(spec.x.first)).dy; for (final chart in tester.widgetList(find.byType(PieChart))) { final box = tester.renderObject(find.byWidget(chart)); final bottom = box.localToGlobal(Offset(0, box.size.height)).dy; expect(bottom, lessThanOrEqualTo(legendTop)); } final slideRect = tester.getRect(find.byType(SlidePreviewWidget)); for (final label in spec.x) { final rect = tester.getRect(find.text(label)); expect(slideRect.contains(rect.topLeft), isTrue); expect(slideRect.contains(rect.bottomRight), isTrue); } expect(tester.takeException(), isNull); }, ); }