I am writing a flutter application that is build around a computation intensive problem that takes several seconds to minutes to solve. The user triggers the computation via a button and I want to show a CircuarProgressIndicator while the computation goes on.
I also want to test this behaviour. I. e. write a widget test that presses the button, finds a CircularProgressIndicator, waits until that is gone and then inspects the result. However, I was unable to find a way to wait for the ProgressIndicator to disappear.
For reproducibility I built a minimal application that has the same issue. The application wants to compute the 5000th prime number, triggered by a button (full application code at the bottom).
The test looks like this:
testWidgets('Wait for Progress Indicator to finish', (WidgetTester tester) async {
await tester.pumpWidget(const MyApp());
await tester.tap(find.text("Trigger Async"));
await tester.pump();
expect(find.byType(CircularProgressIndicator), findsOneWidget);
await tester.pumpAndSettle();
expect(find.byType(CircularProgressIndicator), findsNothing);
});
This code fails at await tester.pumpAndSettle();
with pumpAndSettle timed out
.
I am aware of this post and therefore I also tried a lot of manual pumps by replacing the await tester.pumpAndSettle();
with
for (int i = 0; i < 10000; i++){
await tester.pump();
}
This causes the last expect
to fail. The tester can still find a CircularProgressIndicator. So that is not solving my problem either.
What am I doing wrong? Is there a way to achieve what I want to do.
Full application code:
main.dart
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const LoadingOverlay(
child: MyHomePage(),
),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
void _doSomethingAsync(BuildContext context) async {
LoadingOverlay.of(context).show();
final p = await compute(getnthPrime, 5000);
LoadingOverlay.of(context).hide();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: TextButton(
child: const Text('Trigger Async'),
onPressed: () => _doSomethingAsync(context),
),
)
);
}
}
Future<int> getnthPrime(int n) async{
int currentPrimeCount = 0;
int candidate = 1;
while(currentPrimeCount < n) {
++candidate;
if (isPrime(candidate)) {
++currentPrimeCount;
}
}
return candidate;
}
bool isPrime(int n) {
int count = 0;
for(int i = 1 ; i <= n; ++i) {
if (n % i == 0) {
++count;
}
}
return count == 2;
}
loading_overlay.dart
class LoadingOverlay extends StatefulWidget {
const LoadingOverlay({Key? key, required this.child}) : super(key: key);
final Widget child;
static _LoadingOverlayState of(BuildContext context) {
return context.findAncestorStateOfType<_LoadingOverlayState>()!;
}
@override
State<LoadingOverlay> createState() => _LoadingOverlayState();
}
class _LoadingOverlayState extends State<LoadingOverlay> {
bool _isLoading = false;
void show() {
setState(() {
_isLoading = true;
});
}
void hide() {
setState(() {
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
widget.child,
if (_isLoading)
const Opacity(
opacity: 0.8,
child: ModalBarrier(dismissible: false, color: Colors.black),
),
if (_isLoading)
const Center(
child: CircularProgressIndicator(),
),
],
);
}
}