Skip to content

Commit f592978

Browse files
author
GitLab CI
committed
feat(v0.8.5): CI/CD templates, reset_app tool, doctor CDP checks
- Add GitHub Actions & GitLab CI templates for AI E2E testing - Add reset_app tool: clears state across all platforms - CDP: clear storage/cookies + reload - Bridge: send reset command with hot_restart fallback - Flutter: hot_restart - Android: adb pm clear - Doctor: add Chrome detection, CDP port check, bridge port check - Total MCP tools: 161
1 parent 5f9b6b9 commit f592978

5 files changed

Lines changed: 362 additions & 0 deletions

File tree

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# flutter-skill CI/CD — AI E2E Testing
2+
# Add this to .github/workflows/e2e-test.yml
3+
#
4+
# Zero test code needed — AI tests your app automatically.
5+
# Just describe what to test in the TEST_PROMPT.
6+
7+
name: AI E2E Tests
8+
9+
on:
10+
push:
11+
branches: [main]
12+
pull_request:
13+
branches: [main]
14+
15+
env:
16+
# Describe your test in natural language
17+
TEST_PROMPT: |
18+
Test the login flow:
19+
1. Find the email input and enter "test@example.com"
20+
2. Find the password input and enter "password123"
21+
3. Tap the login button
22+
4. Verify the dashboard appears
23+
24+
jobs:
25+
e2e-web:
26+
name: Web E2E Tests
27+
runs-on: ubuntu-latest
28+
steps:
29+
- uses: actions/checkout@v4
30+
31+
- name: Setup Dart
32+
uses: dart-lang/setup-dart@v1
33+
34+
- name: Install flutter-skill
35+
run: dart pub global activate flutter_skill
36+
37+
- name: Start app (customize this)
38+
run: |
39+
# Example: npm start, flutter run -d chrome, python -m http.server
40+
npm start &
41+
sleep 5
42+
working-directory: ./my-app
43+
44+
- name: Run AI E2E tests
45+
run: |
46+
flutter-skill serve http://localhost:3000 --headless --port=3001 &
47+
sleep 5
48+
49+
# Discover available tools
50+
curl -s http://localhost:3001/tools/list | jq '.tools | length'
51+
52+
# Take snapshot to verify page loaded
53+
curl -s http://localhost:3001/snapshot | jq '.snapshot' | head -20
54+
55+
# Run test via MCP (works with any MCP-compatible AI)
56+
# Or call tools directly:
57+
curl -s -X POST http://localhost:3001/tools/call \
58+
-H "Content-Type: application/json" \
59+
-d '{"name":"fill_email","arguments":{"text":"test@example.com"}}'
60+
61+
curl -s -X POST http://localhost:3001/tools/call \
62+
-H "Content-Type: application/json" \
63+
-d '{"name":"tap_login","arguments":{}}'
64+
65+
# Verify result
66+
curl -s http://localhost:3001/snapshot | jq '.snapshot' | grep -i "dashboard"
67+
68+
e2e-flutter:
69+
name: Flutter E2E Tests
70+
runs-on: ubuntu-latest
71+
steps:
72+
- uses: actions/checkout@v4
73+
74+
- name: Setup Flutter
75+
uses: subosito/flutter-action@v2
76+
with:
77+
channel: stable
78+
79+
- name: Install flutter-skill
80+
run: dart pub global activate flutter_skill
81+
82+
- name: Run Flutter web app
83+
run: |
84+
flutter run -d chrome --web-port=3000 &
85+
sleep 30 # Flutter web build takes time
86+
87+
- name: Run tests
88+
run: |
89+
flutter-skill test http://localhost:3000 &
90+
sleep 5
91+
# Use MCP server at default port
92+
93+
e2e-electron:
94+
name: Electron E2E Tests
95+
runs-on: ubuntu-latest
96+
steps:
97+
- uses: actions/checkout@v4
98+
99+
- name: Setup Node.js
100+
uses: actions/setup-node@v4
101+
with:
102+
node-version: '20'
103+
104+
- name: Install dependencies
105+
run: npm install
106+
107+
- name: Add flutter-skill SDK
108+
run: npm install flutter-skill
109+
110+
- name: Start Electron app
111+
run: |
112+
npx electron . &
113+
sleep 5
114+
115+
- name: Run tests via bridge
116+
run: |
117+
flutter-skill server &
118+
sleep 3
119+
# flutter-skill auto-discovers the running Electron app
120+
121+
e2e-mobile:
122+
name: Mobile E2E Tests (Android)
123+
runs-on: ubuntu-latest
124+
steps:
125+
- uses: actions/checkout@v4
126+
127+
- name: Setup Java
128+
uses: actions/setup-java@v4
129+
with:
130+
distribution: 'temurin'
131+
java-version: '17'
132+
133+
- name: Setup Android SDK
134+
uses: android-actions/setup-android@v3
135+
136+
- name: Start Android emulator
137+
uses: reactivecircus/android-emulator-runner@v2
138+
with:
139+
api-level: 34
140+
arch: x86_64
141+
script: |
142+
# Install and launch app
143+
adb install app-debug.apk
144+
adb shell am start -n com.example.app/.MainActivity
145+
sleep 10
146+
147+
# flutter-skill auto-discovers via bridge
148+
dart pub global activate flutter_skill
149+
flutter-skill server &
150+
sleep 3

