flutterwidget-test-flutterflutter-easy-localization

Flutter with `easy_localization` hangs the widget test forever


Widgets that internally use tr extension fail. Consider this example:

void main() {
  testWidgets('no decks', (WidgetTester tester) async {
    await tester.pumpWidget(const DeckList(decks: []));
    await tester.pumpAndSettle();

    // test code
  });
}

...where DeckList widget here internally uses tr at some Text widgets inside a ListView widget. This fails saying:

The following assertion was thrown building ListView(scrollDirection: horizontal, dirty):
No Directionality widget found.
ListView widgets require a Directionality widget ancestor.
The specific widget that could not find a Directionality ancestor was:
  ListView
The ownership chain for the affected widget is: "ListView ← SizedBox ← DeckList ← [root]"
Typically, the Directionality widget is introduced by the MaterialApp or WidgetsApp widget at the
top of your application widget tree. It determines the ambient reading direction and is used, for
example, to determine how to lay out text, how to interpret "start" and "end" values, and to resolve
EdgeInsetsDirectional, AlignmentDirectional, and other *Directional objects.

What I assume is that those tr extensions use BuildContext to look up through the widget tree to check Directionality widget, which I think is responsible for defining if texts are LTR or RTL. That's, I think, why it fails.

So, wrapping it with EasyLocalization should fix it, right?

void main() {
  testWidgets('no decks', (WidgetTester tester) async {
    await tester.pumpWidget(
      EasyLocalization( // we wrap the widget
        supportedLocales: const [
          Locale('en', 'US'),
          Locale('tr', 'TR'),
        ],
        path: 'assets/translations',
        fallbackLocale: const Locale('en', 'US'),
        assetLoader: YamlAssetLoader(),
        child: const DeckList(decks: []),
      ),
    );
    await tester.pumpAndSettle();

    // test code
  });
}

This also fails naturally saying that:

The following LateError was thrown attaching to the render tree:
LateInitializationError: Field '_deviceLocale@1279168148' has not been initialized.

When the exception was thrown, this was the stack:
#0      EasyLocalizationController._deviceLocale (package:easy_localization/src/easy_localization_controller.dart)
#1      new EasyLocalizationController (package:easy_localization/src/easy_localization_controller.dart:54:9)
#2      _EasyLocalizationState.initState (package:easy_localization/src/easy_localization_app.dart:123:30)
#3      StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:5015:57)
#4      ComponentElement.mount (package:flutter/src/widgets/framework.dart:4853:5)
#5      Element.inflateWidget (package:flutter/src/widgets/framework.dart:3863:16)
#6      Element.updateChild (package:flutter/src/widgets/framework.dart:3586:20)
#7      RenderObjectToWidgetElement._rebuild (package:flutter/src/widgets/binding.dart:1195:16)
#8      RenderObjectToWidgetElement.update (package:flutter/src/widgets/binding.dart:1172:5)
#9      RenderObjectToWidgetElement.performRebuild (package:flutter/src/widgets/binding.dart:1186:7)
#10     Element.rebuild (package:flutter/src/widgets/framework.dart:4604:5)
#11     BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2667:19)
#12     AutomatedTestWidgetsFlutterBinding.drawFrame (package:flutter_test/src/binding.dart:1191:19)
#13     RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:378:5)
#14     SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1175:15)
#15     SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1104:9)
#16     AutomatedTestWidgetsFlutterBinding.pump.<anonymous closure> (package:flutter_test/src/binding.dart:1057:9)
#19     TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:71:41)
#20     AutomatedTestWidgetsFlutterBinding.pump (package:flutter_test/src/binding.dart:1043:27)
#21     WidgetTester.pumpWidget.<anonymous closure> (package:flutter_test/src/widget_tester.dart:554:22)
#24     TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:71:41)
#25     WidgetTester.pumpWidget (package:flutter_test/src/widget_tester.dart:551:27)
#26     main.<anonymous closure> (file:///home/erayerdin/Projects/lexendify/app/test/domains/decks/presentation/components/deck_list_component_test.dart:11:18)
#27     testWidgets.<anonymous closure>.<anonymous closure> (package:flutter_test/src/widget_tester.dart:171:29)
<asynchronous suspension>
<asynchronous suspension>
(elided 5 frames from dart:async and package:stack_trace)

That _deviceLocale is null. So, we should ensure that Flutter widget bindings and easy_localization load. So, the widget test code is:

void main() {
  testWidgets('no decks', (WidgetTester tester) async {
    // we wait for these to initialize
    WidgetsFlutterBinding.ensureInitialized();
    await EasyLocalization.ensureInitialized();
    await tester.pumpWidget(
      EasyLocalization(
        supportedLocales: const [
          Locale('en', 'US'),
          Locale('tr', 'TR'),
        ],
        path: 'assets/translations',
        fallbackLocale: const Locale('en', 'US'),
        assetLoader: YamlAssetLoader(),
        child: const DeckList(decks: []),
      ),
    );
    await tester.pumpAndSettle();

    // test code
  });
}

However, this time, the whole widget test hangs and runs forever with low CPU usage. I think it's because EasyLocalization.ensureInitialized does things properly for Android environment, but not widget testing environment.

I'm pretty sure EasyLocalization.ensureInitialized waits forever because when I remove WidgetsFlutterBinding.ensureInitialized();, it still waits forever.

So, what causes this hang? How can I debug this further?

Thanks in advance.


Environment

[✓] Flutter (Channel stable, 3.3.5, on Fedora Linux 36 (KDE Plasma) 6.0.7-200.fc36.x86_64, locale en_US.UTF-8)
    • Flutter version 3.3.5 on channel stable at /home/erayerdin/snap/flutter/common/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision d9111f6402 (3 weeks ago), 2022-10-19 12:27:13 -0700
    • Engine revision 3ad69d7be3
    • Dart version 2.18.2
    • DevTools version 2.15.0

[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0)
    • Android SDK at /home/erayerdin/.sdks/android/
    • Platform android-33, build-tools 33.0.0
    • Java binary at: /var/lib/snapd/snap/android-studio/125/android-studio/jre/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.13+0-b1751.21-8125866)
    • All Android licenses accepted.

[✓] Chrome - develop for the web
    • Chrome at google-chrome

[✓] Linux toolchain - develop for Linux desktop
    • clang version 10.0.0-4ubuntu1
    • cmake version 3.16.3
    • ninja version 1.10.0
    • pkg-config version 0.29.1

[✓] Android Studio (version 2021.3)
    • Android Studio at /var/lib/snapd/snap/android-studio/125/android-studio
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.13+0-b1751.21-8125866)

[✓] VS Code
    • VS Code at /snap/code/current
    • Flutter extension version 3.52.0

[✓] Connected device (2 available)
    • Linux (desktop) • linux  • linux-x64      • Fedora Linux 36 (KDE Plasma) 6.0.7-200.fc36.x86_64
    • Chrome (web)    • chrome • web-javascript • Google Chrome 107.0.5304.110

[✓] HTTP Host Availability
    • All required HTTP hosts are available

• No issues found!

Related dependencies are:

easy_localization: ^3.0.1
easy_localization_loader: ^1.0.1+1

Solution

  • So, wrapping the widget with MaterialApp solved my problem, which kinda tells this had nothing to do with easy_translation. This was my solution:

    await tester.pumpWidget(
      const MaterialApp(
        home: DeckList(decks: []),
      ),
    );