flutter-skill-mcp 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -0
- package/bin/cli.js +73 -0
- package/dart/.dart_tool/package_config.json +196 -0
- package/dart/.dart_tool/package_graph.json +258 -0
- package/dart/.dart_tool/version +1 -0
- package/dart/bin/server.dart +3 -0
- package/dart/lib/flutter_skill.dart +1167 -0
- package/dart/lib/src/cli/act.dart +118 -0
- package/dart/lib/src/cli/inspect.dart +65 -0
- package/dart/lib/src/cli/launch.dart +89 -0
- package/dart/lib/src/cli/server.dart +594 -0
- package/dart/lib/src/cli/setup.dart +76 -0
- package/dart/lib/src/flutter_skill_client.dart +286 -0
- package/dart/pubspec.lock +237 -0
- package/dart/pubspec.yaml +26 -0
- package/index.js +7 -0
- package/package.json +41 -0
- package/scripts/build.js +67 -0
- package/scripts/check-dart.js +39 -0
|
@@ -0,0 +1,1167 @@
|
|
|
1
|
+
import 'dart:async';
|
|
2
|
+
import 'dart:convert';
|
|
3
|
+
import 'dart:developer' as developer;
|
|
4
|
+
import 'dart:ui' as ui;
|
|
5
|
+
|
|
6
|
+
import 'package:flutter/material.dart';
|
|
7
|
+
import 'package:flutter/rendering.dart';
|
|
8
|
+
import 'package:flutter/services.dart';
|
|
9
|
+
|
|
10
|
+
/// The Binding that enables Flutter Skill automation.
|
|
11
|
+
class FlutterSkillBinding {
|
|
12
|
+
static void ensureInitialized() {
|
|
13
|
+
if (_registered) return;
|
|
14
|
+
_registered = true;
|
|
15
|
+
_registerExtensions();
|
|
16
|
+
print('Flutter Skill Binding Initialized 🚀');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static bool _registered = false;
|
|
20
|
+
static final List<String> _logs = [];
|
|
21
|
+
static final List<Map<String, dynamic>> _errors = [];
|
|
22
|
+
static int _pointerCounter = 1;
|
|
23
|
+
|
|
24
|
+
static void _registerExtensions() {
|
|
25
|
+
// ==================== EXISTING EXTENSIONS ====================
|
|
26
|
+
|
|
27
|
+
// 1. Interactive Elements
|
|
28
|
+
developer.registerExtension('ext.flutter.flutter_skill.interactive', (method, parameters) async {
|
|
29
|
+
try {
|
|
30
|
+
final elements = _findInteractiveElements();
|
|
31
|
+
return developer.ServiceExtensionResponse.result(
|
|
32
|
+
jsonEncode({'type': 'Success', 'elements': elements}),
|
|
33
|
+
);
|
|
34
|
+
} catch (e, stack) {
|
|
35
|
+
return _errorResponse(e, stack);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// 2. Tap
|
|
40
|
+
developer.registerExtension('ext.flutter.flutter_skill.tap', (method, parameters) async {
|
|
41
|
+
try {
|
|
42
|
+
final key = parameters['key'];
|
|
43
|
+
final text = parameters['text'];
|
|
44
|
+
final success = await _performTap(key: key, text: text);
|
|
45
|
+
return developer.ServiceExtensionResponse.result(
|
|
46
|
+
jsonEncode({'success': success, 'message': success ? 'Tap successful' : 'Element not found'}),
|
|
47
|
+
);
|
|
48
|
+
} catch (e, stack) {
|
|
49
|
+
return _errorResponse(e, stack);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// 3. Enter Text
|
|
54
|
+
developer.registerExtension('ext.flutter.flutter_skill.enterText', (method, parameters) async {
|
|
55
|
+
try {
|
|
56
|
+
final key = parameters['key'];
|
|
57
|
+
final text = parameters['text'];
|
|
58
|
+
if (text == null) {
|
|
59
|
+
return developer.ServiceExtensionResponse.error(
|
|
60
|
+
developer.ServiceExtensionResponse.invalidParams,
|
|
61
|
+
'Missing text',
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
final success = await _performEnterText(key: key, text: text);
|
|
65
|
+
return developer.ServiceExtensionResponse.result(
|
|
66
|
+
jsonEncode({'success': success, 'message': success ? 'Text entered' : 'TextField not found'}),
|
|
67
|
+
);
|
|
68
|
+
} catch (e, stack) {
|
|
69
|
+
return _errorResponse(e, stack);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// 4. Scroll
|
|
74
|
+
developer.registerExtension('ext.flutter.flutter_skill.scroll', (method, parameters) async {
|
|
75
|
+
try {
|
|
76
|
+
final key = parameters['key'];
|
|
77
|
+
final text = parameters['text'];
|
|
78
|
+
final success = await _performScroll(key: key, text: text);
|
|
79
|
+
return developer.ServiceExtensionResponse.result(
|
|
80
|
+
jsonEncode({'success': success, 'message': success ? 'Scroll successful' : 'Element not found'}),
|
|
81
|
+
);
|
|
82
|
+
} catch (e, stack) {
|
|
83
|
+
return _errorResponse(e, stack);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ==================== UI INSPECTION EXTENSIONS ====================
|
|
88
|
+
|
|
89
|
+
// 5. Get Widget Tree
|
|
90
|
+
developer.registerExtension('ext.flutter.flutter_skill.getWidgetTree', (method, parameters) async {
|
|
91
|
+
try {
|
|
92
|
+
final maxDepth = int.tryParse(parameters['maxDepth'] ?? '10') ?? 10;
|
|
93
|
+
final tree = _getWidgetTree(maxDepth: maxDepth);
|
|
94
|
+
return developer.ServiceExtensionResponse.result(jsonEncode({'tree': tree}));
|
|
95
|
+
} catch (e, stack) {
|
|
96
|
+
return _errorResponse(e, stack);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// 6. Get Widget Properties
|
|
101
|
+
developer.registerExtension('ext.flutter.flutter_skill.getWidgetProperties', (method, parameters) async {
|
|
102
|
+
try {
|
|
103
|
+
final key = parameters['key'];
|
|
104
|
+
if (key == null) {
|
|
105
|
+
return developer.ServiceExtensionResponse.error(
|
|
106
|
+
developer.ServiceExtensionResponse.invalidParams,
|
|
107
|
+
'Missing key',
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
final properties = _getWidgetProperties(key);
|
|
111
|
+
return developer.ServiceExtensionResponse.result(jsonEncode({'properties': properties}));
|
|
112
|
+
} catch (e, stack) {
|
|
113
|
+
return _errorResponse(e, stack);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// 7. Get Text Content
|
|
118
|
+
developer.registerExtension('ext.flutter.flutter_skill.getTextContent', (method, parameters) async {
|
|
119
|
+
try {
|
|
120
|
+
final textList = _getTextContent();
|
|
121
|
+
return developer.ServiceExtensionResponse.result(jsonEncode({'texts': textList}));
|
|
122
|
+
} catch (e, stack) {
|
|
123
|
+
return _errorResponse(e, stack);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// 8. Find By Type
|
|
128
|
+
developer.registerExtension('ext.flutter.flutter_skill.findByType', (method, parameters) async {
|
|
129
|
+
try {
|
|
130
|
+
final type = parameters['type'];
|
|
131
|
+
if (type == null) {
|
|
132
|
+
return developer.ServiceExtensionResponse.error(
|
|
133
|
+
developer.ServiceExtensionResponse.invalidParams,
|
|
134
|
+
'Missing type',
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
final elements = _findByType(type);
|
|
138
|
+
return developer.ServiceExtensionResponse.result(jsonEncode({'elements': elements}));
|
|
139
|
+
} catch (e, stack) {
|
|
140
|
+
return _errorResponse(e, stack);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ==================== MORE INTERACTIONS ====================
|
|
145
|
+
|
|
146
|
+
// 9. Long Press
|
|
147
|
+
developer.registerExtension('ext.flutter.flutter_skill.longPress', (method, parameters) async {
|
|
148
|
+
try {
|
|
149
|
+
final key = parameters['key'];
|
|
150
|
+
final text = parameters['text'];
|
|
151
|
+
final duration = int.tryParse(parameters['duration'] ?? '500') ?? 500;
|
|
152
|
+
final success = await _performLongPress(key: key, text: text, duration: duration);
|
|
153
|
+
return developer.ServiceExtensionResponse.result(
|
|
154
|
+
jsonEncode({'success': success, 'message': success ? 'Long press successful' : 'Element not found'}),
|
|
155
|
+
);
|
|
156
|
+
} catch (e, stack) {
|
|
157
|
+
return _errorResponse(e, stack);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// 10. Swipe
|
|
162
|
+
developer.registerExtension('ext.flutter.flutter_skill.swipe', (method, parameters) async {
|
|
163
|
+
try {
|
|
164
|
+
final direction = parameters['direction'] ?? 'up';
|
|
165
|
+
final distance = double.tryParse(parameters['distance'] ?? '300') ?? 300;
|
|
166
|
+
final key = parameters['key'];
|
|
167
|
+
final success = await _performSwipe(direction: direction, distance: distance, key: key);
|
|
168
|
+
return developer.ServiceExtensionResponse.result(
|
|
169
|
+
jsonEncode({'success': success, 'message': success ? 'Swipe successful' : 'Swipe failed'}),
|
|
170
|
+
);
|
|
171
|
+
} catch (e, stack) {
|
|
172
|
+
return _errorResponse(e, stack);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// 11. Drag
|
|
177
|
+
developer.registerExtension('ext.flutter.flutter_skill.drag', (method, parameters) async {
|
|
178
|
+
try {
|
|
179
|
+
final fromKey = parameters['fromKey'];
|
|
180
|
+
final toKey = parameters['toKey'];
|
|
181
|
+
final success = await _performDrag(fromKey: fromKey, toKey: toKey);
|
|
182
|
+
return developer.ServiceExtensionResponse.result(
|
|
183
|
+
jsonEncode({'success': success, 'message': success ? 'Drag successful' : 'Drag failed'}),
|
|
184
|
+
);
|
|
185
|
+
} catch (e, stack) {
|
|
186
|
+
return _errorResponse(e, stack);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// 12. Double Tap
|
|
191
|
+
developer.registerExtension('ext.flutter.flutter_skill.doubleTap', (method, parameters) async {
|
|
192
|
+
try {
|
|
193
|
+
final key = parameters['key'];
|
|
194
|
+
final text = parameters['text'];
|
|
195
|
+
final success = await _performDoubleTap(key: key, text: text);
|
|
196
|
+
return developer.ServiceExtensionResponse.result(
|
|
197
|
+
jsonEncode({'success': success, 'message': success ? 'Double tap successful' : 'Element not found'}),
|
|
198
|
+
);
|
|
199
|
+
} catch (e, stack) {
|
|
200
|
+
return _errorResponse(e, stack);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ==================== STATE & VALIDATION ====================
|
|
205
|
+
|
|
206
|
+
// 13. Get Text Value
|
|
207
|
+
developer.registerExtension('ext.flutter.flutter_skill.getTextValue', (method, parameters) async {
|
|
208
|
+
try {
|
|
209
|
+
final key = parameters['key'];
|
|
210
|
+
if (key == null) {
|
|
211
|
+
return developer.ServiceExtensionResponse.error(
|
|
212
|
+
developer.ServiceExtensionResponse.invalidParams,
|
|
213
|
+
'Missing key',
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
final value = _getTextFieldValue(key);
|
|
217
|
+
return developer.ServiceExtensionResponse.result(jsonEncode({'value': value}));
|
|
218
|
+
} catch (e, stack) {
|
|
219
|
+
return _errorResponse(e, stack);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// 14. Get Checkbox State
|
|
224
|
+
developer.registerExtension('ext.flutter.flutter_skill.getCheckboxState', (method, parameters) async {
|
|
225
|
+
try {
|
|
226
|
+
final key = parameters['key'];
|
|
227
|
+
if (key == null) {
|
|
228
|
+
return developer.ServiceExtensionResponse.error(
|
|
229
|
+
developer.ServiceExtensionResponse.invalidParams,
|
|
230
|
+
'Missing key',
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
final state = _getCheckboxState(key);
|
|
234
|
+
return developer.ServiceExtensionResponse.result(jsonEncode({'checked': state}));
|
|
235
|
+
} catch (e, stack) {
|
|
236
|
+
return _errorResponse(e, stack);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// 15. Get Slider Value
|
|
241
|
+
developer.registerExtension('ext.flutter.flutter_skill.getSliderValue', (method, parameters) async {
|
|
242
|
+
try {
|
|
243
|
+
final key = parameters['key'];
|
|
244
|
+
if (key == null) {
|
|
245
|
+
return developer.ServiceExtensionResponse.error(
|
|
246
|
+
developer.ServiceExtensionResponse.invalidParams,
|
|
247
|
+
'Missing key',
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
final value = _getSliderValue(key);
|
|
251
|
+
return developer.ServiceExtensionResponse.result(jsonEncode({'value': value}));
|
|
252
|
+
} catch (e, stack) {
|
|
253
|
+
return _errorResponse(e, stack);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// 16. Wait For Element
|
|
258
|
+
developer.registerExtension('ext.flutter.flutter_skill.waitForElement', (method, parameters) async {
|
|
259
|
+
try {
|
|
260
|
+
final key = parameters['key'];
|
|
261
|
+
final text = parameters['text'];
|
|
262
|
+
final timeout = int.tryParse(parameters['timeout'] ?? '5000') ?? 5000;
|
|
263
|
+
final found = await _waitForElement(key: key, text: text, timeout: timeout);
|
|
264
|
+
return developer.ServiceExtensionResponse.result(
|
|
265
|
+
jsonEncode({'found': found, 'message': found ? 'Element found' : 'Timeout waiting for element'}),
|
|
266
|
+
);
|
|
267
|
+
} catch (e, stack) {
|
|
268
|
+
return _errorResponse(e, stack);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// 17. Wait For Gone
|
|
273
|
+
developer.registerExtension('ext.flutter.flutter_skill.waitForGone', (method, parameters) async {
|
|
274
|
+
try {
|
|
275
|
+
final key = parameters['key'];
|
|
276
|
+
final text = parameters['text'];
|
|
277
|
+
final timeout = int.tryParse(parameters['timeout'] ?? '5000') ?? 5000;
|
|
278
|
+
final gone = await _waitForGone(key: key, text: text, timeout: timeout);
|
|
279
|
+
return developer.ServiceExtensionResponse.result(
|
|
280
|
+
jsonEncode({'gone': gone, 'message': gone ? 'Element is gone' : 'Element still present'}),
|
|
281
|
+
);
|
|
282
|
+
} catch (e, stack) {
|
|
283
|
+
return _errorResponse(e, stack);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ==================== SCREENSHOT ====================
|
|
288
|
+
|
|
289
|
+
// 18. Screenshot
|
|
290
|
+
developer.registerExtension('ext.flutter.flutter_skill.screenshot', (method, parameters) async {
|
|
291
|
+
try {
|
|
292
|
+
final base64Image = await _takeScreenshot();
|
|
293
|
+
return developer.ServiceExtensionResponse.result(jsonEncode({'image': base64Image}));
|
|
294
|
+
} catch (e, stack) {
|
|
295
|
+
return _errorResponse(e, stack);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// 19. Screenshot Element
|
|
300
|
+
developer.registerExtension('ext.flutter.flutter_skill.screenshotElement', (method, parameters) async {
|
|
301
|
+
try {
|
|
302
|
+
final key = parameters['key'];
|
|
303
|
+
if (key == null) {
|
|
304
|
+
return developer.ServiceExtensionResponse.error(
|
|
305
|
+
developer.ServiceExtensionResponse.invalidParams,
|
|
306
|
+
'Missing key',
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
final base64Image = await _takeElementScreenshot(key);
|
|
310
|
+
return developer.ServiceExtensionResponse.result(jsonEncode({'image': base64Image}));
|
|
311
|
+
} catch (e, stack) {
|
|
312
|
+
return _errorResponse(e, stack);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// ==================== NAVIGATION ====================
|
|
317
|
+
|
|
318
|
+
// 20. Get Current Route
|
|
319
|
+
developer.registerExtension('ext.flutter.flutter_skill.getCurrentRoute', (method, parameters) async {
|
|
320
|
+
try {
|
|
321
|
+
final route = _getCurrentRoute();
|
|
322
|
+
return developer.ServiceExtensionResponse.result(jsonEncode({'route': route}));
|
|
323
|
+
} catch (e, stack) {
|
|
324
|
+
return _errorResponse(e, stack);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// 21. Go Back
|
|
329
|
+
developer.registerExtension('ext.flutter.flutter_skill.goBack', (method, parameters) async {
|
|
330
|
+
try {
|
|
331
|
+
final success = _goBack();
|
|
332
|
+
return developer.ServiceExtensionResponse.result(
|
|
333
|
+
jsonEncode({'success': success, 'message': success ? 'Navigated back' : 'Cannot go back'}),
|
|
334
|
+
);
|
|
335
|
+
} catch (e, stack) {
|
|
336
|
+
return _errorResponse(e, stack);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// 22. Get Navigation Stack
|
|
341
|
+
developer.registerExtension('ext.flutter.flutter_skill.getNavigationStack', (method, parameters) async {
|
|
342
|
+
try {
|
|
343
|
+
final stack = _getNavigationStack();
|
|
344
|
+
return developer.ServiceExtensionResponse.result(jsonEncode({'stack': stack}));
|
|
345
|
+
} catch (e, stack) {
|
|
346
|
+
return _errorResponse(e, stack);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// ==================== DEBUG & LOGS ====================
|
|
351
|
+
|
|
352
|
+
// 23. Get Logs
|
|
353
|
+
developer.registerExtension('ext.flutter.flutter_skill.getLogs', (method, parameters) async {
|
|
354
|
+
try {
|
|
355
|
+
return developer.ServiceExtensionResponse.result(jsonEncode({'logs': _logs}));
|
|
356
|
+
} catch (e, stack) {
|
|
357
|
+
return _errorResponse(e, stack);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// 24. Get Errors
|
|
362
|
+
developer.registerExtension('ext.flutter.flutter_skill.getErrors', (method, parameters) async {
|
|
363
|
+
try {
|
|
364
|
+
return developer.ServiceExtensionResponse.result(jsonEncode({'errors': _errors}));
|
|
365
|
+
} catch (e, stack) {
|
|
366
|
+
return _errorResponse(e, stack);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// 25. Clear Logs
|
|
371
|
+
developer.registerExtension('ext.flutter.flutter_skill.clearLogs', (method, parameters) async {
|
|
372
|
+
try {
|
|
373
|
+
_logs.clear();
|
|
374
|
+
_errors.clear();
|
|
375
|
+
return developer.ServiceExtensionResponse.result(jsonEncode({'success': true}));
|
|
376
|
+
} catch (e, stack) {
|
|
377
|
+
return _errorResponse(e, stack);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// 26. Get Performance
|
|
382
|
+
developer.registerExtension('ext.flutter.flutter_skill.getPerformance', (method, parameters) async {
|
|
383
|
+
try {
|
|
384
|
+
final perf = _getPerformanceMetrics();
|
|
385
|
+
return developer.ServiceExtensionResponse.result(jsonEncode(perf));
|
|
386
|
+
} catch (e, stack) {
|
|
387
|
+
return _errorResponse(e, stack);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Setup error handler
|
|
392
|
+
FlutterError.onError = (FlutterErrorDetails details) {
|
|
393
|
+
_errors.add({
|
|
394
|
+
'error': details.exception.toString(),
|
|
395
|
+
'stack': details.stack?.toString(),
|
|
396
|
+
'library': details.library,
|
|
397
|
+
'context': details.context?.toString(),
|
|
398
|
+
'timestamp': DateTime.now().toIso8601String(),
|
|
399
|
+
});
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
static developer.ServiceExtensionResponse _errorResponse(Object e, StackTrace stack) {
|
|
404
|
+
return developer.ServiceExtensionResponse.error(
|
|
405
|
+
developer.ServiceExtensionResponse.extensionError,
|
|
406
|
+
'$e\n$stack',
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ==================== ELEMENT FINDING ====================
|
|
411
|
+
|
|
412
|
+
static Element? _findElementByKey(String key) {
|
|
413
|
+
Element? found;
|
|
414
|
+
void visit(Element element) {
|
|
415
|
+
if (found != null) return;
|
|
416
|
+
final widget = element.widget;
|
|
417
|
+
if (widget.key is ValueKey<String> && (widget.key as ValueKey<String>).value == key) {
|
|
418
|
+
found = element;
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
element.visitChildren(visit);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
final binding = WidgetsBinding.instance;
|
|
425
|
+
// ignore: invalid_use_of_protected_member
|
|
426
|
+
if (binding.rootElement != null) {
|
|
427
|
+
visit(binding.rootElement!);
|
|
428
|
+
}
|
|
429
|
+
return found;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
static Element? _findElementByText(String text) {
|
|
433
|
+
Element? found;
|
|
434
|
+
void visit(Element element) {
|
|
435
|
+
if (found != null) return;
|
|
436
|
+
final widget = element.widget;
|
|
437
|
+
if (widget is Text && widget.data == text) {
|
|
438
|
+
found = element;
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (widget is RichText && widget.text.toPlainText() == text) {
|
|
442
|
+
found = element;
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
element.visitChildren(visit);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
final binding = WidgetsBinding.instance;
|
|
449
|
+
// ignore: invalid_use_of_protected_member
|
|
450
|
+
if (binding.rootElement != null) {
|
|
451
|
+
visit(binding.rootElement!);
|
|
452
|
+
}
|
|
453
|
+
return found;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
static Element? _findElement({String? key, String? text}) {
|
|
457
|
+
if (key != null) return _findElementByKey(key);
|
|
458
|
+
if (text != null) return _findElementByText(text);
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ==================== ACTIONS ====================
|
|
463
|
+
|
|
464
|
+
static Future<bool> _performTap({String? key, String? text}) async {
|
|
465
|
+
final element = _findElement(key: key, text: text);
|
|
466
|
+
if (element == null) {
|
|
467
|
+
_log('Element not found for tap (key: $key, text: $text)');
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
final renderObject = element.renderObject;
|
|
472
|
+
if (renderObject is! RenderBox || !renderObject.hasSize) {
|
|
473
|
+
_log('RenderObject is not a valid RenderBox');
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
final center = renderObject.localToGlobal(renderObject.size.center(Offset.zero));
|
|
478
|
+
_log('Tapping at $center (key: $key, text: $text)');
|
|
479
|
+
|
|
480
|
+
await _dispatchTap(center);
|
|
481
|
+
_log('Tap completed on (key: $key, text: $text)');
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
static Future<void> _dispatchTap(Offset position) async {
|
|
486
|
+
final binding = WidgetsBinding.instance;
|
|
487
|
+
final pointer = _pointerCounter++;
|
|
488
|
+
|
|
489
|
+
binding.handlePointerEvent(PointerDownEvent(position: position, pointer: pointer));
|
|
490
|
+
await Future.delayed(const Duration(milliseconds: 50));
|
|
491
|
+
binding.handlePointerEvent(PointerUpEvent(position: position, pointer: pointer));
|
|
492
|
+
await Future.delayed(const Duration(milliseconds: 100));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
static Future<bool> _performEnterText({String? key, required String text}) async {
|
|
496
|
+
final element = _findElement(key: key);
|
|
497
|
+
if (element == null) {
|
|
498
|
+
_log('TextField not found (key: $key)');
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
final renderObject = element.renderObject;
|
|
503
|
+
if (renderObject is! RenderBox) return false;
|
|
504
|
+
|
|
505
|
+
final center = renderObject.localToGlobal(renderObject.size.center(Offset.zero));
|
|
506
|
+
await _dispatchTap(center);
|
|
507
|
+
await Future.delayed(const Duration(milliseconds: 200));
|
|
508
|
+
|
|
509
|
+
EditableTextState? editableTextState;
|
|
510
|
+
void findEditable(Element e) {
|
|
511
|
+
if (editableTextState != null) return;
|
|
512
|
+
if (e is StatefulElement && e.state is EditableTextState) {
|
|
513
|
+
editableTextState = e.state as EditableTextState;
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
e.visitChildren(findEditable);
|
|
517
|
+
}
|
|
518
|
+
findEditable(element);
|
|
519
|
+
|
|
520
|
+
if (editableTextState != null) {
|
|
521
|
+
editableTextState!.updateEditingValue(TextEditingValue(
|
|
522
|
+
text: text,
|
|
523
|
+
selection: TextSelection.collapsed(offset: text.length),
|
|
524
|
+
));
|
|
525
|
+
_log('Entered text "$text" (key: $key)');
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
SystemChannels.textInput.invokeMethod('TextInput.setEditingState', {
|
|
530
|
+
'text': text,
|
|
531
|
+
'selectionBase': text.length,
|
|
532
|
+
'selectionExtent': text.length,
|
|
533
|
+
'composingBase': -1,
|
|
534
|
+
'composingExtent': -1,
|
|
535
|
+
});
|
|
536
|
+
_log('Text input sent via channel');
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
static Future<bool> _performScroll({String? key, String? text}) async {
|
|
541
|
+
final element = _findElement(key: key, text: text);
|
|
542
|
+
if (element == null) {
|
|
543
|
+
_log('Element not found for scroll (key: $key, text: $text)');
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
try {
|
|
548
|
+
await Scrollable.ensureVisible(element, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut);
|
|
549
|
+
_log('Scrolled to element (key: $key, text: $text)');
|
|
550
|
+
return true;
|
|
551
|
+
} catch (e) {
|
|
552
|
+
_log('Scroll failed: $e');
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
static Future<bool> _performLongPress({String? key, String? text, int duration = 500}) async {
|
|
558
|
+
final element = _findElement(key: key, text: text);
|
|
559
|
+
if (element == null) return false;
|
|
560
|
+
|
|
561
|
+
final renderObject = element.renderObject;
|
|
562
|
+
if (renderObject is! RenderBox || !renderObject.hasSize) return false;
|
|
563
|
+
|
|
564
|
+
final center = renderObject.localToGlobal(renderObject.size.center(Offset.zero));
|
|
565
|
+
final binding = WidgetsBinding.instance;
|
|
566
|
+
final pointer = _pointerCounter++;
|
|
567
|
+
|
|
568
|
+
binding.handlePointerEvent(PointerDownEvent(position: center, pointer: pointer));
|
|
569
|
+
await Future.delayed(Duration(milliseconds: duration));
|
|
570
|
+
binding.handlePointerEvent(PointerUpEvent(position: center, pointer: pointer));
|
|
571
|
+
await Future.delayed(const Duration(milliseconds: 100));
|
|
572
|
+
|
|
573
|
+
_log('Long press completed (key: $key, text: $text)');
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
static Future<bool> _performSwipe({required String direction, double distance = 300, String? key}) async {
|
|
578
|
+
final binding = WidgetsBinding.instance;
|
|
579
|
+
Offset start;
|
|
580
|
+
|
|
581
|
+
if (key != null) {
|
|
582
|
+
final element = _findElementByKey(key);
|
|
583
|
+
if (element == null) return false;
|
|
584
|
+
final renderObject = element.renderObject;
|
|
585
|
+
if (renderObject is! RenderBox) return false;
|
|
586
|
+
start = renderObject.localToGlobal(renderObject.size.center(Offset.zero));
|
|
587
|
+
} else {
|
|
588
|
+
// Use window size for global swipe
|
|
589
|
+
final window = binding.platformDispatcher.views.first;
|
|
590
|
+
final size = window.physicalSize / window.devicePixelRatio;
|
|
591
|
+
start = Offset(size.width / 2, size.height / 2);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
Offset delta;
|
|
595
|
+
switch (direction.toLowerCase()) {
|
|
596
|
+
case 'up':
|
|
597
|
+
delta = Offset(0, -distance);
|
|
598
|
+
break;
|
|
599
|
+
case 'down':
|
|
600
|
+
delta = Offset(0, distance);
|
|
601
|
+
break;
|
|
602
|
+
case 'left':
|
|
603
|
+
delta = Offset(-distance, 0);
|
|
604
|
+
break;
|
|
605
|
+
case 'right':
|
|
606
|
+
delta = Offset(distance, 0);
|
|
607
|
+
break;
|
|
608
|
+
default:
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
final pointer = _pointerCounter++;
|
|
613
|
+
final end = start + delta;
|
|
614
|
+
|
|
615
|
+
binding.handlePointerEvent(PointerDownEvent(position: start, pointer: pointer));
|
|
616
|
+
await Future.delayed(const Duration(milliseconds: 16));
|
|
617
|
+
|
|
618
|
+
const steps = 10;
|
|
619
|
+
for (int i = 1; i <= steps; i++) {
|
|
620
|
+
final current = Offset.lerp(start, end, i / steps)!;
|
|
621
|
+
binding.handlePointerEvent(PointerMoveEvent(position: current, pointer: pointer, delta: delta / steps.toDouble()));
|
|
622
|
+
await Future.delayed(const Duration(milliseconds: 16));
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
binding.handlePointerEvent(PointerUpEvent(position: end, pointer: pointer));
|
|
626
|
+
await Future.delayed(const Duration(milliseconds: 100));
|
|
627
|
+
|
|
628
|
+
_log('Swipe $direction completed');
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
static Future<bool> _performDrag({String? fromKey, String? toKey}) async {
|
|
633
|
+
if (fromKey == null || toKey == null) return false;
|
|
634
|
+
|
|
635
|
+
final fromElement = _findElementByKey(fromKey);
|
|
636
|
+
final toElement = _findElementByKey(toKey);
|
|
637
|
+
if (fromElement == null || toElement == null) return false;
|
|
638
|
+
|
|
639
|
+
final fromRender = fromElement.renderObject;
|
|
640
|
+
final toRender = toElement.renderObject;
|
|
641
|
+
if (fromRender is! RenderBox || toRender is! RenderBox) return false;
|
|
642
|
+
|
|
643
|
+
final start = fromRender.localToGlobal(fromRender.size.center(Offset.zero));
|
|
644
|
+
final end = toRender.localToGlobal(toRender.size.center(Offset.zero));
|
|
645
|
+
|
|
646
|
+
final binding = WidgetsBinding.instance;
|
|
647
|
+
final pointer = _pointerCounter++;
|
|
648
|
+
|
|
649
|
+
binding.handlePointerEvent(PointerDownEvent(position: start, pointer: pointer));
|
|
650
|
+
await Future.delayed(const Duration(milliseconds: 100));
|
|
651
|
+
|
|
652
|
+
const steps = 20;
|
|
653
|
+
for (int i = 1; i <= steps; i++) {
|
|
654
|
+
final current = Offset.lerp(start, end, i / steps)!;
|
|
655
|
+
binding.handlePointerEvent(PointerMoveEvent(position: current, pointer: pointer));
|
|
656
|
+
await Future.delayed(const Duration(milliseconds: 16));
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
binding.handlePointerEvent(PointerUpEvent(position: end, pointer: pointer));
|
|
660
|
+
await Future.delayed(const Duration(milliseconds: 100));
|
|
661
|
+
|
|
662
|
+
_log('Drag from $fromKey to $toKey completed');
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
static Future<bool> _performDoubleTap({String? key, String? text}) async {
|
|
667
|
+
final element = _findElement(key: key, text: text);
|
|
668
|
+
if (element == null) return false;
|
|
669
|
+
|
|
670
|
+
final renderObject = element.renderObject;
|
|
671
|
+
if (renderObject is! RenderBox || !renderObject.hasSize) return false;
|
|
672
|
+
|
|
673
|
+
final center = renderObject.localToGlobal(renderObject.size.center(Offset.zero));
|
|
674
|
+
|
|
675
|
+
await _dispatchTap(center);
|
|
676
|
+
await Future.delayed(const Duration(milliseconds: 50));
|
|
677
|
+
await _dispatchTap(center);
|
|
678
|
+
|
|
679
|
+
_log('Double tap completed (key: $key, text: $text)');
|
|
680
|
+
return true;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// ==================== UI INSPECTION ====================
|
|
684
|
+
|
|
685
|
+
static List<Map<String, dynamic>> _findInteractiveElements() {
|
|
686
|
+
final results = <Map<String, dynamic>>[];
|
|
687
|
+
|
|
688
|
+
void visit(Element element) {
|
|
689
|
+
final widget = element.widget;
|
|
690
|
+
String? type;
|
|
691
|
+
String? text;
|
|
692
|
+
String? key;
|
|
693
|
+
|
|
694
|
+
if (widget.key is ValueKey<String>) {
|
|
695
|
+
key = (widget.key as ValueKey<String>).value;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (widget is ElevatedButton || widget is TextButton || widget is OutlinedButton ||
|
|
699
|
+
widget is IconButton || widget is FloatingActionButton) {
|
|
700
|
+
type = 'Button';
|
|
701
|
+
text = _extractTextFrom(element);
|
|
702
|
+
} else if (widget is TextField || widget is TextFormField) {
|
|
703
|
+
type = 'TextField';
|
|
704
|
+
} else if (widget is Checkbox) {
|
|
705
|
+
type = 'Checkbox';
|
|
706
|
+
} else if (widget is Switch) {
|
|
707
|
+
type = 'Switch';
|
|
708
|
+
} else if (widget is Slider) {
|
|
709
|
+
type = 'Slider';
|
|
710
|
+
} else if (widget is DropdownButton) {
|
|
711
|
+
type = 'Dropdown';
|
|
712
|
+
} else if (widget is InkWell && widget.onTap != null) {
|
|
713
|
+
type = 'Tappable';
|
|
714
|
+
} else if (widget is GestureDetector && widget.onTap != null) {
|
|
715
|
+
type = 'Tappable';
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (type != null) {
|
|
719
|
+
results.add({
|
|
720
|
+
if (key != null) 'key': key,
|
|
721
|
+
if (text != null) 'text': text,
|
|
722
|
+
'type': type,
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
element.visitChildren(visit);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
final binding = WidgetsBinding.instance;
|
|
730
|
+
// ignore: invalid_use_of_protected_member
|
|
731
|
+
if (binding.rootElement != null) {
|
|
732
|
+
visit(binding.rootElement!);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
return results;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
static Map<String, dynamic> _getWidgetTree({int maxDepth = 10}) {
|
|
739
|
+
Map<String, dynamic> buildNode(Element element, int depth) {
|
|
740
|
+
final widget = element.widget;
|
|
741
|
+
final node = <String, dynamic>{
|
|
742
|
+
'type': widget.runtimeType.toString(),
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
if (widget.key is ValueKey<String>) {
|
|
746
|
+
node['key'] = (widget.key as ValueKey<String>).value;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (widget is Text) {
|
|
750
|
+
node['text'] = widget.data;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
final renderObject = element.renderObject;
|
|
754
|
+
if (renderObject is RenderBox && renderObject.hasSize) {
|
|
755
|
+
node['size'] = {'width': renderObject.size.width, 'height': renderObject.size.height};
|
|
756
|
+
final offset = renderObject.localToGlobal(Offset.zero);
|
|
757
|
+
node['position'] = {'x': offset.dx, 'y': offset.dy};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (depth < maxDepth) {
|
|
761
|
+
final children = <Map<String, dynamic>>[];
|
|
762
|
+
element.visitChildren((child) {
|
|
763
|
+
children.add(buildNode(child, depth + 1));
|
|
764
|
+
});
|
|
765
|
+
if (children.isNotEmpty) {
|
|
766
|
+
node['children'] = children;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return node;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
final binding = WidgetsBinding.instance;
|
|
774
|
+
// ignore: invalid_use_of_protected_member
|
|
775
|
+
if (binding.rootElement != null) {
|
|
776
|
+
return buildNode(binding.rootElement!, 0);
|
|
777
|
+
}
|
|
778
|
+
return {};
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
static Map<String, dynamic>? _getWidgetProperties(String key) {
|
|
782
|
+
final element = _findElementByKey(key);
|
|
783
|
+
if (element == null) return null;
|
|
784
|
+
|
|
785
|
+
final widget = element.widget;
|
|
786
|
+
final props = <String, dynamic>{
|
|
787
|
+
'type': widget.runtimeType.toString(),
|
|
788
|
+
'key': key,
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
final renderObject = element.renderObject;
|
|
792
|
+
if (renderObject is RenderBox && renderObject.hasSize) {
|
|
793
|
+
props['size'] = {'width': renderObject.size.width, 'height': renderObject.size.height};
|
|
794
|
+
final offset = renderObject.localToGlobal(Offset.zero);
|
|
795
|
+
props['position'] = {'x': offset.dx, 'y': offset.dy};
|
|
796
|
+
props['visible'] = renderObject.attached && renderObject.size.width > 0 && renderObject.size.height > 0;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (widget is Text) {
|
|
800
|
+
props['text'] = widget.data;
|
|
801
|
+
props['style'] = widget.style?.toString();
|
|
802
|
+
} else if (widget is Container) {
|
|
803
|
+
props['color'] = widget.color?.toString();
|
|
804
|
+
props['padding'] = widget.padding?.toString();
|
|
805
|
+
props['margin'] = widget.margin?.toString();
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
return props;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
static List<Map<String, dynamic>> _getTextContent() {
|
|
812
|
+
final results = <Map<String, dynamic>>[];
|
|
813
|
+
|
|
814
|
+
void visit(Element element) {
|
|
815
|
+
final widget = element.widget;
|
|
816
|
+
String? key;
|
|
817
|
+
if (widget.key is ValueKey<String>) {
|
|
818
|
+
key = (widget.key as ValueKey<String>).value;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (widget is Text && widget.data != null) {
|
|
822
|
+
results.add({
|
|
823
|
+
'text': widget.data,
|
|
824
|
+
if (key != null) 'key': key,
|
|
825
|
+
'type': 'Text',
|
|
826
|
+
});
|
|
827
|
+
} else if (widget is RichText) {
|
|
828
|
+
results.add({
|
|
829
|
+
'text': widget.text.toPlainText(),
|
|
830
|
+
if (key != null) 'key': key,
|
|
831
|
+
'type': 'RichText',
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
element.visitChildren(visit);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
final binding = WidgetsBinding.instance;
|
|
839
|
+
// ignore: invalid_use_of_protected_member
|
|
840
|
+
if (binding.rootElement != null) {
|
|
841
|
+
visit(binding.rootElement!);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
return results;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
static List<Map<String, dynamic>> _findByType(String typeName) {
|
|
848
|
+
final results = <Map<String, dynamic>>[];
|
|
849
|
+
|
|
850
|
+
void visit(Element element) {
|
|
851
|
+
final widget = element.widget;
|
|
852
|
+
final type = widget.runtimeType.toString();
|
|
853
|
+
|
|
854
|
+
if (type.toLowerCase().contains(typeName.toLowerCase())) {
|
|
855
|
+
String? key;
|
|
856
|
+
if (widget.key is ValueKey<String>) {
|
|
857
|
+
key = (widget.key as ValueKey<String>).value;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
final node = <String, dynamic>{'type': type};
|
|
861
|
+
if (key != null) node['key'] = key;
|
|
862
|
+
|
|
863
|
+
final renderObject = element.renderObject;
|
|
864
|
+
if (renderObject is RenderBox && renderObject.hasSize) {
|
|
865
|
+
final offset = renderObject.localToGlobal(Offset.zero);
|
|
866
|
+
node['position'] = {'x': offset.dx, 'y': offset.dy};
|
|
867
|
+
node['size'] = {'width': renderObject.size.width, 'height': renderObject.size.height};
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
results.add(node);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
element.visitChildren(visit);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
final binding = WidgetsBinding.instance;
|
|
877
|
+
// ignore: invalid_use_of_protected_member
|
|
878
|
+
if (binding.rootElement != null) {
|
|
879
|
+
visit(binding.rootElement!);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
return results;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// ==================== STATE & VALIDATION ====================
|
|
886
|
+
|
|
887
|
+
static String? _getTextFieldValue(String key) {
|
|
888
|
+
final element = _findElementByKey(key);
|
|
889
|
+
if (element == null) return null;
|
|
890
|
+
|
|
891
|
+
EditableTextState? editableTextState;
|
|
892
|
+
void findEditable(Element e) {
|
|
893
|
+
if (editableTextState != null) return;
|
|
894
|
+
if (e is StatefulElement && e.state is EditableTextState) {
|
|
895
|
+
editableTextState = e.state as EditableTextState;
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
e.visitChildren(findEditable);
|
|
899
|
+
}
|
|
900
|
+
findEditable(element);
|
|
901
|
+
|
|
902
|
+
return editableTextState?.textEditingValue.text;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
static bool? _getCheckboxState(String key) {
|
|
906
|
+
final element = _findElementByKey(key);
|
|
907
|
+
if (element == null) return null;
|
|
908
|
+
|
|
909
|
+
final widget = element.widget;
|
|
910
|
+
if (widget is Checkbox) {
|
|
911
|
+
return widget.value;
|
|
912
|
+
}
|
|
913
|
+
if (widget is Switch) {
|
|
914
|
+
return widget.value;
|
|
915
|
+
}
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
static double? _getSliderValue(String key) {
|
|
920
|
+
final element = _findElementByKey(key);
|
|
921
|
+
if (element == null) return null;
|
|
922
|
+
|
|
923
|
+
final widget = element.widget;
|
|
924
|
+
if (widget is Slider) {
|
|
925
|
+
return widget.value;
|
|
926
|
+
}
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
static Future<bool> _waitForElement({String? key, String? text, int timeout = 5000}) async {
|
|
931
|
+
final endTime = DateTime.now().add(Duration(milliseconds: timeout));
|
|
932
|
+
|
|
933
|
+
while (DateTime.now().isBefore(endTime)) {
|
|
934
|
+
final element = _findElement(key: key, text: text);
|
|
935
|
+
if (element != null) return true;
|
|
936
|
+
await Future.delayed(const Duration(milliseconds: 100));
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
static Future<bool> _waitForGone({String? key, String? text, int timeout = 5000}) async {
|
|
943
|
+
final endTime = DateTime.now().add(Duration(milliseconds: timeout));
|
|
944
|
+
|
|
945
|
+
while (DateTime.now().isBefore(endTime)) {
|
|
946
|
+
final element = _findElement(key: key, text: text);
|
|
947
|
+
if (element == null) return true;
|
|
948
|
+
await Future.delayed(const Duration(milliseconds: 100));
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// ==================== SCREENSHOT ====================
|
|
955
|
+
|
|
956
|
+
static Future<String?> _takeScreenshot() async {
|
|
957
|
+
try {
|
|
958
|
+
final binding = WidgetsBinding.instance;
|
|
959
|
+
// ignore: invalid_use_of_protected_member
|
|
960
|
+
final renderObject = binding.rootElement?.renderObject;
|
|
961
|
+
if (renderObject is! RenderRepaintBoundary) {
|
|
962
|
+
// Try to find a RenderRepaintBoundary
|
|
963
|
+
RenderRepaintBoundary? boundary;
|
|
964
|
+
void findBoundary(RenderObject obj) {
|
|
965
|
+
if (boundary != null) return;
|
|
966
|
+
if (obj is RenderRepaintBoundary) {
|
|
967
|
+
boundary = obj;
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
obj.visitChildren(findBoundary);
|
|
971
|
+
}
|
|
972
|
+
renderObject?.visitChildren(findBoundary);
|
|
973
|
+
|
|
974
|
+
if (boundary == null) {
|
|
975
|
+
_log('No RenderRepaintBoundary found');
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
final image = await boundary!.toImage(pixelRatio: 1.0);
|
|
980
|
+
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
|
981
|
+
if (byteData == null) return null;
|
|
982
|
+
return base64Encode(byteData.buffer.asUint8List());
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
final image = await renderObject.toImage(pixelRatio: 1.0);
|
|
986
|
+
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
|
987
|
+
if (byteData == null) return null;
|
|
988
|
+
return base64Encode(byteData.buffer.asUint8List());
|
|
989
|
+
} catch (e) {
|
|
990
|
+
_log('Screenshot failed: $e');
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
static Future<String?> _takeElementScreenshot(String key) async {
|
|
996
|
+
try {
|
|
997
|
+
final element = _findElementByKey(key);
|
|
998
|
+
if (element == null) {
|
|
999
|
+
_log('Element not found: $key');
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
final renderObject = element.renderObject;
|
|
1004
|
+
if (renderObject == null) {
|
|
1005
|
+
_log('No render object for element: $key');
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// If it's a RenderRepaintBoundary, capture directly
|
|
1010
|
+
if (renderObject is RenderRepaintBoundary) {
|
|
1011
|
+
final image = await renderObject.toImage(pixelRatio: 1.0);
|
|
1012
|
+
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
|
1013
|
+
if (byteData == null) return null;
|
|
1014
|
+
return base64Encode(byteData.buffer.asUint8List());
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// For other render objects, find the nearest RepaintBoundary ancestor
|
|
1018
|
+
RenderObject? current = renderObject;
|
|
1019
|
+
while (current != null) {
|
|
1020
|
+
if (current is RenderRepaintBoundary) {
|
|
1021
|
+
// Get the element's bounds relative to the boundary
|
|
1022
|
+
if (renderObject is RenderBox) {
|
|
1023
|
+
final box = renderObject;
|
|
1024
|
+
final boundaryBox = current;
|
|
1025
|
+
|
|
1026
|
+
// Get element position relative to boundary
|
|
1027
|
+
final offset = box.localToGlobal(Offset.zero, ancestor: boundaryBox);
|
|
1028
|
+
final size = box.size;
|
|
1029
|
+
|
|
1030
|
+
// Capture the boundary
|
|
1031
|
+
final fullImage = await current.toImage(pixelRatio: 1.0);
|
|
1032
|
+
|
|
1033
|
+
// Crop to element bounds
|
|
1034
|
+
final recorder = ui.PictureRecorder();
|
|
1035
|
+
final canvas = Canvas(recorder);
|
|
1036
|
+
|
|
1037
|
+
// Draw the cropped portion
|
|
1038
|
+
canvas.drawImageRect(
|
|
1039
|
+
fullImage,
|
|
1040
|
+
Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height),
|
|
1041
|
+
Rect.fromLTWH(0, 0, size.width, size.height),
|
|
1042
|
+
Paint(),
|
|
1043
|
+
);
|
|
1044
|
+
|
|
1045
|
+
final picture = recorder.endRecording();
|
|
1046
|
+
final croppedImage = await picture.toImage(size.width.toInt(), size.height.toInt());
|
|
1047
|
+
final byteData = await croppedImage.toByteData(format: ui.ImageByteFormat.png);
|
|
1048
|
+
if (byteData == null) return null;
|
|
1049
|
+
return base64Encode(byteData.buffer.asUint8List());
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
current = current.parent;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
_log('No suitable RenderRepaintBoundary ancestor found for: $key');
|
|
1056
|
+
return null;
|
|
1057
|
+
} catch (e) {
|
|
1058
|
+
_log('Element screenshot failed: $e');
|
|
1059
|
+
return null;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// ==================== NAVIGATION ====================
|
|
1064
|
+
|
|
1065
|
+
static String? _getCurrentRoute() {
|
|
1066
|
+
String? currentRoute;
|
|
1067
|
+
final binding = WidgetsBinding.instance;
|
|
1068
|
+
|
|
1069
|
+
void visit(Element element) {
|
|
1070
|
+
if (element.widget is ModalRoute) {
|
|
1071
|
+
final route = ModalRoute.of(element);
|
|
1072
|
+
if (route != null) {
|
|
1073
|
+
currentRoute = route.settings.name;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
element.visitChildren(visit);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// ignore: invalid_use_of_protected_member
|
|
1080
|
+
if (binding.rootElement != null) {
|
|
1081
|
+
visit(binding.rootElement!);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
return currentRoute;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
static bool _goBack() {
|
|
1088
|
+
final context = _findNavigatorContext();
|
|
1089
|
+
if (context == null) return false;
|
|
1090
|
+
|
|
1091
|
+
final navigator = Navigator.of(context, rootNavigator: false);
|
|
1092
|
+
if (navigator.canPop()) {
|
|
1093
|
+
navigator.pop();
|
|
1094
|
+
return true;
|
|
1095
|
+
}
|
|
1096
|
+
return false;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
static List<String> _getNavigationStack() {
|
|
1100
|
+
final routes = <String>[];
|
|
1101
|
+
final context = _findNavigatorContext();
|
|
1102
|
+
if (context == null) return routes;
|
|
1103
|
+
|
|
1104
|
+
// This is a simplified version - full implementation would need NavigatorState access
|
|
1105
|
+
final currentRoute = _getCurrentRoute();
|
|
1106
|
+
if (currentRoute != null) {
|
|
1107
|
+
routes.add(currentRoute);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
return routes;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
static BuildContext? _findNavigatorContext() {
|
|
1114
|
+
BuildContext? context;
|
|
1115
|
+
final binding = WidgetsBinding.instance;
|
|
1116
|
+
|
|
1117
|
+
void visit(Element element) {
|
|
1118
|
+
if (context != null) return;
|
|
1119
|
+
if (element.widget is Navigator) {
|
|
1120
|
+
context = element;
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
element.visitChildren(visit);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// ignore: invalid_use_of_protected_member
|
|
1127
|
+
if (binding.rootElement != null) {
|
|
1128
|
+
visit(binding.rootElement!);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
return context;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// ==================== DEBUG & LOGS ====================
|
|
1135
|
+
|
|
1136
|
+
static void _log(String message) {
|
|
1137
|
+
final logEntry = '[${DateTime.now().toIso8601String()}] $message';
|
|
1138
|
+
_logs.add(logEntry);
|
|
1139
|
+
print('Flutter Skill: $message');
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
static Map<String, dynamic> _getPerformanceMetrics() {
|
|
1143
|
+
return {
|
|
1144
|
+
'timestamp': DateTime.now().toIso8601String(),
|
|
1145
|
+
'logCount': _logs.length,
|
|
1146
|
+
'errorCount': _errors.length,
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// ==================== HELPERS ====================
|
|
1151
|
+
|
|
1152
|
+
static String? _extractTextFrom(Element element) {
|
|
1153
|
+
String? found;
|
|
1154
|
+
void visit(Element e) {
|
|
1155
|
+
if (found != null) return;
|
|
1156
|
+
if (e.widget is Text) {
|
|
1157
|
+
found = (e.widget as Text).data;
|
|
1158
|
+
} else if (e.widget is RichText) {
|
|
1159
|
+
found = (e.widget as RichText).text.toPlainText();
|
|
1160
|
+
}
|
|
1161
|
+
e.visitChildren(visit);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
visit(element);
|
|
1165
|
+
return found;
|
|
1166
|
+
}
|
|
1167
|
+
}
|