From a4b0c60a3ec634a3a41ed30dc8a52f9ba0d47076 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Tue, 30 Jun 2026 09:35:32 +0200 Subject: [PATCH] Run AXe against an Xcode that ships SimulatorKit AXe dlopens the private SimulatorKit.framework from $DEVELOPER_DIR/Library/PrivateFrameworks. Xcode 27 relocated that framework to Contents/SharedFrameworks, so under a selected Xcode 27 every axe command fails with "Failed to load essential private frameworks" and all UI assertions fail uniformly. Detect when the active Xcode lacks SimulatorKit at the path AXe expects and inject DEVELOPER_DIR pointing at the first installed Xcode that has it, for every axe subprocess. No global xcode-select switch required. Claude-Session: https://claude.ai/code/session_01Aqq5bSGrYfjQNvkTEzWcva --- spec/ui/support/axe.rb | 46 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/spec/ui/support/axe.rb b/spec/ui/support/axe.rb index cef5d3d7..e16d2973 100644 --- a/spec/ui/support/axe.rb +++ b/spec/ui/support/axe.rb @@ -155,7 +155,7 @@ module UITest # Type text # @param text [String] Text to type def type(text) - output, status = Open3.capture2e('axe', 'type', '--stdin', '--udid', @udid, stdin_data: text) + output, status = Open3.capture2e(self.class.axe_env, 'axe', 'type', '--stdin', '--udid', @udid, stdin_data: text) output = output.dup.force_encoding('UTF-8') if output.is_a?(String) raise AxeError, "type failed: #{output}" unless status.success? end @@ -202,11 +202,53 @@ module UITest private def run_axe(*) - output, status = Open3.capture2e('axe', *, '--udid', @udid) + output, status = Open3.capture2e(self.class.axe_env, 'axe', *, '--udid', @udid) output = output.dup.force_encoding('UTF-8') if output.is_a?(String) [output, status] end + class << self + # Environment overrides for every `axe` invocation. + # + # AXe loads Xcode's private SimulatorKit.framework at runtime. Some Xcode + # betas ship without it (e.g. Xcode 27 beta), which makes every `axe` + # command fail with "Failed to load essential private frameworks". When + # the active Xcode is missing SimulatorKit, fall back to any installed + # Xcode that has it by overriding DEVELOPER_DIR for the axe subprocess. + # @return [Hash] Environment hash to pass to Open3 + def axe_env + return @axe_env if defined?(@axe_env) + + @axe_env = simulatorkit?(active_developer_dir) ? {} : fallback_env + end + + private + + def fallback_env + dir = usable_developer_dir + dir ? { 'DEVELOPER_DIR' => dir } : {} + end + + # Path to the active Xcode's Developer directory (xcode-select -p) + def active_developer_dir + @active_developer_dir ||= `xcode-select -p 2>/dev/null`.strip + end + + # First installed Xcode Developer directory that ships SimulatorKit + def usable_developer_dir + Dir.glob('/Applications/Xcode*.app/Contents/Developer').find do |dir| + simulatorkit?(dir) + end + end + + # Whether a Developer directory ships the SimulatorKit private framework + def simulatorkit?(developer_dir) + return false if developer_dir.nil? || developer_dir.empty? + + File.exist?(File.join(developer_dir, 'Library', 'PrivateFrameworks', 'SimulatorKit.framework')) + end + end + # Wait for a file to exist and have non-zero size # Helps avoid race conditions where screenshot isn't fully written # @param path [String] Path to the file