Custom widget testing needs Material widget to test

Issue

I am trying to perform basic widget testing in Flutter. Basically I would like to have a list with list of data, and display each of the items in a custom widget (BasicListItem) which also has a ListTile widget in it.

Root widget:

class MyApp extends StatelessWidget {
  final List taskList = ['List-1', 'List-2', 'List-3'];

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        body: ListView.builder(
            itemCount: taskList.length, itemBuilder: _itemBuilder),
      ),
    );
  }

  Widget _itemBuilder(BuildContext context, int index) {
    final String item = taskList[index];
    return BasicListItem(key: Key(item), title: item);
  }
}

The list item widget (BasicListItem) takes a title, and use it inside the ListTile widget.

class BasicListItem extends StatelessWidget {
  final String title;

  const BasicListItem({required Key key, required this.title})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: Icon(Icons.map),
      title: Text(title),
    );
  }
}

This is the test for it:

testWidgets('has title and Icons', (WidgetTester tester) async {

  const testKey = Key('my-key-1');
  const testTitle = 'Demo title';

  await tester.pumpWidget(BasicListItem(key: testKey, title: testTitle));

  expect(find.text(testTitle), findsOneWidget);
});

But the test throws an error:

No Material widget found. ListTile widgets require a Material widget
ancestor.


The following TestFailure object was thrown running a test:
Expected: exactly one matching node in the widget tree Actual:
_TextFinder:<zero widgets with text "Demo title" (ignoring offstage widgets)>

However, the test does pass if I wrap ListTile around a MaterialApp, inside the BasicListItem build method. Like so:

@override
Widget build(BuildContext context) {
  return MaterialApp(
    title: title,
    home: Scaffold(
      body: ListTile(
        leading: Icon(Icons.map),
        title: Text(title),
      ),
    )
  );
}

But doing this I cannot use it inside the ListView widget. And also I would like to have modular/separate custom widgets so that I can use it on different places as well. I am new and maybe I am missing something. How can I build custom widget and test it out? Could you help me out please.

Solution

The ListTile component comes from the Material part of Flutter UI components & is not an independent widget, therefore it needs a MaterialApp as parent.

You can check that the ListTile is under material library here: https://api.flutter.dev/flutter/material/ListTile-class.html

Also, you can create as many custom Widgets to use in separate modules,
the only requirement would be to use MaterialApp at the very beginning of the app initialisation.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    /// Only this needs to be a MaterialApp
    return MaterialApp(
      title: 'Welcome to Flutter',

      /// this point to different screen widget also, like MainScreen()
      /// Or you can start using Scaffold from here as well.
      home: Scaffold(
        appBar: AppBar(
          title: Text('Welcome to Flutter'),
        ),
        body: Center(
          child: Text('Hello World'),
        ),
      ),
    );
  }
}

It is not necessary to use MaterialApp as a parent on every custom widget you build. Just the root can be fine too.
But if you are using a single widget to simply test out, & it requires a Material ancestor, you can simply wrap the widget in a Material widget as well.

Answered By – DarShan

Answer Checked By – Robin (FlutterFixes Admin)

Leave a Reply

Your email address will not be published. Required fields are marked *