patrol-migrate-from-maestro
Skill1.5k starsChanged 9 days agoApache-2.0View on GitHub
Migrate a Flutter app's Maestro UI flows (`.yaml`) to Patrol end-to-end tests — inventory the flows, translate each command to Patrol, replace text and relative selectors with widget keys, move JS scripts and hooks to Dart, and prove every migrated test green before the next one. Use when a project has a `maestro/` (or `.maestro/`) directory and the user wants those flows in Patrol.
- Reads credentials
Adopted by
Steady: 0 new projects took it in the last 4 weeks, none the 4 weeks before.thin data
Who's talking about leancodepl/patrol
- flutter/flutter“Integration Testing docs are very basic and don't cover common real-world scenarios”GitHub · Jan 23, 2024 · 11 reactions
- flutter/flutter“`WidgetTester`'s "end of test verifications" cause integration tests to always fail when Android …”GitHub · Jun 21, 2023 · 10 reactions
- flutter/flutter“Improve `Finder` APIs”GitHub · Nov 22, 2022 · 6 reactions
- VeryGoodOpenSource/very_good_cli“fix: Google Analytics (UA) will stop working on July 1st, 2023”GitHub · Mar 29, 2023 · 2 reactions
- team-telnyx/flutter-voice-sdk“chore(deps): bump patrol from 3.20.0 to 4.1.1”GitHub · Feb 10, 2026 · 1 reactions
The biggest 5 of 53 mentions linking to the repository that our crawl found. Each line is their own headline.
See all 53
Talked about on GitHub and Stack Overflow (53 times).
On GitHub · 50
- Jan 23, 2024Integration Testing docs are very basic and don't cover common real-world scenarios — flutter/fluttersays it tested it11 reactions · 1 comment
- Jun 21, 2023`WidgetTester`'s "end of test verifications" cause integration tests to always fail when Android `UiAutomation` is used — flutter/flutter10 reactions · 10 comments
- Nov 22, 2022Improve `Finder` APIs — flutter/flutter6 reactions · 6 comments
- Mar 29, 2023fix: Google Analytics (UA) will stop working on July 1st, 2023 — VeryGoodOpenSource/very_good_cli2 reactions · 1 comment
- Feb 10, 2026chore(deps): bump patrol from 3.20.0 to 4.1.1 — team-telnyx/flutter-voice-sdk1 reaction · 2 comments
45 more on GitHub
- Nov 3, 2026agent-skills.github.leancodepl.patrol: 4.10.0 → 4.11.0 — sudosubin/agents.nix0 reactions
- Nov 3, 2026claude-code-plugins.github.leancodepl.patrol: 4.10.0 → 4.11.0 — sudosubin/agents.nix0 reactions
- Nov 1, 2026build(deps): bump the mobile-non-major group across 1 directory with 9 updates — sujayamindev/reclaima0 reactions
- Oct 30, 2026chore(deps): bump the all group across 1 directory with 6 updates — stuart-bradley/watch-nook0 reactions · 1 comment
- Oct 25, 2026build(deps): bump the mobile-non-major group across 1 directory with 6 updates — sujayamindev/reclaima0 reactions · 1 comment
- Oct 23, 2026chore(deps): bump the all group across 1 directory with 5 updates — stuart-bradley/watch-nook0 reactions · 1 comment
- Oct 16, 2026build(deps): bump the pub-dependencies group across 1 directory with 16 updates — thiagomiranda3/Getman0 reactions
- Oct 16, 2026chore(deps): bump the all group across 1 directory with 4 updates — stuart-bradley/watch-nook0 reactions · 1 comment
- Oct 15, 2026chore(deps): bump the example-flutter-dependencies group across 1 directory with 5 updates — Ali-Mitchell/googl-nav-sdk-example0 reactions · 1 comment
- Oct 13, 2026chore(deps): bump the example-flutter-dependencies group across 1 directory with 4 updates — googlemaps/flutter-navigation-sdk0 reactions · 1 comment
- Oct 13, 2026Bump patrol from 4.1.1 to 4.10.0 — VictorAlexMM/backup-converter-manga0 reactions
- Oct 8, 2026chore(deps): bump the example-flutter-dependencies group across 1 directory with 4 updates — Ali-Mitchell/googl-nav-sdk-example0 reactions · 1 comment
- Oct 6, 2026chore(deps): bump the example-flutter-dependencies group across 1 directory with 3 updates — googlemaps/flutter-navigation-sdk0 reactions · 1 comment
- Oct 4, 2026claude-code-plugins.github.leancodepl.patrol: init at 4.10.0 — sudosubin/agents.nix0 reactions
- Sep 30, 2026chore(deps): bump patrol from 4.9.0 to 4.10.0 in /mobile/flutter — cuesoftinc/apparule0 reactions · 1 comment
- Sep 24, 2026chore(deps): bump the example-flutter-dependencies group across 1 directory with 4 updates — Ali-Mitchell/googl-nav-sdk-example0 reactions · 1 comment
- Sep 22, 2026chore(deps): bump the example-flutter-dependencies group across 1 directory with 3 updates — googlemaps/flutter-navigation-sdk0 reactions · 1 comment
- Sep 21, 2026build(deps): bump patrol from 4.6.1 to 4.10.0 — naipaka/onepage0 reactions · 1 comment
- Sep 21, 2026chore(deps): bump patrol_mcp from 0.2.0 to 0.2.1 in /tool/patrol_mcp in the patrol-mcp-dependencies group — zhangheng2022/meet_trace0 reactions
- Sep 8, 2026chore(deps): bump patrol from 3.20.0 to 4.9.0 in /apps/mobile — mfurkanturan/CultureBox0 reactions
- Aug 20, 2026spike: evaluate agent-assisted Flutter testing and visual evidence — studyu-health/studyusays it tested it0 reactions
- Aug 18, 2026chore(deps): bump the flutter-dependencies group across 1 directory with 3 updates — Algieba-dean/modern-flutter-template0 reactions
- Aug 17, 2026build(deps): bump the pub-minor-patch group across 1 directory with 9 updates — mdarif/Al-Tawheed0 reactions · 1 comment
- Aug 1, 2026Bump patrol from 3.20.0 to 4.8.0 — SharezoneApp/sharezone-app0 reactions · 1 comment
- Jul 13, 2026chore(deps): bump the example-flutter-dependencies group across 1 directory with 3 updates — googlemaps/flutter-driver-sdk0 reactions
- Jul 13, 2026chore(deps): bump patrol_finders from 3.5.0 to 3.6.0 — Yubico/yubioath-flutter0 reactions
- Jun 4, 2026Draft: leancodepl/patrol — Patrol + GitHub Actions + iOS: Unable to shutdown device in current state: Shutdown — gistrec/community-assistant0 reactions · 1 comment
- Jun 1, 2026chore(deps)(deps): bump patrol and patrol_finders — upendraprasad19/AVYA0 reactions · 3 comments
- May 27, 2026Bump patrol from 4.1.1 to 4.6.1 — ecolab-neil/my_flavored_app0 reactions
- Apr 13, 2026chore(deps): bump patrol from 3.15.2 to 4.5.0 in /app — navrattan/bharattesting-utilities0 reactions · 1 comment
- Apr 11, 2026chore(deps): bump patrol from 3.20.0 to 4.5.0 — lucssilva/message-suggester-app0 reactions
- Mar 25, 2026chore(deps): bump patrol from 4.1.1 to 4.5.0 — xhermesxenl/vgv0 reactions
- Mar 25, 2026chore(deps): bump patrol_finders from 3.1.0 to 3.2.0 — xhermesxenl/vgv0 reactions
- Feb 21, 2026Bump patrol from 4.1.0 to 4.1.1 in /frontend — titabash/gemini-hackathon-game0 reactions
- Feb 9, 2026chore(deps): bump patrol_finders from 2.9.0 to 3.1.0 — GangemiLorenzo/aura_box0 reactions
- Jan 26, 2026Bump the production-major group across 1 directory with 3 updates — AlphaHasher/ChurchLink0 reactions · 1 comment
- Jan 26, 2026chore(deps): bump patrol from 3.17.0 to 3.20.0 — anirac-tech/memverse_project0 reactions · 2 comments
- Dec 29, 2025chore(deps): bump patrol from 3.20.0 to 4.1.0 in /example — Bb-03/Safe-Yatra0 reactions
- Dec 29, 2025chore(deps): Bump patrol from 3.19.0 to 4.1.0 in /mobile — BiscuitNick/rapid-photo0 reactions · 1 comment
- Dec 16, 2025bs_android: Patrol 4.0.1 tests return "0 tests" on BrowserStack (test discovery fails) — leancodepl/mobile-tools0 reactions · 1 comment
- Dec 9, 2025build(deps): bump patrol from 3.19.0 to 4.0.0 — juancjr99/gym_tracker0 reactions
- Dec 8, 2025Use Patrol for Flutter Web Tests — SharezoneApp/sharezone-app0 reactions
- Jul 23, 2025chore(deps): bump patrol_finders from 2.8.2 to 2.9.0 — anirac-tech/memverse_project0 reactions · 2 comments
- Mar 31, 2025chore(deps): bump patrol from 3.11.2 to 3.15.1 in /example — ATHARVAKPICT/safeyatraapp0 reactions
- Mar 31, 2025chore(deps): bump patrol from 3.11.2 to 3.15.1 in /example — ATHARVAKPICT/Safe_Yatra_test0 reactions
On Stack Overflow · 3
- Oct 17, 2023Starting Patrol tests brings "Serializer for class 'ConfigureRequest' is not found" error1 vote
- Aug 23, 2024App Tracking Transparency Dialog does not appear on iOS0 votes
- Apr 23, 2024Execution failed for task ':app:mergeLibDexDebug'. error white dexing0 votes
In this repository8 other files it gives its agents.
- .cursor/rules/android-kotlin.mdcCursor rule
- .cursor/rules/github-workflows.mdcCursor rule
- fix-issueSkill
- issue-triageSkill
- prepare-releaseSkill
- patrol-setupSkill
- patrol-test-architectureSkill
- patrol-write-testSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
17 rules. About 0 are common, 8 are distinctive, and 9 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈8
Distinctive · estimate
This author's own choices. Worth a look.
9
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 23
Read it before translating the first flow.
- Very specific: line 38
Never drop an assertion or a step silently: Every assert* becomes an assertion.
- Distinctive: line 71
Read everything before writing code:
- Very specific: line 73
Every flow file and config.yaml in the Maestro directory, including nested directories and subflows referenced by runFlow: file:.
- Very specific: line 75
Every runScript / evalScript JS file — these are backend setup and teardown.
- Distinctive: line 84
Write a migration table and show it to the user before step 2.
- Very specific: line 96
If the project has no Patrol setup, run the patrol-setup skill first.
- Very specific: line 102
When the Android build fails on a toolchain version, raise only the version the Gradle error names — a missing compilerOptions() method …
- Very specific: line 114
Write one helper that starts the app the way main() does: the same initialization, then await $.pumpWidgetAndSettle(const MyApp()).
- Very specific: line 185
Put assertions where the flow has them.
- Distinctive: line 187
Run the test until it is green (see *Running tests*).
- Distinctive: line 188
If the project uses modules, move the new code into them and rerun.
- Distinctive: line 247
When all flows are done, give the user:
- Very specific: line 252
Every change of meaning: regex asserts that became exact text, point: taps that became a native API call, travel timing, optional steps …
- Very specific: line 255
Every TODO(maestro: …) left, every app file changed, every build version raised.
- Distinctive: line 275
Network errors from the app only in tests: Check DNS on the device.
- Distinctive: line 278
Only fails on one platform: Check whether the Maestro flow had a platform branch there — it often documents the same platform difference.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Migrate Maestro flows to Patrol
A migration is done when every Maestro flow has a Patrol test that proves the same behavior and passes on a device. Maestro drives an installed app from outside, through the accessibility tree; Patrol runs inside the app and finds Flutter widgets. Selectors, animation waits and platform workarounds written for Maestro mostly disappear.
Companion skills — load them if installed, they own their topics:
patrol-setup— first-time Patrol setup, when the project has none.patrol-write-test— Patrol API, assertion, native-dialog and test-key rules. They apply here, except for the points listed in Rules that change for a migration.patrol-test-architecture— add-on topatrol-write-testfor the modular layout (Modules,System,ApiClients).
The command-by-command mapping is in references/command-mapping.md. Read it before translating the first flow.
Hard rules
- Never modify app behavior. In app code, the only change allowed is adding a
key:to an existing widget, plus the keys files. No new widgets, no refactors, no newSemantics, no deletingSemanticsor labels that Maestro needed. Build configuration Patrol needs is allowed: the pubspec, the native test runner wiring, and the Kotlin / Android Gradle plugin / Gradle versions Patrol requires. A key that is notconstforces droppingconstfrom the widget that takes it — that edit is part of adding the key. - Format only the files you created. Running the formatter over
lib/rewrites files you never touched when the project uses an older formatter style. - Never delete or edit the Maestro flows. They are the specification. Removing them is the user's call once the Patrol suite is green.
- Never drop an assertion or a step silently. Every
assert*becomes an assertion. A step with no Patrol equivalent gets a// TODO(maestro: <command>)marker and goes in the final report. A flow that ends without an assertion gets one that proves its effect, listed in the report as added. - One flow at a time. Translate, run, get it green, then start the next flow.
- Never weaken a test to make it pass — no deleted assertions, no
try/catcharound a failing step, noskip. If it fails, find out why (see Debugging).
Rules that change for a migration
The Maestro flow is the specification, so where patrol-write-test or
patrol-test-architecture say otherwise, these win:
onFlowComplete, and restoring device state the test changed (airplane mode, mock location), become apatrolTearDown, even though those skills say not to write teardowns on your own. The teardown is registered inmain(), outsidetestApp, so it builds its own clients (BackendClient(), andSystem(config: PlatformAutomatorConfig.defaultConfig())in the modular layout orPlatformAutomator(config: ...)without it), and the test file importspackage:patrol.- A mid-flow
assertVisiblestays in the middle of the test, where the flow has it. assertNotVisibleandassertTrueneedexpectand its matchers, whichpackage:patroldoes not export: import them frompackage:flutter_testwith ashowlist (show expect, isFalse, findsNothing).- Text finders are allowed for two things: asserting content the flow asserts by text
(an item name the test created, a message), and tapping widgets the app does not
build itself, which cannot take a key without changing the app — the buttons of
showDatePickerandshowTimePicker, and the items of an open dropdown menu. Scope them when the text is ambiguous:$(DatePickerDialog).$('OK')(the widget types come frompackage:flutter/material.dart).
1. Inventory
Read everything before writing code:
- Every flow file and
config.yamlin the Maestro directory, including nested directories and subflows referenced byrunFlow: file:. - Every
runScript/evalScriptJS file — these are backend setup and teardown. - How flows are run: CI config, shell scripts, the README. Note
--include-tags,--exclude-tags,-eenv vars and per-platform install scripts — they decide which flows run on which platform. lib/main.dart— everythingmain()does beforerunApp(Firebase init, DI, env loading) is what the Patrol test must do before pumping the app.- Existing Patrol or
integration_testcode, and any existing keys files. If a test structure already exists, follow it.
Write a migration table and show it to the user before step 2. When nobody is there to answer, put it in the report and continue.
| Maestro flow | Patrol test | Platforms | Setup / teardown | Native interactions | Keys to add |
The Platforms column comes from the tags together with how CI runs the flows. Comments in flows explain platform limits and selector workarounds — keep them in mind when translating.
2. Set up Patrol
If the project has no Patrol setup, run the patrol-setup skill first. The example/
directory of the patrol package in the pub cache has a working native setup for the
installed version. Take the patrol: block values from the flows: appId is the
Android package_name and the iOS bundle_id (check applicationId and the bundle id
in the native projects — a flow's appId can be a flavor's id).
When the Android build fails on a toolchain version, raise only the version the Gradle
error names — a missing compilerOptions() method means the Kotlin Gradle plugin
(org.jetbrains.kotlin.android in android/settings.gradle(.kts)) is too old. Each fix
can reveal the next error. List every version change in the report. Run the first
build in the background.
Values the flows read from env: blocks or -e, and credentials or account names
written into the flows, go to .patrol.env and are read with String.fromEnvironment.
Gitignore .patrol.env and the generated patrol_test/test_bundle.dart.
3. App start helper
Write one helper that starts the app the way main() does: the same initialization,
then await $.pumpWidgetAndSettle(const MyApp()). Every test calls it first. This is
the Patrol launchApp. Initialization that may run only once per process needs a
guard that skips it when it already ran: tests share one process on Android without
the test orchestrator, and across hot restarts under patrol develop.
Setup that runs before the app starts can need the same initialization (a backend call
through one of the app's repositories). Split the helper: one guarded init that setup
and teardown can call, and one that runs it and pumps the app.
With no existing Patrol code and patrol-test-architecture installed, use its layout:
create Modules, System and ApiClients from that skill's examples before the first
test, and this base class and wrapper next to the start helper:
abstract class Module {
const Module(this.$);
final PatrolIntegrationTester $;
}
void testApp(
String description,
Future<void> Function(
PatrolIntegrationTester $,
Modules modules,
System system,
ApiClients apiClients,
) callback, {
dynamic tags,
}) {
patrolTest(description, tags: tags, ($) async {
final system = System(config: PlatformAutomatorConfig.defaultConfig());
await callback($, Modules($), system, ApiClients());
});
}Each test sets up its own state, so a flow that relied on state left by an earlier
flow needs that state created in its own setup. Clean state between tests differs by
platform: on Android, clearPackageData wipes app data before every test; on iOS a
plain patrol test keeps app data, --clear-permissions resets permissions before
each test, and only --full-isolation (Simulator, experimental) reinstalls the app
between tests. For each flow that needs a clean start
on iOS, pick a reset — --full-isolation, or resetting the state in setup — and name
it in the report.
4. Migrate one flow
- Walk the flow and, for every element it touches inside the app, find the widget
in the code. Add a key to it (key rules:
patrol-write-test/patrol-test-architecture). Text taps,id:,index:,rightOf:,below:,childOf:andpoint:on in-app UI all become keys. A widget whose label changes with state gets one key per state, chosen by the same condition. - Translate the commands with references/command-mapping.md.
tapOnan empty field followed byinputTextis oneenterText. - A flow that runs on one platform only is registered for that platform only:
if (patrolTargetPlatform == PatrolTargetPlatform.android) { testApp(...); }. With build-time test discoverymain()also runs on the host, wherePlatformdescribes the computer and would drop the test from the manifest. Inside a test bodyPlatformis fine. - Move
onFlowStartsetup into the test body, before the app starts, andonFlowCompleteinto apatrolTearDownregistered inmain(). ArunScriptin the middle of a flow stays at its position, as a Dart call; values it passes on (output.x) become local variables. Write the JS HTTP calls in Dart, reusing the app's own repositories or API clients when they already do the call. Cleanup must succeed when there is nothing to delete, and must wait for data the app wrote through its own SDK: the test can see it in the app before the backend has it.patrolTearDownhas no$: native calls there go through an automator built with a config,PlatformAutomator(config: PlatformAutomatorConfig.defaultConfig()). With no config every call throwsUnimplementedError. - Put assertions where the flow has them; the final
assertVisiblebecomes awaitUntilVisible()at the end. - Run the test until it is green (see Running tests).
- If the project uses modules, move the new code into them and rerun.
What disappears in translation
- Platform branches that exist because of selectors.
runFlow: when: platform:blocks with different selectors for Android and iOS collapse into one key. Keep aPlatform.isIOSbranch only for behavior that really differs. - Waits.
waitForAnimationToEndandhideKeyboardbefore a tap are not needed: Patrol waits for the widget before acting. AnextendedWaitUntilkeeps its condition and timeout: when the next step taps the element it waits for, pass the timeout astap(visibleTimeout: ...); otherwise it becomeswaitUntilVisible(timeout: ...). Patrol's default wait is 10 seconds. optional: true. Decide whether the element appears in the test environment: if it does, the step is mandatory; if it never does, drop the step and list it in the report. An optional permission dialog becomesif (await $.platform.mobile.isPermissionDialogVisible(timeout: ...)) { ... }, with a timeout as long as the dialog can take to appear — the default is one second. Any other optional step that depends on device state is the user's call; when nobody is there to answer, make it mandatory and say so in the report.
What needs native automation
Patrol finders see only Flutter widgets. These go through $.platform, using the
Patrol MCP native-tree tool to read the native UI and pick selectors:
- Permission dialogs —
grantPermissionWhenInUse(),grantPermissionOnlyThisTime(),denyPermission(). Maestro'slaunchAppgrants all permissions by default and Patrol does not, so a dialog can appear in Patrol where the flow had no step for it. These calls throw when no dialog is shown. - Notifications, quick settings, Wi-Fi / cellular / airplane mode, mock location.
- WebView content and other apps —
$.platform.mobile.tap(MobileSelector(...)),$.platform.mobile.waitUntilVisible(...). On iOS, native queries search the app under test unless given another app's bundle id:$.platform.tap(selector, appId: 'com.apple.mobilesafari').
A tap that opens a native dialog, a WebView, a map, a camera preview or anything that
keeps animating needs tap(settlePolicy: SettlePolicy.noSettle), otherwise the tap
waits for a frame that never settles.
Running tests
- One test while developing: Patrol MCP
runif it is configured, otherwisepatrol test -t <file>. With more than one device attached, passdevice(the MCPdevicestool lists them). - Patrol MCP
runispatrol develop. It hot-restarts the app: app data and granted permissions stay from the previous run, andpatrolTearDownmay not run.quitbefore running a different file — while a session is open,runwith a new file returns the previous file's result with awarning. Before rerunning a test that needs a fresh install state (a permission dialog, a first-launch screen),quitand, on Android,adb shell pm clear <package>. - Proof of a green test is a
patrol testrun: it builds and installs the app, and on Android clears its data before every test (on iOS see the clean-state note in step 3). Finish with the full suite, twice. - On a shared machine, check that no other test run is using the device before a suite run. Another app's instrumentation on the same device kills yours midway and the summary silently lists fewer tests.
5. Report
When all flows are done, give the user:
- The migration table with a status per flow: green on which platforms, how many clean suite runs, and every flow that could not run on the available device (and why).
- Every change of meaning: regex asserts that became exact text,
point:taps that became a native API call,traveltiming, optional steps made mandatory, assertions added. - Every
TODO(maestro: …)left, every app file changed, every build version raised. - That the Maestro flows are still in place and can be removed once the user has reviewed the suite.
Debugging
- Patrol MCP
runshows no output for minutes. The build is likely failing. Build the test bundle directly to see the error:flutter build apk --debug --target patrol_test/test_bundle.dart. - Finder found nothing. Take a screenshot (Patrol MCP
screenshot). If the element is off-screen, add.scrollTo(). If it is native UI, use$.platform. - A key matches a widget that cannot be tapped. A
DropdownButtonbuilds its items both in the button and in the open menu, and a key onDropdownMenuItemcan match the copy that is not hit-testable. Tap the item by its text and remove the unused key. - Text matches nothing but Maestro found it. Maestro matches accessibility labels,
which merge the text of a whole subtree and include
Semanticslabels and tooltips. Find the widget and key it. - Test hangs after a tap. The next screen never settles — use
SettlePolicy.noSettleon that tap. - Network errors from the app only in tests. Check DNS on the device.
- Passes alone, fails in the suite. State leaked from another test or the backend; check the teardown.
- Only fails on one platform. Check whether the Maestro flow had a platform branch there — it often documents the same platform difference.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

