flutterflutter-integration-test

How do you combine two Finders in a Flutter widget test?


Using package:flutter_test, I can create a finder that finds widgets with a key:

expect(find.byKey(const ValueKey('counter')), findsOneWidget);

or by text:

expect(find.text('0'), findsOneWidget);

I can also find widgets descending from this widget:

expect(
  find.descendant(
    of: find.byKey(const ValueKey('counter')),
    matching: find.text('0'),
  ),
  findsNothing,
);

Or an ancestor:

expect(
  find.ancestor(
    of: find.text('0'),
    matching: find.byKey(const ValueKey('counter')),
  ),
  findsNothing,
);

But how do I combine these finders to verify that there is a widget with a 'counter' Key and with '0' as its text? For example:

Text(
  '$_counter',
  key: const Key('counter'),
  style: Theme.of(context).textTheme.headline4,
),

Solution

  • I found this question while trying to combine finders myself. I ended up finding out how to combine finders, but while looking, I also found what I think is a better answer to your specific case. Sharing them both here.

    Question 1: How do you verify properties on widgets using finders?

    Since you're using keys, I'd suggest separating out the ideas of "finding the widget" and "validating the properties on the widget".

    So, instead of trying to find and validate in one go, you can use the WidgetController.widget<T> method to get the widget, then validate properties on that widget separately using expect. In your case, it'd look something like this:

    expect(
      tester.widget<Text>(find.byKey(const ValueKey('counter'))).data,
      equals('0'),
    );
    

    Question 2: How do you combine finders?

    This is more generally what you're asking, so I figured I'd share my findings on that as well.

    Here's what I came up with using extension methods and the ChainedFinder abstract class.

    It creates the find.chained method that takes a list of Finders and links them together using the ChainedFinderLink class. It ensures that the finders in the list are applied in the order given, filtering the list of candidates further on each application.

    import 'dart:collection';
    
    import 'package:darq/darq.dart';
    import 'package:flutter/src/widgets/framework.dart';
    import 'package:flutter_test/flutter_test.dart';
    
    extension CommonFinderX on CommonFinders {
      Finder chained(List<Finder> finders) {
        assert(finders.isNotEmpty);
    
        final findersQueue = Queue<Finder>.from(finders);
        var current = findersQueue.removeFirst();
        while (findersQueue.isNotEmpty) {
          current = ChainedFinderLink(
            parent: current,
            finder: findersQueue.removeFirst(),
          );
        }
    
        return current;
      }
    }
    
    class ChainedFinderLink extends ChainedFinder {
      ChainedFinderLink({
        required Finder parent,
        required this.finder,
      }) : super(parent);
    
      final Finder finder;
    
      @override
      String get description => '${parent.description} THEN ${finder.description}';
    
      @override
      Iterable<Element> filter(Iterable<Element> parentCandidates) {
        /// We have to apply against the interection of `parentCandidates` and
        /// `finder.allCandidates` because some finders (such as ancestor) filter
        /// out invalid candidates through the `allCandidates` getter instead of
        /// as part of the `apply` method itself.
        return finder.apply(parentCandidates.intersect(finder.allCandidates));
      }
    }
    

    It makes use of the darq library for the intersect method, but you could write your own if you didn't want to include another package.

    For your case, it'd be used like this:

    expect(
      find.chained([
        find.text('0'),
        find.byKey(const ValueKey('counter')),
      ]),
      findsOneWidget,
    );