Automated regression testing for Flutter through direct callback invocation. Unlike traditional testing frameworks that simulate UI interactions, self_test invokes widget callbacks directly, providing fast and reliable testing for development and pre-production environments.
🤖 AI-Powered Testing: Pair with self_test_mcp to enable Claude and other AI agents to test your Flutter apps with Playwright feature parity - works on iOS, Android, Web, and Desktop!
- Direct Callback Invocation - Execute user actions by calling widget callbacks directly instead of injecting touch events
- Runtime Testing - Run tests in live app environments without external test frameworks
- Text Assertions - Built-in text validation for input fields
- Memory Safe - Automatic registration/unregistration prevents memory leaks
- Hot Restart Compatible - Works seamlessly with Flutter's hot restart
- Test Scenarios - Define and run multi-step test scenarios with
TestScenario
- 60+ Playwright-Equivalent Tools - Full feature parity with Playwright browser testing
- State Management Inspection - Inspect and modify Riverpod, Bloc, and Provider state
- Network Mocking - Mock HTTP responses, block requests, monitor traffic
- Visual Regression - Golden file comparison for UI testing
- Platform Mocking - Mock GPS, permissions, platform channels, sensors
- Cross-Platform - Works on iOS, Android, Web (with or without bridge), Desktop
- Bridgeless Web Testing - Test any Flutter web app via Playwright + semantics tree
Add to your pubspec.yaml:
dependencies:
self_test: ^0.1.0Then run:
flutter pub getFor AI agent integration with Claude Code:
-
Install MCP server:
git clone https://github.com/loonix/self_test cd self_test/packages/self_test_mcp npm install && npm run build ./scripts/setup-claude.sh
-
Add bridge to your app:
dependencies: self_test_bridge: git: url: https://github.com/loonix/self_test path: packages/self_test_bridge
See Architecture section below for details.
import 'package:self_test/self_test.dart';
void main() {
runApp(SelfTestRoot(child: MyApp()));
}SelfTestableWidget(
id: 'username_field',
onTextChange: (value) => setState(() => username = value),
child: TextField(
decoration: InputDecoration(labelText: 'Username'),
onChanged: (value) => setState(() => username = value),
),
),SelfTestManager().enterText('username_field', 'john_doe');
SelfTestManager().trigger('login_button');
await SelfTestManager().waitForAnimations();For type-safe test controllers, annotate your callback methods:
import 'package:self_test/self_test.dart';
part 'login_form.self_test.g.dart';
class _LoginFormState extends State<LoginForm> {
@SelfTestButton('login_btn')
void onLoginPressed() { /* ... */ }
@SelfTestInput('username_field')
void onUsernameChanged(String value) { /* ... */ }
@SelfTestInput('password_field')
void onPasswordChanged(String value) { /* ... */ }
}Generate the controller:
flutter pub run build_runner buildUse the generated controller:
final controller = LoginFormTestController();
controller.enterUsernameField('john_doe');
controller.enterPasswordField('secret123');
controller.tapLoginBtn();
controller.expectUsernameFieldText('john_doe');Define multi-step test scenarios:
final scenario = TestScenario(
name: 'Login flow',
steps: [
TestStep.enterText('username_field', 'user@example.com'),
TestStep.enterText('password_field', 'password123'),
TestStep.tap('login_button'),
TestStep.wait(Duration(milliseconds: 500)),
TestStep.screenshot('after_login'),
],
);
final result = await scenario.run();
print(result.allPassed ? 'All steps passed' : 'Failed at step ${result.failedAtStep}');Integrate with flutter_test:
testWidgets('Login flow test', (WidgetTester tester) async {
SelfTestManager().setTestMode(true);
await tester.pumpWidget(MyApp());
await tester.pumpAndSettle();
SelfTestManager().enterText('username_field', 'testuser');
SelfTestManager().trigger('login_button');
await tester.pump();
expect(find.text('Login successful!'), findsOneWidget);
});// In debug/profile builds
SelfTestManager().setSelfTestModeActive(true);
SelfTestManager().restartWidgetTree();
// In test environments
SelfTestManager().setTestMode(true);Singleton managing test nodes and actions.
| Method | Description |
|---|---|
trigger(id) |
Tap a button by ID |
enterText(id, text) |
Enter text in a field by ID |
waitForAnimations() |
Wait for UI updates |
restartWidgetTree() |
Force widget tree rebuild |
captureScreenshot([name]) |
Capture a screenshot |
registerTestNode(node) |
Register a test node |
unregisterTestNode(id) |
Unregister a test node |
setSelfTestModeActive(bool) |
Enable/disable in debug/profile |
setTestMode(bool) |
Enable/disable in test environments |
SelfTestableWidget({
required String id,
required Widget child,
VoidCallback? onTap,
ValueSetter<String>? onTextChange,
})@SelfTestButton(String id) // For tappable widgets
@SelfTestInput(String id) // For text input widgetsThe self_test ecosystem consists of three components:
┌─────────────────────────────────────────────────────────────────┐
│ AI Agent (Claude) │
└─────────────────────────────┬───────────────────────────────────┘
│ MCP Protocol
▼
┌─────────────────────────────────────────────────────────────────┐
│ self_test_mcp Server │
│ • 60+ Playwright-equivalent tools │
│ • Actions: tap, type, scroll, drag │
│ • Assertions: expect, visual regression │
│ • State inspection: Riverpod, Bloc, Provider │
│ • Network mocking & monitoring │
└─────────────────────────────┬───────────────────────────────────┘
│ WebSocket
▼
┌─────────────────────────────────────────────────────────────────┐
│ Flutter App + self_test_bridge │
│ • Receives commands from MCP │
│ • Executes via self_test callbacks │
│ • Works on iOS, Android, Web, Desktop │
└─────────────────────────────────────────────────────────────────┘
| Package | Description | When to Use |
|---|---|---|
| self_test (this package) | Core testing framework with direct callback invocation | Always - enables runtime testing in your Flutter app |
| self_test_bridge | WebSocket bridge connecting MCP to Flutter | When using AI-powered testing with Claude |
| self_test_mcp | MCP server with 60+ tools for AI agents | When using AI agents for automated testing |
The self_test MCP (Model Context Protocol) server enables AI agents like Claude to test your Flutter apps with Playwright feature parity.
| Platform | Bridge Required | How It Works |
|---|---|---|
| iOS | ✅ Yes | Bridge runs WebSocket server on device, MCP connects |
| Android | ✅ Yes | Bridge runs WebSocket server on device, MCP connects |
| Web (with bridge) | ✅ Yes | Bridge embedded in web app, MCP connects |
| Web (bridgeless) | ❌ No | MCP uses Playwright + Flutter semantics tree |
| Desktop | ✅ Yes | Bridge runs WebSocket server in app, MCP connects |
cd packages/self_test_mcp
npm install && npm run build
# Add to Claude Code automatically
./scripts/setup-claude.shOr manually add to ~/.claude/settings.json:
{
"mcpServers": {
"flutter-self-test": {
"command": "node",
"args": ["/path/to/self_test_mcp/dist/index.js"],
"env": {
"FLUTTER_APP_HOST": "localhost",
"FLUTTER_APP_PORT": "9999"
}
}
}
}Add to pubspec.yaml:
dependencies:
self_test_bridge:
git:
url: https://github.com/loonix/self_test
path: packages/self_test_bridgeAdd to main.dart:
import 'package:flutter/foundation.dart';
import 'package:self_test_bridge/self_test_bridge.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Start bridge in debug mode only
if (kDebugMode) {
final bridge = SelfTestBridge(port: 9999);
await bridge.start();
}
runApp(SelfTestRoot(child: MyApp()));
}Open Claude Code and ask:
Test the login flow in my Flutter app:
1. Enter username "test@example.com"
2. Enter password "password123"
3. Tap login button
4. Verify we navigated to the dashboard
Claude will use the MCP tools to interact with your app!
Test any Flutter web app without code changes using Playwright:
# Configure for web-external mode
export BRIDGE_MODE=web-external
export FLUTTER_APP_URL=https://your-app.com
export PLAYWRIGHT_HEADLESS=false
# Run MCP server
npm startPerfect for:
- Production web apps
- Third-party Flutter apps
- CI/CD smoke tests
- Quick exploratory testing
The MCP server provides 60+ tools with Playwright feature parity:
Locators & Queries:
flutter_snapshot- Get widget treeflutter_get_by_role- Find by semantic roleflutter_get_by_text- Find by text content
Actions:
flutter_tap,flutter_type,flutter_clear,flutter_scrollflutter_drag,flutter_hover,flutter_focusflutter_long_press,flutter_double_tap
Assertions:
flutter_expect- Assert widget state (toBeVisible, toHaveText, etc.)flutter_expect_screenshot- Visual regression testing
State Management:
flutter_get_state- Inspect Riverpod/Bloc/Provider stateflutter_dispatch_action- Dispatch events/actionsflutter_watch_state- Subscribe to state changes
Network:
flutter_mock_http- Mock API responsesflutter_block_http- Block requestsflutter_network_log- Monitor network traffic
Platform Mocking:
flutter_set_geolocation- Mock GPSflutter_set_permission- Mock permissionsflutter_mock_channel- Mock platform channels
[See full tool list in packages/self_test_mcp/README.md]
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Run the test suite:
flutter test - Submit a pull request
Copyright (c) 2025-2026 Ari Silva, Daniel Carneiro. All rights reserved.
This software may be used and modified in your own products and services, but may not be sold or redistributed as a standalone product. See the LICENSE file for details.