diff --git a/maestro-ios-driver/src/main/resources/driver-iPhoneSimulator/Debug-iphonesimulator/maestro-driver-ios.zip b/maestro-ios-driver/src/main/resources/driver-iPhoneSimulator/Debug-iphonesimulator/maestro-driver-ios.zip index bb76b9a5c9..fb61299b17 100644 Binary files a/maestro-ios-driver/src/main/resources/driver-iPhoneSimulator/Debug-iphonesimulator/maestro-driver-ios.zip and b/maestro-ios-driver/src/main/resources/driver-iPhoneSimulator/Debug-iphonesimulator/maestro-driver-ios.zip differ diff --git a/maestro-ios-driver/src/main/resources/driver-iPhoneSimulator/Debug-iphonesimulator/maestro-driver-iosUITests-Runner.zip b/maestro-ios-driver/src/main/resources/driver-iPhoneSimulator/Debug-iphonesimulator/maestro-driver-iosUITests-Runner.zip index d22c5b2b91..8e7e83d99d 100644 Binary files a/maestro-ios-driver/src/main/resources/driver-iPhoneSimulator/Debug-iphonesimulator/maestro-driver-iosUITests-Runner.zip and b/maestro-ios-driver/src/main/resources/driver-iPhoneSimulator/Debug-iphonesimulator/maestro-driver-iosUITests-Runner.zip differ diff --git a/maestro-ios-xctest-runner/MaestroDriverLib/Sources/MaestroDriverLib/Models/AXElement.swift b/maestro-ios-xctest-runner/MaestroDriverLib/Sources/MaestroDriverLib/Models/AXElement.swift index 28613ae899..7811488e9b 100644 --- a/maestro-ios-xctest-runner/MaestroDriverLib/Sources/MaestroDriverLib/Models/AXElement.swift +++ b/maestro-ios-xctest-runner/MaestroDriverLib/Sources/MaestroDriverLib/Models/AXElement.swift @@ -97,6 +97,23 @@ public struct AXElement: Codable, Equatable { return max ?? 1 } + /// Every element in this subtree (including `self`) whose `elementType` matches, + /// in depth-first pre-order. Nested matches are included, like + /// `XCUIElementQuery.allElementsBoundByIndex` on a descendants query. + public func descendants(ofType elementType: Int) -> [AXElement] { + var matches = [AXElement]() + + func collect(_ element: AXElement) { + if element.elementType == elementType { + matches.append(element) + } + element.children?.forEach(collect) + } + + collect(self) + return matches + } + public func filterAllChildrenNotInKeyboardBounds(_ keyboardFrame: CGRect) -> [AXElement] { var filteredChildren = [AXElement]() diff --git a/maestro-ios-xctest-runner/MaestroDriverLib/Tests/MaestroDriverLibTests/AXElementTests.swift b/maestro-ios-xctest-runner/MaestroDriverLib/Tests/MaestroDriverLibTests/AXElementTests.swift index 658e8a3f0b..bfc4f880f0 100644 --- a/maestro-ios-xctest-runner/MaestroDriverLib/Tests/MaestroDriverLibTests/AXElementTests.swift +++ b/maestro-ios-xctest-runner/MaestroDriverLib/Tests/MaestroDriverLibTests/AXElementTests.swift @@ -163,4 +163,30 @@ final class AXElementTests: XCTestCase { XCTAssertNotEqual(element1, element2) } -} \ No newline at end of file + + // MARK: - descendants(ofType:) + + func testDescendantsOfType_returnsMatchesInPreOrderIncludingNested() { + let statusBar = 25 + let nested = AXElement(identifier: "nested", elementType: statusBar) + let outer = AXElement(identifier: "outer", elementType: statusBar, children: [nested]) + let other = AXElement(identifier: "second", elementType: statusBar) + let root = AXElement( + identifier: "springboard", + elementType: 2, + children: [ + AXElement(identifier: "window", elementType: 4, children: [outer]), + AXElement(identifier: "button", elementType: 9), + other + ] + ) + + XCTAssertEqual(root.descendants(ofType: statusBar).map(\.identifier), ["outer", "nested", "second"]) + } + + func testDescendantsOfType_includesSelfAndReturnsEmptyWhenNoMatch() { + let leaf = AXElement(identifier: "bar", elementType: 25) + XCTAssertEqual(leaf.descendants(ofType: 25).map(\.identifier), ["bar"]) + XCTAssertTrue(AXElement(children: [leaf]).descendants(ofType: 9).isEmpty) + } +} diff --git a/maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/Handlers/ViewHierarchyHandler.swift b/maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/Handlers/ViewHierarchyHandler.swift index 9224359fab..2c31015569 100644 --- a/maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/Handlers/ViewHierarchyHandler.swift +++ b/maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/Handlers/ViewHierarchyHandler.swift @@ -242,16 +242,23 @@ struct ViewHierarchyHandler: HTTPHandler { return try? elementHierarchy(xcuiElement: alert) } + /// Status bars are read from one throwing snapshot of `element` (SpringBoard). + /// + /// Do not use `statusBars.firstMatch.exists` / `statusBars.allElementsBoundByIndex` here. + /// On iOS 26 one of SpringBoard's status bars is hosted by the keyboard process + /// (com.apple.InputUI). When the keyboard goes away mid-query, resolving that element + /// fails with kAXErrorInvalidUIElement, and those non-throwing APIs record an XCTest + /// failure ("Failed to resolve query") instead of throwing. That fails testHttpServer + /// and the runner exits. `snapshot()` throws instead, so a race just drops the status bars. func fullStatusBars(_ element: XCUIApplication) -> [AXElement]? { - guard element.statusBars.firstMatch.exists else { + do { + let root = try elementHierarchy(xcuiElement: element) + let statusBars = root.descendants(ofType: Int(XCUIElement.ElementType.statusBar.rawValue)) + return statusBars.isEmpty ? nil : statusBars + } catch { + NSLog("[ViewHierarchyHandler] Status bar snapshot failed, continuing without status bars: \(error.localizedDescription)") return nil } - - let snapshots = try? element.statusBars.allElementsBoundByIndex.compactMap{ (statusBar) in - try elementHierarchy(xcuiElement: statusBar) - } - - return snapshots } /// Fetches the Safari WebView hierarchy for iOS 26+ where SFSafariViewController