Tips and tricks

Inspecting native view hierarchy

It's hard to tap on or enter text into a view you don't know how to refer to. In such situation we recommend doing a native view hierarchy dump and finding the properties of the view you want to act on.

Android

First, perform a native view hierarchy dump using adb:

adb shell uiautomator dump

Then, copy the dump file from the device to your machine:

adb pull /sdcard/window_dump.xml .

iOS

The easiest way to perform the native view hierarchy dumb on iOS is to use the idb tool.

Once you have idb installed, perform a dump:

idb ui describe-all

Avoiding hardcoding credentials in tests

It's a bad practice to hardcode data such as emails, usernames, and passwords in test code.

await $(#nameTextField).enterText('Bartek'); // bad!
await $(#passwordTextField).enterText('ny4ncat'); // bad as well!

To fix this, we recommend removing the hardcoded credentials from test code and providing them through the environment:

await $(#nameTextField).enterText(const String.fromEnvironment('USERNAME'));
await $(#passwordTextField).enterText(const String.fromEnvironment('PASSWORD'));

Make sure that you're using const here because of issue #55870.

To set USERNAME and PASSWORD, use --dart-define:

patrol test --dart-define 'USERNAME=Bartek' --dart-define 'PASSWORD=ny4ncat'

Alternatively you can create a .patrol.env file in your project's root. Here's an example:

$ cat .patrol.env
EMAIL=user@example.com
PASSWORD=ny4ncat
DISABLE_ANALYTICS=true

Granting sensitive permission through the Settings app

Some particularly sensitive permissions (such as access to background location or controlling the Do Not Disturb feature) cannot be requested in the permission dialog like most of the common permissions. Instead, you have to ask the user to go to the Settings app and grant your app the permission you need.

Testing such flows is not as simple as simply granting normal permission, but it's totally possible with Patrol.

Below we present you with a snippet that will make the built-in Camera app have access to the Do Not Disturb feature on Android. Let's assume that the Settings app on the device we want to run the tests on looks like this:

settings_screenshot

And here's the code:

await $.native.tap(Selector(text: 'Camera')); // tap on the list tile
await $.native.tap(Selector(text: 'ALLOW')); // handle the confirmation dialog
await $.native.pressBack(); // go back to the app under test

Please note that the UI of the Settings app differs across operating systems, their versions, and OEM flavors (in case of Android). You'll have to handle all edge cases yourself.

Ignoring exceptions

If an exception is thrown during a test, it is marked as failed. This is Flutter's default behavior and it's usually good – after all, it's better to fix the cause of a problem instead of ignoring it.

That said, sometimes you do have a legitimate reason to ignore an exception. This can be accomplished with the help of the WidgetTester.takeException() method, which returns the last exception that occurred and removes it from the internal list of uncaught exceptions, so that it won't mark the test as failed. To use it, just call it once:

final widgetTester = $.tester;
widgetTester.takeException();

If more than a single exception is thrown during the test and you want to ignore all of them, the below snippet should come in handy:

var exceptionCount = 0;
dynamic exception = $.tester.takeException();
while (exception != null) {
  exceptionCount++;
  exception = $.tester.takeException();
}
if (exceptionCount != 0) {
  $.log('Warning: $exceptionCount exceptions were ignored');
}

Handling permission dialogs before the main app widget is pumped

Sometimes you might want to manually request permissions in the test before the main app widget is pumped. Let's say that you're using the geolocator package:

final permission = await Geolocator.requestPermission();
final position = await Geolocator.getCurrentPosition();
await $.pumpWidgetAndSettle(MyApp(position: position));

In such case, first call the requestPermission() method, but instead of awaiting it, assign the Future it returns to some final. Then, use Patrol to grant the permissions, and finally, await the Future from the first step:

// 1. request the permission
final permissionRequestFuture = Geolocator.requestPermission();
// 2. grant the permission using Patrol
await $.native.grantPermissionWhenInUse();
// 3. wait for permission being granted
final permissionRequestResult = await permissionRequestFuture;
expect(permissionRequestResult, equals(LocationPermission.whileInUse));
final position = await Geolocator.getCurrentPosition();
await $.pumpWidgetAndSettle(MyApp(position: position));

See also: