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>
124 lines
4.1 KiB
Dart
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());
|
|
});
|
|
}
|