You can also wrap Flutter’s built-in finders with $:
// Use semantics finderawait $(find.bySemanticsLabel('Edit profile')).tap();// Use any Flutter finder$(find.text('Hello'))$(find.byWidgetPredicate((widget) => widget is Text))
// Check visibility at center (default)expect($('Log in').visible, equals(true));// Check visibility at specific alignmentexpect($(TextField).isVisibleAt(alignment: Alignment.topLeft), equals(true));
Flutter’s default matchers like findsOneWidget only check if a widget exists in the widget tree, not if it’s visible to the user. Use the visible getter for visibility checks!
expect($(#signInButton).text, equals('Sign in'));// Wait for visibility first to avoid errorsexpect( await $(#statusLabel).waitUntilVisible().text, equals('Success'),);
// Tap on first widget foundawait $('Subscribe').tap();// Tap on specific widget by indexawait $('Subscribe').at(2).tap();// Tap at specific alignmentawait $(#button).tap(alignment: Alignment.topRight);
Smart waiting: tap() doesn’t immediately fail if the widget isn’t visible. It waits up to the configured timeout and taps as soon as the widget becomes visible!
await $(#emailField).enterText('[email protected]');await $(#passwordField).enterText('secret123');// Enter text into third TextFieldawait $(TextField).at(2).enterText('Code ought to be lean');
// Scroll to widget in the first Scrollableawait $('Delete account').scrollTo().tap();// Scroll in specific directionawait $('Bottom item').scrollTo( scrollDirection: AxisDirection.down,);// Customize scroll behaviorawait $('Far away widget').scrollTo( step: 128.0, // pixels per scroll maxScrolls: 50, // maximum scroll attempts);
// Wait for widget to become visibleawait $('Success message').waitUntilVisible();// Wait with custom timeoutawait $('Slow loader').waitUntilVisible( timeout: Duration(seconds: 30),);// Wait for existence (not necessarily visible)await $('Background widget').waitUntilExists();
await $('Button').tap( settlePolicy: SettlePolicy.settle, // How to pump frames visibleTimeout: Duration(seconds: 20), // Wait time settleTimeout: Duration(seconds: 10), // Settle time alignment: Alignment.center, // Where to tap);
When multiple widgets match, you can select specific ones:
// Get first widget (default for actions)await $('Subscribe').first.tap();// Get last widgetawait $('Subscribe').last.tap();// Get widget at indexawait $('Subscribe').at(2).tap();
Indexing is zero-based: .at(0) gets the first widget, .at(1) gets the second, etc.
Here’s a comprehensive example showing multiple techniques:
import 'package:flutter/material.dart';import 'package:flutter_test/flutter_test.dart';import 'package:patrol_finders/patrol_finders.dart';void main() { patrolWidgetTest( 'user signs up successfully', ($) async { await $.pumpWidgetAndSettle(const MyApp()); // Find by key and enter text await $(#emailTextField).enterText('[email protected]'); await $(#nameTextField).enterText('Charlie'); await $(#passwordTextField).enterText('ny4ncat'); // Scroll to checkbox and tap await $(#termsCheckbox).scrollTo().tap(); // Find button by text and tap await $('Sign Up').tap(); // Wait for success message await $('Welcome, Charlie!').waitUntilVisible(); // Make assertions expect($('Welcome, Charlie!').visible, true); expect($(Icons.check_circle).exists, true); }, );}