Msw Tm
Msw Tm

Reputation: 508

Custom widget testing needs Material widget to test

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.

Upvotes: 3

Views: 2561

Answers (3)

Fred Grott
Fred Grott

Reputation: 3476

Okay, this is not specifically in Flutter Docs but is hinted about all over the place. On flutter test side we are pumping a root widget to render a frame as our palette used to test widgets.

Translates to you need to create a Root App Widget to wrap the widget under test. eBay's Golden Toolkit supplies the hooks to make this possible via pumpWidgetBuilder which is an extension of Widget Tester.

For more see my blog, https://fredgrott.medium.com

Upvotes: 0

Msw Tm
Msw Tm

Reputation: 508

I didn't understand Darshan's answer at first, because I think the code he provided made me implement the MaterialApp and Material widget into BasicListItem widget class build method directly, instead of implementing it on just the test suit. But that gave me the clue to implement it.

So, this is the final test case. I did wrapped MaterialApp and Material widget with BasicListItem, but not in the build method, instead I wrapped them just on the test case:

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

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

  await await tester.pumpWidget(MaterialApp(
    home: Material(
      child: BasicListItem(key: testKey, title: testTitle),
    ),
  ));;

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

I hope this will help others like me as well.

Upvotes: 2

darshan
darshan

Reputation: 4569

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.

Upvotes: 1

Related Questions