Skip to main content
PatrolFinder is Patrol’s custom finder that wraps Flutter’s Finder with enhanced capabilities like automatic waiting, scrolling, and chainable syntax.

Creating Finders

Using the $ Syntax

The primary way to create finders is through the $() method on PatrolIntegrationTester:
patrolTest('example', ($) async {
  // By widget type
  $(ElevatedButton)
  
  // By key (using Symbol for keys)
  $(#loginButton)
  
  // By text
  $('Submit')
  
  // By text pattern (RegExp)
  $(RegExp(r'Click.*'))
  
  // By icon
  $(Icons.add)
  
  // By widget instance
  $(Text('Hello'))
});

Chaining Finders

You can chain finders to locate nested widgets:
// Find a Text widget inside a Container
$(Container).$(Text)

// Find a specific button in a dialog
$(Dialog).$(ElevatedButton).$(Text('OK'))

// Find by key inside a specific widget
$(Scaffold).$(

Core Methods

Interaction Methods

tap({...})
Future<void>
Waits for the widget to be visible and taps it.Parameters:
  • settlePolicy - How to pump frames after action
  • visibleTimeout - Max time to wait for visibility
  • settleTimeout - Timeout for settling
  • alignment - Where to tap (default: center)
await $(#submitButton).tap();

// Tap at specific alignment
await $(Card).tap(alignment: Alignment.topRight);
longPress({...})
Future<void>
Waits for visibility and performs a long press.
await $(#contextMenuItem).longPress();
enterText(String text, {...})
Future<void>
Waits for the text field to be visible and enters text.
await $(#emailField).enterText('[email protected]');

Waiting Methods

waitUntilVisible({...})
Future<PatrolFinder>
Waits until the finder locates at least one visible widget.Parameters:
  • timeout - Max time to wait
  • alignment - Point to check for visibility
await $(Text('Welcome')).waitUntilVisible();

// With custom timeout
await $(Text('Slow Widget')).waitUntilVisible(
  timeout: Duration(seconds: 30),
);
waitUntilExists({...})
Future<PatrolFinder>
Waits until the widget exists (not necessarily visible).
await $(Text('Hidden')).waitUntilExists();

Scrolling Methods

scrollTo({...})
Future<PatrolFinder>
Scrolls until this widget becomes visible.Parameters:
  • view - The scrollable widget
  • step - Distance to scroll per iteration
  • scrollDirection - Direction to scroll
  • maxScrolls - Maximum scroll attempts
await $(Text('Item 50')).scrollTo();

// With custom parameters
await $(Text('Bottom')).scrollTo(
  view: $(ListView),
  step: 200,
  maxScrolls: 30,
);

Refinement Methods

at(int index)
PatrolFinder
Returns the widget at the specified index when multiple matches exist.
// Tap the third button
await $(ElevatedButton).at(2).tap();
first
PatrolFinder
Returns the first widget found.
await $(TextField).first.enterText('First name');
last
PatrolFinder
Returns the last widget found.
await $(ListTile).last.tap();
$(dynamic matching)
PatrolFinder
Finds a descendant widget. Chain finders together.
// Find Text inside a Card
$(Card).$(Text)

// Deeper nesting
$(Scaffold).$(AppBar).$(Text)
containing(dynamic matching)
PatrolFinder
Finds an ancestor widget containing the specified descendant.
// Find the Card that contains specific text
$(Card).containing(Text('Title'))
which<T>(bool Function(T) predicate)
PatrolFinder
Filters widgets using a predicate function.
// Find enabled buttons only
$(ElevatedButton).which<ElevatedButton>(
  (btn) => btn.enabled,
)

// Find text with specific style
$(Text).which<Text>(
  (text) => text.style?.color == Colors.red,
)
hitTestable({Alignment at})
PatrolFinder
Returns only widgets that are hit-testable at the specified alignment.
$(Button).hitTestable(at: Alignment.center)

Property Getters

text
String?
Returns the text content if the widget is a Text or RichText.
final buttonText = $(#submitButton).text;
expect(buttonText, 'Submit');
exists
bool
Returns true if the finder locates at least one widget.
if ($(Text('Optional')).exists) {
  await $(Text('Optional')).tap();
}
visible
bool
Returns true if the finder locates at least one visible widget.Checks visibility at Alignment.center.
expect($(#errorMessage).visible, false);
isVisibleAt({Alignment alignment})
bool
Returns true if the widget is visible at the specified alignment.
if ($(Card).isVisibleAt(alignment: Alignment.topLeft)) {
  // Widget is visible
}

Usage Examples

Basic Widget Finding

patrolTest('find widgets', ($) async {
  await $.pumpWidgetAndSettle(MyApp());
  
  // Find by type
  await $(ElevatedButton).tap();
  
  // Find by key
  await $(#loginButton).tap();
  
  // Find by text
  await $('Sign In').tap();
  
  // Find by icon
  await $(Icons.menu).tap();
});

Chaining and Nesting

patrolTest('nested finders', ($) async {
  await $.pumpWidgetAndSettle(MyApp());
  
  // Find a button inside a specific card
  await $(Card).$(ElevatedButton).tap();
  
  // Find text inside app bar
  final title = $(AppBar).$(Text).text;
  expect(title, 'My App');
  
  // Complex nesting
  await $(Scaffold)
    .$(BottomNavigationBar)
    .$(Icon).at(1)
    .tap();
});

Handling Multiple Matches

patrolTest('multiple widgets', ($) async {
  await $.pumpWidgetAndSettle(MyApp());
  
  // Tap first button
  await $(ElevatedButton).first.tap();
  
  // Tap third button
  await $(ElevatedButton).at(2).tap();
  
  // Tap last button
  await $(ElevatedButton).last.tap();
});

Using Predicates

patrolTest('filter with predicate', ($) async {
  await $.pumpWidgetAndSettle(MyApp());
  
  // Find and tap only enabled buttons
  await $(ElevatedButton)
    .which<ElevatedButton>((btn) => btn.enabled)
    .first
    .tap();
  
  // Find text with specific color
  final redText = $(Text).which<Text>(
    (text) => text.style?.color == Colors.red,
  ).text;
});

Scrolling

patrolTest('scroll to element', ($) async {
  await $.pumpWidgetAndSettle(MyApp());
  
  // Scroll to item and tap it
  await $(Text('Item 100')).scrollTo().tap();
  
  // Scroll with custom parameters
  await $(Text('Bottom Item'))
    .scrollTo(
      view: $(ListView),
      step: 150,
      maxScrolls: 50,
    )
    .tap();
});

Conditional Logic

patrolTest('conditional interactions', ($) async {
  await $.pumpWidgetAndSettle(MyApp());
  
  // Check if widget exists
  if ($(Text('Optional Dialog')).exists) {
    await $(TextButton).containing(Text('Dismiss')).tap();
  }
  
  // Check if widget is visible
  if ($(#notificationBanner).visible) {
    await $(#dismissButton).tap();
  }
});

Getting Text Content

patrolTest('verify text', ($) async {
  await $.pumpWidgetAndSettle(MyApp());
  
  // Get text from widget
  final counter = $(#counterText).text;
  expect(counter, '0');
  
  await $(Icons.add).tap();
  
  final updatedCounter = $(#counterText).text;
  expect(updatedCounter, '1');
});

Chained Actions

patrolTest('chain actions with scrolling', ($) async {
  await $.pumpWidgetAndSettle(MyApp());
  
  // Scroll to widget, wait for it, then tap
  await $(Text('Submit'))
    .scrollTo()
    .waitUntilVisible()
    .tap();
});

createFinder() Function

The underlying function that maps types to Flutter finders:
Finder createFinder(dynamic matching)
Supported types:
  • Typefind.byType()
  • Keyfind.byKey()
  • Symbolfind.byKey(Key(symbol.name))
  • Stringfind.text()
  • Patternfind.textContaining()
  • IconDatafind.byIcon()
  • PatrolFinder → Returns its underlying finder
  • Finder → Returns itself
  • Widgetfind.byWidget()

See Also

Build docs developers (and LLMs) love