fix: improve daemon diagnostics and remove compact snapshots (#786)

* fix: improve daemon startup diagnostics

* perf: relax query-sweep recovery budget

* fix: make compact snapshot flag a no-op

* fix: remove compact snapshot mode

* test: align tvOS remote provider expectation

* test: drop compact snapshot progress target
This commit is contained in:
Michał Pierzchała
2026-06-12 17:32:56 +02:00
committed by GitHub
parent 4142daa0c3
commit 5c83fe4e50
77 changed files with 391 additions and 291 deletions
@@ -107,15 +107,14 @@ extension RunnerTests {
let typeName = elementTypeName(rawElementType: rawType)
let enabled = privateAXBool(rawNode["enabled"]) ?? true
let visible = isVisibleInViewport(rect, viewport)
let compactCandidate = privateAXFlatCompactCandidate(rawElementType: rawType)
let interactiveCandidate = privateAXInteractiveCandidate(rawElementType: rawType)
let filterDecision = flatSnapshotFilterDecision(
FlatSnapshotFilterNode(
isRoot: parentIndex == nil,
label: label,
identifier: identifier,
valueText: value.isEmpty ? nil : value,
visible: visible,
compactCandidate: compactCandidate
visible: visible
),
options: options,
insideMatchedScope: insideMatchedScope
@@ -137,7 +136,7 @@ extension RunnerTests {
enabled: enabled,
focused: privateAXBool(rawNode["focused"]) == true ? true : nil,
selected: privateAXBool(rawNode["selected"]) == true ? true : nil,
hittable: visible && enabled && compactCandidate,
hittable: visible && enabled && interactiveCandidate,
depth: depth,
parentIndex: parentIndex,
hiddenContentAbove: nil,
@@ -230,8 +229,7 @@ extension RunnerTests {
appendPrivateAXNode(
tree,
to: &nodes,
options: SnapshotOptions(
interactiveOnly: false, compact: false, depth: nil, scope: "homeScreen", raw: false),
options: SnapshotOptions(interactiveOnly: false, depth: nil, scope: "homeScreen", raw: false),
viewport: .infinite,
depth: 0,
parentIndex: nil,
@@ -245,7 +243,7 @@ extension RunnerTests {
XCTAssertFalse(labels.contains("unrelated sibling"))
}
func testPrivateAXCompactInteractiveFiltersLoginLikeHiddenDrawer() {
func testPrivateAXInteractiveFiltersLoginLikeHiddenDrawer() {
let tree: [String: Any] = [
"type": Int(XCUIElement.ElementType.application.rawValue),
"label": "Blue Sky",
@@ -313,8 +311,7 @@ extension RunnerTests {
appendPrivateAXNode(
tree,
to: &nodes,
options: SnapshotOptions(
interactiveOnly: true, compact: true, depth: nil, scope: nil, raw: false),
options: SnapshotOptions(interactiveOnly: true, depth: nil, scope: nil, raw: false),
viewport: CGRect(x: 0, y: 0, width: 390, height: 844),
depth: 0,
parentIndex: nil,
@@ -691,7 +691,6 @@ extension RunnerTests {
case .snapshot:
let options = SnapshotOptions(
interactiveOnly: command.interactiveOnly ?? false,
compact: command.compact ?? false,
depth: command.depth,
scope: command.scope,
raw: command.raw ?? false
@@ -6,7 +6,6 @@ struct FlatSnapshotFilterNode {
let identifier: String
let valueText: String?
let visible: Bool
let compactCandidate: Bool
var hasContent: Bool {
return !label.isEmpty || !identifier.isEmpty || valueText != nil
@@ -46,8 +45,6 @@ extension RunnerTests {
include = false
} else if options.interactiveOnly && !node.visible {
include = false
} else if options.compact {
include = node.hasContent || node.compactCandidate
} else {
include = true
}
@@ -55,14 +52,7 @@ extension RunnerTests {
return FlatSnapshotFilterDecision(include: include, insideMatchedScope: nowInsideScope)
}
func querySweepFlatCompactCandidate(
elementType: XCUIElement.ElementType,
hittable: Bool
) -> Bool {
return hittable || interactiveTypes.contains(elementType)
}
func privateAXFlatCompactCandidate(rawElementType: Int) -> Bool {
func privateAXInteractiveCandidate(rawElementType: Int) -> Bool {
guard let type = flatSnapshotElementType(rawElementType: rawElementType) else {
return false
}
@@ -84,55 +74,48 @@ extension RunnerTests {
label: "Welcome back",
identifier: "",
valueText: nil,
visible: true,
compactCandidate: false
visible: true
)
let hiddenInteractive = FlatSnapshotFilterNode(
isRoot: false,
label: "Hidden menu",
identifier: "",
valueText: nil,
visible: false,
compactCandidate: true
visible: false
)
let decorative = FlatSnapshotFilterNode(
isRoot: false,
label: "",
identifier: "",
valueText: nil,
visible: true,
compactCandidate: false
visible: true
)
XCTAssertTrue(
flatSnapshotFilterDecision(
visibleContent,
options: SnapshotOptions(
interactiveOnly: false, compact: true, depth: nil, scope: nil, raw: false),
options: SnapshotOptions(interactiveOnly: false, depth: nil, scope: nil, raw: false),
insideMatchedScope: false
).include
)
XCTAssertFalse(
flatSnapshotFilterDecision(
hiddenInteractive,
options: SnapshotOptions(
interactiveOnly: true, compact: true, depth: nil, scope: nil, raw: false),
insideMatchedScope: false
).include
)
XCTAssertFalse(
flatSnapshotFilterDecision(
decorative,
options: SnapshotOptions(
interactiveOnly: false, compact: true, depth: nil, scope: nil, raw: false),
options: SnapshotOptions(interactiveOnly: true, depth: nil, scope: nil, raw: false),
insideMatchedScope: false
).include
)
XCTAssertTrue(
flatSnapshotFilterDecision(
decorative,
options: SnapshotOptions(
interactiveOnly: false, compact: false, depth: nil, scope: nil, raw: false),
options: SnapshotOptions(interactiveOnly: false, depth: nil, scope: nil, raw: false),
insideMatchedScope: false
).include
)
XCTAssertTrue(
flatSnapshotFilterDecision(
decorative,
options: SnapshotOptions(interactiveOnly: false, depth: nil, scope: nil, raw: false),
insideMatchedScope: false
).include
)
@@ -144,19 +127,16 @@ extension RunnerTests {
label: "",
identifier: "homeScreen",
valueText: nil,
visible: true,
compactCandidate: false
visible: true
)
let unmatchedDescendant = FlatSnapshotFilterNode(
isRoot: false,
label: "Post body without the scope text",
identifier: "",
valueText: nil,
visible: true,
compactCandidate: false
visible: true
)
let options = SnapshotOptions(
interactiveOnly: false, compact: false, depth: nil, scope: "homeScreen", raw: false)
let options = SnapshotOptions(interactiveOnly: false, depth: nil, scope: "homeScreen", raw: false)
let rootDecision = flatSnapshotFilterDecision(
scopeRoot,
@@ -182,14 +162,10 @@ extension RunnerTests {
)
}
func testFlatSnapshotCompactCandidatesPreserveBackendInputs() {
XCTAssertFalse(
querySweepFlatCompactCandidate(elementType: .scrollView, hittable: false),
"query sweep should not newly admit contentless scroll containers"
)
func testPrivateAXInteractiveCandidatesPreserveBackendInputs() {
XCTAssertTrue(
privateAXFlatCompactCandidate(rawElementType: Int(XCUIElement.ElementType.scrollView.rawValue)),
"private AX keeps its existing scroll-container compact candidate behavior"
privateAXInteractiveCandidate(rawElementType: Int(XCUIElement.ElementType.scrollView.rawValue)),
"private AX marks scroll containers as interactive candidates"
)
}
}
@@ -138,7 +138,6 @@ struct Command: Codable {
let fps: Int?
let quality: Int?
let interactiveOnly: Bool?
let compact: Bool?
let depth: Int?
let scope: String?
let raw: Bool?
@@ -351,7 +350,6 @@ struct SnapshotNode: Codable {
struct SnapshotOptions {
let interactiveOnly: Bool
let compact: Bool
let depth: Int?
let scope: String?
let raw: Bool
@@ -105,11 +105,8 @@ extension RunnerTests {
if let blocking = blockingSystemAlertSnapshot() {
return blocking
}
let plan = options.interactiveOnly && options.compact
? Self.compactInteractivePlan
: Self.regularVisiblePlan
return try runSnapshotCapturePlan(
plan,
Self.regularVisiblePlan,
app: app,
options: options,
terminal: .sparseWithFatalOnAXFailure
@@ -307,7 +304,7 @@ extension RunnerTests {
func snapshotFlatInteractive(app: XCUIApplication, options: SnapshotOptions) -> DataPayload {
var nodes: [SnapshotNode] = [
compactInteractiveRootNode(rect: .zero)
interactiveRootNode(rect: .zero)
]
if options.depth == 0 {
return DataPayload(nodes: nodes, truncated: false)
@@ -355,9 +352,9 @@ extension RunnerTests {
// inside nodes[0].rect): use the real screen viewport when capture produced a finite one,
// so off-screen candidates can never inflate the root and masquerade as on-screen.
let rootRect = viewport.isInfinite || viewport.isNull || viewport.isEmpty
? compactInteractiveRootFrame(for: candidates)
? interactiveRootFrame(for: candidates)
: viewport
nodes[0] = compactInteractiveRootNode(rect: rootRect)
nodes[0] = interactiveRootNode(rect: rootRect)
for candidate in candidates {
nodes.append(
SnapshotNode(
@@ -421,7 +418,7 @@ extension RunnerTests {
) -> DataPayload {
return DataPayload(
message: message,
nodes: [compactInteractiveRootNode(rect: .zero)],
nodes: [interactiveRootNode(rect: .zero)],
truncated: true,
snapshotQuality: snapshotQuality,
runnerFatal: runnerFatal,
@@ -472,7 +469,7 @@ extension RunnerTests {
XCTAssertEqual(failure.hint, Self.rawSnapshotTooLargeHint)
}
private func compactInteractiveRootNode(rect: CGRect) -> SnapshotNode {
private func interactiveRootNode(rect: CGRect) -> SnapshotNode {
SnapshotNode(
index: 0,
type: "Application",
@@ -491,7 +488,7 @@ extension RunnerTests {
)
}
private func compactInteractiveRootFrame(for candidates: [SnapshotNode]) -> CGRect {
private func interactiveRootFrame(for candidates: [SnapshotNode]) -> CGRect {
guard !candidates.isEmpty else {
return .zero
}
@@ -523,9 +520,6 @@ extension RunnerTests {
) -> Bool {
let type = snapshot.elementType
let hasContent = !label.isEmpty || !identifier.isEmpty || (valueText != nil)
if options.compact && type == .other && !hasContent && !hittable {
if snapshot.children.count <= 1 { return false }
}
if options.interactiveOnly {
if isScrollableContainer(snapshot, visible: visible) { return true }
#if os(macOS)
@@ -538,9 +532,6 @@ extension RunnerTests {
if hasContent { return true }
return false
}
if options.compact {
return hasContent || hittable
}
if regularSnapshot {
if type == .application || type == .window { return true }
return visible
@@ -1121,8 +1112,7 @@ extension RunnerTests {
label: label,
identifier: identifier,
valueText: valueText,
visible: visible,
compactCandidate: querySweepFlatCompactCandidate(elementType: elementType, hittable: hittable)
visible: visible
)
if !flatSnapshotFilterDecision(filterNode, options: options, insideMatchedScope: false).include {
return
@@ -61,7 +61,6 @@ extension RunnerTests {
// MARK: Plan definitions
static let regularVisiblePlan: [SnapshotBackendKind] = [.recursiveTree, .querySweep, .privateAX]
static let compactInteractivePlan: [SnapshotBackendKind] = [.querySweep, .privateAX]
static let rawDiagnosticPlan: [SnapshotBackendKind] = [.recursiveTree, .privateAX]
// MARK: Plan runner
+5 -5
View File
@@ -35,16 +35,16 @@ Protocol and maintenance references:
## Snapshot Strategy
iOS snapshots have two explicit capture modes:
iOS snapshots have two explicit public capture modes:
- full/raw snapshots use recursive XCTest snapshots for rich hierarchy and diagnostics;
- compact interactive snapshots use a bounded flat XCTest query path for fast agent-facing refs.
- interactive snapshots filter the same visible tree down to agent-facing refs.
Some simulator apps expose accessibility trees that lower-level AX services can inspect but XCTest
cannot serialize reliably. In those cases compact interactive snapshots may return a sparse root
quickly, while full snapshots preserve the XCTest error. See
cannot serialize reliably. In those cases interactive snapshots may return a sparse root quickly,
while full snapshots preserve the XCTest error. See
[`../docs/adr/0004-ios-snapshot-backend-strategy.md`](../docs/adr/0004-ios-snapshot-backend-strategy.md)
for the backend boundary and the rationale for a future simulator AX-service backend.
for the backend boundary and future simulator AX-service direction.
## Protocol Notes
-1
View File
@@ -25,7 +25,6 @@ Examples:
{
"command": "snapshot",
"interactiveOnly": true,
"compact": true,
"depth": 2,
"scope": "app",
"raw": false