Patrol is a powerful, open-source testing framework created by LeanCode that enhances Flutter's testing capabilities by enabling interaction with native platform features directly in Dart. It allows to handle permission dialogs, notifications, WebViews, and device settings—features previously unavailable in standard Flutter tests, making it truly possible to test the whole app.
This tutorial will take you through writing your first substantial Patrol test, interacting both with the Flutter app itself and also with native permission dialogs and notifications.
Before writing any tests, make sure you install the Patrol CLI. Then just clone the following repository from GitHub to follow along. The app we’re going to be testing is fully functional and ready to be tested, with Patrol already configured.
App Walkthrough
Before we can start writing automated Patrol tests, we need to know what the app does and to test it manually.
The first screen of our app is for signing in. It’s not using any actual sign-in provider but it only validates the email address and password. In order to successfully “sign in” and get to the home screen, we need to input a valid email and a password that’s at least 8 characters long.
On the second screen, we’re greeted with a notifications permission dialog. Once we allow them, we can tap on the notification button in the app bar to manually trigger a local notification which will be displayed after 3 seconds both when the app is running in the foreground or in the background.
Once we open the native notification bar and tap on the notification from our app, we’re gonna see a snackbar on the bottom saying "Notification was tapped!”
Testing the “Happy Path”
You’ve just seen the full walkthrough of the app, including errors that can show up if you input an invalid email or password. UI tests (integration tests), like the ones we’re going to write with Patrol, should only be testing the “happy path” of a UI flow. We only want them to fail if the app suddenly stops the user from doing what the app is for - in this case, that’s displaying a notification. Validation error messages are not “what the app is for”, they exist only to allow the user to successfully sign in with a proper email and password. That’s why we won’t be checking for them in the tests.
Writing the Test
We have only one UI flow in this app, that is signing in, showing the notification and then tapping on that notification. This means, we’re going to have only a single test. Let’s create it in /integration_test/app_test.dart
.
Like any other test, we need to have a main()
top-level function. Inside it we’re going to have our single patrolTest
with a description properly describing what we’re about to test. An optional step is to set the frame policy to “fully live” to make sure all frames are shown, even those which are not pumped by our test code. Without it, we would see that our app stutters and animations are not played properly.
app_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:patrol/patrol.dart';
void main() {
patrolTest(
'signs in, triggers a notification, and taps on it',
framePolicy: LiveTestWidgetsFlutterBindingFramePolicy.fullyLive,
($) async {
// Test code will go here
},
);
}
We could start writing the test right now and then re-run it from scratch every time we add a new line of test code by calling patrol test --target integration_test/app_test.dart
but since we’re writing a Patrol test that runs on an Android or iOS device, constantly building the whole Flutter app is not time effective. Thankfully, Patrol offers a different approach - hot restarting the tests! We can run the command patrol develop --target integration_test/app_test.dart
right now and anytime we add a new line of test code, we can just type “r” in the terminal to re-run the tests without the time-costly app building. Just make sure that you have an emulator running first - Patrol will select it automatically.
First, we need to perform any initializations that need to happen before the app is run and pump the top-level widget of our app. We’re effectively doing what the main
function inside main.dart
does - this time not for just running the app as usual but for running an automated Patrol test.
app_test.dart
patrolTest(
'signs in, triggers a notification, and taps on it',
framePolicy: LiveTestWidgetsFlutterBindingFramePolicy.fullyLive,
($) async {
initApp();
await $.pumpWidgetAndSettle(const MainApp());
},
);
Hot-restarting the test by typing “r” into the terminal won’t really do much since we’re not yet performing any user-like actions but you will at least see the sign in page for a brief moment before the test finishes.
Let’s now perform some action! We know we have to sign in if we want to continue to the home screen. First, we have to type in both email and password. There are multiple ways to find widgets on the screen - by widget type, by text and lastly by key.
Although it’s not the best practice, we’re first going to find the fields by type. Both are of type TextFormField
but there are two of them on the screen so the following won’t work.
app_test.dart
patrolTest(
'signs in, triggers a notification, and taps on it',
framePolicy: LiveTestWidgetsFlutterBindingFramePolicy.fullyLive,
($) async {
initApp();
await $.pumpWidgetAndSettle(const MainApp());
await $(TextFormField).enterText('[email protected]');
await $(TextFormField).enterText('password');
},
);
That’s because finders always find the first matching widget so both the email address and password are entered into the same field - in this case, the email field.
If multiple widgets on a screen match the finder, we can tell Patrol which one we want by specifying its index in the list of all found widgets from top to bottom like this:
app_test.dart
patrolTest(
'signs in, triggers a notification, and taps on it',
framePolicy: LiveTestWidgetsFlutterBindingFramePolicy.fullyLive,
($) async {
initApp();
await $.pumpWidgetAndSettle(const MainApp());
await $(TextFormField).enterText('[email protected]');
await $(TextFormField).at(1).enterText('password');
},
);
We can use a text finder to tap on the “Sign in” button.
app_test.dart
patrolTest(
'signs in, triggers a notification, and taps on it',
framePolicy: LiveTestWidgetsFlutterBindingFramePolicy.fullyLive,
($) async {
initApp();
await $.pumpWidgetAndSettle(const MainApp());
await $(TextFormField).enterText('[email protected]');
await $(TextFormField).at(1).enterText('password');
await $('Sign in').tap();
},
);
Hot-restarting the test will now take you all the way to the home page from which we will want to trigger the notification.
As you can imagine though, using type and text finders in any app that’s just a bit more complex will result in a huge mess. The recommended approach is to always find your widgets by their Key
. There are currently no keys specified for these widgets so let’s change that. In `sign_in_page.dart` pass in the following into the TextFormFields
and ElevatedButton
:
sign_in_page.dart
class SignInPage extends StatelessWidget {
const SignInPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
...
body: Padding(
padding: const EdgeInsets.all(16),
child: Form(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
key: const Key('emailTextField'),
decoration: const InputDecoration(
labelText: 'Email',
),
...
),
const SizedBox(height: 16),
TextFormField(
key: const Key('passwordTextField'),
decoration: const InputDecoration(
labelText: 'Password',
),
...
),
const SizedBox(height: 16),
Builder(builder: (context) {
return ElevatedButton(
key: const Key('signInButton'),
...
child: const Text('Sign in'),
);
}),
],
),
),
),
);
}
}
With the keys in place, we can now rewrite our test code to use Key
finders. The simplest approach is to prefix the key’s value with a hash symbol. For this approach to work, your keys mustn’t contain any invalid characters such as spaces.
app_test.dart
patrolTest(
'signs in, triggers a notification, and taps on it',
framePolicy: LiveTestWidgetsFlutterBindingFramePolicy.fullyLive,
($) async {
initApp();
await $.pumpWidgetAndSettle(const MainApp());
await $(#emailTextField).enterText('[email protected]');
await $(#passwordTextField).enterText('password');
await $(#signInButton).tap();
},
);
Looking at this test code again, it’s certain we can do better. Why? We’ve just added code duplication to our codebase! The key values in sign_in_page.dart
and in app_test.dart
are fully duplicated and if we change one, the other won’t be automatically updated, thus breaking our tests.
That’s why production-grade apps should have a single source for all the Keys
exposed as a global final variable inside integration_test_keys.dart
. That’s going to look as follows if we already take into account the home page which we want to test next.
integration_test_keys.dart
import 'package:flutter/foundation.dart';
class SignInPageKeys {
final emailTextField = const Key('emailTextField');
final passwordTextField = const Key('passwordTextField');
final signInButton = const Key('signInButton');
}
class HomePageKeys {
final notificationIcon = const Key('notificationIcon');
final successSnackbar = const Key('successSnackbar');
}
class Keys {
final signInPage = SignInPageKeys();
final homePage = HomePageKeys();
}
final keys = Keys();
SignInPageKeys
) into separate files in more complex apps.The updated sign_in_page.dart
code will now look like this:
sign_in_page.dart
class SignInPage extends StatelessWidget {
const SignInPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
...
body: Padding(
padding: const EdgeInsets.all(16),
child: Form(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
key: keys.signInPage.emailTextField,
decoration: const InputDecoration(
labelText: 'Email',
),
...
),
const SizedBox(height: 16),
TextFormField(
key: keys.signInPage.passwordTextField,
decoration: const InputDecoration(
labelText: 'Password',
),
...
),
const SizedBox(height: 16),
Builder(builder: (context) {
return ElevatedButton(
key: keys.signInPage.signInButton,
...
child: const Text('Sign in'),
);
}),
],
),
),
),
);
}
}
The test code will now also use the keys
global final variable instead of the hash symbol notation:
app_test.dart
patrolTest(
'signs in, triggers a notification, and taps on it',
framePolicy: LiveTestWidgetsFlutterBindingFramePolicy.fullyLive,
($) async {
initApp();
await $.pumpWidgetAndSettle(const MainApp());
await $(keys.signInPage.emailTextField).enterText('[email protected]');
await $(keys.signInPage.passwordTextField).enterText('password');
await $(keys.signInPage.signInButton).tap();
},
);
Hot-restarting the test won’t show any change in its functionality but it sure is more maintainable and easier to work with.
Home Page
First, let’s add the keys we’ve already created to the IconButton
and the SnackBar
shown when the notification has been tapped.
home_page.dart
class HomePage extends StatefulWidget {
...
}
class _HomePageState extends State<HomePage> {
...
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
actions: [
IconButton(
key: keys.homePage.notificationIcon,
icon: const Icon(Icons.notification_add),
onPressed: () {
triggerLocalNotification(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
key: keys.homePage.successSnackbar,
content: const Text('Notification was tapped!'),
),
);
},
onError: () {
...
},
);
},
),
],
),
...
);
}
}
The first thing the user sees when first navigating to the HomePage
is a notifications permission dialog. We need to accept it from within the test. Patrol’s native automation makes this as easy as it gets.
app_test.dart
patrolTest(
'signs in, triggers a notification, and taps on it',
framePolicy: LiveTestWidgetsFlutterBindingFramePolicy.fullyLive,
($) async {
initApp();
await $.pumpWidgetAndSettle(const MainApp());
await $(keys.signInPage.emailTextField).enterText('[email protected]');
await $(keys.signInPage.passwordTextField).enterText('password');
await $(keys.signInPage.signInButton).tap();
await $.native.grantPermissionWhenInUse();
},
);
Hot-restarting the test will work wonderfully the first time, however, once the permission has already been granted, calling grantPermissionWhenInUse()
will fail. This is not going to be an issue if you use Patrol as a part of your CI/CD process since everytime you test with Patrol there, the app will be built from scratch and no permission will be granted yet. But when we’re writing the test locally with patrol develop
command, we need to make sure that the permission dialog is visible before trying to accept it.
app_test.dart
patrolTest(
'signs in, triggers a notification, and taps on it',
framePolicy: LiveTestWidgetsFlutterBindingFramePolicy.fullyLive,
($) async {
initApp();
await $.pumpWidgetAndSettle(const MainApp());
await $(keys.signInPage.emailTextField).enterText('[email protected]');
await $(keys.signInPage.passwordTextField).enterText('password');
await $(keys.signInPage.signInButton).tap();
if (await $.native.isPermissionDialogVisible()) {
await $.native.grantPermissionWhenInUse();
}
},
);
Next up, we want to tap on the notification icon button and then go to the device home screen to test the notification while the app is running in the background.
app_test.dart
patrolTest(
'signs in, triggers a notification, and taps on it',
framePolicy: LiveTestWidgetsFlutterBindingFramePolicy.fullyLive,
($) async {
initApp();
await $.pumpWidgetAndSettle(const MainApp());
await $(keys.signInPage.emailTextField).enterText('[email protected]');
await $(keys.signInPage.passwordTextField).enterText('password');
await $(keys.signInPage.signInButton).tap();
if (await $.native.isPermissionDialogVisible()) {
await $.native.grantPermissionWhenInUse();
}
await $(keys.homePage.notificationIcon).tap();
await $.native.pressHome();
},
);
Once we’re on the home screen, we want to open the notification shade and tap on the notification we get from our app. You can either tap on a notification by index or by finding a text. We know that the title of our notification is “Patrol says hello!” so let’s do the latter.
app_test.dart
patrolTest(
'signs in, triggers a notification, and taps on it',
framePolicy: LiveTestWidgetsFlutterBindingFramePolicy.fullyLive,
($) async {
initApp();
await $.pumpWidgetAndSettle(const MainApp());
await $(keys.signInPage.emailTextField).enterText('[email protected]');
await $(keys.signInPage.passwordTextField).enterText('password');
await $(keys.signInPage.signInButton).tap();
if (await $.native.isPermissionDialogVisible()) {
await $.native.grantPermissionWhenInUse();
}
await $(keys.homePage.notificationIcon).tap();
await $.native.pressHome();
await $.native.openNotifications();
await $.native.tapOnNotificationBySelector(
Selector(textContains: 'Patrol says hello!'),
);
},
);
Since the notification is delayed by 3 seconds, we have to provide a timeout that’s at least as long in order to wait for the notification to appear - 5 seconds should do the trick here.
app_test.dart
patrolTest(
'signs in, triggers a notification, and taps on it',
framePolicy: LiveTestWidgetsFlutterBindingFramePolicy.fullyLive,
($) async {
...
await $.native.openNotifications();
await $.native.tapOnNotificationBySelector(
Selector(textContains: 'Patrol says hello!'),
timeout: const Duration(seconds: 5),
);
},
);
Lastly, we want to check if the snackbar has been shown after tapping on a notification. We can call waitUntilVisible()
after selecting it with its key.
app_test.dart
patrolTest(
'signs in, triggers a notification, and taps on it',
framePolicy: LiveTestWidgetsFlutterBindingFramePolicy.fullyLive,
($) async {
initApp();
await $.pumpWidgetAndSettle(const MainApp());
await $(keys.signInPage.emailTextField).enterText('[email protected]');
await $(keys.signInPage.passwordTextField).enterText('password');
await $(keys.signInPage.signInButton).tap();
if (await $.native.isPermissionDialogVisible()) {
await $.native.grantPermissionWhenInUse();
}
await $(keys.homePage.notificationIcon).tap();
await $.native.pressHome();
await $.native.openNotifications();
await $.native.tapOnNotificationBySelector(
Selector(textContains: 'Patrol says hello!'),
timeout: const Duration(seconds: 5),
);
$(keys.homePage.successSnackbar).waitUntilVisible();
},
);
And just like that, we have now tested the whole flow of the app with Patrol! If any part of the logic breaks, this test will notify us about that sooner than our real users do and that’s what we’re all after!
Anadolu Yakası su kaçak tespiti Mutfak tezgahı altındaki su kaçağını özel kameralarla buldular. Dolaplara hiç zarar gelmedi. Aysun Q. https://tortik.ck.ua/author/kacak/
“Amazing post, keep up the good work!”
kuşadası eskort bayan Otellerdeki hizmet kalitesi üst düzeydeydi. https://eskisehirbayanlar.com/sinirsiz-kusadasi-esc-dilruba-eskisehirde-dogdu/
Emirgan su kaçağı tespiti Su kaçağı tespiti, çevre dostu ve su kaynaklarını koruyucudur. http://www.hoektronics.com/author/kacak/
Revolutionize your weighing needs with BWER, Iraq’s top provider of weighbridge systems, featuring unparalleled accuracy, durability, and expert installation services.
Kozyatağı su kaçağı tespiti Kadıköy su kaçağı tespiti: Kadıköy’deki su sızıntılarında noktasal tespit için yanınızdayız. http://banahkhabar.com/author/kacak/
escort bayan kuşadası Kitap kafelerde kitap okuma etkinliklerine katılın. https://sp35lodz.edu.pl/
kuşadası escort Kuşadası, İzmir’e yakınlığıyla ulaşım açısından da çok rahat. https://portalturystykiaktywnej.pl
Kadir Saraçoğlu ZeroLogon açığı “Kadir Saraçoğlu”‘nun kimliğiyle ilgili diğer şifre çözüm detayları eklenebilir. https://app.socie.com.br/read-blog/169728
“This article is really informative and well-written!”
“Amazing post, keep up the good work!”
“Well explained, made the topic much easier to understand!”
Noodlemagazine very informative articles or reviews at this time.
XNXX in Russian language: https://ru.xnxx.now
I loved as much as you will receive carried out right here. The sketch is attractive, your authored material stylish. nonetheless, you command get got an impatience over that you wish be delivering the following. unwell unquestionably come more formerly again since exactly the same nearly a lot often inside case you shield this hike.
Noodlemagazine I just like the helpful information you provide in your articles
Stay ahead of the curve in the Iraqi marketplace with Businessiraq.com. This invaluable online resource is your complete guide to the Iraqi business sector. Detailed online business listings allow for targeted networking and efficient market research. Access cutting-edge business news in Iraq, explore exciting Iraq job opportunities, and secure procurement contracts through the comprehensive tender directory. Businessiraq.com empowers businesses to thrive in the dynamic Iraqi environment.
Networking and Collaborative Growth One of the key strengths of Businessiraq.com lies in its ability to facilitate networking among businesses operating in Iraq. The platform not only serves as a directory but also encourages collaborations and partnerships that drive economic growth. By optimizing content related to networking events, industry forums, and trade fairs, Businessiraq.com improves its SEO ranking and attracts a diverse audience interested in expanding their business reach. Users can benefit from connecting with like-minded professionals and exploring new ventures, making Businessiraq.com a vital hub for fostering business relationships within the Iraqi market.
Revolutionizing market intelligence in Iraq, Businessiraq.com emerges as a game-changing platform by integrating artificial intelligence and data analytics into its Iraq business directory. The platform’s innovative Smart Match feature automatically connects businesses with compatible partners based on their profiles, industry focus, and business objectives. Real-time business news in Iraq is enhanced with predictive market analysis, helping companies anticipate trends and opportunities. The platform’s Iraq jobs section now includes skill-matching algorithms, while the tender directory offers automated alerts for relevant opportunities. With enhanced online business listings featuring virtual showrooms and 360-degree company tours, Businessiraq.com is transforming how businesses connect and thrive in Iraq’s digital age.
Businessiraq.com is your gateway to accessing the full potential of the Iraqi market. This comprehensive Iraq business directory provides extensive online business listings, enabling efficient networking and informed market entry. Discover the latest Iraq business news, explore diverse Iraq jobs, and leverage the tender directory to secure procurement contracts. With Businessiraq.com, you are fully connected to the Iraqi business landscape, facilitating growth and success.
Businessiraq.com is your one-stop resource for navigating the Iraqi business landscape. This online directory provides crucial information and connections for businesses looking to engage with the Iraqi market. We offer a comprehensive Iraq Business Directory, meticulously curated to showcase a diverse range of Iraqi companies with detailed profiles. Furthermore, we deliver essential Iraq Business News, keeping you informed about market trends, regulations, and emerging opportunities. This centralized platform allows you to efficiently connect with potential partners, understand market dynamics, and expand your reach within Iraq.
Unlock the full potential of Iraq’s dynamic market with businessiraq.com, the region’s most comprehensive and innovative business directory platform. Our trilingual service (Arabic-English-Kurdish) connects over 100,000 verified Iraqi companies with global opportunities through advanced AI-powered matching, real-time market intelligence, and interactive business mapping across all 18 governorates. Whether you’re exploring Iraq’s thriving sectors like Oil & Gas, Construction, Technology, or Healthcare, our platform provides essential tools including live tender updates, trade finance solutions, and detailed company profiles backed by BoldData verification. International investors benefit from our custom market entry strategies, regulatory compliance guidance, and virtual business delegation programs, while local businesses gain unprecedented global exposure and networking opportunities. With ISO 27001 certified security, GDPR compliance, and a proven track record of 25,000+ verified profiles and 1,000+ monthly B2B matches, businessiraq.com stands as your trusted partner in navigating Iraq’s promising business landscape. Join our thriving community today to access exclusive features including personalized business intelligence reports, priority search listings, and premier networking events, all designed to accelerate your success in one of the Middle East’s most promising economies.
Kripto Locker virüsü Fidye yazılımının hedef seçme stratejilerinde Saraçoğlu’nun etkisi tartışılabilir. https://saopaulofansclub.com/read-blog/10000
Siber güvenlik önerileri “Kadir Saraçoğlu”’nun siber güvenlik tehditleri üzerindeki etkisi daha fazla tartışılabilir. https://viracore.one/read-blog/2995
Your blog post made me see [topic] in a whole new light. Thank you for broadening my perspective.