Yattee v2 rewrite

This commit is contained in:
Arkadiusz Fal
2026-02-08 18:31:16 +01:00
parent 20d0cfc0c7
commit 05f921d605
1043 changed files with 163875 additions and 68430 deletions

210
spec/ui/support/axe.rb Normal file
View File

@@ -0,0 +1,210 @@
# frozen_string_literal: true
require 'open3'
require 'json'
require 'fileutils'
module UITest
# Wrapper for AXe CLI tool for iOS Simulator automation
class Axe
class AxeError < StandardError; end
attr_reader :udid
def initialize(udid)
@udid = udid
end
# Get the full accessibility UI tree as parsed JSON
# @return [Hash] Parsed accessibility tree
def describe_ui
output, status = run_axe('describe-ui')
raise AxeError, "describe-ui failed: #{output}" unless status.success?
JSON.parse(output)
rescue JSON::ParserError => e
raise AxeError, "Failed to parse accessibility tree: #{e.message}"
end
# Check if an element with the given accessibility identifier exists
# @param identifier [String] Accessibility identifier to find
# @return [Boolean] true if element exists
def element_exists?(identifier)
tree = describe_ui
find_element_in_tree(tree, identifier: identifier).present?
rescue AxeError
false
end
# Find an element by accessibility identifier
# @param identifier [String] Accessibility identifier
# @return [Hash, nil] Element data or nil if not found
def find_element(identifier)
tree = describe_ui
find_element_in_tree(tree, identifier: identifier)
end
# Check if text is visible anywhere in the accessibility tree
# @param text [String] Text to search for
# @return [Boolean] true if text is visible
def text_visible?(text)
tree = describe_ui
find_element_in_tree(tree, label: text).present?
rescue AxeError
false
end
# Tap on an element by accessibility identifier
# @param identifier [String] Accessibility identifier
def tap_id(identifier)
output, status = run_axe('tap', '--id', identifier)
raise AxeError, "tap failed: #{output}" unless status.success?
end
# Tap on an element by accessibility label
# @param label [String] Accessibility label
def tap_label(label)
output, status = run_axe('tap', '--label', label)
raise AxeError, "tap failed: #{output}" unless status.success?
end
# Tap at specific coordinates
# @param x [Integer] X coordinate
# @param y [Integer] Y coordinate
def tap_coordinates(x:, y:)
output, status = run_axe('tap', '-x', x.to_s, '-y', y.to_s)
raise AxeError, "tap failed: #{output}" unless status.success?
end
# Perform a swipe gesture
# @param start_x [Integer] Starting X coordinate
# @param start_y [Integer] Starting Y coordinate
# @param end_x [Integer] Ending X coordinate
# @param end_y [Integer] Ending Y coordinate
# @param duration [Float] Duration in seconds (optional)
def swipe(start_x:, start_y:, end_x:, end_y:, duration: nil)
args = ['swipe', '--start-x', start_x.to_s, '--start-y', start_y.to_s,
'--end-x', end_x.to_s, '--end-y', end_y.to_s]
args += ['--duration', duration.to_s] if duration
output, status = run_axe(*args)
raise AxeError, "swipe failed: #{output}" unless status.success?
end
# Perform a preset gesture
# @param preset [String] Gesture preset (scroll-up, scroll-down, etc.)
def gesture(preset)
output, status = run_axe('gesture', preset)
raise AxeError, "gesture failed: #{output}" unless status.success?
end
# Type text
# @param text [String] Text to type
def type(text)
output, status = Open3.capture2e('axe', 'type', '--stdin', '--udid', @udid, stdin_data: text)
raise AxeError, "type failed: #{output}" unless status.success?
end
# Press the home button
def home_button
output, status = run_axe('button', 'home')
raise AxeError, "home button failed: #{output}" unless status.success?
end
# Press a key by keycode
# @param keycode [Integer] HID keycode (e.g., 40 for Return/Enter)
def press_key(keycode)
output, status = run_axe('key', keycode.to_s)
raise AxeError, "key press failed: #{output}" unless status.success?
end
# Press Return/Enter key
def press_return
press_key(40)
end
# Press Escape key
def press_escape
press_key(41)
end
# Take a screenshot and save it
# @param name [String] Screenshot name (without extension)
# @return [String] Path to the saved screenshot
def screenshot(name)
Config.ensure_directories!
path = File.join(Config.current_dir, "#{name}.png")
output, status = run_axe('screenshot', '--output', path)
raise AxeError, "screenshot failed: #{output}" unless status.success?
# Wait for file to be fully written to disk
wait_for_file(path)
path
end
private
def run_axe(*)
Open3.capture2e('axe', *, '--udid', @udid)
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
# @param timeout [Float] Maximum time to wait in seconds
def wait_for_file(path, timeout: 2.0)
start_time = Time.now
loop do
return if File.exist?(path) && File.size(path) > 100
break if Time.now - start_time > timeout
sleep 0.1
end
end
# Recursively search the accessibility tree for an element
# @param node [Hash, Array] Current node in the tree
# @param identifier [String, nil] Accessibility identifier to match (AXUniqueId)
# @param label [String, nil] Accessibility label to match (AXLabel)
# @return [Hash, nil] Found element or nil
def find_element_in_tree(node, identifier: nil, label: nil)
case node
when Hash
# Check if this node matches by identifier (AXUniqueId in AXe output)
return node if identifier && node['AXUniqueId'] == identifier
# Check if this node matches by label (AXLabel in AXe output)
return node if label && node['AXLabel']&.include?(label)
# Recursively search children
node.each_value do |value|
result = find_element_in_tree(value, identifier: identifier, label: label)
return result if result
end
when Array
node.each do |item|
result = find_element_in_tree(item, identifier: identifier, label: label)
return result if result
end
end
nil
end
end
end
# Add present? method for nil/empty checking
class Object
def present?
respond_to?(:empty?) ? !empty? : !nil?
end
end
class NilClass
def present?
false
end
end