Skip to content

Commit 8b698e8

Browse files
authored
fix: stabilize private AX settle snapshots (#1784)
* fix: stabilize private AX settle snapshots * fix: harden private AX settling
1 parent 75a4817 commit 8b698e8

20 files changed

Lines changed: 728 additions & 210 deletions

.github/workflows/ios.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ jobs:
113113
-only-testing:AgentDeviceRunnerUITests/RunnerTests/testPrivateAXDepthLimitedRequiresEveryFrontierResolved \
114114
-only-testing:AgentDeviceRunnerUITests/RunnerTests/testDeepExtensionCountsMissedFrontiers \
115115
-only-testing:AgentDeviceRunnerUITests/RunnerTests/testPreferredPrivateAXBackendPlansAsPenalized \
116+
-only-testing:AgentDeviceRunnerUITests/RunnerTests/testPrivateAXRegularPresentationProjectsToViewportAndKeepsScrollHint \
117+
-only-testing:AgentDeviceRunnerUITests/RunnerTests/testPrivateAXGeometrylessSemanticsAreNeverActionableOrScrollContexts \
118+
-only-testing:AgentDeviceRunnerUITests/RunnerTests/testPrivateAXScopeSelectsSubtreeNotMatchingLabels \
119+
-only-testing:AgentDeviceRunnerUITests/RunnerTests/testPrivateAXInteractiveFiltersLoginLikeHiddenDrawer \
116120
-only-testing:AgentDeviceRunnerUITests/RunnerTests/testDecodedPreferredBackendReachesOptionsAndApplicablePlan \
117121
-only-testing:AgentDeviceRunnerUITests/RunnerTests/testSparsePayloadReasonMatrix \
118122
-only-testing:AgentDeviceRunnerUITests/RunnerTests/testRunnerScreenshotStabilitySettledNeedsEnoughSamples \

apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift

Lines changed: 13 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -175,15 +175,10 @@ extension RunnerTests {
175175

176176
let rootFrame = privateAXRect(root["frame"])
177177
let viewport = privateAXSnapshotViewport(app: app, rootFrame: rootFrame)
178-
var nodes: [SnapshotNode] = []
179-
appendPrivateAXNode(
180-
root,
181-
to: &nodes,
178+
let nodes = privateAXPresentation(
179+
rawRoot: root,
182180
options: options,
183-
viewport: viewport,
184-
depth: 0,
185-
parentIndex: nil,
186-
insideMatchedScope: false
181+
viewport: viewport
187182
)
188183
if nodes.count <= 1 {
189184
NSLog("AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_SPARSE=%ld", nodes.count)
@@ -250,110 +245,7 @@ extension RunnerTests {
250245
}
251246
}
252247

253-
private func appendPrivateAXNode(
254-
_ rawNode: [String: Any],
255-
to nodes: inout [SnapshotNode],
256-
options: SnapshotOptions,
257-
viewport: CGRect,
258-
depth: Int,
259-
parentIndex: Int?,
260-
insideMatchedScope: Bool
261-
) {
262-
if let limit = options.depth, depth > limit { return }
263-
264-
let rect = privateAXRect(rawNode["frame"])
265-
let label = privateAXString(rawNode["label"])
266-
let identifier = privateAXString(rawNode["identifier"])
267-
let value = privateAXString(rawNode["value"])
268-
let rawType = privateAXInt(rawNode["type"]) ?? 0
269-
let typeName = elementTypeName(rawElementType: rawType)
270-
let enabled = privateAXBool(rawNode["enabled"]) ?? true
271-
let visible = isVisibleInViewport(rect, viewport)
272-
let interactiveCandidate = privateAXInteractiveCandidate(rawElementType: rawType)
273-
let filterDecision = flatSnapshotFilterDecision(
274-
FlatSnapshotFilterNode(
275-
isRoot: parentIndex == nil,
276-
label: label,
277-
identifier: identifier,
278-
valueText: value.isEmpty ? nil : value,
279-
visible: visible
280-
),
281-
options: options,
282-
insideMatchedScope: insideMatchedScope
283-
)
284-
let include = filterDecision.include
285-
let nowInsideScope = filterDecision.insideMatchedScope
286-
287-
let currentIndex: Int?
288-
if include {
289-
currentIndex = nodes.count
290-
nodes.append(
291-
SnapshotNode(
292-
index: nodes.count,
293-
type: typeName,
294-
label: label.isEmpty ? nil : label,
295-
identifier: identifier.isEmpty ? nil : identifier,
296-
value: value.isEmpty ? nil : value,
297-
rect: snapshotRect(from: rect),
298-
enabled: enabled,
299-
focused: privateAXBool(rawNode["focused"]) == true ? true : nil,
300-
selected: privateAXBool(rawNode["selected"]) == true ? true : nil,
301-
hittable: visible && enabled && interactiveCandidate,
302-
depth: depth,
303-
parentIndex: parentIndex,
304-
hiddenContentAbove: nil,
305-
hiddenContentBelow: nil,
306-
actions: rawNode["actions"] as? [String]
307-
)
308-
)
309-
} else {
310-
currentIndex = parentIndex
311-
}
312-
313-
guard let children = rawNode["children"] as? [[String: Any]] else {
314-
return
315-
}
316-
for child in children {
317-
appendPrivateAXNode(
318-
child,
319-
to: &nodes,
320-
options: options,
321-
viewport: viewport,
322-
depth: depth + 1,
323-
parentIndex: currentIndex,
324-
insideMatchedScope: nowInsideScope
325-
)
326-
}
327-
}
328-
329-
private func elementTypeName(rawElementType: Int) -> String {
330-
if let type = flatSnapshotElementType(rawElementType: rawElementType) {
331-
return elementTypeName(type)
332-
}
333-
return "Element(\(rawElementType))"
334-
}
335-
336-
private func privateAXString(_ value: Any?) -> String {
337-
guard let value else { return "" }
338-
if let string = value as? String {
339-
return string.trimmingCharacters(in: .whitespacesAndNewlines)
340-
}
341-
return String(describing: value).trimmingCharacters(in: .whitespacesAndNewlines)
342-
}
343-
344-
private func privateAXInt(_ value: Any?) -> Int? {
345-
if let value = value as? Int { return value }
346-
if let value = value as? NSNumber { return value.intValue }
347-
return nil
348-
}
349-
350-
private func privateAXBool(_ value: Any?) -> Bool? {
351-
if let value = value as? Bool { return value }
352-
if let value = value as? NSNumber { return value.boolValue }
353-
return nil
354-
}
355-
356-
private func privateAXRect(_ value: Any?) -> CGRect {
248+
func privateAXRect(_ value: Any?) -> CGRect {
357249
guard let frame = value as? [String: Any] else {
358250
return .zero
359251
}
@@ -742,15 +634,10 @@ extension RunnerTests {
742634
],
743635
],
744636
]
745-
var nodes: [SnapshotNode] = []
746-
appendPrivateAXNode(
747-
tree,
748-
to: &nodes,
637+
let nodes = privateAXPresentation(
638+
rawRoot: tree,
749639
options: SnapshotOptions(interactiveOnly: false, depth: nil, scope: nil, raw: false),
750-
viewport: CGRect(x: 0, y: 0, width: 390, height: 844),
751-
depth: 0,
752-
parentIndex: nil,
753-
insideMatchedScope: false
640+
viewport: CGRect(x: 0, y: 0, width: 390, height: 844)
754641
)
755642

756643
let card = nodes.first { $0.label == "feedItem-by-whiskers.test" }
@@ -772,15 +659,10 @@ extension RunnerTests {
772659
["type": 9, "label": "unrelated sibling", "children": []],
773660
],
774661
]
775-
var nodes: [SnapshotNode] = []
776-
appendPrivateAXNode(
777-
tree,
778-
to: &nodes,
662+
let nodes = privateAXPresentation(
663+
rawRoot: tree,
779664
options: SnapshotOptions(interactiveOnly: false, depth: nil, scope: "homeScreen", raw: false),
780-
viewport: .infinite,
781-
depth: 0,
782-
parentIndex: nil,
783-
insideMatchedScope: false
665+
viewport: .infinite
784666
)
785667

786668
let labels = nodes.compactMap { $0.label ?? $0.identifier }
@@ -854,15 +736,10 @@ extension RunnerTests {
854736
]
855737
],
856738
]
857-
var nodes: [SnapshotNode] = []
858-
appendPrivateAXNode(
859-
tree,
860-
to: &nodes,
739+
let nodes = privateAXPresentation(
740+
rawRoot: tree,
861741
options: SnapshotOptions(interactiveOnly: true, depth: nil, scope: nil, raw: false),
862-
viewport: CGRect(x: 0, y: 0, width: 390, height: 844),
863-
depth: 0,
864-
parentIndex: nil,
865-
insideMatchedScope: false
742+
viewport: CGRect(x: 0, y: 0, width: 390, height: 844)
866743
)
867744

868745
let labels = nodes.compactMap { $0.label }

apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,16 @@ struct FlatSnapshotFilterDecision {
1818
let insideMatchedScope: Bool
1919
}
2020

21+
enum FlatSnapshotVisibilityPolicy {
22+
case interactiveOnly
23+
case viewportProjected
24+
}
25+
2126
extension RunnerTests {
2227
func flatSnapshotFilterDecision(
2328
_ node: FlatSnapshotFilterNode,
2429
options: SnapshotOptions,
30+
visibilityPolicy: FlatSnapshotVisibilityPolicy,
2531
insideMatchedScope: Bool
2632
) -> FlatSnapshotFilterDecision {
2733
let scope = options.scope?.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -39,7 +45,9 @@ extension RunnerTests {
3945
include = true
4046
} else if scopeActive && !nowInsideScope {
4147
include = false
42-
} else if options.interactiveOnly && !node.visible {
48+
} else if !node.visible
49+
&& (options.interactiveOnly || visibilityPolicy == .viewportProjected)
50+
{
4351
include = false
4452
} else {
4553
include = true
@@ -87,32 +95,59 @@ extension RunnerTests {
8795
valueText: nil,
8896
visible: true
8997
)
98+
let hiddenRoot = FlatSnapshotFilterNode(
99+
isRoot: true,
100+
label: "App",
101+
identifier: "",
102+
valueText: nil,
103+
visible: false
104+
)
90105

91106
XCTAssertTrue(
92107
flatSnapshotFilterDecision(
93108
visibleContent,
94109
options: SnapshotOptions(interactiveOnly: false, depth: nil, scope: nil, raw: false),
110+
visibilityPolicy: .interactiveOnly,
95111
insideMatchedScope: false
96112
).include
97113
)
98114
XCTAssertFalse(
99115
flatSnapshotFilterDecision(
100116
hiddenInteractive,
101117
options: SnapshotOptions(interactiveOnly: true, depth: nil, scope: nil, raw: false),
118+
visibilityPolicy: .interactiveOnly,
119+
insideMatchedScope: false
120+
).include
121+
)
122+
XCTAssertFalse(
123+
flatSnapshotFilterDecision(
124+
hiddenInteractive,
125+
options: SnapshotOptions(interactiveOnly: false, depth: nil, scope: nil, raw: false),
126+
visibilityPolicy: .viewportProjected,
102127
insideMatchedScope: false
103128
).include
104129
)
105130
XCTAssertTrue(
106131
flatSnapshotFilterDecision(
107-
decorative,
132+
hiddenInteractive,
133+
options: SnapshotOptions(interactiveOnly: false, depth: nil, scope: nil, raw: false),
134+
visibilityPolicy: .interactiveOnly,
135+
insideMatchedScope: false
136+
).include
137+
)
138+
XCTAssertTrue(
139+
flatSnapshotFilterDecision(
140+
hiddenRoot,
108141
options: SnapshotOptions(interactiveOnly: false, depth: nil, scope: nil, raw: false),
142+
visibilityPolicy: .viewportProjected,
109143
insideMatchedScope: false
110144
).include
111145
)
112146
XCTAssertTrue(
113147
flatSnapshotFilterDecision(
114148
decorative,
115149
options: SnapshotOptions(interactiveOnly: false, depth: nil, scope: nil, raw: false),
150+
visibilityPolicy: .interactiveOnly,
116151
insideMatchedScope: false
117152
).include
118153
)
@@ -138,6 +173,7 @@ extension RunnerTests {
138173
let rootDecision = flatSnapshotFilterDecision(
139174
scopeRoot,
140175
options: options,
176+
visibilityPolicy: .interactiveOnly,
141177
insideMatchedScope: false
142178
)
143179
XCTAssertTrue(rootDecision.include)
@@ -147,13 +183,15 @@ extension RunnerTests {
147183
flatSnapshotFilterDecision(
148184
unmatchedDescendant,
149185
options: options,
186+
visibilityPolicy: .interactiveOnly,
150187
insideMatchedScope: rootDecision.insideMatchedScope
151188
).include
152189
)
153190
XCTAssertFalse(
154191
flatSnapshotFilterDecision(
155192
unmatchedDescendant,
156193
options: options,
194+
visibilityPolicy: .interactiveOnly,
157195
insideMatchedScope: false
158196
).include
159197
)

0 commit comments

Comments
 (0)