Advanced
We aim to make Patrol as simple as possible, but there are still a few matters that we feel require some more attention. We'll explain them in this section.
How is Patrol's tap()
different from Flutter tap()
?#
Let's consider this test, written without Patrol:
await tester.tap(find.byKey(Key('addComment')).first);
await tester.pumpAndSettle();
This code:
- Immediately atttempts to find the first widget with the
addComment
key - After finding the widget, it immediately attempts to tap on it
This is the default behavior, but in our experience, it's often a source of
flakiness. For example, the widget having addComment
key might not be visible
at the time when the finder is run. This usually doesn't means that the test
should fail. Probably an HTTP request was made to fetch the post, and when the
fetching is done, the widget having addComment
key will show up.
To achieve this behavior, you'd have to do:
while (find.byKey(Key('addComment')).first.evaluate().isEmpty) {
await tester.pump(Duration(milliseconds: 100));
}
await tester.tap(find.byKey(Key('addComment')).first);
await tester.pumpAndSettle();
Our tiny example got really big, but it's still got two problems.
-
If something goes wrong and
addComment
never shows up, we'll keep waiting indefinitely. -
The widget with
addComment
key might be present in the widget tree, but still not be visible to the user. By default, Flutter's defaultWidgetTester
doesn't care. This is almost never desirable.
Fortunately, you don't have to overcome these problems. Patrol already did it!
Below is the same test, with all the above problems fixed, written with Patrol's custom finders:
await $(#addComment).tap();
This code:
- Attempts to find the first widget with
addComment
that is visible on screen. If it's not found immediately, it keeps trying until it finds it, or throws an exception if timeout. - Taps on it.
The timeout can be configured globally:
patrolTest(
'logs in successfully',
config: PatrolTestConfig(findTimeout: Duration(seconds: 10)),
($) async {
// your test code
},
);
You can also change the timeout ad-hoc:
await $(#addComment).tap(findTimeout: Duration(seconds: 30));
How does scrollTo()
work?#
The scrollTo()
method is simple to use, yet very powerful. Here's how you use
it to scroll to and tap on the first widget with the "Delete account"
text:
await $('Delete account').scrollTo().tap();
And here's how scrollTo()
works:
- Waits for at least 1 Scrollable widget to become visible
- Scrolls this
Scrollable
widget in its scrolling direction until the target widget becomes visible - If the target widget becomes visible within timeout, it finishes, otherwise it throws an exception
Most of the time, you use scrollTo()
and it just works, but there's 1
important thing to keep in mind when using scrollTo()
:
scrollTo by default scrolls the first Scrollable widget
This default is reasonable and what you want most of the time. Unfortunately,
this behavior can sometimes cause problems in more complicated UIs, where more
than a single Scrollable
widget is visible at the same time. In such cases we
strongly recommend explicitly specifying the Scrollable
that scrollTo()
should scroll, to avoid the problem of the target widget never becoming visible
because the wrong scrollable was scrolled.
To demonstrate this problem, let's consider this very simple app:
class App extends StatelessWidget {
App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(
children: [
Expanded(child: ListView(key: Key('listView1'))),
Expanded(
child: ListView.builder(
key: Key('listView2'),
itemCount: 101,
itemBuilder: (context, index) => Text('index: $index'),
),
),
],
),
),
);
}
}
Now let's say that you're writing a test and want to scroll to and tap on the
first widget with the "index: 100"
text (that is the last Text
widget built
by the second ListView
widget):
There's a high chance that you'd write this:
await $('index: 100').scrollTo().tap();
Unfortunately, running this test gives the pumpAndSettle timed out
error.
That's because the scrollTo()
was trying to scroll the first visible
Scrollable
widget, which happens to be the first ListView
(the one with
listView1
key and no children).
To fix this problem, you have to explicitly specify which Scrollable
you want
to use:
await $('index: 100').scrollTo(scrollable: $(#listView2).$(Scrollable)).tap();
The above snippet will scroll the second Scrollable
and find the widget with
"index: 100"
text.
Why so verbose?
You might be wondering why scrollTo(scrollable: $(#listView2))
is not enough?
Why is it needed to look for a Scrollable
widget inside the widget with the
listView2
key?
This is because the ListView widget doesn't extend Scrollable – instead, it builds a subclass of Scrollable itself. This is a known Flutter problem.