Search code examples
flutterflutter-layoutflutter-teststatefulwidgetflutter-state

Flutter test finder doesn't find my stateful widget


I am following this tutorial https://docs.flutter.dev/development/ui/interactive#the-parent-widget-manages-the-widgets-state and I have the following in main.dart:

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    Widget statesSection = Container(
        padding: const EdgeInsets.all(32),
        child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: const [StatefulParentWidget()]));
    return MaterialApp(
        title: 'Flutter Layout',
        home: Scaffold(
            appBar: AppBar(title: const Text("Flutter Layout")),
            body: ListView(children: [
              statesSection
            ])));
  }

It doesn't find anything at all in the following test code:

testWidgets('State management tests', (WidgetTester tester) async {
    // Build our app and trigger a frame.
    await tester.pumpWidget(const MyApp());
    expect(find.byType(StatefulParentWidget), findsOneWidget); // Fails!
    expect(find.text("Inactive"), findsOneWidget); // Fails!
    expect(find.text("Active"), findsNothing); // Fails!
});

Test error message:

══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure was thrown running a test:
Expected: exactly one matching node in the widget tree
  Actual: _WidgetTypeFinder:<zero widgets with type "StatefulParentWidget" (ignoring offstage
widgets)>
   Which: means none were found but one was expected

Any advice and insight is appreciated. https://github.com/khteh/flutter