docs/ci-templates/gitlab-ci.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# flutter-skill GitLab CI — AI E2E Testing
2+
# Add this to .gitlab-ci.yml
3+
4+
stages:
5+
- test
6+
7+
e2e-web:
8+
stage: test
9+
image: dart:stable
10+
services:
11+
- name: selenium/standalone-chrome:latest
12+
alias: chrome
13+
script:
14+
- dart pub global activate flutter_skill
15+
# Start your app
16+
- cd my-app && npm start &
17+
- sleep 5
18+
# Run flutter-skill serve
19+
- flutter-skill serve http://localhost:3000 --headless --port=3001 &
20+
- sleep 5
21+
# Verify tools discovered
22+
- curl -s http://localhost:3001/health | grep '"status":"ok"'
23+
- curl -s http://localhost:3001/tools/list | python3 -c "import sys,json; print(len(json.load(sys.stdin)['tools']), 'tools')"
24+
# Run your tests
25+
- curl -s http://localhost:3001/snapshot
26+
27+
e2e-flutter:
28+
stage: test
29+
image: cirrusci/flutter:stable
30+
script:
31+
- dart pub global activate flutter_skill
32+
- flutter run -d chrome --web-port=3000 &
33+
- sleep 30
34+
- flutter-skill test http://localhost:3000

lib/src/cli/doctor.dart

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,59 @@ Future<void> runDoctor(List<String> args) async {
156156
}
157157
}
158158

159+
// Check Chrome for CDP/serve mode
160+
print('');
161+
print('CDP / Serve Mode:');
162+
try {
163+
String chromePath;
164+
if (Platform.isMacOS) {
165+
chromePath =
166+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
167+
} else if (Platform.isLinux) {
168+
chromePath = 'google-chrome';
169+
} else {
170+
chromePath = 'chrome.exe';
171+
}
172+
if (Platform.isMacOS && await File(chromePath).exists()) {
173+
final result = await Process.run(chromePath, ['--version']);
174+
_printOk('Chrome: ${(result.stdout as String).trim()}');
175+
okCount++;
176+
} else {
177+
final result = await Process.run('which', ['google-chrome']);
178+
if (result.exitCode == 0) {
179+
_printOk('Chrome found');
180+
okCount++;
181+
} else {
182+
_printWarn('Chrome not found — needed for serve/test commands');
183+
warnCount++;
184+
}
185+
}
186+
} catch (_) {
187+
_printInfo('Could not check Chrome');
188+
}
189+
190+
// Check for common port conflicts
191+
try {
192+
final result = await Process.run('lsof', ['-i', ':9222', '-t']);
193+
if (result.exitCode == 0 && (result.stdout as String).trim().isNotEmpty) {
194+
_printInfo('Port 9222 in use — CDP may need --cdp-port flag');
195+
} else {
196+
_printOk('Port 9222 available for CDP');
197+
okCount++;
198+
}
199+
} catch (_) {}
200+
201+
// Check bridge port range
202+
try {
203+
final result = await Process.run('lsof', ['-i', ':18118', '-t']);
204+
if (result.exitCode == 0 && (result.stdout as String).trim().isNotEmpty) {
205+
_printOk('Bridge port 18118 active (app may be running)');
206+
okCount++;
207+
} else {
208+
_printInfo('Bridge port 18118 free (no app detected)');
209+
}
210+
} catch (_) {}
211+
159212
// Summary
160213
print('');
161214
print('=' * 50);

lib/src/cli/tool_handlers/dev_tool_handlers.dart

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,100 @@ extension _DevToolHandlers on FlutterMcpServer {
7676
return await fc.getIndicatorStatus();
7777
}
7878

