Flutter

Flutter is a first class citizen for Maestro. It can test both pure and hybrid (i.e add-to-app) Flutter mobile apps.
Interacting with widgets by semantics label
Maestro can interact with widgets that have semantics information attached. By default, this includes all widgets that display text (data
in the Text widget, hintText
in the TextField, and so on). You can also attach semantics information to any widget using Flutter's Semantics widget.
Example: Tap on a widget
Given an InkWell
widget with a Text
widget child:
InkWell(
child: Text('Open Browser'),
onTap: () => launch('https://mobile.dev'),
)
The following command will tap on it:
- tapOn: Open Browser
Some widget, such as Icon
, don't have implicit semantics. In such cases you can often pass a semanticLabel
:
FloatingActionButton(
onPressed: _incrementCounter,
child: Icon(Icons.add, semanticLabel: 'fabAddIcon'),
)
Then the FloatingActionButton
can be interacted with using the following command:
- tapOn: fabAddIcon
Finally, you can wrap any widget with Semantics:
Semantics(
label: 'funky yellow box',
child: Container(
color: Colors.yellow,
width: 100,
height: 100,
),
)
- tapOn: funky yellow box
Example: Enter text in a widget
To enter text in the following text field widget:
TextField(
decoration: InputDecoration(
border: UnderlineInputBorder(),
labelText: 'Enter your username',
),
)
Use this command:
- tapOn: Enter your username
- inputText: charlie_root
Example: Assert a widget is visible
Text(
'Welcome back, dear $fullName! ๐๐',
semanticsLabel: 'Welcome back, dear $fullName!',
),
- assertVisible: Welcome back, dear Test User!
Interacting with widgets by semantic identifier
When your app grows, testing often becomes harder.
Maybe the app gets multi-language support, and now you have to decide on the language in which you test it. Maybe some of the strings displayed are non-static (e.g. becase of A/B tests). And the sheer number of screens makes tests harder to maintain.
When you start facing these problems, you should consider using the accessibility identifier instead of semantics labels.
Example: Tap on a widget by semantics identifier
Semantics(
identifier: 'signin_button',
child: ElevatedButton(
onPressed: _signIn,
child: Text('Sign in'),
),
)
- tapOn:
id: signin_button
Example: Enter text in a widget by semantics identifier
Semantics(
identifier: 'username_textfield',
child: TextField(
decoration: InputDecoration(
border: UnderlineInputBorder(),
labelText: 'Enter your username',
),
),
)
- tapOn:
id: username_textfield
- inputText: charlie_root
Good practices
Let's say you have a FancyButton
widget in your app. These buttons are important for you, and you want to ensure they always have an accessibility identifier assigned so they can be reliably interacted with using Maestro. The code sample below requires all callers of FancyButton
to pass an accessibility identifier:
class FancyButton extends StatelessWidget {
FancyButton({
super.key,
required this.identifier,
required this.onPressed,
});
final String identifier;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return Semantics(
identifier: identifier,
child: RawMaterialButton(
onPressed: onPressed,
// ...
),
);
}
}
This also has the benefit of reducing widget nesting at the call site:
FancyButton(
identifier: 'buy_premium',
onPressed: _buyPremium,
)
// instead of:
Semantics(
identifier: 'buy_premium',
FancyButton(
onPressed: _buyPremium,
),
)
Of course, there's always danger of a developer accidentally not using the FancyButton
widget and defering to the built-in ElevatedButton
. To combat that, we recommend setting up lint rules that forbid using ElevatedButton
and enforce replacing it with a FancyButton
instead. For example you can use the leancode_lint package with the following configuration in analysis_options.yaml
:
include: package:leancode_lint/analysis_options.yaml
custom_lint:
rules:
- use_design_system_item:
FancyButton:
- instead_of: ElevatedButton
from_package: flutter
analyzer:
plugins:
- custom_lint
Why not Flutter keys?
Flutter widget keys cannot be used in Maestro because there's no linkage between widget keys and Flutter's accessibility bridge system. This makes using Keys impossible since Maestro is accessibility-tree based.
Also, Flutter API docs for the Key class and Widget.key field say it's for "controlling how one widget replaces another (e.g. in a list)". Keys are just not a mechanism for assigning unique IDs to widgets for testing purposes.
We strongly recommend making your app accessible (not just for UI tests, but for all of your users with different needs). When testing at scale, you should also consider using an accessibility identifier.
Here's also a little trick that you may find useful if you really want to use keys in Maestro (using the FancyButton
example from above):
class FancyButton extends StatelessWidget {
FancyButton({
required String key,
required this.onPressed,
}) : _key = key,
super(key: ValueKey(key));
final String _key;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return Semantics(
identifier: _key,
child: RawMaterialButton(
onPressed: onPressed,
// ...
),
);
}
}
Callers are required to pass a string key:
FancyButton(
key: 'unlock_reward',
onPressed: _unlockReward,
)
And you can easily interact with the widget using Maestro:
- tapOn:
id: unlock_reward
Known Limitations
Maestro cannot be used to test Flutter Desktop apps yet.
But if you've got a Flutter Web app, it'll work exactly the same as Web. Annotate with Semantics where you need to, and test as you would any other webpage.
Last updated
Was this helpful?