Skip to content

Commit e4df6c4

Browse files
author
GitLab CI
committed
feat: highlight_element, mock_response, download_file, cancel_operation, highlight_elements
1 parent 14fbbad commit e4df6c4

File tree

2 files changed

+112
-0
lines changed

2 files changed

+112
-0
lines changed

lib/src/bridge/cdp_driver.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1983,6 +1983,33 @@ function _dqAll(sel, root) {
19831983
}
19841984
}
19851985

1986+
/// Highlight an element with a colored overlay.
1987+
Future<Map<String, dynamic>> highlightElement(String selector, {String? color, int duration = 3000}) async {
1988+
final c = color ?? 'red';
1989+
final js = '''
1990+
(function() {
1991+
var el = document.querySelector('$selector') || document.getElementById('$selector') || document.querySelector('[data-testid="$selector"]');
1992+
if (!el) return JSON.stringify({success: false, error: 'Element not found'});
1993+
var rect = el.getBoundingClientRect();
1994+
var overlay = document.createElement('div');
1995+
overlay.id = '__fs_highlight_' + Date.now();
1996+
overlay.style.cssText = 'position:fixed;top:'+rect.top+'px;left:'+rect.left+'px;width:'+rect.width+'px;height:'+rect.height+'px;border:3px solid $c;background:${c}33;z-index:2147483647;pointer-events:none;transition:opacity 0.3s;';
1997+
document.body.appendChild(overlay);
1998+
setTimeout(function(){ overlay.style.opacity='0'; setTimeout(function(){ overlay.remove(); }, 300); }, $duration);
1999+
return JSON.stringify({success: true, selector: '$selector', color: '$c', duration: $duration, bounds: {x: rect.x, y: rect.y, w: rect.width, h: rect.height}});
2000+
})()
2001+
''';
2002+
final result = await _evalJs(js);
2003+
final v = result['result']?['value'] as String?;
2004+
if (v != null) return jsonDecode(v) as Map<String, dynamic>;
2005+
return {'success': false, 'error': 'Eval failed'};
2006+
}
2007+
2008+
/// Mock a network response for requests matching a URL pattern.
2009+
Future<Map<String, dynamic>> mockResponse(String urlPattern, int statusCode, String body, {Map<String, String>? headers}) async {
2010+
return await interceptRequests(urlPattern, statusCode: statusCode, body: body, headers: headers);
2011+
}
2012+
19862013
void _onDisconnect() {
19872014
_connected = false;
19882015
_failAllPending('Connection lost');

lib/src/cli/server.dart

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,9 @@ class FlutterMcpServer {
168168
String _pluginsDir = '${Platform.environment['HOME'] ?? '.'}/.flutter-skill/plugins';
169169
final List<Map<String, dynamic>> _pluginTools = [];
170170

171+
// Cancellable operations
172+
final Map<String, Completer<void>> _activeCancellables = {};
173+
171174
// Last known connection info for auto-reconnect
172175
String? _lastConnectionUri;
173176
int? _lastConnectionPort;
@@ -378,6 +381,7 @@ class FlutterMcpServer {
378381
'block_urls', 'throttle_network', 'go_offline', 'clear_browser_data',
379382
'accessibility_audit', 'set_geolocation', 'set_timezone',
380383
'set_color_scheme', 'upload_file', 'compare_screenshot',
384+
'highlight_element', 'mock_response', 'highlight_elements',
381385
};
382386

383387
// Flutter VM Service-only tools
@@ -838,6 +842,11 @@ After starting, point the web SDK at ws://127.0.0.1:<port>.""",
838842
{"name": "get_session_storage", "description": "Get all sessionStorage key-value pairs", "inputSchema": {"type": "object", "properties": {}}},
839843
{"name": "type_text", "description": "Type text character by character (realistic typing simulation)", "inputSchema": {"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}},
840844
{"name": "set_timezone", "description": "Override browser timezone", "inputSchema": {"type": "object", "properties": {"timezone": {"type": "string", "description": "IANA timezone (e.g. America/New_York)"}}, "required": ["timezone"]}},
845+
{"name": "highlight_element", "description": "Highlight an element with a colored overlay for visual debugging. Injects a temporary colored border+background on the matched element.", "inputSchema": {"type": "object", "properties": {"key": {"type": "string", "description": "CSS selector, element ID, or data-testid"}, "ref": {"type": "string", "description": "Element ref from snapshot"}, "color": {"type": "string", "description": "Highlight color (default: red)", "default": "red"}, "duration_ms": {"type": "integer", "description": "How long to show highlight in ms (default: 3000)", "default": 3000}}, "required": ["key"]}},
846+
{"name": "mock_response", "description": "Mock/intercept network responses for a URL pattern. Returns custom status code and body for matching requests.", "inputSchema": {"type": "object", "properties": {"url_pattern": {"type": "string", "description": "URL pattern to match (glob)"}, "status_code": {"type": "integer", "description": "HTTP status code to return"}, "body": {"type": "string", "description": "Response body to return"}, "headers": {"type": "object", "description": "Response headers"}}, "required": ["url_pattern", "status_code", "body"]}},
847+
{"name": "download_file", "description": "Download a file from a URL and save it to disk.", "inputSchema": {"type": "object", "properties": {"url": {"type": "string", "description": "URL to download"}, "save_path": {"type": "string", "description": "Local file path to save to"}}, "required": ["url", "save_path"]}},
848+
{"name": "cancel_operation", "description": "Cancel a running long operation (wait_for_element, wait_for_gone, wait_for_network_idle) by operation ID.", "inputSchema": {"type": "object", "properties": {"operation_id": {"type": "string", "description": "ID of the operation to cancel"}}, "required": ["operation_id"]}},
849+
{"name": "highlight_elements", "description": "Toggle colored outlines on ALL interactive elements (like Playwright's inspector). Useful for visual debugging and test development.", "inputSchema": {"type": "object", "properties": {"show": {"type": "boolean", "description": "true to show highlights, false to remove them", "default": true}}}},
841850

842851
// Basic Inspection
843852
{
@@ -2737,6 +2746,45 @@ function toggleImg(el) { el.classList.toggle('expanded'); }
27372746
}
27382747

27392748
Future<dynamic> _executeToolInner(String name, Map<String, dynamic> args) async {
2749+
// Download file tool (platform-independent)
2750+
if (name == 'download_file') {
2751+
final url = args['url'] as String?;
2752+
final savePath = args['save_path'] as String?;
2753+
if (url == null || savePath == null) {
2754+
return {'success': false, 'error': 'url and save_path are required'};
2755+
}
2756+
try {
2757+
final client = http.Client();
2758+
try {
2759+
final response = await client.get(Uri.parse(url));
2760+
if (response.statusCode >= 200 && response.statusCode < 300) {
2761+
final file = File(savePath);
2762+
await file.parent.create(recursive: true);
2763+
await file.writeAsBytes(response.bodyBytes);
2764+
return {'success': true, 'path': savePath, 'size_bytes': response.bodyBytes.length, 'status_code': response.statusCode};
2765+
} else {
2766+
return {'success': false, 'error': 'HTTP ${response.statusCode}', 'status_code': response.statusCode};
2767+
}
2768+
} finally {
2769+
client.close();
2770+
}
2771+
} catch (e) {
2772+
return {'success': false, 'error': e.toString()};
2773+
}
2774+
}
2775+
2776+
// Cancel operation tool
2777+
if (name == 'cancel_operation') {
2778+
final opId = args['operation_id'] as String?;
2779+
if (opId == null) return {'success': false, 'error': 'operation_id is required'};
2780+
final completer = _activeCancellables.remove(opId);
2781+
if (completer != null && !completer.isCompleted) {
2782+
completer.complete();
2783+
return {'success': true, 'cancelled': opId};
2784+
}
2785+
return {'success': false, 'error': 'Operation not found or already completed', 'active_operations': _activeCancellables.keys.toList()};
2786+
}
2787+
27402788
// Session management tools
27412789
if (name == 'list_sessions') {
27422790
return {
@@ -5801,6 +5849,43 @@ function toggleImg(el) { el.classList.toggle('expanded'); }
58015849
timeoutMs: (args['timeout_ms'] as num?)?.toInt() ?? 30000,
58025850
);
58035851

5852+
case 'highlight_element':
5853+
final selector = args['key'] as String? ?? args['ref'] as String? ?? '';
5854+
final color = args['color'] as String? ?? 'red';
5855+
final duration = (args['duration_ms'] as num?)?.toInt() ?? 3000;
5856+
return await cdp.highlightElement(selector, color: color, duration: duration);
5857+
5858+
case 'mock_response':
5859+
final urlPattern = args['url_pattern'] as String? ?? '*';
5860+
final statusCode = (args['status_code'] as num?)?.toInt() ?? 200;
5861+
final body = args['body'] as String? ?? '';
5862+
final headers = (args['headers'] as Map<String, dynamic>?)?.cast<String, String>();
5863+
return await cdp.mockResponse(urlPattern, statusCode, body, headers: headers);
5864+
5865+
case 'highlight_elements':
5866+
final show = args['show'] ?? true;
5867+
if (show) {
5868+
final js = '''
5869+
(function() {
5870+
if (window.__fsHighlightStyle) return JSON.stringify({success: true, message: 'Already active'});
5871+
var style = document.createElement('style');
5872+
style.id = '__fs_highlight_style';
5873+
style.textContent = 'a,button,input,select,textarea,[role="button"],[role="link"],[role="tab"],[onclick],[tabindex] { outline: 2px solid rgba(255,0,128,0.7) !important; outline-offset: 2px !important; } a:hover,button:hover,input:hover,select:hover,textarea:hover,[role="button"]:hover { outline-color: rgba(0,128,255,0.9) !important; }';
5874+
document.head.appendChild(style);
5875+
window.__fsHighlightStyle = style;
5876+
var count = document.querySelectorAll('a,button,input,select,textarea,[role="button"],[role="link"],[role="tab"],[onclick],[tabindex]').length;
5877+
return JSON.stringify({success: true, highlighted: count});
5878+
})()
5879+
''';
5880+
final result = await cdp.eval(js);
5881+
final v = result['result']?['value'] as String?;
5882+
if (v != null) return jsonDecode(v);
5883+
return {'success': true};
5884+
} else {
5885+
await cdp.eval("if(window.__fsHighlightStyle){window.__fsHighlightStyle.remove();delete window.__fsHighlightStyle;}");
5886+
return {'success': true, 'message': 'Highlights removed'};
5887+
}
5888+
58045889
default:
58055890
throw Exception('Tool "$name" is not supported in CDP mode.');
58065891
}

0 commit comments

Comments
 (0)