@@ -9,6 +9,8 @@ use std::time::Duration;
99
1010const RECOVERABLE_RESTART_EXIT_CODE : i32 = 75 ;
1111const RESTART_ON_CORE_SIMULATOR_MISMATCH_ENV : & str = "SIMDECK_RESTART_ON_CORE_SIMULATOR_MISMATCH" ;
12+ const ACCESSIBILITY_SNAPSHOT_MAX_ATTEMPTS : usize = 10 ;
13+ const ACCESSIBILITY_SNAPSHOT_RETRY_DELAY_MS : u64 = 100 ;
1214
1315static RECOVERABLE_RESTART_SCHEDULED : AtomicBool = AtomicBool :: new ( false ) ;
1416
@@ -289,17 +291,25 @@ impl NativeBridge {
289291 ) -> Result < serde_json:: Value , AppError > {
290292 let udid = CString :: new ( udid) . map_err ( |e| AppError :: bad_request ( e. to_string ( ) ) ) ?;
291293 let max_depth = max_depth. unwrap_or ( 80 ) . min ( 80 ) ;
292- let json = match native_accessibility_snapshot_json ( & udid, point, max_depth) {
293- Ok ( json) => json,
294- Err ( error) if is_core_simulator_service_mismatch ( & error. to_string ( ) ) => {
295- std:: thread:: sleep ( Duration :: from_millis ( 250 ) ) ;
296- native_accessibility_snapshot_json ( & udid, point, max_depth) ?
294+ for attempt in 1 ..=ACCESSIBILITY_SNAPSHOT_MAX_ATTEMPTS {
295+ let json = match native_accessibility_snapshot_json ( & udid, point, max_depth) {
296+ Ok ( json) => json,
297+ Err ( error) if is_core_simulator_service_mismatch ( & error. to_string ( ) ) => {
298+ std:: thread:: sleep ( Duration :: from_millis ( 250 ) ) ;
299+ native_accessibility_snapshot_json ( & udid, point, max_depth) ?
300+ }
301+ Err ( error) => return Err ( error) ,
302+ } ;
303+ let snapshot: serde_json:: Value =
304+ serde_json:: from_str ( & json) . map_err ( |e| AppError :: internal ( e. to_string ( ) ) ) ?;
305+ if !accessibility_snapshot_is_transient_empty ( & snapshot)
306+ || attempt == ACCESSIBILITY_SNAPSHOT_MAX_ATTEMPTS
307+ {
308+ return Ok ( snapshot) ;
297309 }
298- Err ( error) => return Err ( error) ,
299- } ;
300- let snapshot: serde_json:: Value =
301- serde_json:: from_str ( & json) . map_err ( |e| AppError :: internal ( e. to_string ( ) ) ) ?;
302- Ok ( snapshot)
310+ std:: thread:: sleep ( Duration :: from_millis ( ACCESSIBILITY_SNAPSHOT_RETRY_DELAY_MS ) ) ;
311+ }
312+ unreachable ! ( "accessibility snapshot retry loop always returns" )
303313 }
304314
305315 pub fn send_touch ( & self , udid : & str , x : f64 , y : f64 , phase : & str ) -> Result < ( ) , AppError > {
@@ -769,6 +779,40 @@ fn is_core_simulator_service_mismatch(message: &str) -> bool {
769779 && message. contains ( "does not match expected service version" )
770780}
771781
782+ fn accessibility_snapshot_is_transient_empty ( snapshot : & serde_json:: Value ) -> bool {
783+ let Some ( roots) = snapshot. get ( "roots" ) . and_then ( serde_json:: Value :: as_array) else {
784+ return true ;
785+ } ;
786+ roots. is_empty ( ) || roots. iter ( ) . all ( |root| node_is_zero_sized_leaf ( root) )
787+ }
788+
789+ fn node_is_zero_sized_leaf ( node : & serde_json:: Value ) -> bool {
790+ let has_children = node
791+ . get ( "children" )
792+ . and_then ( serde_json:: Value :: as_array)
793+ . is_some_and ( |children| !children. is_empty ( ) ) ;
794+ !has_children && node_frame_is_empty ( node)
795+ }
796+
797+ fn node_frame_is_empty ( node : & serde_json:: Value ) -> bool {
798+ let Some ( frame) = node
799+ . get ( "frame" )
800+ . or_else ( || node. get ( "frameInScreen" ) )
801+ . or_else ( || node. get ( "bounds" ) )
802+ else {
803+ return true ;
804+ } ;
805+ let width = frame
806+ . get ( "width" )
807+ . and_then ( serde_json:: Value :: as_f64)
808+ . unwrap_or ( 0.0 ) ;
809+ let height = frame
810+ . get ( "height" )
811+ . and_then ( serde_json:: Value :: as_f64)
812+ . unwrap_or ( 0.0 ) ;
813+ width <= 0.0 || height <= 0.0
814+ }
815+
772816unsafe fn string_from_raw ( raw : * mut i8 , error : * mut i8 ) -> Result < String , AppError > {
773817 if raw. is_null ( ) {
774818 return Err ( take_error ( error) . unwrap_or_else ( || AppError :: native ( "Unknown native error." ) ) ) ;
@@ -814,7 +858,8 @@ fn schedule_recoverable_restart_if_needed(message: &str) {
814858#[ cfg( test) ]
815859mod tests {
816860 use super :: {
817- is_core_simulator_service_mismatch, log_entry_matches, LogEntry , LogFilters , Simulator ,
861+ accessibility_snapshot_is_transient_empty, is_core_simulator_service_mismatch,
862+ log_entry_matches, LogEntry , LogFilters , Simulator ,
818863 } ;
819864 use serde_json:: json;
820865
@@ -903,4 +948,44 @@ mod tests {
903948 "Unable to initialize the private simulator display bridge."
904949 ) ) ;
905950 }
951+
952+ #[ test]
953+ fn accessibility_snapshot_retry_detects_empty_native_ax_tree ( ) {
954+ assert ! ( accessibility_snapshot_is_transient_empty( & json!( {
955+ "source" : "native-ax" ,
956+ "roots" : [ ]
957+ } ) ) ) ;
958+ assert ! ( accessibility_snapshot_is_transient_empty( & json!( {
959+ "source" : "native-ax" ,
960+ "roots" : [ {
961+ "role" : "Application" ,
962+ "frame" : { "x" : 0 , "y" : 0 , "width" : 0 , "height" : 0 } ,
963+ "children" : [ ]
964+ } ]
965+ } ) ) ) ;
966+ }
967+
968+ #[ test]
969+ fn accessibility_snapshot_retry_keeps_usable_native_ax_tree ( ) {
970+ assert ! ( !accessibility_snapshot_is_transient_empty( & json!( {
971+ "source" : "native-ax" ,
972+ "roots" : [ {
973+ "role" : "Application" ,
974+ "frame" : { "x" : 0 , "y" : 0 , "width" : 393 , "height" : 852 } ,
975+ "children" : [ ]
976+ } ]
977+ } ) ) ) ;
978+ assert ! ( !accessibility_snapshot_is_transient_empty( & json!( {
979+ "source" : "native-ax" ,
980+ "roots" : [ {
981+ "role" : "Application" ,
982+ "frame" : { "x" : 0 , "y" : 0 , "width" : 0 , "height" : 0 } ,
983+ "children" : [ {
984+ "role" : "Button" ,
985+ "label" : "Continue" ,
986+ "frame" : { "x" : 10 , "y" : 20 , "width" : 100 , "height" : 44 }
987+ } ]
988+ } ]
989+ } ) ) ) ;
990+ }
906991}
0 commit comments