Files
callstack__agent-device/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+BlockingSystemModalResolution.swift
Michał Pierzchała c6a8e78768 fix(tvos): skip SpringBoard system-modal probe on tvOS (#1351) (#1353)
tvOS has no SpringBoard (it uses PineBoard/HeadBoard), so probing
com.apple.springboard for blocking system modals raises an XCTest failure
("Application com.apple.springboard is not running") that safely() cannot
trap, terminating the whole runner test. Every snapshot and alert
resolution then failed, and the CLI misreported it as the screen
"overwhelming the accessibility capture".

resolveBlockingSystemModal — the single chokepoint every snapshot/alert
path funnels through — assumed a SpringBoard host always exists. Model that
assumption explicitly with `hasSpringBoardSystemModalHost` and bail to
`.absent` when no host exists, so app-owned alert queries still run. The
fourth probe site (shouldRouteToSpringboardBlockingSystemModal) is already
`#if os(iOS)` and needs no change.

Add tvOS regression tests covering the snapshot and alert-resolution paths.
2026-07-21 07:57:51 +02:00

114 lines
3.9 KiB
Swift

import XCTest
extension RunnerTests {
struct ResolvedBlockingSystemModal {
let root: XCUIElement
let ownerApp: XCUIApplication
let actions: [XCUIElement]
}
enum BlockingSystemModalResolution {
case absent
case unresolved
case resolved(ResolvedBlockingSystemModal)
}
func resolveBlockingSystemModal(
deadline: Date
) -> BlockingSystemModalResolution {
guard Self.hasSpringBoardSystemModalHost else {
return .absent
}
guard let springboardModal = firstBlockingSystemModal(
in: springboard,
deadline: deadline
) else {
return .absent
}
let springboardActions = actionableElements(in: springboardModal)
if !RemoteHostedSystemModalPolicy.shouldProbeRemoteHost(
springboardActionCount: springboardActions.count
) {
return .resolved(
ResolvedBlockingSystemModal(
root: springboardModal,
ownerApp: springboard,
actions: springboardActions
)
)
}
guard Date() < deadline, let remoteModal = remoteHostedSystemModal(deadline: deadline) else {
return .unresolved
}
return .resolved(remoteModal)
}
// AccessorySetupUI mirrors into SpringBoard, but its mirrored elements are not
// reliably hittable. Query the host directly; activating it breaks the picker.
private static let remoteSystemModalHostBundleIds = [
"com.apple.AccessorySetupUI"
]
private func remoteHostedSystemModal(deadline: Date) -> ResolvedBlockingSystemModal? {
for bundleId in Self.remoteSystemModalHostBundleIds {
guard Date() < deadline else { return nil }
let host = XCUIApplication(bundleIdentifier: bundleId)
let state = safely("REMOTE_MODAL_STATE", XCUIApplication.State.unknown) { host.state }
guard RemoteHostedSystemModalPolicy.isEligibleHostState(state) else { continue }
guard Date() < deadline else { return nil }
// A dismissed remote host can raise kAXErrorServerNotFound. The safe queries
// inside actionableElements turn that disappearance into an empty result.
let actions = actionableElements(in: host)
guard !actions.isEmpty else { continue }
return ResolvedBlockingSystemModal(root: host, ownerApp: host, actions: actions)
}
return nil
}
}
enum RemoteHostedSystemModalPolicy {
static func shouldProbeRemoteHost(springboardActionCount: Int) -> Bool {
springboardActionCount == 0
}
static func isEligibleHostState(_ state: XCUIApplication.State) -> Bool {
state == .runningForeground
}
}
#if AGENT_DEVICE_RUNNER_UNIT_TESTS
extension RunnerTests {
func testRemoteHostProbeRunsOnlyWhenSpringboardModalHasNoActions() {
XCTAssertTrue(RemoteHostedSystemModalPolicy.shouldProbeRemoteHost(springboardActionCount: 0))
XCTAssertFalse(RemoteHostedSystemModalPolicy.shouldProbeRemoteHost(springboardActionCount: 1))
XCTAssertFalse(RemoteHostedSystemModalPolicy.shouldProbeRemoteHost(springboardActionCount: 3))
}
func testRemoteHostStateGateFailsClosedToForeground() {
XCTAssertTrue(RemoteHostedSystemModalPolicy.isEligibleHostState(.runningForeground))
XCTAssertFalse(RemoteHostedSystemModalPolicy.isEligibleHostState(.runningBackground))
XCTAssertFalse(RemoteHostedSystemModalPolicy.isEligibleHostState(.notRunning))
XCTAssertFalse(RemoteHostedSystemModalPolicy.isEligibleHostState(.unknown))
}
// tvOS has no SpringBoard host, so both the snapshot and alert-resolution paths
// must resolve without probing com.apple.springboard (#1351).
#if os(tvOS)
func testResolveBlockingSystemModalIsAbsentWithoutSpringBoardOnTvOS() {
guard case .absent = resolveBlockingSystemModal(deadline: .distantFuture) else {
XCTFail("tvOS blocking system-modal resolution must be .absent")
return
}
}
func testBlockingSystemAlertSnapshotIsNilOnTvOS() {
XCTAssertNil(blockingSystemAlertSnapshot(deadline: .distantFuture))
}
#endif
}
#endif