Title
snapshot --raw returns XCUITest runner's own UI tree instead of target app on physical iOS devices
Description
Summary
On physical iOS devices, agent-device snapshot --raw returns the UI tree of the XCUITest runner app (AgentDeviceRunner) instead of the target app under test. The default snapshot (without --raw) works correctly because it uses a different underlying API.
Environment
- Device: iPhone 13 Pro Max, iOS 18.6.2 (physical device)
- agent-device: latest (installed via npm), 0.7.1
- Xcode: 16.x
Steps to reproduce
- Connect a physical iOS device
- Launch any app (e.g. Outlook):
agent-device open com.microsoft.Office.Outlook
- Take a default snapshot:
agent-device snapshot -i → returns correct target app UI tree
- Take a raw snapshot:
agent-device snapshot --raw --json → returns the runner app's UI tree (AgentDeviceRunner) instead of Outlook
On a simulator, both commands return the correct target app tree.
Root cause
snapshotFast (default path) and snapshotRaw (--raw path) use fundamentally different XCUITest APIs to traverse the element tree:
snapshotFast — uses static snapshot() API (works cross-process)
// RunnerTests.swift L1330-1332
let rootSnapshot: XCUIElementSnapshot
rootSnapshot = try queryRoot.snapshot()
// Then traverses via snapshot.children — all in-memory, no IPC
XCUIElementSnapshot.snapshot() invokes the _XCT_snapshotForElement:attributes:parameters:reply: XPC protocol, which is Apple's designed mechanism for cross-process element tree retrieval. It returns an atomic, in-memory tree of the target app's UI.
snapshotRaw — uses live XCUIElement queries (fails cross-process on physical devices)
// RunnerTests.swift L1470
let children = element.children(matching: .any).allElementsBoundByIndex
// Each access triggers cross-process IPC via XCUIElementQuery
XCUIElement.children(matching:).allElementsBoundByIndex resolves elements through XCUIElementQuery. On simulators, both processes share the same Mac-hosted accessibility server, so cross-process queries work. On physical devices, the runner and target app are fully separate processes communicating through the device's testmanagerd, and the query silently falls back to the runner's own UI tree.
Why snapshotRaw uses live queries
This is not an oversight. snapshotRaw uses live XCUIElement queries to access element.isHittable — a property that XCUIElementSnapshot does not expose:
// RunnerTests.swift L1760-1762
private func snapshotHittable(_ snapshot: XCUIElementSnapshot) -> Bool {
// XCUIElementSnapshot does not expose isHittable; use enabled as a lightweight proxy.
return snapshot.isEnabled
}
The shouldInclude filtering logic (L1705-1728) heavily relies on isHittable for compact and interactive-only modes. This was a deliberate trade-off: live queries for accurate isHittable vs static snapshots for cross-process reliability.
Impact
snapshot --raw is unusable on physical iOS devices — it returns the runner's tree, not the target app's
snapshot --raw --json is often used to get detailed element attributes (value, hittable state) that the default snapshot omits
- The current docs say
--raw is "for troubleshooting only" but do not mention this physical device limitation
External validation
This is not specific to agent-device. The same cross-process limitation is well-known across the iOS automation ecosystem:
-
Appium WebDriverAgent deliberately avoids live element queries for tree retrieval. Its page source, XML, and XPath operations all use the static snapshotWithError: API. The one method that uses descendantsMatchingType + allElementsBoundByIndex (fb_stableInstanceWithUid) explicitly marks results as fb_isResolvedNatively = @NO.
-
Appium XCUITest driver docs explicitly warn: "Sometimes, even if visually it looks like UI elements belong to the same application, they are referenced by absolutely different apps" and "There are known cases where application interface/behavior might differ in simulators and real devices."
-
Detox issue wix/Detox#3463 proposes injecting unique accessibility identifiers as a workaround for unreliable cross-process element matching — confirming XCUITest's native queries are insufficient. Detox has never fully supported physical device testing partly due to these constraints.
-
Appelium SnapshotQuery was created specifically because live cross-process queries are "less reliable and flakier" — snapshot-based queries are ~1000x faster (60μs vs 50-100ms per element) and eliminate race conditions.
-
The original Facebook WDA was explicitly "A WebDriver server for iOS that runs inside the Simulator" — physical device support was added later by the Appium fork with significant architectural adjustments.
Proposed fix
Replace live XCUIElement tree traversal in snapshotRaw with the same snapshot() API used by snapshotFast, and compute isHittable from snapshot properties instead of querying it live:
private func computeHittable(snapshot: XCUIElementSnapshot, viewport: CGRect, siblings: [XCUIElementSnapshot]) -> Bool {
guard snapshot.isEnabled else { return false }
let frame = snapshot.frame
guard frame.width > 0 && frame.height > 0 else { return false }
guard viewport.intersects(frame) else { return false }
// Check if any later sibling fully occludes this element
for sibling in siblings {
if sibling.frame.contains(frame) { return false }
}
return true
}
This approach:
- Uses
snapshot() for reliable cross-process tree on both simulators and physical devices
- Computes
isHittable from frame geometry, enabled state, viewport intersection, and sibling occlusion — eliminating the need for live XCUIElement access
- Aligns with how WDA, Detox, and SnapshotQuery handle this problem
- Maintains the accuracy goals of
--raw mode without the cross-process limitation
Title
snapshot --rawreturns XCUITest runner's own UI tree instead of target app on physical iOS devicesDescription
Summary
On physical iOS devices,
agent-device snapshot --rawreturns the UI tree of the XCUITest runner app (AgentDeviceRunner) instead of the target app under test. The defaultsnapshot(without--raw) works correctly because it uses a different underlying API.Environment
Steps to reproduce
agent-device open com.microsoft.Office.Outlookagent-device snapshot -i→ returns correct target app UI treeagent-device snapshot --raw --json→ returns the runner app's UI tree (AgentDeviceRunner) instead of OutlookOn a simulator, both commands return the correct target app tree.
Root cause
snapshotFast(default path) andsnapshotRaw(--rawpath) use fundamentally different XCUITest APIs to traverse the element tree:snapshotFast— uses staticsnapshot()API (works cross-process)XCUIElementSnapshot.snapshot()invokes the_XCT_snapshotForElement:attributes:parameters:reply:XPC protocol, which is Apple's designed mechanism for cross-process element tree retrieval. It returns an atomic, in-memory tree of the target app's UI.snapshotRaw— uses liveXCUIElementqueries (fails cross-process on physical devices)XCUIElement.children(matching:).allElementsBoundByIndexresolves elements throughXCUIElementQuery. On simulators, both processes share the same Mac-hosted accessibility server, so cross-process queries work. On physical devices, the runner and target app are fully separate processes communicating through the device'stestmanagerd, and the query silently falls back to the runner's own UI tree.Why
snapshotRawuses live queriesThis is not an oversight.
snapshotRawuses liveXCUIElementqueries to accesselement.isHittable— a property thatXCUIElementSnapshotdoes not expose:The
shouldIncludefiltering logic (L1705-1728) heavily relies onisHittablefor compact and interactive-only modes. This was a deliberate trade-off: live queries for accurateisHittablevs static snapshots for cross-process reliability.Impact
snapshot --rawis unusable on physical iOS devices — it returns the runner's tree, not the target app'ssnapshot --raw --jsonis often used to get detailed element attributes (value, hittable state) that the default snapshot omits--rawis "for troubleshooting only" but do not mention this physical device limitationExternal validation
This is not specific to agent-device. The same cross-process limitation is well-known across the iOS automation ecosystem:
Appium WebDriverAgent deliberately avoids live element queries for tree retrieval. Its page source, XML, and XPath operations all use the static
snapshotWithError:API. The one method that usesdescendantsMatchingType+allElementsBoundByIndex(fb_stableInstanceWithUid) explicitly marks results asfb_isResolvedNatively = @NO.Appium XCUITest driver docs explicitly warn: "Sometimes, even if visually it looks like UI elements belong to the same application, they are referenced by absolutely different apps" and "There are known cases where application interface/behavior might differ in simulators and real devices."
Detox issue wix/Detox#3463 proposes injecting unique accessibility identifiers as a workaround for unreliable cross-process element matching — confirming XCUITest's native queries are insufficient. Detox has never fully supported physical device testing partly due to these constraints.
Appelium SnapshotQuery was created specifically because live cross-process queries are "less reliable and flakier" — snapshot-based queries are ~1000x faster (60μs vs 50-100ms per element) and eliminate race conditions.
The original Facebook WDA was explicitly "A WebDriver server for iOS that runs inside the Simulator" — physical device support was added later by the Appium fork with significant architectural adjustments.
Proposed fix
Replace live
XCUIElementtree traversal insnapshotRawwith the samesnapshot()API used bysnapshotFast, and computeisHittablefrom snapshot properties instead of querying it live:This approach:
snapshot()for reliable cross-process tree on both simulators and physical devicesisHittablefrom frame geometry, enabled state, viewport intersection, and sibling occlusion — eliminating the need for liveXCUIElementaccess--rawmode without the cross-process limitation