Solution

  • Finale

    Change your code to:

                  // titleSection,
                  // buttonsSection,
                  // textSection,
                  statesSection
    

    and the test will pass for

      testWidgets('State management tests', (WidgetTester tester) async {
        // Build our app and trigger a frame.
        await tester.pumpWidget(const MyApp());
        expect(find.byType(Scaffold), findsOneWidget);
        expect(find.byType(StatefulParentWidget), findsOneWidget);
    
        expect(find.text("Inactive"), findsOneWidget);
        expect(find.text("Active"), findsNothing);
        //StatefulParentWidget statefulParentWidget = const StatefulParentWidget();
        //tester.state(find.byWidget(statefulParentWidget));
        //expect(find.byWidget(tapboxB), findsOneWidget);
      });
    

    So the test only can find widgets rendered already, and in your case, the widgets in statesSection were once off the stage.

    Former Discussion

    1 With Scaffod

    If you were facing the same exception message as follows:

    flutter: (The following exception is now available via WidgetTester.takeException:)
    flutter: ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
    flutter: The following assertion was thrown building MainPage:
    flutter: No MediaQuery widget ancestor found.
    flutter: Scaffold widgets require a MediaQuery widget ancestor.
    

    Change your test code to this, it should work

    void main() {
      testWidgets('State management tests', (WidgetTester tester) async {
        // Build our app and trigger a frame.
        await tester.pumpWidget(MaterialApp(
          home: const MainPage(),
        ));
        expect(find.byType(ParentWidget), findsOneWidget); // Fails!
        expect(find.text("Inactive"), findsOneWidget); // Fails!
        expect(find.text("Active"), findsNothing); // Fails!
      });
    }
    

    Full code is here:

    void main() {
      testWidgets('State management tests', (WidgetTester tester) async {
        // Build our app and trigger a frame.
        await tester.pumpWidget(MaterialApp(home: const MainPage(),));
        expect(find.byType(ParentWidget), findsOneWidget); // Fails!
        expect(find.text("Inactive"), findsOneWidget); // Fails!
        expect(find.text("Active"), findsNothing); // Fails!
      });
    }
    
    class MainPage extends StatelessWidget {
      const MainPage({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
              title: Text("Hello"),
            ),
            body: Container(
                padding: const EdgeInsets.all(32),
                child: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: const [ParentWidget()])));
      }
    }
    
    // ParentWidget manages the state for TapboxB.
    
    //------------------------ ParentWidget --------------------------------
    
    class ParentWidget extends StatefulWidget {
      const ParentWidget({super.key});
    
      @override
      _ParentWidgetState createState() => _ParentWidgetState();
    }
    
    class _ParentWidgetState extends State<ParentWidget> {
      bool _active = false;
    
      void _handleTapboxChanged(bool newValue) {
        setState(() {
          _active = newValue;
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return SizedBox(
          child: TapboxB(
            active: _active,
            onChanged: _handleTapboxChanged,
          ),
        );
      }
    }
    
    //------------------------- TapboxB ----------------------------------
    
    class TapboxB extends StatelessWidget {
      const TapboxB({
        super.key,
        this.active = false,
        required this.onChanged,
      });
    
      final bool active;
      final ValueChanged<bool> onChanged;
    
      void _handleTap() {
        onChanged(!active);
      }
    
      @override
      Widget build(BuildContext context) {
        return GestureDetector(
          onTap: _handleTap,
          child: Container(
            child: Center(
              child: Text(
                active ? 'Active' : 'Inactive',
                style: const TextStyle(fontSize: 32.0, color: Colors.white),
              ),
            ),
            width: 200.0,
            height: 200.0,
            decoration: BoxDecoration(
              color: active ? Colors.lightGreen[700] : Colors.grey[600],
            ),
          ),
        );
      }
    }
    
    

    2 Without Scaffod

    If you want to test the widget solely, then you have to replace your Text widget with this:

              child: Text(
                active ? 'Active' : 'Inactive',
                textDirection: TextDirection.ltr,
                style: const TextStyle(fontSize: 32.0, color: Colors.white),
              ),
    

    In my first example, the Scallfold does the trick so you can test without telling textDirection attribution. Please refer to TextDirection enum for further reading.

    Full code is here:

    void main() {
      testWidgets('State management tests', (WidgetTester tester) async {
        // Build our app and trigger a frame.
        await tester.pumpWidget(statesSection);
        expect(find.byType(ParentWidget), findsOneWidget); // Fails!
        expect(find.text("Inactive"), findsOneWidget); // Fails!
        expect(find.text("Active"), findsNothing); // Fails!
      });
    }
    
    Widget statesSection = Container(
        padding: const EdgeInsets.all(32),
        child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: const [ParentWidget()]));
    
    class MainPage extends StatelessWidget {
      const MainPage({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
              title: Text("Hello", textDirection: TextDirection.ltr),
            ),
            body: Container(
                padding: const EdgeInsets.all(32),
                child: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: const [ParentWidget()])));
      }
    }
    
    // ParentWidget manages the state for TapboxB.
    
    //------------------------ ParentWidget --------------------------------
    
    class ParentWidget extends StatefulWidget {
      const ParentWidget({super.key});
    
      @override
      _ParentWidgetState createState() => _ParentWidgetState();
    }
    
    class _ParentWidgetState extends State<ParentWidget> {
      bool _active = false;
    
      void _handleTapboxChanged(bool newValue) {
        setState(() {
          _active = newValue;
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return SizedBox(
          child: TapboxB(
            active: _active,
            onChanged: _handleTapboxChanged,
          ),
        );
      }
    }
    
    //------------------------- TapboxB ----------------------------------
    
    class TapboxB extends StatelessWidget {
      const TapboxB({
        super.key,
        this.active = false,
        required this.onChanged,
      });
    
      final bool active;
      final ValueChanged<bool> onChanged;
    
      void _handleTap() {
        onChanged(!active);
      }
    
      @override
      Widget build(BuildContext context) {
        return GestureDetector(
          onTap: _handleTap,
          child: Container(
            child: Center(
              child: Text(
                active ? 'Active' : 'Inactive',
                textDirection: TextDirection.ltr,
                style: const TextStyle(fontSize: 32.0, color: Colors.white),
              ),
            ),
            width: 200.0,
            height: 200.0,
            decoration: BoxDecoration(
              color: active ? Colors.lightGreen[700] : Colors.grey[600],
            ),
          ),
        );
      }
    }