// Renders the progress bar in the states an operator actually meets // and writes PNGs to build/progress/, so the visual result can be // judged without launching the desktop app and stealing focus. // // Not a golden comparison: there is no committed reference to diff // against. It exists to produce evidence, and to guard that every one // of these states renders at all in both themes. import 'dart:io'; import 'dart:ui' as ui; import 'package:chain_studio/theme/theme.dart'; import 'package:chain_studio/widgets/chain_progress_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; class _Case { final String label; final Widget bar; const _Case(this.label, this.bar); } const _cases = <_Case>[ _Case( 'Nichts messbar: unbestimmt statt leer', ChainProgressBar(stage: 'Modul wird gesucht'), ), _Case( 'Download mit Groesse', ChainProgressBar( value: 0.42, stage: 'Wird heruntergeladen', detail: '5,0 von 12,0 MB', ), ), _Case( 'Wert steht, Arbeit laeuft weiter', ChainProgressBar(value: 0.74, stage: 'Wird geprueft und entpackt'), ), _Case( 'Fertig: nichts bewegt sich mehr', ChainProgressBar(value: 1.0, busy: false, stage: 'Fertig'), ), ]; Widget _sheet(ThemeData theme, String title) => MaterialApp( theme: theme, debugShowCheckedModeBanner: false, home: Scaffold( body: RepaintBoundary( key: const ValueKey('sheet'), child: Center( child: SizedBox( width: 520, child: Card( margin: const EdgeInsets.all(24), child: Padding( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ Text(title, style: theme.textTheme.titleMedium), const SizedBox(height: 20), for (final c in _cases) ...[ Text(c.label, style: theme.textTheme.labelSmall), const SizedBox(height: 6), c.bar, const SizedBox(height: 22), ], ], ), ), ), ), ), ), ), ); Future _capture(WidgetTester tester, String name) async { final boundary = tester.renderObject( find.byKey(const ValueKey('sheet')), ); // toImage is real asynchronous engine work. Awaiting it directly in a // widget test leaves the binding waiting forever afterwards, which // showed up as this file passing in six seconds and then sitting // there until the ten-minute timeout killed the whole suite. final image = await tester.runAsync(() => boundary.toImage(pixelRatio: 2)); if (image == null) return; final bytes = await tester.runAsync( () => image.toByteData(format: ui.ImageByteFormat.png), ); final dir = Directory('build/progress')..createSync(recursive: true); File('${dir.path}/$name.png').writeAsBytesSync(bytes!.buffer.asUint8List()); } void main() { // One capture per run: a second toImage() in the same test binding // never completes, so the theme comes from the environment and the // sheet is rendered twice from outside: // // flutter test test/progress_bar_visual_test.dart // PROGRESS_THEME=dark flutter test test/progress_bar_visual_test.dart testWidgets('renders every waiting state', (tester) async { final dark = Platform.environment['PROGRESS_THEME'] == 'dark'; tester.view.physicalSize = const Size(1100, 900); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.reset); await tester.pumpWidget( _sheet( dark ? ChainTheme.dark() : ChainTheme.light(), dark ? 'Dunkel' : 'Hell', ), ); // Mid-sweep, so the highlight sits inside the bar in the capture. await tester.pump(const Duration(milliseconds: 900)); expect(find.byType(ChainProgressBar), findsNWidgets(_cases.length)); await _capture(tester, dark ? 'states-dark' : 'states-light'); await tester.pumpWidget(const SizedBox()); }); }