Tools: How to Test Any App with AI in 30 Seconds — Flutter, React Native, iOS, Android & More

Tools: How to Test Any App with AI in 30 Seconds — Flutter, React Native, iOS, Android & More

Source: Dev.to

How to Test Any App with AI in 30 Seconds ## The Problem Nobody Talks About ## The Idea: Let AI Be the User ## How It Works ## Setup: Actually 30 Seconds ## Step 1: Install (5 seconds) ## Step 2: Initialize your project (10 seconds) ## Step 3: Add to your AI tool (15 seconds) ## What AI Can Do ## Inspection ## Interaction ## Verification ## Navigation ## Advanced ## Real Example: Testing a Login Flow ## The Numbers ## Lessons from Building for 8 Platforms ## Android: PNG Screenshots Kill WebSocket ## Tauri: eval() Is Fire-and-Forget ## React Native: Skip the Full Build ## The go_back Race Condition ## When to Use This (and When Not To) ## Getting Started What if testing your app required zero test code? Not "low-code testing." Not "AI-assisted test generation." Literally zero lines of test code — you describe what should happen, and AI does it. That's what we built with flutter-skill: an open-source MCP server that gives AI agents eyes and hands inside any running app. E2E testing is universally hated for a reason: You're not testing your app. You're maintaining a second codebase that mirrors your UI. Every refactor breaks it. Every design change means rewriting tests. And it gets worse: every platform has its own testing framework. Flutter has integration_test. iOS has XCUITest. Android has Espresso. React Native has Detox. Web has Playwright. Each with its own API, its own quirks, its own debug cycle. What if there was one tool for all of them? Instead of writing robot instructions, what if we just... talked to the robot? That's a complete E2E test. No selectors. No framework-specific code. No maintenance when the UI changes — because AI understands what a "login button" looks like, regardless of its internal key. This is what MCP (Model Context Protocol) makes possible. MCP lets AI tools like Claude, Cursor, and Windsurf connect to external services. flutter-skill is one of those services — it bridges AI to your running app's UI. The architecture is simple: The SDK exposes your app's accessibility tree — every button, text field, label, and container — so the AI can see exactly what's on screen. This auto-detects your project type and patches your entry point: Add to your MCP config (e.g., Claude Desktop claude_desktop_config.json): That's it. Your AI can now see and interact with your app. Once connected, your AI has access to 40+ tools: Here's what happens when you tell Claude: "Test the login flow with invalid credentials and verify the error message." Claude (via flutter-skill): No test file created. No selectors maintained. If the UI changes tomorrow, the AI adapts — it looks for "Submit" by understanding the UI, not by memorizing a widget key. We tested flutter-skill across 8 platforms with a comprehensive E2E test suite: Every test is AI-driven. Zero hand-written test code. Building SDKs for 8 platforms taught us things no tutorial covers: Full-resolution PNG screenshots on Android are huge. Sending them over WebSocket caused timeouts. The fix: JPEG at 80% quality, downscaled to 720p. AI reads the UI just fine at lower resolution, and it saves ~90% bandwidth. Tauri v2's eval() function doesn't return values. It executes JavaScript in the webview and... that's it. No callback, no promise, no return. Our solution: open a secondary WebSocket on port 18120. The JavaScript sends its result there, and Rust receives it via a oneshot channel. Three ports total: HTTP health (18118), WS commands (18119), WS results (18120). We also had to add ws://127.0.0.1:* to Tauri's CSP, otherwise WebSocket connections from the tauri:// origin to localhost are silently blocked. Building a full React Native project requires native modules, CocoaPods, Gradle — the works. For testing, we used a Node.js mock that implements the bridge protocol directly. Much faster, same result. On Android, clearing currentActivity on onActivityPaused caused a race condition with go_back. The previous activity pauses before the new one resumes, leaving a brief window where the SDK thinks there's no activity. Fix: only clear on onActivityDestroyed. Use flutter-skill when: Add to your MCP config, and start talking to your app through AI. GitHub: github.com/ai-dashboad/flutter-skill MIT licensed. Contributions welcome. flutter-skill supports Flutter, iOS, Android, Web, Electron, Tauri, KMP, React Native, and .NET MAUI. Works with Claude, Cursor, Windsurf, and any MCP-compatible AI tool. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: // This breaks every time someone moves a button final loginButton = find.byKey(Key('loginBtn')); await tester.tap(loginButton); await tester.pumpAndSettle(); final emailField = find.byKey(Key('emailField')); await tester.enterText(emailField, '[email protected]'); // ... 50 more lines of brittle selectors Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // This breaks every time someone moves a button final loginButton = find.byKey(Key('loginBtn')); await tester.tap(loginButton); await tester.pumpAndSettle(); final emailField = find.byKey(Key('emailField')); await tester.enterText(emailField, '[email protected]'); // ... 50 more lines of brittle selectors CODE_BLOCK: // This breaks every time someone moves a button final loginButton = find.byKey(Key('loginBtn')); await tester.tap(loginButton); await tester.pumpAndSettle(); final emailField = find.byKey(Key('emailField')); await tester.enterText(emailField, '[email protected]'); // ... 50 more lines of brittle selectors CODE_BLOCK: "Tap the login button, enter [email protected] as the email, enter password123 as the password, tap submit, and verify the dashboard loads." Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: "Tap the login button, enter [email protected] as the email, enter password123 as the password, tap submit, and verify the dashboard loads." CODE_BLOCK: "Tap the login button, enter [email protected] as the email, enter password123 as the password, tap submit, and verify the dashboard loads." CODE_BLOCK: ┌─────────────┐ MCP ┌────────────────┐ WebSocket ┌─────────────┐ │ AI Client │ ◄──────────► │ flutter-skill │ ◄────────────► │ Your App │ │ (Claude, │ JSON-RPC │ (MCP Server) │ JSON-RPC │ (any │ │ Cursor, │ │ │ on :18118 │ platform) │ │ Windsurf) │ └────────────────┘ └─────────────┘ └─────────────┘ Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: ┌─────────────┐ MCP ┌────────────────┐ WebSocket ┌─────────────┐ │ AI Client │ ◄──────────► │ flutter-skill │ ◄────────────► │ Your App │ │ (Claude, │ JSON-RPC │ (MCP Server) │ JSON-RPC │ (any │ │ Cursor, │ │ │ on :18118 │ platform) │ │ Windsurf) │ └────────────────┘ └─────────────┘ └─────────────┘ CODE_BLOCK: ┌─────────────┐ MCP ┌────────────────┐ WebSocket ┌─────────────┐ │ AI Client │ ◄──────────► │ flutter-skill │ ◄────────────► │ Your App │ │ (Claude, │ JSON-RPC │ (MCP Server) │ JSON-RPC │ (any │ │ Cursor, │ │ │ on :18118 │ platform) │ │ Windsurf) │ └────────────────┘ └─────────────┘ └─────────────┘ COMMAND_BLOCK: npm i -g flutter-skill Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: npm i -g flutter-skill COMMAND_BLOCK: npm i -g flutter-skill CODE_BLOCK: cd your-app flutter-skill init Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: cd your-app flutter-skill init CODE_BLOCK: cd your-app flutter-skill init CODE_BLOCK: { "mcpServers": { "flutter-skill": { "command": "flutter-skill" } } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: { "mcpServers": { "flutter-skill": { "command": "flutter-skill" } } } CODE_BLOCK: { "mcpServers": { "flutter-skill": { "command": "flutter-skill" } } } CODE_BLOCK: 1. inspect() → sees the login screen with email field, password field, submit button 2. tap(element: "Email field") 3. enter_text(text: "[email protected]") 4. tap(element: "Password field") 5. enter_text(text: "wrongpassword") 6. tap(element: "Submit") 7. screenshot() → captures the error state 8. assert_exists(element: "Invalid credentials") → ✅ verified Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: 1. inspect() → sees the login screen with email field, password field, submit button 2. tap(element: "Email field") 3. enter_text(text: "[email protected]") 4. tap(element: "Password field") 5. enter_text(text: "wrongpassword") 6. tap(element: "Submit") 7. screenshot() → captures the error state 8. assert_exists(element: "Invalid credentials") → ✅ verified CODE_BLOCK: 1. inspect() → sees the login screen with email field, password field, submit button 2. tap(element: "Email field") 3. enter_text(text: "[email protected]") 4. tap(element: "Password field") 5. enter_text(text: "wrongpassword") 6. tap(element: "Submit") 7. screenshot() → captures the error state 8. assert_exists(element: "Invalid credentials") → ✅ verified COMMAND_BLOCK: # Install npm i -g flutter-skill # Auto-detect and configure your project cd your-app flutter-skill init # Or try the built-in demo flutter-skill demo Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: # Install npm i -g flutter-skill # Auto-detect and configure your project cd your-app flutter-skill init # Or try the built-in demo flutter-skill demo COMMAND_BLOCK: # Install npm i -g flutter-skill # Auto-detect and configure your project cd your-app flutter-skill init # Or try the built-in demo flutter-skill demo - Your app includes a lightweight SDK (a few lines of code) that connects via WebSocket - flutter-skill runs as an MCP server, translating AI commands into app interactions - Your AI tool sends natural language instructions, which flutter-skill converts to precise UI operations - pubspec.yaml → Flutter - Package.swift → iOS native - build.gradle.kts + AndroidManifest.xml → Android native - package.json + react-native → React Native - index.html → Web - package.json + electron → Electron - Cargo.toml + tauri → Tauri - build.gradle.kts + kotlin → KMP - .csproj + Maui → .NET MAUI - inspect — See the full UI element tree (accessibility labels, types, positions) - get_element_details — Deep-dive into any specific element - tap — Tap any element by description or index - enter_text — Type into text fields - scroll — Scroll in any direction - swipe — Swipe gestures (e.g., dismiss, navigate) - long_press — Long press for context menus - screenshot — Capture what the app looks like right now - assert_exists — Verify an element is on screen - get_text — Read text content from any element - go_back — Navigate back - open_url — Deep link to any route - eval — Execute platform-native code (Dart, JS, Swift, Kotlin) - get_logs — Read app console output - You want to test user-facing flows without writing test code - You're building across multiple platforms and want one testing approach - You're doing vibe coding and need AI to verify what it builds - You want to prototype and test simultaneously - Unit testing (that's a different problem) - Performance benchmarking (AI interaction adds latency) - Tests that need to run in < 1 second (AI thinking time)