0

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

Kok How Teh
  • 3,298
  • 6
  • 47
  • 85
  • 1
    A minimal project to reproduce this is needed. – Lebecca Jul 02 '22 at 06:49
  • what you really need is to use `skipOffstage` parameter: for example: `expect(find.byType(StatefulParentWidget, skipOffstage: false), findsOneWidget);` – pskink Jul 02 '22 at 10:05

1 Answers1

1

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],
        ),
      ),
    );
  }
}
Lebecca
  • 2,406
  • 15
  • 32
  • `Scaffold` is there. It is a boilerplate code generated by the `Flutter` extension in VSCode. The test error message is very straight-forward mentioning what it doesn't find. Not the one you mentioned. I have included it in my post. – Kok How Teh Jul 02 '22 at 07:46
  • @KokHowTeh I have pasted both code with & without Scaffold here and all of them passed the tests. You may compare your code with mine and figure out what happened. From your error message, maybe your widget the test is trying to find is in offstage state so it cannot be found. Just a possibility. – Lebecca Jul 02 '22 at 07:55
  • Why ` await tester.pumpWidget(MaterialApp(home: const MainPage(),));` is needed? Why can't I use `await tester.pumpWidget(const MyApp());`? – Kok How Teh Jul 02 '22 at 07:58
  • And why do you have to copy-paste the code in `main.dart` into the test `Widget statesSection = Container(...`? – Kok How Teh Jul 02 '22 at 07:59
  • If your `MyApp` already has a `MaterialApp` decorated, then it is not needed to do so in the test code. I put all the code in one file just to make it simple to run and reproduce. Most code is copied from the tutorial you mentioned. – Lebecca Jul 02 '22 at 08:05
  • For more information about MaterialApp & Scaffold & MediaQuery concept, please refer to other QA like this one: https://stackoverflow.com/questions/48498709/widget-test-fails-with-no-mediaquery-widget-found – Lebecca Jul 02 '22 at 08:07
  • I don't see any difference in your code that "works". And I don't think option 2 without `Scaffold` is necessary. – Kok How Teh Jul 02 '22 at 08:08
  • My suggestion is to offer a minimal project to reproduce the problem. – Lebecca Jul 02 '22 at 08:10
  • No, why comment out production code just to pass the test? All the sections are needed. Why is the `statesSection` "off the stage"? – Kok How Teh Jul 02 '22 at 09:00
  • The screen height is limited, you can scroll the screen to make it in stage. You can scroll screen in your test code with something like this https://stackoverflow.com/a/56292562/9304616 @KokHowTeh – Lebecca Jul 02 '22 at 09:01
  • 2
    what you really need is to use `skipOffstage` parameter: for example: `expect(find.byType(StatefulParentWidget, skipOffstage: false), findsOneWidget);` – pskink Jul 02 '22 at 10:04