From a63e948dc0221ab0eddff43c0032ecd0197cf1ae Mon Sep 17 00:00:00 2001 From: GenericJam Date: Tue, 25 Aug 2026 15:53:13 -0600 Subject: [PATCH 1/2] MOB-99: honor the documented settle delay after iOS accessibility activation MobDev.Connector.connect_all/1 has called IOS.enable_accessibility/1 for every iOS simulator target for a while, but never actually waited the ~500ms CLAUDE.md's own iOS accessibility activation section says to wait for the notifyutil broadcast to propagate before SwiftUI's accessibility tree is reliably ready. In practice wait_for_nodes's polling usually eats more than that anyway, but a caller driving taps the instant nodes connect had no guarantee. Companion fix in mob (ios/mob_nif.m) retry-hardens the point-based accessibility lookup itself as defense in depth for whatever residual race this doesn't fully close. --- lib/mob_dev/connector.ex | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/mob_dev/connector.ex b/lib/mob_dev/connector.ex index a077120..e63457f 100644 --- a/lib/mob_dev/connector.ex +++ b/lib/mob_dev/connector.ex @@ -15,6 +15,9 @@ defmodule MobDev.Connector do @connect_timeout 25_000 # ms between polls @connect_interval 500 + # ms to let SwiftUI's accessibility tree rebuild after enable_accessibility's + # notifyutil broadcast — see MOB-99. + @ios_accessibility_settle_ms 500 @doc """ Discovers all connected devices, sets up tunnels, restarts apps, and waits @@ -63,10 +66,13 @@ defmodule MobDev.Connector do # Activate accessibility on iOS simulators so ui_tree() returns elements. # SwiftUI lazily populates its a11y tree; this one-time activation persists - # for the simulator session (survives app restarts). - tunneled - |> Enum.filter(&(&1.platform == :ios)) - |> Enum.each(fn d -> IOS.enable_accessibility(d.serial) end) + # for the simulator session (survives app restarts). MOB-99: give it a + # beat to propagate before any caller can start driving taps — the app + # itself is still booting (wait_for_nodes below) and SwiftUI needs a + # moment after the notifyutil broadcast to actually rebuild its tree. + ios_targets = Enum.filter(tunneled, &(&1.platform == :ios)) + Enum.each(ios_targets, fn d -> IOS.enable_accessibility(d.serial) end) + if ios_targets != [], do: Process.sleep(@ios_accessibility_settle_ms) # Wait for nodes to come online IO.puts("\n Waiting for nodes...") From e85ede4d0abae0f6b2577ff75a0bc63e92323f18 Mon Sep 17 00:00:00 2001 From: GenericJam Date: Tue, 25 Aug 2026 20:37:11 -0600 Subject: [PATCH 2/2] MOB-99 review fix: gate the accessibility settle sleep on simulator, not just iOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From code review on PR #46: Process.sleep(@ios_accessibility_settle_ms) was gated on platform == :ios alone, which also matches physical devices — IOS.enable_accessibility/1 shells out to `xcrun simctl spawn ...`, simulator-only tooling that's a no-op/error against a physical UDID. Every mix mob.connect against a physical iPhone paid the full 500ms tax for zero benefit. Now gated on the same platform == :ios && type == :simulator predicate kill_stale_simulator_apps/1 already uses for the identical reason. Not addressed (documented, not silently dropped): the fix is still a bare fixed-duration sleep, not a poll-until-ready check. A real readiness poll would need to run AFTER dist connects (there's no RPC path into the device yet at this point in connect_all/1), which is a bigger restructuring than this fix — flagging as a known limitation rather than attempting it here. Original PR description already disclosed this as "defense in depth for whatever residual race this doesn't fully close." --- lib/mob_dev/connector.ex | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/mob_dev/connector.ex b/lib/mob_dev/connector.ex index e63457f..e3b3498 100644 --- a/lib/mob_dev/connector.ex +++ b/lib/mob_dev/connector.ex @@ -70,9 +70,14 @@ defmodule MobDev.Connector do # beat to propagate before any caller can start driving taps — the app # itself is still booting (wait_for_nodes below) and SwiftUI needs a # moment after the notifyutil broadcast to actually rebuild its tree. - ios_targets = Enum.filter(tunneled, &(&1.platform == :ios)) - Enum.each(ios_targets, fn d -> IOS.enable_accessibility(d.serial) end) - if ios_targets != [], do: Process.sleep(@ios_accessibility_settle_ms) + # + # Simulator only, not just iOS: IOS.enable_accessibility/1 shells out to + # `xcrun simctl spawn ...`, which is simulator-only tooling — a + # no-op (or error) against a physical device's UDID. Same predicate + # kill_stale_simulator_apps/1 above already uses for the same reason. + ios_sim_targets = Enum.filter(tunneled, &(&1.platform == :ios && &1.type == :simulator)) + Enum.each(ios_sim_targets, fn d -> IOS.enable_accessibility(d.serial) end) + if ios_sim_targets != [], do: Process.sleep(@ios_accessibility_settle_ms) # Wait for nodes to come online IO.puts("\n Waiting for nodes...")