| name | flutter-device-crud-testing |
| description | Comprehensive Flutter app CRUD testing on real devices/simulators using Mobile MCP + Dart MCP. Use when user asks to "test all pages", "verify all buttons", "run device CRUD test", "ๅ
จ้ ้ขๆธฌ่ฉฆ", "ๆจกๆฌๅจๆธฌ่ฉฆ", "่ทไธ้ๆๆๆ้". Triggers on keywords like "test all screens", "CRUD test", "device test", "tap every button", "full page test". |
Flutter Device CRUD Testing โ Mobile MCP + Dart MCP
Systematically test every interactive element of a Flutter app on a real device or emulator using Mobile MCP (screenshot/tap/swipe/type) and Dart MCP (launch/hot_reload/runtime_errors/widget_tree/flutter_driver).
When to Use
- Before release, to validate all pages visually and functionally
- After dark mode / theme changes, to verify readability
- After refactoring, to confirm no regressions
- When user says "่ทไธ้", "test all screens", "verify every button"
Phase 0: Pre-flight โ Codebase Analysis
Before touching any device, understand the app structure:
0.1 Read Router/Navigation
Find the app's router file (usually app_router.dart, router.dart, or main.dart):
- GoRouter: look for GoRoute, ShellRoute definitions
- Navigator 2.0: look for Router, RouteInformationParser
- Navigator 1.0: look for MaterialPageRoute, pushNamed calls
Output: List of ALL navigable screens with their paths.
0.2 Analyze Each Screen
For each screen .dart file, catalogue:
INTERACTIVE ELEMENTS:
โโโ Buttons: ElevatedButton, TextButton, IconButton, FloatingActionButton
โโโ Inputs: TextField, TextFormField, SearchBar
โโโ Selection: ChoiceChip, FilterChip, Switch, Checkbox, Radio
โ SegmentedButton, DropdownButton, PopupMenuButton
โโโ Navigation: InkWell/GestureDetector with push/go/pop
โ ListTile with onTap, Card with onTap
โโโ Scrollable: RefreshIndicator, ListView, GridView, CustomScrollView
โโโ Dialogs: showDialog, showModalBottomSheet, showSnackBar
โโโ Special: Slider, DatePicker, TimePicker, ExpansionTile
EXPECTED BEHAVIORS:
โโโ CRUD: API calls, state mutations, provider updates
โโโ Navigation: route changes, page transitions, back navigation
โโโ UI Feedback: SnackBar, Toast, Dialog, loading indicator, shimmer
โโโ Validation: required field checks, format checks, error messages
โโโ State changes: toggle on/off, expand/collapse, select/deselect
0.3 Build Test Matrix
| # | Screen | Element | Action | Tool(s) | Expected Result |
|---|
| 1.1 | Dashboard | โ | Screenshot | take_screenshot | Layout correct |
| 1.2 | Dashboard | RefreshIndicator | swipe(down) | swipe_on_screen | Data refreshes |
| 1.3 | Dashboard | Item card | Tap | list_elementsโclick | Nav to detail |
| 1.4 | Detail | Back button | Tap | click | Returns to list |
| 2.1 | Form | Name field | Type text | clickโtype_keys | Text appears |
| 2.2 | Form | Submit (empty) | Tap | click | Validation error |
| 2.3 | Form | Submit (filled) | Tap | click | Success feedback |
| 3.1 | Settings | Theme toggle | Tap | click | Theme changes |
| 3.2 | Settings | Switch | Toggle | click | State flips |
Phase 1: Setup & Launch
1.1 Identify Device
โ mobile_mcp list_available_devices
โ Pick iOS Simulator or Android Emulator
โ Prefer a named testing device (e.g., "E2E-iPhone16Pro")
โ Note the device ID for all subsequent calls
1.2 Launch App
โ dart_mcp launch_app(root=<project_root>, device=<device_id>)
โ Returns DTD URI โ save it!
โ โ ๏ธ First launch can take 60-120 seconds for iOS Simulator
1.3 Connect Dart Tooling Daemon
โ dart_mcp connect_dart_tooling_daemon(uri=<dtd_uri>)
โ This enables: runtime errors, hot reload, widget tree
1.4 Verify Launch
โ mobile_mcp take_screenshot(device)
โ Confirm app is on screen and not showing a crash/error
โ Save screenshot as baseline
โ ๏ธ Launch Troubleshooting
| Symptom | Fix |
|---|
launch_app hangs >3 min | Kill zombie Flutter processes: flutter_driver or dart processes. Check with ps aux | grep flutter |
| DTD URI connection fails | Re-launch the app, get fresh URI. Never reuse old URIs. |
| App shows white screen | Check get_runtime_errors() โ likely a provider/init crash |
| Simulator not found | Run xcrun simctl list devices or check Android emulator is running |
| "No supported devices" | Ensure device platform matches app targets (iOS/Android) |
Phase 2: Execute Tests โ Screen by Screen
Core Loop: For each screen
NAVIGATE โ SCREENSHOT โ INTERACT โ VERIFY โ RECORD
Step A: Navigate to Screen
Bottom Tab Navigation (most common):
1. mobile_mcp list_elements_on_screen(device)
2. Find tab label by text (e.g., "Home", "Settings", "Profile")
3. mobile_mcp click_on_screen_at_coordinates(device, x, y)
4. Wait 1-2 seconds for animation
Push Navigation (detail pages):
1. Find the trigger element (list item, card, button)
2. Tap it โ app pushes new route
3. Verify new screen loaded via screenshot
Deep Link / Direct Navigation (if supported):
1. Use mobile_mcp open_url(device, url) for deep links
Step B: Screenshot Initial State
โ mobile_mcp take_screenshot(device)
โ Save screenshot path for walkthrough
โ This is your BEFORE state
Step C: Test Each Interactive Element
Process elements top to bottom, left to right on the screen.
For each element:
1. LOCATE
โ mobile_mcp list_elements_on_screen(device)
โ Find element by text, accessibility label, or type
โ โ ๏ธ NEVER guess coordinates โ ALWAYS call list_elements first
2. TRIGGER ACTION
โ Button/Chip/Toggle: click_on_screen_at_coordinates(device, x, y)
โ Text input: click(field) โ type_keys(device, text, submit=false)
โ Scroll: swipe_on_screen(device, direction, distance)
โ Pull-to-refresh: swipe_on_screen(device, direction="down", y=300)
3. VERIFY RESULT
โ mobile_mcp take_screenshot(device) // Visual check
โ dart_mcp get_runtime_errors() // Zero-crash check
โ mobile_mcp list_elements_on_screen(device) // New UI state
4. RECORD
โ Save screenshot + pass/fail status
โ If error: save error details + screenshot as evidence
Step D: Edge Cases per Screen Type
Form Screens:
- [ ] Submit empty form โ expect validation error (SnackBar/inline)
- [ ] Submit with only partial fields โ expect specific field error
- [ ] Submit with valid data โ expect success feedback
- [ ] Submit twice rapidly โ no duplicate submission
- [ ] Type very long text โ no overflow / text truncation is graceful
List Screens:
- [ ] Empty state โ appropriate "no data" message
- [ ] Pull-to-refresh โ RefreshIndicator appears, data reloads
- [ ] Scroll to bottom โ no overflow, footer visible or infinite scroll works
- [ ] Tap item โ navigates to detail
- [ ] Swipe item (if dismissible) โ delete/archive action
Detail Screens:
- [ ] Back navigation โ returns to previous screen with state preserved
- [ ] Action buttons (edit/delete/share) โ appropriate action or confirmation
- [ ] Scroll through all content โ no clipping or overflow
Settings Screens:
- [ ] Toggle switches โ state persists after leaving and returning
- [ ] Theme change โ entire app re-renders correctly
- [ ] Segmented buttons โ correct option selected visually
- [ ] External links โ url_launcher opens browser/app
Chat/Input Screens:
- [ ] Send message โ appears in chat
- [ ] Suggestions/quick actions โ tapping sends or fills input
- [ ] Keyboard appears โ content scrolls up, not covered
- [ ] Dismiss keyboard โ layout returns to normal
Step E: Navigate Back / Cleanup
โ Return to stable state before testing next screen
โ Android: mobile_mcp press_button(device, "BACK")
โ iOS: Tap back arrow or swipe right
โ Or tap the next bottom tab directly
Phase 3: Cross-Cutting Tests
3.1 Theme Testing
1. Run core visual checks in default (light) mode
2. Navigate to Settings โ switch to Dark mode
3. Re-check EVERY screen for:
- Text contrast (no invisible text on dark backgrounds)
- Card backgrounds (not clashing with dark bg)
- Icon visibility
- Input field hint text readability
- SnackBar/Dialog contrast
4. Switch to System mode โ verify it follows device setting
3.2 Rotation Testing (if relevant)
โ mobile_mcp set_orientation(device, "landscape")
โ Screenshot each main screen
โ mobile_mcp set_orientation(device, "portrait")
โ Verify layout restored
3.3 State Persistence Testing
1. Fill a form partially
2. Switch to another tab
3. Return to the form tab
4. Verify: data preserved or intentionally cleared?
3.4 Error State Testing
1. Turn off network (if possible on emulator)
2. Trigger API-dependent features
3. Verify: error messages shown, app doesn't crash
4. Turn network back on โ verify recovery
Phase 4: Validation Checkpoints
After EVERY action, run:
dart_mcp get_runtime_errors()
mobile_mcp take_screenshot(device)
dart_mcp get_widget_tree(summaryOnly=true)
Error Handling Decision Tree
Error Found?
โโโ RenderFlex overflow โ P2, cosmetic, note and continue
โโโ Null check error โ P0, crash, STOP and fix
โโโ setState after dispose โ P1, memory leak, note and continue
โโโ Network/API error โ Expected in test env? If yes, note. If no, P1.
โโโ Framework assertion โ P0, structural bug, STOP and fix
โโโ No error โ โ
Continue
Phase 5: Common Interaction Patterns
Tap a Button by Text
elements = mobile_mcp list_elements_on_screen(device)
# Find element with matching text โ get center coordinates
mobile_mcp click_on_screen_at_coordinates(device, x, y)
Fill a Text Field
# 1. Tap the field to focus
mobile_mcp click_on_screen_at_coordinates(device, fieldX, fieldY)
# 2. Type text
mobile_mcp type_keys(device, text="ๆธฌ่ฉฆๆๅญ", submit=false)
# 3. If keyboard blocks next element, dismiss:
# - Android: mobile_mcp press_button(device, "BACK")
# - iOS: tap outside the field
Select a ChoiceChip / SegmentedButton
elements = mobile_mcp list_elements_on_screen(device)
# Find chip by label text โ get coordinates
mobile_mcp click_on_screen_at_coordinates(device, chipX, chipY)
# Verify: chip now shows selected state (different color/style)
Pull-to-Refresh
mobile_mcp swipe_on_screen(device, direction="down", y=400)
# Wait ~2 seconds for refresh to complete
mobile_mcp take_screenshot(device)
Scroll Down (to reveal more content)
mobile_mcp swipe_on_screen(device, direction="up")
# โ swipe UP = scroll DOWN (finger motion direction)
Toggle a Switch
# Switches often don't have text labels in list_elements
# Use the coordinates from list_elements for the parent ListTile
mobile_mcp click_on_screen_at_coordinates(device, switchX, switchY)
Navigate via Bottom Tab
elements = mobile_mcp list_elements_on_screen(device)
# Bottom nav labels are stable regardless of scroll position
# Find by label text โ click
mobile_mcp click_on_screen_at_coordinates(device, tabX, tabY)
Dismiss a Dialog / Bottom Sheet
# Option 1: Tap the action button (OK, Cancel, etc.)
mobile_mcp click_on_screen_at_coordinates(device, btnX, btnY)
# Option 2: Tap outside the dialog (on the scrim)
mobile_mcp click_on_screen_at_coordinates(device, 50, 50)
# Option 3: Android back button
mobile_mcp press_button(device, "BACK")
Handle Keyboard Covering Content
# After typing in a TextField, keyboard may cover buttons below.
# Solutions:
# 1. Scroll down while keyboard is up
mobile_mcp swipe_on_screen(device, direction="up", distance=300)
# 2. Dismiss keyboard first
mobile_mcp press_button(device, "BACK") # Android
# 3. Tap outside the text field
mobile_mcp click_on_screen_at_coordinates(device, 200, 100) # Safe empty area
Phase 6: Using Dart MCP Flutter Driver (Advanced)
For programmatic interaction without coordinate guessing:
Find a Widget by Text and Tap
dart_mcp flutter_driver(
command="tap",
finderType="ByText",
text="Submit"
)
Find a Widget by Type
dart_mcp flutter_driver(
command="tap",
finderType="ByType",
type="FloatingActionButton"
)
Find a Widget by Key
dart_mcp flutter_driver(
command="tap",
finderType="ByValueKey",
keyValueString="submit_button",
keyValueType="String"
)
Enter Text into a TextField
# First tap to focus
dart_mcp flutter_driver(command="tap", finderType="ByType", type="TextField")
# Then enter text
dart_mcp flutter_driver(command="enter_text", text="ๆธฌ่ฉฆๅ
งๅฎน")
Get Text Content
dart_mcp flutter_driver(
command="get_text",
finderType="ByValueKey",
keyValueString="total_count",
keyValueType="String"
)
Wait for Widget to Appear
dart_mcp flutter_driver(
command="waitFor",
finderType="ByText",
text="Success!",
timeout="10000"
)
Scroll Into View
dart_mcp flutter_driver(
command="scrollIntoView",
finderType="ByValueKey",
keyValueString="footer_widget",
keyValueType="String",
alignment="0.0"
)
Get Widget Tree (for debugging)
dart_mcp flutter_driver(
command="get_diagnostics_tree",
finderType="ByType",
type="Scaffold",
diagnosticsType="widget",
subtreeDepth="3",
includeProperties="true"
)
โ ๏ธ Flutter Driver Gotchas
- Cannot find tooltips: ByTooltipMessage only matches Tooltip widgets
- ByText is literal: Must match exactly (case-sensitive, including trailing spaces)
- Multiple matches: If multiple widgets match, it taps the FIRST one found
- Nested finders: Use Descendant/Ancestor for complex widget trees
- Timing: Use
waitFor before tap if the widget loads asynchronously
- get_widget_tree first: Always use
get_widget_tree before trying to tap widgets via driver so you know exact widget types and text that exist
Phase 7: Reporting
Structure your walkthrough:
# CRUD Test Report โ [App Name]
## Test Environment
- Device: [device name and ID]
- Flutter: [version]
- Date: [date]
- Theme tested: Light / Dark / Both
## Summary
| Screen | Tests | Passed | Failed | Errors |
|--------|-------|--------|--------|--------|
| Dashboard | 5 | 5 | 0 | 0 |
| Settings | 8 | 7 | 1 | 0 |
| Form | 6 | 5 | 0 | 1 |
| TOTAL | 19 | 17 | 1 | 1 |
## [Screen Name] โ Test Results
### Initial State

### Test 1.1: [Action Description]
- Action: Tapped "Submit" with empty form
- Expected: Validation SnackBar
- Actual: SnackBar shown โ
- Runtime errors: None โ

### Test 1.2: [Action Description]
- Action: Tapped "Submit" with filled form
- Expected: Success message
- Actual: โ App crashed โ RenderFlex overflow
- Runtime errors: "RenderFlex overflowed by 24.0 pixels on the right"

## Issues Found
| # | Screen | Issue | Severity | Status |
|---|--------|-------|----------|--------|
| 1 | Settings | Switch doesn't persist | P2 | TODO |
| 2 | Form | Overflow on submit | P1 | Fixed |
## Fixes Applied During Testing
- [commit/description of fix]
- Used `dart_mcp hot_reload()` to apply fix mid-test
Checklist Template
Copy this for each project:
## Pre-Test
- [ ] Codebase analyzed: all screens and interactive elements catalogued
- [ ] Test matrix created with expected behaviors
- [ ] Device selected and ready
## Setup
- [ ] App launched on device
- [ ] DTD connected
- [ ] Baseline screenshot taken
## Screen Tests (repeat per screen)
- [ ] Screen [NAME]: Navigate to screen
- [ ] Screen [NAME]: Screenshot initial state
- [ ] Screen [NAME]: Test all buttons/taps
- [ ] Screen [NAME]: Test all inputs
- [ ] Screen [NAME]: Test all toggles/selections
- [ ] Screen [NAME]: Test scroll / refresh
- [ ] Screen [NAME]: Test edge cases (empty, invalid, rapid)
- [ ] Screen [NAME]: Runtime errors = 0
- [ ] Screen [NAME]: Navigate back cleanly
## Cross-Cutting
- [ ] Theme: Dark mode visual verification
- [ ] Theme: Light mode visual verification
- [ ] Navigation: All routes reachable
- [ ] Navigation: Back button works from all screens
- [ ] Forms: Empty submission validation
- [ ] Runtime errors: 0 across all pages
## Reporting
- [ ] Walkthrough created with screenshots
- [ ] Issues table with severity
- [ ] Fixes documented
Tips & Gotchas
-
list_elements is essential โ NEVER guess coordinates. Always call list_elements_on_screen first. Coordinates change with screen size, orientation, and content.
-
Take screenshots liberally โ They're your proof of work. Embed them in walkthroughs. Save them with descriptive names.
-
Check runtime errors after EVERY action โ Not just at the end. Errors can cascade. A single uncaught error can invalidate all subsequent tests.
-
Keyboard may cover buttons โ After typing, dismiss keyboard via press_button(BACK) on Android or tap outside the field on iOS before trying to tap elements below.
-
Animations take time โ Wait 1-2 seconds after navigation before taking screenshots. Let transitions complete.
-
Bottom nav coordinates are stable โ They don't change with scroll, so you can reuse coordinates across tests within the same session.
-
For both themes โ Run the full suite once in light mode, switch to dark via Settings, then rerun key visual checks.
-
Hot reload for fixes โ If you find and fix a bug mid-test, use dart_mcp hot_reload() to apply without restarting. But remember: hot reload doesn't update const values or global initializers โ use hot_restart() for those.
-
Don't use browser subagents โ Mobile MCP tools are direct device controls. Never try to control a simulator through a browser subagent โ it won't work and wastes time.
-
Device list can change โ If a simulator crashes or disconnects, re-check list_available_devices before continuing.
-
GoRouter ShellRoute = nested Scaffolds โ SnackBars may appear behind bottom navigation. If testing SnackBars, verify they're visible by checking the screenshot carefully.
-
Save screenshots to artifacts โ Use mobile_mcp save_screenshot(device, saveTo=<path>) for persistent storage. take_screenshot returns a transient image.
-
Process cleanup before testing โ Kill zombie processes that may interfere:
# Kill lingering Flutter/Dart processes
pkill -f "flutter_tools"
pkill -f "dart.*tooling_daemon"
# Kill stuck Gradle daemons (Android)
pkill -f "GradleDaemon"
-
Timeout on launch_app โ iOS Simulator cold starts can take 2+ minutes. If it seems stuck, check if Xcode is doing a first-run compilation.
-
ChoiceChip testing โ When testing chip selection, verify BOTH:
- The newly selected chip has selected styling
- The previously selected chip lost its selected styling
Related skills
flutter-unit-testing โ use for isolated widget unit tests without device hardware. flutter-device-crud-testing focuses on multi-device integration scenarios.
flutter-integration-testing โ use for cross-module widget interactions on devices.