@@ -1920,6 +1920,31 @@ Detailed diagnostic report with:
19201920 },
19211921 },
19221922 },
1923+ {
1924+ "name" : "assert_batch" ,
1925+ "description" : "Run multiple assertions in a single call. Returns all results (does not fail-fast)." ,
1926+ "inputSchema" : {
1927+ "type" : "object" ,
1928+ "properties" : {
1929+ "assertions" : {
1930+ "type" : "array" ,
1931+ "description" : "List of assertions to run" ,
1932+ "items" : {
1933+ "type" : "object" ,
1934+ "properties" : {
1935+ "type" : {"type" : "string" , "enum" : ["visible" , "not_visible" , "text" , "element_count" ], "description" : "Assertion type" },
1936+ "key" : {"type" : "string" , "description" : "Element key" },
1937+ "text" : {"type" : "string" , "description" : "Text to find (for visible/not_visible) or expected text (for text assertion)" },
1938+ "expected" : {"type" : "string" , "description" : "Expected value for text assertion" },
1939+ "count" : {"type" : "integer" , "description" : "Expected count for element_count assertion" },
1940+ },
1941+ "required" : ["type" ],
1942+ },
1943+ },
1944+ },
1945+ "required" : ["assertions" ],
1946+ },
1947+ },
19231948
19241949 // === NEW: Page State ===
19251950 {
@@ -2137,7 +2162,78 @@ Detailed diagnostic report with:
21372162 return 'session_${DateTime .now ().millisecondsSinceEpoch }' ;
21382163 }
21392164
2165+ /// Check if an error is retryable (transient connection/timeout issues)
2166+ bool _isRetryableError (dynamic error) {
2167+ final msg = error.toString ().toLowerCase ();
2168+ // NOT retryable
2169+ if (msg.contains ('unknown tool' )) return false ;
2170+ if (msg.contains ('required' ) && msg.contains ('parameter' )) return false ;
2171+ if (msg.contains ('element not found' )) return false ;
2172+ if (msg.contains ('is required' )) return false ;
2173+ // Retryable
2174+ if (msg.contains ('websocket' )) return true ;
2175+ if (msg.contains ('connection closed' )) return true ;
2176+ if (msg.contains ('connection reset' )) return true ;
2177+ if (msg.contains ('not connected' )) return true ;
2178+ if (msg.contains ('connection lost' )) return true ;
2179+ if (msg.contains ('timed out' ) || msg.contains ('timeout' )) return true ;
2180+ if (msg.contains ('socket' ) && (msg.contains ('closed' ) || msg.contains ('error' ))) return true ;
2181+ return false ;
2182+ }
2183+
2184+ /// Attempt auto-reconnect using last known connection info
2185+ Future <bool > _attemptAutoReconnect () async {
2186+ if (_lastConnectionUri != null ) {
2187+ stderr.writeln ('Attempting auto-reconnect to $_lastConnectionUri (port: $_lastConnectionPort )...' );
2188+ try {
2189+ final client = _clients[_activeSessionId];
2190+ if (client is BridgeDriver ) {
2191+ await client.connect ();
2192+ stderr.writeln ('Auto-reconnect successful' );
2193+ return true ;
2194+ }
2195+ } catch (e) {
2196+ stderr.writeln ('Auto-reconnect failed: $e ' );
2197+ }
2198+ }
2199+ if (_cdpDriver != null && ! _cdpDriver! .isConnected) {
2200+ stderr.writeln ('CDP connection lost, attempting reconnect...' );
2201+ try {
2202+ await _cdpDriver! .connect ();
2203+ stderr.writeln ('CDP auto-reconnect successful' );
2204+ return true ;
2205+ } catch (e) {
2206+ stderr.writeln ('CDP auto-reconnect failed: $e ' );
2207+ }
2208+ }
2209+ return false ;
2210+ }
2211+
21402212 Future <dynamic > _executeTool (String name, Map <String , dynamic > args) async {
2213+ const maxRetries = 2 ;
2214+ for (int attempt = 0 ; attempt <= maxRetries; attempt++ ) {
2215+ try {
2216+ final result = await _executeToolInner (name, args);
2217+ return result;
2218+ } catch (e) {
2219+ if (attempt < maxRetries && _isRetryableError (e)) {
2220+ stderr.writeln ('Retryable error on attempt ${attempt + 1 }: $e ' );
2221+ // Try auto-reconnect on connection errors
2222+ final msg = e.toString ().toLowerCase ();
2223+ if (msg.contains ('not connected' ) || msg.contains ('connection lost' ) || msg.contains ('connection closed' )) {
2224+ await _attemptAutoReconnect ();
2225+ }
2226+ await Future .delayed (Duration (milliseconds: 500 * (attempt + 1 )));
2227+ continue ;
2228+ }
2229+ rethrow ;
2230+ }
2231+ }
2232+ // Unreachable, but satisfies analyzer
2233+ throw StateError ('Retry loop exited unexpectedly' );
2234+ }
2235+
2236+ Future <dynamic > _executeToolInner (String name, Map <String , dynamic > args) async {
21412237 // Session management tools
21422238 if (name == 'list_sessions' ) {
21432239 return {
@@ -2268,6 +2364,10 @@ Detailed diagnostic report with:
22682364 // Always switch to the newly created session
22692365 _activeSessionId = sessionId;
22702366
2367+ // Store for auto-reconnect
2368+ _lastConnectionUri = uri;
2369+ _lastConnectionPort = int .tryParse (uri.split (':' ).last.split ('/' ).first);
2370+
22712371 return {
22722372 "success" : true ,
22732373 "message" : "Connected to $uri " ,
@@ -4208,6 +4308,44 @@ Detailed diagnostic report with:
42084308 final fc = _asFlutterClient (client! , 'scroll_until_visible' );
42094309 return await _scrollUntilVisible (args, fc);
42104310
4311+ // === Batch Assertions ===
4312+ case 'assert_batch' :
4313+ final assertions = (args['assertions' ] as List <dynamic >? ) ?? [];
4314+ final results = < Map <String , dynamic >> [];
4315+ int passed = 0 ;
4316+ int failed = 0 ;
4317+ for (final assertion in assertions) {
4318+ final a = assertion as Map <String , dynamic >;
4319+ final aType = a['type' ] as String ;
4320+ try {
4321+ final toolName = aType == 'visible' ? 'assert_visible'
4322+ : aType == 'not_visible' ? 'assert_not_visible'
4323+ : aType == 'text' ? 'assert_text'
4324+ : aType == 'element_count' ? 'assert_element_count'
4325+ : aType;
4326+ final toolArgs = < String , dynamic > {
4327+ if (a['key' ] != null ) 'key' : a['key' ],
4328+ if (a['text' ] != null ) 'text' : a['text' ],
4329+ if (a['expected' ] != null ) 'expected' : a['expected' ],
4330+ if (a['count' ] != null ) 'expected_count' : a['count' ],
4331+ };
4332+ final result = await _executeToolInner (toolName, toolArgs);
4333+ final success = result is Map && result['success' ] == true ;
4334+ if (success) passed++ ; else failed++ ;
4335+ results.add ({'type' : aType, 'success' : success, 'result' : result});
4336+ } catch (e) {
4337+ failed++ ;
4338+ results.add ({'type' : aType, 'success' : false , 'error' : e.toString ()});
4339+ }
4340+ }
4341+ return {
4342+ 'success' : failed == 0 ,
4343+ 'total' : assertions.length,
4344+ 'passed' : passed,
4345+ 'failed' : failed,
4346+ 'results' : results,
4347+ };
4348+
42114349 // === NEW: Assertions ===
42124350 case 'assert_visible' :
42134351 if (client is BridgeDriver ) {
0 commit comments