chain-studio/test/progress_bar_visual_test.dart
flemming-it 1549334767 feat(studio): real install progress in the store dialog and the wizard
Both surfaces showed an indeterminate spinner for the whole install,
because the hub only offered a unary call. They now follow
HubAdmin/InstallModuleStream:

- The store's install dialog shows the phase in plain language, the
  overall percentage, megabytes while the size is known, and a seconds
  counter. The counter is the second, independent signal: it keeps
  running even if the hub goes quiet, which is what tells 'slow' apart
  from 'stuck'.
- The setup wizard shows the same bar per module instead of a spinner
  inside the button.
- New strings in both locales for the phases, the byte line and the
  elapsed counter.

progress_bar_visual_test writes the four waiting states to
build/progress/ in either theme, so the result can be judged without
launching the desktop app. Writing it turned up a real trap worth
recording: awaiting toImage() directly in a widget test leaves the
binding waiting forever - the file passed in six seconds, then sat
there until the ten-minute timeout failed the whole suite.
tester.runAsync() is the fix. Full suite: 186 passed.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
2026-09-08 14:00:24 +02:00

124 lines
4.1 KiB
Dart

// 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<void> _capture(WidgetTester tester, String name) async {
final boundary = tester.renderObject<RenderRepaintBoundary>(
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());
});
}