79+
if (name == 'reset_app') {
80+
final client = _getClient(args);
81+
final clearStorage = args['clear_storage'] ?? true;
82+
final clearCookies = args['clear_cookies'] ?? true;
83+
84+
// CDP: clear browser state and reload
85+
if (client is CdpDriver) {
86+
final actions = <String>[];
87+
if (clearStorage) {
88+
await client.call('Runtime.evaluate', {
89+
'expression': 'localStorage.clear(); sessionStorage.clear();',
90+
'returnByValue': true,
91+
});
92+
actions.add('storage cleared');
93+
}
94+
if (clearCookies) {
95+
await client.call('Network.clearBrowserCookies');
96+
actions.add('cookies cleared');
97+
}
98+
// Reload page
99+
await client.call('Page.reload', {'ignoreCache': true});
100+
await Future.delayed(const Duration(seconds: 2));
101+
actions.add('page reloaded');
102+
return {
103+
'success': true,
104+
'platform': 'cdp',
105+
'actions': actions,
106+
};
107+
}
108+
109+
// Bridge: send reset command
110+
if (client is BridgeDriver) {
111+
try {
112+
final result = await client.callMethod('reset_app', {
113+
'clear_storage': clearStorage,
114+
});
115+
return {'success': true, 'platform': 'bridge', 'result': result};
116+
} catch (_) {
117+
// Fallback: hot restart if bridge doesn't support reset
118+
try {
119+
await client.callMethod('hot_restart');
120+
return {
121+
'success': true,
122+
'platform': 'bridge',
123+
'fallback': 'hot_restart'
124+
};
125+
} catch (e) {
126+
return {'success': false, 'error': e.toString()};
127+
}
128+
}
129+
}
130+
131+
// Flutter: hot restart
132+
if (client != null) {
133+
try {
134+
final fc = _asFlutterClient(client, 'reset_app');
135+
await fc.hotRestart();
136+
return {
137+
'success': true,
138+
'platform': 'flutter',
139+
'action': 'hot_restart'
140+
};
141+
} catch (e) {
142+
return {'success': false, 'error': e.toString()};
143+
}
144+
}
145+
146+
// Android: adb clear
147+
try {
148+
// Try to find package name from active session
149+
final adbResult = await Process.run('adb', [
150+
'shell',
151+
'dumpsys',
152+
'activity',
153+
'activities',
154+
]);
155+
final output = adbResult.stdout as String;
156+
final match =
157+
RegExp(r'mResumedActivity.*?(\w+\.\w+[\.\w]*)/').firstMatch(output);
158+
if (match != null) {
159+
final pkg = match.group(1)!;
160+
await Process.run('adb', ['shell', 'pm', 'clear', pkg]);
161+
return {
162+
'success': true,
163+
'platform': 'android',
164+
'package': pkg,
165+
'action': 'pm clear'
166+
};
167+
}
168+
} catch (_) {}
169+
170+
return {'success': false, 'error': 'No connected app to reset'};
171+
}
172+
79173
// Native platform interaction tools (no VM Service connection required)
80174

81175
return null; // Not handled by this group

lib/src/cli/tool_handlers/tool_definitions.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ extension _ToolDefinitions on FlutterMcpServer {
7171
'find_by_type',
7272
'hot_reload',
7373
'hot_restart',
74+
'reset_app',
7475
};
7576

7677
// Mobile-only tools
@@ -1865,6 +1866,36 @@ Each request includes: method, url, status_code, duration_ms, response_body (tru
18651866
"description": "Trigger hot restart (slower, resets app state)",
18661867
"inputSchema": {"type": "object", "properties": {}},
18671868
},
1869+
{
1870+
"name": "reset_app",
1871+
"description": """Reset app to clean state for testing.
1872+
1873+
Platform behavior:
1874+
• Flutter: hot_restart (resets all state)
1875+
• Android: clears app data via adb (pm clear)
1876+
• Web/CDP: clears localStorage, sessionStorage, cookies, then reloads
1877+
• Electron/Tauri/KMP/.NET MAUI: sends reset command via bridge
1878+
1879+
Use between test runs to ensure clean state.""",
1880+
"inputSchema": {
1881+
"type": "object",
1882+
"properties": {
1883+
"session_id": {
1884+
"type": "string",
1885+
"description": "Optional session ID"
1886+
},
1887+
"clear_storage": {
1888+
"type": "boolean",
1889+
"description":
1890+
"Clear local/session storage (web platforms, default: true)"
1891+
},
1892+
"clear_cookies": {
1893+
"type": "boolean",
1894+
"description": "Clear cookies (web platforms, default: true)"
1895+
},
1896+
},
1897+
},
1898+
},
18681899
{
18691900
// Native platform interaction tools
18701901
"name": "native_screenshot",

0 commit comments

Comments
 (0)