error `pumpAndSettle timed out` MAYBE due to riverpod

17.7k views Asked by At

I'm stuck with a widget test and I could use some help
to reproduce the behavior please run the code sample below

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'home_page.dart';

void main() => runApp(
      const ProviderScope(
        child: MaterialApp(
          home: Material(
            child: MyHomePage(),
          ),
        ),
      ),
    );
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

extension RoundX on double {
  double roundToPrecision(int n) {
    final f = pow(10, n);
    return (this * f).round() / f;
  }
}

final tasksPod = Provider<List<Future<void> Function()>>(
  (ref) => [
    for (var i = 0; i < 10; ++i)
      () async {
        await Future.delayed(kThemeAnimationDuration);
      }
  ],
);

final progressPod = Provider.autoDispose<ValueNotifier<double>>((ref) {
  final notifier = ValueNotifier<double>(0);
  ref.onDispose(notifier.dispose);
  return notifier;
});

class MyHomePage extends HookWidget {
  const MyHomePage() : super(key: const ValueKey('MyHomePage'));

  @override
  Widget build(BuildContext context) {
    final progress = useProvider(progressPod);
    final tasks = useProvider(tasksPod);
    useMemoized(() async {
      final steps = tasks.length;
      if (steps < 1) {
        progress.value = 1;
      } else {
        for (final task in tasks) {
          final current = progress.value;
          if (current >= 1) {
            break;
          }
          await task();
          final value = (current + 1 / steps).roundToPrecision(1);
          print('$value');
          progress.value = value;
        }
      }
    });
    return Center(
      child: ValueListenableBuilder<double>(
        valueListenable: progress,
        child: const FlutterLogo(),
        builder: (context, value, child) =>
            value < 1 ? const CircularProgressIndicator() : child!,
      ),
    );
  }
}

running the app everything is fine

✓  Built build/app/outputs/flutter-apk/app-debug.apk.
Installing build/app/outputs/flutter-apk/app.apk...                 4.7s
Syncing files to device Pixel 3a...                                 93ms

Flutter run key commands.
r Hot reload. 
R Hot restart.
h Repeat this help message.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

 Running with sound null safety 

An Observatory debugger and profiler on Pixel 3a is available at: http://127.0.0.1:36517/50vVndYZ3l4=/
I/flutter (19990): 0.1
I/flutter (19990): 0.2
I/flutter (19990): 0.3
I/flutter (19990): 0.4
I/flutter (19990): 0.5
I/flutter (19990): 0.6
I/flutter (19990): 0.7
The Flutter DevTools debugger and profiler on Pixel 3a is available at: http://127.0.0.1:9101?uri=http%3A%2F%2F127.0.0.1%3A36517%2F50vVndYZ3l4%3D%2F
I/flutter (19990): 0.8
I/flutter (19990): 0.9
I/flutter (19990): 1.0
Application finished.

but fails this test

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:timeout_issue/home_page.dart';

void main() {
  testWidgets(
      'WHEN tasks are not completed'
      'THEN shows `CircularProgressIndicator`', (tester) async {
    TestWidgetsFlutterBinding.ensureInitialized();

    await tester.runAsync(() async {
      await tester.pumpWidget(
        ProviderScope(
          child: const MaterialApp(
            home: Material(
              child: MyHomePage(),
            ),
          ),
        ),
      );

      await tester.pumpAndSettle(kThemeAnimationDuration);

      expect(
        find.byType(CircularProgressIndicator),
        findsOneWidget,
        reason: 'CircularProgressIndicator should be shown',
      );
    });
  });
}

with this output

00:05 +0: WHEN tasks are not completedTHEN shows `CircularProgressIndicator`                                                                                                                                
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following assertion was thrown while running async test code:
pumpAndSettle timed out

When the exception was thrown, this was the stack:
#0      WidgetTester.pumpAndSettle.<anonymous closure> (package:flutter_test/src/widget_tester.dart:651:11)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)
...
════════════════════════════════════════════════════════════════════════════════════════════════════
00:05 +0 -1: WHEN tasks are not completedTHEN shows `CircularProgressIndicator` [E]                                                                                                                         
  Test failed. See exception logs above.
  The test description was: WHEN tasks are not completedTHEN shows `CircularProgressIndicator`
  
00:05 +0 -1: Some tests failed.                

the environment is

Flutter version 2.2.0-11.0.pre.176

environment:
  sdk: ">=2.12.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  hooks_riverpod: ^0.14.0
  flutter_hooks: ^0.16.0

any help is apprecciated

6

There are 6 answers

