From patrol-qa-automation
Creates Patrol UI test files (Dart) for Flutter mobile applications. Use when the user wants to write Patrol tests from a test plan, generate testcases for a screen, create end-to-end user journey scenarios, or automate mobile UI testing. Covers folder structure, selector strategies, localization patterns, and test execution.
How this skill is triggered — by the user, by Claude, or both
Slash command
/patrol-qa-automation:create-patrol-testThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Creates Patrol UI test files (Dart testcases and scenarios) from a CSV test plan. For Flutter-based mobile applications.
Creates Patrol UI test files (Dart testcases and scenarios) from a CSV test plan. For Flutter-based mobile applications.
Use this skill when the user mentions:
Use this skill whenever you want to:
| Aspect | Testcase | Scenario |
|---|---|---|
| Purpose | Atomic test — single action/verification | User journey — orchestrates multiple testcases |
| Scope | Single screen, single interaction | Multiple screens, complete flow |
| Structure | AAA (Arrange-Act-Assert) | Orchestrates testcases + state management |
| Reusability | Reused across scenarios | Self-contained user journey |
| Example | tap_login_button.dart | login_success_and_failure.dart |
patrol_test/
├── testcases/ # Atomic tests (AAA pattern)
│ ├── login/
│ │ ├── verify_login_form_visible.dart
│ │ └── tap_login_button.dart
│ └── home/
│ └── verify_welcome_visible.dart
├── scenarios/ # User journeys (orchestrates testcases)
│ └── login/
│ └── login_success_and_failure.dart
├── helpers/ # Shared helper functions (login, logout, launch)
│ ├── app_launch.dart
│ └── login.dart
└── utils/ # Dart utilities & locale setup
├── locale_helper.dart
└── test_config.dart
When executing this skill, IGNORE all existing test patterns in the codebase. Only follow the patterns, folder structure, naming conventions, and rules defined in THIS skill document.
Existing test files may use legacy conventions. These are outdated and must NOT be replicated. This skill document is the single source of truth.
Before using this skill, ensure you have:
Patrol MCP tools available — The MCP server must be running and accessible
Patrol finder API reference — Refer to the Patrol documentation and the patterns in this skill
Understanding of folder structure:
testcases/ — Atomic tests (single screen, AAA pattern)scenarios/ — User journeys (orchestrates testcases via function calls)helpers/ — Shared helper functions (login, logout, app launch)utils/ — Dart utilitiesTestcase naming format — Start with action-based prefixes:
tap_, verify_, check_, etc._C123456)01_, 02_)Each testcases/<feature>/ folder maps 1-to-1 with a screen (or a major section of a screen).
testcases/home/ → home_screen.darttestcases/<feature>/ → <feature>_screen.darttestcases/home/) may already have testcases from other features. Always scan existing files before creating new ones — reuse if an equivalent test exists.IMPORTANT: Testcases are atomic and simple.
Function call policy for testcases:
if (await $('text').exists) for conditional execution (e.g., dismiss dialogs that may or may not appear)while loops for repeating patternsFunction call policy for scenarios:
helpers/ for shared multi-step flows (login, logout, app launch)Full reference: See shared-references/selector-rules.md for the complete selector decision tree, accessibility node merging rules, and timeout conventions.
Selector Priority Hierarchy
Before writing selectors, gather context from:
mcp_patrol_mcp_native-tree for runtime element identifiers/text/statesKey('...')), Semantics(identifier: '...'), and stable string constantsThen follow this priority order:
$('Login').tap()$(#emailField).tap()$(Scaffold).$('Submit').tap()containing — last resort before code changeSemantics(identifier: '...', container: true) to Flutter source if nothing works — never use coordinatesCommon patterns:
// Text selector
await $('Login').tap();
// Key selector
await $(#emailField).tap();
// Ancestor chaining for duplicate labels
await $(Scaffold).$('Submit').tap();
// Containing finder for relative positioning
await $(Scrollable).containing($('Submit')).tap();
IMPORTANT: After adding Semantics or any code changes to enable selectors, you MUST rebuild and reinstall the app before re-running tests:
flutter build
Patrol tests the compiled APK/IPA on the device, not your source code. Changes to Flutter code (including Semantics) are not reflected until you rebuild and reinstall.
Flutter widgets that render a label + value in a Row (e.g., a list tile with caption + value) often combine both texts into a single accessibility node. The resulting text is a multi-line string like:
Field label
Field value
This means $('Field label') fails, because the element's actual text is Field label\nField value.
Fix: use the containing finder or match the full merged text:
// WRONG — exact match fails when value is merged into same node
expect($('Field label'), findsOneWidget);
// CORRECT — use containing to find the parent row
expect($(Row).containing($('Field label')), findsOneWidget);
// Or match the full merged text
await $('Field label Field value').waitUntilVisible();
Route prefix merging:
Some parent containers emit a combined text that includes the screen route + all child values. Use ancestor chaining to scope the search:
// WRONG — text doesn't start with the label
expect($('Section heading'), findsOneWidget);
// CORRECT — scope with ancestor chaining
await $(#sectionContainer).$('Section heading').tap();
How to detect which case you're in: Use mcp_patrol_mcp_native-tree and look at the element's text or label field. If it contains extra content beyond the expected label, use ancestor chaining or containing to disambiguate.
Every string in $(), expect(), or waitUntilVisible() MUST be verified against actual app text.
Two-step verification (MANDATORY before writing Dart test):
If the string is dynamic (parameterized), match the filled-in value, not the template literal.
Localization in Patrol tests:
Use the app's AppLocalizations class for locale-agnostic text matching, or use the actual visible text directly:
// Option 1: Direct text matching (simpler, but locale-dependent)
await $('Login').tap();
// Option 2: Using AppLocalizations (locale-agnostic)
// Note: Requires importing the app's localization
await $(find.text(AppLocalizations.of($.native).loginButtonLabel)).tap();
Default locale — Check your project's default locale. When verifying localization keys, check both the primary locale and any secondary locales in your ARB files.
Use environment variables or test accounts — never commit real credentials.
Always validate test files by running them through Patrol MCP.
When a test fails:
mcp_patrol_mcp_screenshot to capture current screen statemcp_patrol_mcp_native-tree to reveal real element text, identifiers, statesmcp_patrol_mcp_run to validateNavigation debugging:
waitUntilVisible() before the tapnative-tree to get the exact text/identifierPremature failures (10–30s) — retry up to 3 times.
Patrol's enterText combines tap + focus + type into a single call:
CORRECT:
await $(#emailField).enterText('[email protected]');
await $(#passwordField).enterText('SecurePass123!');
await $('Login').tap();
INCORRECT (separate tap + input):
// Don't do this — enterText already handles focus
await $(#emailField).tap();
await $(#emailField).enterText('[email protected]');
Patrol uses waitUntilVisible() with configurable timeout and pumpWidgetAndSettle() for animation settling:
// Wait for element with default timeout
await $('Sign In').waitUntilVisible();
// Wait with custom timeout for slow transitions
await $('Home Screen').waitUntilVisible(timeout: Duration(seconds: 5));
// Let animations settle after navigation
await $.pumpWidgetAndSettle();
Timeout guidelines:
| Situation | Approach |
|---|---|
| Wait for element to appear | $('text').waitUntilVisible() |
| Wait for animations to settle | await $.pumpWidgetAndSettle() |
| Slow network/loading transitions | $('text').waitUntilVisible(timeout: Duration(seconds: 10)) |
Use enterText which handles tap + focus + input in one call:
await $(#emailField).enterText('[email protected]');
await $(#passwordField).enterText('SecurePass123!');
await $('Login').tap();
Run code only when an element is visible:
if (await $('Login Button').exists) {
await $('Login Button').tap();
}
Navigate back to a known state before independent error scenarios:
// Happy path
await verifyLoginSuccess($);
// Reset state
await navigateToHome($);
await performLogout($);
// Error path
await verifyLoginFailure($);
Repeat while an element is visible:
while (await $('Load More').exists) {
await $('Load More').tap();
await $.pumpWidgetAndSettle();
}
Repeat for tap retry (screen not ready):
When a screen might not be ready for interaction immediately (e.g., home screen after login), use a retry loop:
for (var attempt = 0; attempt < 3; attempt++) {
await $('Target Button').tap();
if (await $('Expected Next Screen Element').exists) break;
await $.pumpWidgetAndSettle();
}
Pass test data from scenarios to testcases via function parameters:
// In scenario:
await selectOption($, optionValue: 'Test Name');
// In testcase (select_option.dart):
Future<void> selectOption(PatrolIntegrationTester $, {required String optionValue}) async {
await $(#optionDropdown).tap();
await $(#searchField).enterText(optionValue);
await $(optionValue).tap();
}
Testcases that need external data should document required parameters in their function signature.
Scenarios use setUp() or initial setup code for app launch and state reset:
void main() {
patrolTest(
'user journey description',
($) async {
// Launch app with clean state
await $.pumpWidgetAndSettle(const MyApp());
// Grant permissions
await $.platform.mobile.grantPermissionWhenInUse();
// ... test steps
},
);
}
Important: Testcases do NOT include app launch — only scenarios handle setup.
| Pitfall | Why It's Wrong | Correct Approach |
|---|---|---|
| Using coordinates | Fragile, breaks on different screen sizes | Use text/key selectors |
| Hardcoded strings | May not match actual app text | Use AppLocalizations or verify against ARB files |
| Calling testcase functions from testcases | Testcases must be atomic/simple | Scenarios handle orchestration; testcases only use conditionals/loops |
| Duplicating testcases | Maintenance nightmare | Reuse existing testcases |
| Skipping state reset | Flaky tests due to leftover state | Reset between independent scenarios |
| Not using ancestor chaining for duplicates | Taps wrong element when text appears multiple times | Use $(Parent).$('text').tap() for disambiguation |
Not using container: true in Semantics | Widget children not exposed to accessibility | Always add container: true when adding Semantics for testing |
| Exact text match on label+value nodes | Flutter Row widgets merge label+value into one node | Use containing finder or match full merged text |
Using $.native.tap(Offset(x,y)) | Coordinates break across screen sizes | Use text/key selectors, add Semantics if needed |
Step 1 · Parse the test case file
When user provides a test case spreadsheet (CSV, Jira, etc.), extract for each case:
Step 2 · Separate by screen (Rule 1)
Group test cases by the screen they exercise. Use section headers as primary signal; cross-reference with screen names in the Flutter codebase.
Finding screen source code: Look for files with _screen.dart suffix (e.g., login_screen.dart, home_screen.dart).
Step 3 · Triage — decide what to skip
Classify each test case:
// SKIP: comment explaining why.Step 4 · Produce mapping table
Before writing Dart test files, produce a mapping table of feasible cases. Present to developer for confirmation before proceeding.
Recommended columns: CSV Test Case, Priority, Automate?, Screen Folder, Testcase File, Notes.
Step 5 · Write the testcases
After mapping is confirmed, implement each file following the testcase template:
mcp_patrol_mcp_native-tree + source code to find selectors (Rule 2)mcp_patrol_mcp_run to validate before savingStep 6 · Write the scenario
Once all testcases exist, compose the scenario:
if (await $('text').exists)This skill uses the following Patrol MCP tools:
mcp_patrol_mcp_run — Run a Dart test file (starts patrol develop session or hot-restarts)mcp_patrol_mcp_screenshot — Capture device screen for visual debuggingmcp_patrol_mcp_native-tree — Fetch native UI view hierarchy for selector discoverymcp_patrol_mcp_status — Get session status and recent logsmcp_patrol_mcp_quit — Quit active Patrol sessionNote: Patrol MCP cannot run inline Dart code. Always write the complete test file, then run it. Use the write-run-observe-edit loop.
Template files are available in the references/ folder:
references/testcase_template.dart — AAA pattern template for atomic testcasesreferences/scenario_template.dart — User journey orchestration templatereferences/flutter-semantics.md — Guide for adding Semantics(identifier: '...') to Flutter widgetsnpx claudepluginhub iqbal-mekari/claude-plugins --plugin patrol-qa-automationGuides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.