diff --git a/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Exceptions.swift b/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Exceptions.swift new file mode 100644 index 000000000..76d47e43c --- /dev/null +++ b/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Exceptions.swift @@ -0,0 +1,29 @@ +import XCTest + +extension RunnerTests { + /// Runs `block` and returns its value. If it raises an Objective-C exception, logs the message + /// under `AGENT_DEVICE_RUNNER__IGNORED_EXCEPTION` and returns `fallback`. + /// + /// Consolidates the catch-log-and-default band-aid the runner uses around exception-prone + /// XCUITest queries (flaky `allElementsBoundByIndex` snapshots, stale element reads), giving the + /// "silently logged and continued" path one searchable format and one place to add per-tag + /// exception telemetry later. `RunnerObjCExceptionCatcher.catchException` takes a non-escaping + /// block, so `block` may capture `inout` state. + func safely(_ tag: String, _ fallback: T, _ block: () -> T) -> T { + var result = fallback + let exceptionMessage = RunnerObjCExceptionCatcher.catchException({ + result = block() + }) + if let exceptionMessage { + NSLog("AGENT_DEVICE_RUNNER_%@_IGNORED_EXCEPTION=%@", tag, exceptionMessage) + return fallback + } + return result + } + + /// Optional-returning convenience: returns `nil` on exception (matching the common + /// `var x: T?` + catch-and-return-nil shape). + func safely(_ tag: String, _ block: () -> T?) -> T? { + safely(tag, nil, block) + } +} diff --git a/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift b/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift index 2c5ad1250..6fb48aecb 100644 --- a/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +++ b/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift @@ -285,14 +285,13 @@ extension RunnerTests { } private func textInputCandidatesAt(app: XCUIApplication, point: CGPoint) -> [XCUIElement] { - var candidates: [XCUIElement] = [] - let exceptionMessage = RunnerObjCExceptionCatcher.catchException({ + safely("TEXT_INPUT_AT_POINT", []) { // Query the text-input element types directly instead of enumerating the entire tree // (app.descendants(.any).allElementsBoundByIndex snapshots every element and is ~10x // slower — it dominated fill latency because resolveTextEntryElement re-runs this on // each verify/repair poll once the focused field reference goes stale). // Prefer the smallest matching field so nested editable controls win over large containers. - candidates = [ + [ app.textFields, app.secureTextFields, app.searchFields, @@ -318,15 +317,7 @@ extension RunnerTests { } return left.elementType.rawValue < right.elementType.rawValue } - }) - if let exceptionMessage { - NSLog( - "AGENT_DEVICE_RUNNER_TEXT_INPUT_AT_POINT_IGNORED_EXCEPTION=%@", - exceptionMessage - ) - return [] } - return candidates } private func frameContainsPoint(_ frame: CGRect, _ point: CGPoint, tolerance: CGFloat) -> Bool { @@ -431,9 +422,8 @@ extension RunnerTests { private func singleTextEntryElement(app: XCUIApplication) -> XCUIElement? { #if os(iOS) - var matches: [XCUIElement] = [] - let exceptionMessage = RunnerObjCExceptionCatcher.catchException({ - matches = app.descendants(matching: .any).allElementsBoundByIndex.filter { element in + let matches = safely("KEYBOARD_RETURN_TEXT_ENTRY_QUERY", []) { + app.descendants(matching: .any).allElementsBoundByIndex.filter { element in guard element.exists else { return false } switch element.elementType { case .textField, .secureTextField, .searchField, .textView: @@ -442,13 +432,6 @@ extension RunnerTests { return false } } - }) - if let exceptionMessage { - NSLog( - "AGENT_DEVICE_RUNNER_KEYBOARD_RETURN_TEXT_ENTRY_QUERY_IGNORED_EXCEPTION=%@", - exceptionMessage - ) - return nil } return matches.count == 1 ? matches[0] : nil #else @@ -784,22 +767,13 @@ extension RunnerTests { private func visibleKeyboardFrame(app: XCUIApplication) -> CGRect? { #if os(iOS) - var frame: CGRect? - let exceptionMessage = RunnerObjCExceptionCatcher.catchException({ + return safely("KEYBOARD_FRAME") { let keyboard = app.keyboards.firstMatch - guard keyboard.exists else { return } + guard keyboard.exists else { return nil } let keyboardFrame = keyboard.frame - guard !keyboardFrame.isEmpty else { return } - frame = keyboardFrame - }) - if let exceptionMessage { - NSLog( - "AGENT_DEVICE_RUNNER_KEYBOARD_FRAME_IGNORED_EXCEPTION=%@", - exceptionMessage - ) - return nil + guard !keyboardFrame.isEmpty else { return nil } + return keyboardFrame } - return frame #else return nil #endif diff --git a/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift b/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift index a19d915ed..e7bbb7fe2 100644 --- a/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +++ b/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift @@ -354,14 +354,7 @@ extension RunnerTests { } private func safeSnapshotViewport(app: XCUIApplication) -> CGRect { - var viewport = CGRect.infinite - let exceptionMessage = RunnerObjCExceptionCatcher.catchException({ - viewport = snapshotViewport(app: app) - }) - if let exceptionMessage { - NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_VIEWPORT_IGNORED_EXCEPTION=%@", exceptionMessage) - } - return viewport + safely("SNAPSHOT_VIEWPORT", CGRect.infinite) { snapshotViewport(app: app) } } private func describeSnapshotError(_ error: Error) -> String { @@ -718,18 +711,7 @@ extension RunnerTests { } private func safeSnapshotElementsQuery(_ fetch: () -> [XCUIElement]) -> [XCUIElement] { - var elements: [XCUIElement] = [] - let exceptionMessage = RunnerObjCExceptionCatcher.catchException({ - elements = fetch() - }) - if let exceptionMessage { - NSLog( - "AGENT_DEVICE_RUNNER_SNAPSHOT_QUERY_IGNORED_EXCEPTION=%@", - exceptionMessage - ) - return [] - } - return elements + safely("SNAPSHOT_QUERY", [], fetch) } private func isScrollableContainer(_ snapshot: XCUIElementSnapshot, visible: Bool) -> Bool { diff --git a/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift b/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift index d78e33b1f..01f0344f2 100644 --- a/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +++ b/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift @@ -77,33 +77,11 @@ extension RunnerTests { } func safeElementsQuery(_ fetch: () -> [XCUIElement]) -> [XCUIElement] { - var elements: [XCUIElement] = [] - let exceptionMessage = RunnerObjCExceptionCatcher.catchException({ - elements = fetch() - }) - if let exceptionMessage { - NSLog( - "AGENT_DEVICE_RUNNER_MODAL_QUERY_IGNORED_EXCEPTION=%@", - exceptionMessage - ) - return [] - } - return elements + safely("MODAL_QUERY", [], fetch) } private func safeIsBlockingSystemModal(_ element: XCUIElement, in springboard: XCUIApplication) -> Bool { - var isBlocking = false - let exceptionMessage = RunnerObjCExceptionCatcher.catchException({ - isBlocking = isBlockingSystemModal(element, in: springboard) - }) - if let exceptionMessage { - NSLog( - "AGENT_DEVICE_RUNNER_MODAL_CHECK_IGNORED_EXCEPTION=%@", - exceptionMessage - ) - return false - } - return isBlocking + safely("MODAL_CHECK", false) { isBlockingSystemModal(element, in: springboard) } } private func isBlockingSystemModal(_ element: XCUIElement, in springboard: XCUIApplication) -> Bool { @@ -134,25 +112,16 @@ extension RunnerTests { } private func safeIsActionableCandidate(_ candidate: XCUIElement, seen: inout Set) -> Bool { - var include = false - let exceptionMessage = RunnerObjCExceptionCatcher.catchException({ - if !candidate.exists || !candidate.isHittable { return } - if !actionableTypes.contains(candidate.elementType) { return } + safely("MODAL_ACTION", false) { + if !candidate.exists || !candidate.isHittable { return false } + if !actionableTypes.contains(candidate.elementType) { return false } let frame = candidate.frame - if frame.isNull || frame.isEmpty { return } + if frame.isNull || frame.isEmpty { return false } let key = "\(candidate.elementType.rawValue)-\(frame.origin.x)-\(frame.origin.y)-\(frame.size.width)-\(frame.size.height)-\(candidate.label)" - if seen.contains(key) { return } + if seen.contains(key) { return false } seen.insert(key) - include = true - }) - if let exceptionMessage { - NSLog( - "AGENT_DEVICE_RUNNER_MODAL_ACTION_IGNORED_EXCEPTION=%@", - exceptionMessage - ) - return false + return true } - return include } private func preferredSystemModalTitle(_ element: XCUIElement) -> String { @@ -205,9 +174,8 @@ extension RunnerTests { depth: Int, hittableOverride: Bool? = nil ) -> SnapshotNode? { - var node: SnapshotNode? - let exceptionMessage = RunnerObjCExceptionCatcher.catchException({ - node = makeSnapshotNode( + safely("MODAL_NODE") { + makeSnapshotNode( element: element, index: index, type: type, @@ -216,14 +184,6 @@ extension RunnerTests { depth: depth, hittableOverride: hittableOverride ) - }) - if let exceptionMessage { - NSLog( - "AGENT_DEVICE_RUNNER_MODAL_NODE_IGNORED_EXCEPTION=%@", - exceptionMessage - ) - return nil } - return node } } diff --git a/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift b/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift index 1b86bdcbb..b06f5bda6 100644 --- a/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +++ b/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift @@ -73,8 +73,7 @@ extension RunnerTests { // under XCUITest, so text entry readiness is driven by tap/keyboard state. return nil #else - var focused: XCUIElement? - let exceptionMessage = RunnerObjCExceptionCatcher.catchException({ + return safely("FOCUSED_INPUT_QUERY") { let candidates = app .descendants(matching: .any) .matching(NSPredicate(format: "hasKeyboardFocus == 1")) @@ -82,21 +81,13 @@ extension RunnerTests { for candidate in candidates where candidate.exists { switch candidate.elementType { case .textField, .secureTextField, .searchField, .textView: - focused = candidate - return + return candidate default: continue } } - }) - if let exceptionMessage { - NSLog( - "AGENT_DEVICE_RUNNER_FOCUSED_INPUT_QUERY_IGNORED_EXCEPTION=%@", - exceptionMessage - ) return nil } - return focused #endif } @@ -650,18 +641,7 @@ extension RunnerTests { private func keyboardElementExists(app: XCUIApplication) -> Bool { #if os(iOS) - var exists = false - let exceptionMessage = RunnerObjCExceptionCatcher.catchException({ - exists = app.keyboards.firstMatch.exists - }) - if let exceptionMessage { - NSLog( - "AGENT_DEVICE_RUNNER_KEYBOARD_EXISTS_IGNORED_EXCEPTION=%@", - exceptionMessage - ) - return false - } - return exists + return safely("KEYBOARD_EXISTS", false) { app.keyboards.firstMatch.exists } #else return false #endif