Patrol finders - usage
This page introduces Patrol finder system. Let's get our hands dirty and find some widgets!
Finding widgets
Let's say you want to find some Text
widget – nothing easier than that!
find.byType(Text);
Using Patrol finder, you'd write the above as:
$(Text);
Or let's find a Text
widget with a specific text:
find.text('Subscribe');
Using Patrol finder, you'd write the above as:
$('Subscribe');
Worth mentioning is also Key
. The below lines are equivalent:
find.byKey(Key('loginButton'));
$(Key('loginButton'));
$(#loginButton);
For those wondering what is that #
thing – it's a
Symbol! Yes, we're Dart
(ab)users.
$
are listed here.Making assertions
Creating a finder doesn't do anything – it just is. Let's put them to use and write a few simple assertions.
Here's how you can make sure that a widget with text Log in
exists in the
widget tree:
expect(find.text('Log in'), findsOneWidget);
With our Patrol finders, you'd write the above as:
expect($('Log in'), findsOneWidget);
Alternatively, you could also use the exists
getter, which returns true if the
finder finds at least 1 widget:
expect($('Log in').exists, equals(true));
We can also make sure that no widget exists, or that a particular number of widgets exist:
expect(find.text("Can't touch this"), findsNothing);
expect(find.byType(Card), findsNWidgets(3));
The above expressed with Patrol finders:
expect($("Can't touch this"), findsNothing);
expect($(Card), findsNWidgets(3));
You could alternatively write the first line as:
expect($("Can't touch this").exists, equals(false));
It's important to note that Flutter's default finder functions, such as findsNothing and findsOneWidget, check if the widget is present in the widget tree, not if it is visible to the user, which is usually not what we're interested in.
To check if the finder finds at least 1 visible widget, use the visible
getter:
expect($('Log in').visible, equals(true));
And to wait for at least 1 widget with the "Log in" text to become visible:
await $('Log in').waitUntilVisible();
Performing actions
Finding widgets alone is cool, but what's even cooler is being able to tap on them! Let's tap on the first "Subscribe" text:
await tester.tap(find.text('Subscribe').first);
It's usually a good practice to use first
, because if there were multiple
"Subscribe" texts, tap()
would throw an exception.
With Patrol, you get concise code, but you preserve the flexibility:
await $('Subscribe').tap();
What's very cool about Patrol's tap()
is that it doesn't immediately fail if
the finder finds no visible widgets – instead, it waits for some time (which you
can specify globally in PatrolTesterConfig or as argument to the tap()
method) and taps on the first widget as soon as it becomes visible. This lets
you get rid of fixed timeouts and test your app just like a real user would.
If you wanted to tap on the third "Subscribe" text, you'd do:
await $('Subscribe').at(2).tap();
And if the "Subscribe" text was in a Scrollable widget, such as
SingleChildScrollView or ListView, and you want to make sure that it is
visible (so you can tap()
on it), you can scroll to it very easily:
await $('Subscribe').scrollTo().tap();
Going deeper
But hey, these were very simple examples. In real apps, unfortunately, finding widgets is not that easy.
Often, you'll need to tap on a widget which is in some other widget.
await tester.tap(
find.descendant(
of: find.byType(ListView),
matching: find.text('Subscribe'),
).first
);
Flutter's finders are starting to grow, while Patrol stays lean:
await $(ListView).$('Subscribe').tap();
Now, we also make sure that the Subscribe
text is in a ListTile
:
await tester.tap(
find.descendant(
of: find.byType(ListView),
matching: find.descendant(
of: find.byType(ListTile),
matching: find.text('Subscribe'),
),
).first
);
Hey, this is starting to look complex! Fortunately, you have Patrol:
await $(ListView).$(ListTile).$('Subscribe').tap();
Sometimes, you might want to perform a lookahead check. Let's say that you want
tap on the first widget with the Key('learnMore')
that is a descendant of some
ListTile
, but that ListTile
must also have the Text
descendant with the
Activated
text.
If you were to express the above as a Finder
, you'd get:
await tester.tap(
find.ancestor(
of: find.text('Activated'),
matching: find.descendant(
of: find.byType(ListTile),
matching: find.byKey(Key('learnMore')),
),
).first
);
With the help of Patrol's custom finders, it's much easier:
await $(ListTile).containing('Activated').$(#learnMore).tap();
Sometimes, however, the logic required to find a widget cannot be expressed by the descendant/ancestor relationship like above. In situations like this, when all ways of finding widgets known to you fail, Patrol has an ace up its sleeve: the which() method. You can use it to find widgets by their properties. A few examples include: .
-
entering a text into a text field with no text entered:
await $(#cityTextField) .which<TextField>((widget) => widget.controller.text.isNotEmpty) .enterText('Warsaw, Poland');
-
asserting that the icon has the correct color:
await $(Icons.error) .which<Icon>((widget) => widget.color == Colors.red) .waitUntilVisible();
-
asserting that the button is disabled and has the correct color
await $('Delete account') .which<ElevatedButton>((button) => !button.enabled) .which<ElevatedButton>( (btn) => btn.style?.backgroundColor?.resolve({}) == Colors.red, ) .waitUntilVisible();
Falling back
What's cool about Patrol is that it builds on top of flutter_test
instead of
replacing it. This means that you can freely mix Patrol's finders with finders
from flutter_test
, PatrolTester
with WidgetTester
, and so on.
Here's how you can access the default WidgetTester
:
patrolWidgetTest('adds comment', (PatrolTester $) async {
final WidgetTester tester = $.tester;
await tester.enterText(find.byKey(Key('commentTextField')), 'Very nice!');
});