13

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

Francesco Iapicca
  • 2,618
  • 5
  • 40
  • 85

5 Answers5

22

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

Martyns
  • 3,605
  • 22
  • 33
7

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));
}
Catalin
  • 1,821
  • 4
  • 26
  • 32
  • Its quite interesting cause am using flutter_bloc for state management and beamer for navigation but using the loop provided a succesful test ... Can you explain this hack ? – Enos Okello Feb 25 '22 at 13:39
2

@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('testNew@test.com', '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: 'testNew@test.com');
      
      // 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.

Jake Boomgaarden
  • 3,394
  • 1
  • 17
  • 31
1

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);
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Mar 26 '22 at 20:36
0

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);
HJo
  • 1,902
  • 1
  • 19
  • 30