0

I'm currently writing golden tests for my app, that uses Riverpod and Golden Toolkit, and I'm facing a bug that I don't understand. When running a test (see below), I get the following error:

Pending timers:
Timer (duration: 0:00:00.000000, periodic: false), created:
#0      new FakeTimer._ (package:fake_async/fake_async.dart:308:62)
#1      FakeAsync._createTimer (package:fake_async/fake_async.dart:252:27)
#2      FakeAsync.run.<anonymous closure> (package:fake_async/fake_async.dart:185:19)
#7      _defaultVsync (package:riverpod/src/framework/scheduler.dart:9:3)
#8      _ProviderScheduler._scheduleTask (package:riverpod/src/framework/scheduler.dart:42:10)
#9      _ProviderScheduler.scheduleProviderDispose (package:riverpod/src/framework/scheduler.dart:75:5)
#10     AutoDisposeProviderElementMixin.mayNeedDispose (package:riverpod/src/framework/auto_dispose.dart:40:29)
#11     ProviderElementBase._onRemoveListener (package:riverpod/src/framework/element.dart:897:5)
#12     _ExternalProviderSubscription.close (package:riverpod/src/framework/provider_base.dart:167:22)
#13     ConsumerStatefulElement.unmount (package:flutter_riverpod/src/consumer.dart:577:21)
#14     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:1953:13)
#15     _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:1951:7)
#16     ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5138:14)
#17     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:1949:13)
(...)
#354    _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:1951:7)
#355    ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5138:14)
#356    _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:1949:13)
#357    ListIterable.forEach (dart:_internal/iterable.dart:39:13)
#358    _InactiveElements._unmountAll (package:flutter/src/widgets/framework.dart:1962:25)
#359    BuildOwner.lockState (package:flutter/src/widgets/framework.dart:2640:15)
#360    BuildOwner.finalizeTree (package:flutter/src/widgets/framework.dart:3050:7)
#361    AutomatedTestWidgetsFlutterBinding.drawFrame (package:flutter_test/src/binding.dart:1405:19)
#362    RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:358:5)
#363    SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1284:15)
#364    SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1214:9)
#365    AutomatedTestWidgetsFlutterBinding.scheduleWarmUpFrame (package:flutter_test/src/binding.dart:1332:5)
#366    runApp (package:flutter/src/widgets/binding.dart:1083:7)
#367    TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:993:7)
<asynchronous suspension>
(elided 4 frames from dart:async)

══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following assertion was thrown running a test:
A Timer is still pending even after the widget tree was disposed.
'package:flutter_test/src/binding.dart':
Failed assertion: line 1498 pos 12: '!timersPending'

When the exception was thrown, this was the stack:
#2      AutomatedTestWidgetsFlutterBinding._verifyInvariants (package:flutter_test/src/binding.dart:1498:12)
#3      TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:1003:7)
<asynchronous suspension>
(elided 2 frames from class _AssertionError)

Here is the simplified test class, that I run using the following command: flutter test --update-goldens --tags=test:

@Tags(["test"])

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';

void main()
{
  WidgetsFlutterBinding.ensureInitialized();

  Future<void> generateScreenshot({
    required WidgetTester tester,
    required Widget widget,
    required String pageName,
    required Size sizeDp,
    required double density,
    CustomPump? customPump,
  }) async
  {
    await tester.pumpWidgetBuilder(widget);
    await multiScreenGolden(
      tester,
      pageName,
      customPump: customPump,
      devices: [
        Device(
          name: "final",
          size: sizeDp,
          textScale: 1,
          devicePixelRatio: density,
        ),
      ],
    );
  }

  Widget getPage(int a)
  {
    return ProviderScope(
      key: ValueKey("page$a"),
      child: const IntroPage(),
    );
  }

  testGoldens("Test ProviderScopes", (WidgetTester tester) async
  {
    const density = 2.0;
    const sizeBaseDp = Size(2048 / density, 2732 / density);

    for (int a = 0; a < 2; a++) // If I do only one loop, it works...
    {
      await generateScreenshot(
          tester: tester,
          widget: getPage(a),
          pageName: "page$a",
          sizeDp: sizeBaseDp,
          density: density);
    }
  });

}


final setupIntroNotifierProvider = AutoDisposeAsyncNotifierProvider<SetupIntroNotifier, bool>(() => SetupIntroNotifier());
class SetupIntroNotifier extends AutoDisposeAsyncNotifier<bool>
{
  @override
  FutureOr<bool> build() => false;
}


class IntroPage extends ConsumerStatefulWidget
{
  const IntroPage({super.key});
  @override
  ConsumerState<IntroPage> createState() => _IntroPageState();
}

class _IntroPageState extends ConsumerState<IntroPage>
{
  @override
  Widget build(BuildContext context)
  {
    ref.listen<AsyncValue<bool>>(setupIntroNotifierProvider, (previous, next) {});
    return const Placeholder();
  }
}

The purpose here is to generate multiple images of the same page, but in different conditions (that I didn't put here for clarity sake).

The test passes if, in my test, I call generateScreenshot() only once. But if I call it even two times, it crashes (even though the images are generated as expected). I also noticed that the test also passes if I remove the key for the ProviderScope, but then it doesn't behave as expected (the overrides don't seem to work)

Did I miss something? How can I make my test pass with multiple loops, while keeping the key for each ProviderScope?

Thanks for your help.

matteoh
  • 2,810
  • 2
  • 29
  • 54

1 Answers1

0

After adding the following lines to the test, it works:

await tester.pumpWidget(Container());
await tester.pumpAndSettle();

Full test:

testGoldens("Test ProviderScopes", (WidgetTester tester) async
  {
    const density = 2.0;
    const sizeBaseDp = Size(2048 / density, 2732 / density);

    for (int a = 0; a < 2; a++) // If I do only one loop, it works...
    {
      await generateScreenshot(
          tester: tester,
          widget: getPage(a),
          pageName: "page$a",
          sizeDp: sizeBaseDp,
          density: density);
    }

    // add this:
    await tester.pumpWidget(Container());
    await tester.pumpAndSettle();
  });
matteoh
  • 2,810
  • 2
  • 29
  • 54