···123123124124# Basic interactions
125125axe tap -x 100 -y 200 --udid $UDID
126126+axe tap --id "Safari" --udid $UDID
127127+axe tap --label "Safari" --udid $UDID
126128axe type 'Hello World!' --udid $UDID
127129axe swipe --start-x 100 --start-y 300 --end-x 300 --end-y 100 --udid $UDID
128130axe button home --udid $UDID
···144146# Tap at coordinates
145147axe tap -x 100 -y 200 --udid SIMULATOR_UDID
146148axe tap -x 100 -y 200 --pre-delay 1.0 --post-delay 0.5 --udid SIMULATOR_UDID
149149+150150+# Tap by accessibility element (uses describe-ui accessibility tree)
151151+axe tap --id "Safari" --udid SIMULATOR_UDID
152152+axe tap --label "Safari" --udid SIMULATOR_UDID
147153148154# Swipe gestures
149155axe swipe --start-x 100 --start-y 300 --end-x 300 --end-y 100 --udid SIMULATOR_UDID
+5-1
Sources/AXe/Commands/DescribeUI.swift
···3737 }
38383939 // Fetch accessibility information
4040- try await AccessibilityFetcher.fetchAccessibilityInfo(for: simulatorUDID, logger: logger)
4040+ let jsonData = try await AccessibilityFetcher.fetchAccessibilityInfoJSONData(for: simulatorUDID, logger: logger)
4141+ guard let jsonString = String(data: jsonData, encoding: .utf8) else {
4242+ throw CLIError(errorDescription: "Failed to convert accessibility info to JSON string.")
4343+ }
4444+ print(jsonString)
4145 }
4246}
+87-9
Sources/AXe/Commands/Tap.swift
···5566struct Tap: AsyncParsableCommand {
77 static let configuration = CommandConfiguration(
88- abstract: "Tap on a specific point on the screen."
88+ abstract: "Tap on a specific point on the screen, or locate an element by accessibility and tap its center."
99 )
10101111 @Option(name: .customShort("x"), help: "The X coordinate of the point to tap.")
1212- var pointX: Double
1212+ var pointX: Double?
13131414 @Option(name: .customShort("y"), help: "The Y coordinate of the point to tap.")
1515- var pointY: Double
1515+ var pointY: Double?
1616+1717+ @Option(name: [.customLong("id")], help: "Tap the center of the element matching AXUniqueId (accessibilityIdentifier). Ignored if -x and -y are provided.")
1818+ var elementID: String?
1919+2020+ @Option(name: [.customLong("label")], help: "Tap the center of the element matching AXLabel (accessibilityLabel). Ignored if -x and -y are provided.")
2121+ var elementLabel: String?
16221723 @Option(name: .customLong("pre-delay"), help: "Delay before tapping in seconds.")
1824 var preDelay: Double?
···2430 var simulatorUDID: String
25312632 func validate() throws {
2727- // Validate coordinates are non-negative
2828- guard pointX >= 0, pointY >= 0 else {
2929- throw ValidationError("Coordinates must be non-negative values.")
3333+ if pointX != nil || pointY != nil {
3434+ guard let pointX, let pointY else {
3535+ throw ValidationError("Both -x and -y must be provided together.")
3636+ }
3737+ guard pointX >= 0, pointY >= 0 else {
3838+ throw ValidationError("Coordinates must be non-negative values.")
3939+ }
4040+ } else {
4141+ if elementID == nil && elementLabel == nil {
4242+ throw ValidationError("Either provide both -x/-y, or use --id/--label to tap an element.")
4343+ }
4444+ if elementID != nil && elementLabel != nil {
4545+ throw ValidationError("Use only one of --id or --label.")
4646+ }
4747+ if let elementID, elementID.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
4848+ throw ValidationError("--id must not be empty.")
4949+ }
5050+ if let elementLabel, elementLabel.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
5151+ throw ValidationError("--label must not be empty.")
5252+ }
3053 }
31543255 // Validate delays if provided
···49725073 try await performGlobalSetup(logger: logger)
51745252- logger.info().log("Tapping at (\(pointX), \(pointY))")
7575+ let resolvedPoint: (x: Double, y: Double)
7676+ let resolvedDescription: String
7777+7878+ if let pointX, let pointY {
7979+ resolvedPoint = (x: pointX, y: pointY)
8080+ resolvedDescription = "(\(pointX), \(pointY))"
8181+ } else {
8282+ let roots = try await AccessibilityFetcher.fetchAccessibilityElements(for: simulatorUDID, logger: logger)
8383+ let element: AccessibilityElement = try locateElement(in: roots)
8484+8585+ guard let frame = element.frame else {
8686+ print("Warning: Matched element has no frame. No tap performed.", to: &standardError)
8787+ throw CLIError(errorDescription: "Matched element has no frame.")
8888+ }
8989+ guard frame.width > 0, frame.height > 0 else {
9090+ print("Warning: Matched element has an invalid frame size (\(frame.width)x\(frame.height)). No tap performed.", to: &standardError)
9191+ throw CLIError(errorDescription: "Matched element has an invalid frame size.")
9292+ }
9393+9494+ let centerX = frame.x + (frame.width / 2.0)
9595+ let centerY = frame.y + (frame.height / 2.0)
9696+ resolvedPoint = (x: centerX, y: centerY)
9797+ resolvedDescription = "center of matched element at (\(centerX), \(centerY))"
9898+ }
9999+100100+ logger.info().log("Tapping at \(resolvedDescription)")
5310154102 // Create tap events with timing controls
55103 var events: [FBSimulatorHIDEvent] = []
···61109 }
6211063111 // Add the main tap event
6464- let tapEvent = FBSimulatorHIDEvent.tapAt(x: pointX, y: pointY)
112112+ let tapEvent = FBSimulatorHIDEvent.tapAt(x: resolvedPoint.x, y: resolvedPoint.y)
65113 events.append(tapEvent)
6611467115 // Add post-delay if specified
···84132 logger.info().log("Tap completed successfully")
8513386134 // Output success message to stdout
8787- print("✓ Tap at (\(pointX), \(pointY)) completed successfully")
135135+ print("✓ Tap at (\(resolvedPoint.x), \(resolvedPoint.y)) completed successfully")
136136+ }
137137+138138+ private func locateElement(in roots: [AccessibilityElement]) throws -> AccessibilityElement {
139139+ let allElements = roots.flatMap { $0.flattened() }
140140+141141+ if let elementID {
142142+ let query = elementID.trimmingCharacters(in: .whitespacesAndNewlines)
143143+ let matches = allElements.filter { $0.normalizedUniqueId == query }
144144+ return try selectUniqueMatch(matches, kind: "--id", value: elementID)
145145+ }
146146+147147+ if let elementLabel {
148148+ let query = elementLabel.trimmingCharacters(in: .whitespacesAndNewlines)
149149+ let matches = allElements.filter { $0.normalizedLabel == query }
150150+ return try selectUniqueMatch(matches, kind: "--label", value: elementLabel)
151151+ }
152152+153153+ throw CLIError(errorDescription: "Unexpected state: no coordinates and no element query.")
154154+ }
155155+156156+ private func selectUniqueMatch(_ matches: [AccessibilityElement], kind: String, value: String) throws -> AccessibilityElement {
157157+ guard !matches.isEmpty else {
158158+ print("Warning: No accessibility element matched \(kind) '\(value)'. No tap performed.", to: &standardError)
159159+ throw CLIError(errorDescription: "No accessibility element matched \(kind) '\(value)'.")
160160+ }
161161+ guard matches.count == 1 else {
162162+ print("Warning: Multiple (\(matches.count)) accessibility elements matched \(kind) '\(value)'. No tap performed.", to: &standardError)
163163+ throw CLIError(errorDescription: "Multiple accessibility elements matched \(kind) '\(value)'.")
164164+ }
165165+ return matches[0]
88166 }
89167}
+33
Sources/AXe/Utilities/AccessibilityElement.swift
···11+import Foundation
22+33+struct AccessibilityElement: Decodable {
44+ struct Frame: Decodable {
55+ let x: Double
66+ let y: Double
77+ let width: Double
88+ let height: Double
99+ }
1010+1111+ let type: String?
1212+ let frame: Frame?
1313+ let children: [AccessibilityElement]?
1414+1515+ let AXLabel: String?
1616+ let AXUniqueId: String?
1717+1818+ var normalizedLabel: String? {
1919+ AXLabel?.trimmingCharacters(in: .whitespacesAndNewlines)
2020+ }
2121+2222+ var normalizedUniqueId: String? {
2323+ AXUniqueId?.trimmingCharacters(in: .whitespacesAndNewlines)
2424+ }
2525+2626+ func flattened() -> [AccessibilityElement] {
2727+ var result: [AccessibilityElement] = [self]
2828+ if let children {
2929+ result.append(contentsOf: children.flatMap { $0.flattened() })
3030+ }
3131+ return result
3232+ }
3333+}
+20-29
Sources/AXe/Utilities/AccessibilityFetcher.swift
···55// MARK: - Accessibility Fetcher
66@MainActor
77struct AccessibilityFetcher {
88- static func fetchAccessibilityInfo(for simulatorUDID: String, logger: AxeLogger) async throws {
99- logger.info().log("Accessibility Info Fetcher started for simulator UDID: \(simulatorUDID)")
1010-88+ static func fetchAccessibilityInfoJSONData(for simulatorUDID: String, logger: AxeLogger) async throws -> Data {
119 let simulatorSet = try await getSimulatorSet(deviceSetPath: nil, logger: logger, reporter: EmptyEventReporter.shared)
1212- logger.info().log("FBSimulatorSet obtained.")
1313-1010+1411 guard let target = simulatorSet.allSimulators.first(where: { $0.udid == simulatorUDID }) else {
1512 throw CLIError(errorDescription: "Simulator with UDID \(simulatorUDID) not found in set.")
1613 }
1717- logger.info().log("Target (FBSimulator) obtained: \(target.udid) - \(target.name), State: \(FBiOSTargetStateStringFromState(target.state))")
18141919- logger.info().log("Fetching accessibility info directly from FBSimulator...")
2020- // FBSimulator conforms to FBAccessibilityCommands, which has accessibilityElementsWithNestedFormat:
2121- // It returns FBFuture<id> which becomes FBFuture<AnyObject> in Swift.
1515+ // FBSimulator conforms to FBAccessibilityCommands.
2216 let accessibilityInfoFuture: FBFuture<AnyObject> = target.accessibilityElements(withNestedFormat: true)
2323-2417 let infoAnyObject: AnyObject = try await FutureBridge.value(accessibilityInfoFuture)
2525- logger.info().log("Accessibility info raw object (AnyObject) received.")
26182727- // Check if it's NSDictionary or NSArray, as both are valid top-level JSON structures.
2828- let jsonData: Data
2919 if let nsDict = infoAnyObject as? NSDictionary {
3030- logger.info().log("Successfully cast to NSDictionary.")
3131- jsonData = try JSONSerialization.data(withJSONObject: nsDict, options: [.prettyPrinted])
3232- } else if let nsArray = infoAnyObject as? NSArray {
3333- logger.info().log("Successfully cast to NSArray.")
3434- jsonData = try JSONSerialization.data(withJSONObject: nsArray, options: [.prettyPrinted])
3535- } else {
3636- logger.error().log("Accessibility info was not an NSDictionary or NSArray as expected. Type: \(type(of: infoAnyObject))")
3737- throw CLIError(errorDescription: "Accessibility info was not a dictionary or array as expected.")
2020+ return try JSONSerialization.data(withJSONObject: nsDict, options: [.prettyPrinted])
2121+ }
2222+ if let nsArray = infoAnyObject as? NSArray {
2323+ return try JSONSerialization.data(withJSONObject: nsArray, options: [.prettyPrinted])
3824 }
3939-4040- if let jsonString = String(data: jsonData, encoding: .utf8) {
4141- print(jsonString)
4242- } else {
4343- logger.error().log("Failed to convert accessibility info to JSON string.")
4444- throw CLIError(errorDescription: "Failed to convert accessibility info to JSON string.")
2525+2626+ throw CLIError(errorDescription: "Accessibility info was not a dictionary or array as expected.")
2727+ }
2828+2929+ static func fetchAccessibilityElements(for simulatorUDID: String, logger: AxeLogger) async throws -> [AccessibilityElement] {
3030+ let jsonData = try await fetchAccessibilityInfoJSONData(for: simulatorUDID, logger: logger)
3131+ let decoder = JSONDecoder()
3232+3333+ if let roots = try? decoder.decode([AccessibilityElement].self, from: jsonData) {
3434+ return roots
4535 }
4646-4747- logger.info().log("Accessibility Info Fetcher finished successfully.")
3636+3737+ let root = try decoder.decode(AccessibilityElement.self, from: jsonData)
3838+ return [root]
4839 }
4940}