0
Martyns On BEST ANSWER

I'd say the problem is related to using pumpAndSettle and an infinite animation (Circular progress indicator). You can try using pump without the settle to build frames yourself.

https://api.flutter.dev/flutter/flutter_test/WidgetTester/pumpAndSettle.html

0
Jake Boomgaarden On

@zuldyc is correct. By running the step asynchronously, it gives the Timer what it needs to finish successfully before continuing. I've got a working example now that will hopefully make things more clear.

BROKEN CODE

testWidgets('Testing Login Button Success - New User', (tester) async {
      final amplifyAuthMock = MockAmplifyAuth();
      final dbInterfaceMock = MockDatabaseInterface();

      when(amplifyAuthMock.login('[email protected]', 'password!'))
          .thenAnswer((result) async => true);
      when(dbInterfaceMock.startStopDBSync())
          .thenAnswer((realInvocation) async => true);
      when(dbInterfaceMock.restartDBSync())
          .thenAnswer((realInvocation) async => true);

      // CREATING FORM TO TEST
      await tester
          .pumpWidget(createLoginForm(amplifyAuthMock, dbInterfaceMock));
      await inputDummyLoginText(tester, email: '[email protected]');
      
      // PRESSING LOGIN BUTTON AND SHOULD GO TO HOME PAGE
      await tester.tap(find.byType(SkillTreeElevatedButton));

      // BREAKS HERE ON PUMP AND SETTLE******
      await tester.pumpAndSettle(const Duration(seconds: 1));
      expect(find.byType(CircularProgressIndicator), findsOneWidget);
    });

It breaks because of the reasons described in accepted answer. Well, sort of. You get a sort of race condition because we are using a future which is asynchronous, but the code above doesn't account for that so it executes the future widget's code but does not know to wait for it to finish creating, so it exists and everything explodes. We need to make the ENTIRE process asynchronous. We do this by following Zuldyc's answer. By changing my code to the following it works without issue

// THE ABOVE CODE HAS NOT CHANGED, NEW CODE STARTS HERE
await tester
          .runAsync(() => tester.tap(find.byType(SkillTreeElevatedButton)));
      await tester.pump(const Duration(seconds: 1));
      expect(find.byType(CircularProgressIndicator), findsOneWidget);
    });

To be clear the change is as follows

//BEFORE
await tester.tap(find.byType(SkillTreeElevatedButton));
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.byType(CircularProgressIndicator), findsOneWidget);
//AFTER
await tester.runAsync(() => tester.tap(find.byType(SkillTreeElevatedButton)));
await tester.pump(const Duration(seconds: 1));
expect(find.byType(CircularProgressIndicator), findsOneWidget);

My tap action was triggering the new screen and the loading indicator, so i needed to make that action async so that it could finish.

1
Zuldyc Dico On

it seems runAsync solves the issue

await tester.runAsync(() => tester.pumpWidget(
    ProviderScope(child: MyApp()), const Duration(milliseconds: 100)));
final indicator = const CircularProgressIndicator();

await tester.pumpWidget(indicator);

expect(find.byWidget(indicator), findsOneWidget);
2
Catalin On

It seems atm riverpod and pumpAndSettle are not working, as a nasty quick hack you can try something like this:

for (int i = 0; i < 5; i++) {
  // because pumpAndSettle doesn't work with riverpod
  await tester.pump(Duration(seconds: 1));
}
0
kaiber On

For those using the CircularProgressIndicator to indicate the loading state while awaiting a future, I found that in my specific scenario, simply using

await widgetTester.pump(Duration(seconds: 1));

helped transition the widgets into a valid data state after a one-second delay.

0
HJo On

pumpAndSettle() wasn't working for me, even with runAsync. I think the reason is this from the docs:

If it takes longer that the given timeout to settle, then the test will fail (this method will throw an exception). In particular, this means that if there is an infinite animation in progress (for example, if there is an indeterminate progress indicator spinning), this method will throw.

I created a more efficient version of @catalyn's answer that stops pumping once a finder has evaluated as true.

extension PumpUntilFound on WidgetTester {
  Future<void> pumpUntilFound(
    Finder finder, {
    Duration duration = const Duration(milliseconds: 100),
    int tries = 10,
  }) async {
    for (var i = 0; i < tries; i++) {
      await pump(duration);

      final result = finder.precache();

      if (result) {
        finder.evaluate();

        break;
      }
    }
  }
}

Usage example:

final finder = find.byType(AuthScreen);

await tester.pumpUntilFound(finder);

expect(finder, findsOneWidget);