Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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_<tag>_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<T>(_ 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<T>(_ tag: String, _ block: () -> T?) -> T? {
safely(tag, nil, block)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -134,25 +112,16 @@ extension RunnerTests {
}

private func safeIsActionableCandidate(_ candidate: XCUIElement, seen: inout Set<String>) -> 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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,30 +73,21 @@ 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"))
.allElementsBoundByIndex
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
}

Expand Down Expand Up @@ -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
Expand Down
Loading