slint_patrol

Drive Slint UIs from Patrol tests.

Slint renders its whole UI into a single Flutter widget, so Flutter's own finders — and Patrol's $(...) — see one opaque box. There is no widget for the button you want to press. This package adds $.slint(...) finders that search the live component's accessibility tree instead, then tap and type through real Flutter gestures aimed at where the element actually sits on screen. The input travels the same path a user's finger does: gesture → SlintView's Listener → Slint. No test-only side door.

import 'package:patrol_finders/patrol_finders.dart';
        import 'package:slint_patrol/slint_patrol.dart';
        
        patrolWidgetTest('adding a todo', ($) async {
          await $.pumpWidget(const MyApp()); // an app that loads ui/todo.slint
          await $.slintSettle();
        
          await $.slintById('TodoView::edit').enterText('buy milk');
          await $.slint('Add').tap();
          await $.slintSettle();
        
          expect(TodoApp($.slintComponent()).todoModel.last.title, 'buy milk');
        });
        

It extends PatrolTester, so it composes with the rest of Patrol: the same test can drive Flutter widgets with $(...), Slint elements with $.slint(...), and native UI with Patrol's $.native.

Finding elements

Method Matches
$.slint(label) the accessible label — a button's text, a checkbox's caption
$.slintById(id) an element id qualified by the component that declares it, TodoView::edit
$.slintByType(name) the element's type, Button, LineEdit
$.slintByRole(role) the accessible role, Button, Checkbox, TextInput
$.slintAll() every element in the tree

Prefer $.slint(label): it matches what a user reads. $.slintAll() walks into the widgets' own implementation — a LineEdit is followed by the Rectangle, TextInput, and Text that make it up — which is useful for exploring an unfamiliar screen and noisy for assertions.

Finders are lazy, like Patrol's. Nothing is queried until you await an action or read current, exists, or count.

await $.slint('Save').tap();                  // waits, then taps
        await $.slintByType('Text').first.tap();      // narrow an ambiguous match
        $.slint('Save').exists;                       // right now, no waiting
        await $.slint('Save').resolve();              // the SlintElementInfo itself
        

resolve() throws when several elements match, so a test never silently acts on an arbitrary one — narrow with first or at(index) when that is what you mean.

Waiting, and why not pumpAndSettle

SlintView drives a Ticker that renders every frame, so the widget tree never goes quiescent and pumpAndSettle always times out. Use $.slintSettle() — a bounded handful of frames — wherever a Flutter-only test would settle.

Actions wait on their own: tap() and enterText() pump until the element exists and has a non-zero size, because Slint computes geometry during layout and an element found before the first frame reports an empty rect that is not yet somewhere you can click. The budget comes from Patrol's config.visibleTimeout, spent as frames rather than wall-clock time — the test clock is fake, so real waiting would simply hang.

Typing

enterText taps the element to focus it, then types through the text input connection a focused SlintView holds on every platform — the path a soft keyboard takes, and the one the desktop text input plugin takes behind a hardware keyboard. It is the app's real input path. SlintView maps the edits to characters, backspace and enter, which is the limit here too.

Properties and callbacks

$.slintComponent() returns the live component. Wrap it in the generated wrapper — its constructor takes any backend's component — and use the same typed members the app uses, never a Slint property name:

final app = TodoApp($.slintComponent());
        expect(app.todoModel.map((t) => t.title), contains('buy milk'));
        app.invokeAddTodo('Groceries'); // runs the app's own handler
        

examples/todo/test/todo_patrol_test.dart drives the real TodoPage this way.

Which backend works

Element queries need SlintInspectableComponent, which the interpreter backend implements — that is what debug builds use, including flutter test and patrol test. The AOT backend of release and profile builds does not implement it yet, and finders throw a message saying so rather than failing obscurely.

How it relates to slint_testing

slint_testing tests a component headlessly: no window, no renderer, no Flutter. It is the fast unit-level tool, and it owns the element model — SlintElementInfo, which this package reuses.

slint_patrol tests the running app: a real widget tree, real geometry, real gestures. Both describe elements identically because both descriptors come from one Rust implementation in slint-dart-interpreter, exposed by the testing backend's FFI and the runtime backend's alike.

Reach for slint_testing to check a component's logic, and slint_patrol to check that the app a user touches actually works.

Layout

lib/slint_patrol.dart           public API
        lib/src/slint_finder.dart       SlintFinder: query, wait, tap, type
        lib/src/patrol_tester_ext.dart  the $.slint* methods on PatrolTester
        

No Rust of its own: the query entry point lives in slint_interpreter.