Using custom finders and custom commands with flutter driver extension (advanced level)
While working with flutter driver extension in the flutter version 1.25 while it still was in beta I noticed two interesting parameters that can be passed to enableFlutterDriverExtension
function. Those are finders
and commands
. Those parameters are now in stable version of flutter 2 but still are described poorly and did not find any documentation or examples in the wild on how to use those. So I tried to figure out on how to use those myself.
The source code comments regarding those params are lacking proper documentation on how to use those at the moment and does not provide working examples but still I was able to implement this feature and it is awesome.
As you know at the moment there are very few types of finders in flutter driver (byType
, byText
, byValueKey
, etc). But there are even fewer commands that you can do with those (tap
, enterText
, maybe smth else). So using this approach you can extend those without limits.
Here I continue to work with app and tests from previous lessons. It is a basic app that is created when you start new flutter project, the one that increases counter text every time you press + button. The is a single source code for the app and tests within it and it is available here: https://github.com/arturk/flutter_driver_tutorials/tree/custom_finder_commands .Within the source code of that app I created a folder for new finders: lib/finders
. Within that folder I first created finders file that creates a new finder for the FloatingActionButton
type as well as a new command that will use this finder. The finder will find a single instance of the FloatingActionButton
. And the new command will Tap
FloatingActionButton
specified number of times. This file basically does not contain any working functionality. It serves only to describe the finder and command, like how those are called (finderType
is passed to driver extension for finder and kind
for command), how those should be serialized to be sent to the flutter driver extension and how those should be deserialized to populate all own parameters. Also this file contains structure of the new commands.
Finder:
import 'package:flutter_driver/flutter_driver.dart';class FloatingActionButtonFinder extends SerializableFinder {
const FloatingActionButtonFinder();@override
String get finderType => 'FloatingActionButtonFinder';@override
Map<String, String> serialize() => super.serialize();
}
Command:
class FloatingActionButtonCommand extends CommandWithTarget {
FloatingActionButtonCommand(SerializableFinder finder, this.times,
{Duration timeout})
: super(finder, timeout: timeout);FloatingActionButtonCommand.deserialize(
Map<String, String> json, DeserializeFinderFactory finderFactory)
: times = int.parse(json['times']),
super.deserialize(json, finderFactory);@override
Map<String, String> serialize() {
return super.serialize()..addAll(<String, String>{'times': '$times'});
}@override
String get kind => 'FloatingActionButtonCommand';final int times;
}class FloatingActionButtonCommandResult extends Result {
const FloatingActionButtonCommandResult(this.result);final String result;@override
Map<String, dynamic> toJson() {
return <String, dynamic>{
'result': result,
};
}
}
Now when we have finished describing our new finders and commands we can create a file that will implement functionality for those. I placed it in lib/finders/finders_extension.dart
.
This file should import the previously created file. It will map commands to commands extension using its commandKind
parameter and kind
parameter from previous file. finderType
will be matched to the param with the same name. The createFinder
function should return true
or false
. true
should be returned when your find predicate succeeds with some widget. In my case I returned true
as soon as element linked to widget of type FloatingActionButton
was found.
import 'package:flutter_driver/driver_extension.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_driver/flutter_driver.dart'
show SerializableFinder, Command, Tap, Result;
import 'finders.dart';class FloatingActionButtonFinderExtension extends FinderExtension {
String get finderType => 'FloatingActionButtonFinder';SerializableFinder deserialize(
Map<String, String> params, DeserializeFinderFactory finderFactory) {
return FloatingActionButtonFinder();
}Finder createFinder(
SerializableFinder finder, CreateFinderFactory finderFactory) {
return find.byElementPredicate((Element element) {
if (element.widget is FloatingActionButton) {
return true;
}
return false;
});
}
}
You command extension will use the finder within its call
function to execute your logic for that element. In my case I Tap
times
number of times on that element.
class FloatingActionButtonCommandExtension extends CommandExtension {
@override
String get commandKind => 'FloatingActionButtonCommand'; @override
Future<Result> call(
Command command,
WidgetController prober,
CreateFinderFactory finderFactory,
CommandHandlerFactory handlerFactory) async {
final FloatingActionButtonCommand someCommand = command; final Finder finder = finderFactory.createFinder(someCommand.finder);
await handlerFactory.waitForElement(finder);
for (int index = 0; index < someCommand.times; index++) {
await handlerFactory.handleCommand(
Tap(someCommand.finder), prober, finderFactory);
} return FloatingActionButtonCommandResult(
'Tapped ${someCommand.times} times');
}@override
Command deserialize(
Map<String, String> params,
DeserializeFinderFactory finderFactory,
DeserializeCommandFactory commandFactory) {
return FloatingActionButtonCommand.deserialize(params, finderFactory);
}
}
Now in order to use these you need to enable your flutter driver extension in such way:
void main() {
enableFlutterDriverExtension(
handler: dataHandlerFunction,
commands: [FloatingActionButtonCommandExtension()],
finders: [FloatingActionButtonFinderExtension()]);
app.main();
}
Here I also use my dataHandlerFunction
from previous articles but that one is not important for this topic.
Because one part of the finder is used in testing side (finder) and the other part is used in app side (finder extension) the finders and commands declared in such approach should be done as packages so they can be imported using pubspec.yaml and used both — in app and in tests while having single code base.
Now finally I would write tests like:
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';
import 'package:testing_tutorial/finders/finders.dart';void main() {
group('Counter App', () {
final counterTextFinder = find.byValueKey('counter');
FlutterDriver driver;setUpAll(() async {
driver = await FlutterDriver.connect();
});tearDownAll(() async => driver?.close());test('increments using custom commands by 5', () async {
final expected = int.parse(await driver.getText(counterTextFinder)) + 5;
await driver.sendCommand(FloatingActionButtonCommand(
FloatingActionButtonFinder(), 5,
timeout: Duration(seconds: 10)));
final actual = int.parse(await driver.getText(counterTextFinder));
expect(actual, equals(expected));
});
});
}
And that is it! Using this method you can actually replace all your dataHandler
functionality from flutter driver extension. You can create finders for all your custom widgets. And you can create custom commands for anything that you want to check: widget properties, custom application state changes, code injections, etc!
You can checkout the code from the github and try to run it using:
flutter drive --target=test_driver/driver.dart