agent-device 0.17.2 → 0.17.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.17.2.apk → agent-device-android-multitouch-helper-0.17.4.apk} +0 -0
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.17.4.apk.sha256 +1 -0
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.17.2.manifest.json → agent-device-android-multitouch-helper-0.17.4.manifest.json} +4 -4
- package/android-snapshot-helper/README.md +3 -0
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.17.2.apk → agent-device-android-snapshot-helper-0.17.4.apk} +0 -0
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.17.4.apk.sha256 +1 -0
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.17.2.manifest.json → agent-device-android-snapshot-helper-0.17.4.manifest.json} +6 -6
- package/dist/src/123.js +1 -1
- package/dist/src/1231.js +1 -1
- package/dist/src/1352.js +1 -1
- package/dist/src/221.js +2 -2
- package/dist/src/2403.js +3 -3
- package/dist/src/2415.js +37 -29
- package/dist/src/4012.js +4 -0
- package/dist/src/5611.js +1 -0
- package/dist/src/5827.js +1 -0
- package/dist/src/6088.js +1 -0
- package/dist/src/6133.js +1 -0
- package/dist/src/7847.js +1 -1
- package/dist/src/9471.js +1 -1
- package/dist/src/9542.js +6 -3
- package/dist/src/9616.js +2 -1
- package/dist/src/9818.js +1 -1
- package/dist/src/android-snapshot-helper.d.ts +0 -1
- package/dist/src/android.js +5 -5
- package/dist/src/apple.js +1 -1
- package/dist/src/apps.js +10 -10
- package/dist/src/args.js +53 -14
- package/dist/src/batch.js +1 -1
- package/dist/src/cli-output.js +30 -0
- package/dist/src/cli.js +15 -16
- package/dist/src/command-surface.js +1 -1
- package/dist/src/contracts.d.ts +52 -0
- package/dist/src/contracts.js +1 -1
- package/dist/src/find.js +1 -1
- package/dist/src/generic.js +5 -5
- package/dist/src/index.d.ts +127 -8
- package/dist/src/index.js +1 -1
- package/dist/src/input-actions.js +1 -1
- package/dist/src/interaction.js +1 -1
- package/dist/src/react-native.js +1 -1
- package/dist/src/record-trace-recording.js +3 -3
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/selectors.d.ts +14 -0
- package/dist/src/selectors.js +1 -1
- package/dist/src/server.js +2 -2
- package/dist/src/session.js +8 -10
- package/dist/src/snapshot.js +2 -2
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +1 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +14 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +297 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +327 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +122 -173
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +153 -5
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +171 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +0 -10
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +64 -18
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +200 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +450 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +82 -178
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +425 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +77 -6
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +10 -1
- package/ios-runner/README.md +5 -5
- package/ios-runner/RUNNER_PROTOCOL.md +0 -1
- package/package.json +3 -3
- package/server.json +2 -2
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.17.2.apk.sha256 +0 -1
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.17.2.apk.sha256 +0 -1
- package/dist/src/1534.js +0 -1
- package/dist/src/1620.js +0 -1
- package/dist/src/1644.js +0 -2
- package/dist/src/2284.js +0 -1
- package/dist/src/2474.js +0 -1
- package/dist/src/2672.js +0 -1
- package/dist/src/3393.js +0 -1
- package/dist/src/5898.js +0 -1
- package/dist/src/695.js +0 -1
- package/dist/src/7455.js +0 -1
- package/dist/src/8173.js +0 -27
- package/dist/src/8407.js +0 -1
- package/dist/src/8699.js +0 -1
- package/dist/src/9238.js +0 -4
- package/dist/src/9673.js +0 -1
- package/dist/src/9974.js +0 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
|
|
3
|
+
struct FlatSnapshotFilterNode {
|
|
4
|
+
let isRoot: Bool
|
|
5
|
+
let label: String
|
|
6
|
+
let identifier: String
|
|
7
|
+
let valueText: String?
|
|
8
|
+
let visible: Bool
|
|
9
|
+
|
|
10
|
+
var hasContent: Bool {
|
|
11
|
+
return !label.isEmpty || !identifier.isEmpty || valueText != nil
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
func matchesScope(_ scope: String) -> Bool {
|
|
15
|
+
let haystack = [label, identifier, valueText ?? ""].joined(separator: "\n")
|
|
16
|
+
return haystack.localizedCaseInsensitiveContains(scope)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
struct FlatSnapshotFilterDecision {
|
|
21
|
+
let include: Bool
|
|
22
|
+
let insideMatchedScope: Bool
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
extension RunnerTests {
|
|
26
|
+
func flatSnapshotFilterDecision(
|
|
27
|
+
_ node: FlatSnapshotFilterNode,
|
|
28
|
+
options: SnapshotOptions,
|
|
29
|
+
insideMatchedScope: Bool
|
|
30
|
+
) -> FlatSnapshotFilterDecision {
|
|
31
|
+
let scope = options.scope?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
32
|
+
let scopeActive = scope?.isEmpty == false
|
|
33
|
+
let matchesScope: Bool
|
|
34
|
+
if scopeActive, let scope {
|
|
35
|
+
matchesScope = node.matchesScope(scope)
|
|
36
|
+
} else {
|
|
37
|
+
matchesScope = false
|
|
38
|
+
}
|
|
39
|
+
let nowInsideScope = insideMatchedScope || matchesScope
|
|
40
|
+
|
|
41
|
+
let include: Bool
|
|
42
|
+
if node.isRoot {
|
|
43
|
+
include = true
|
|
44
|
+
} else if scopeActive && !nowInsideScope {
|
|
45
|
+
include = false
|
|
46
|
+
} else if options.interactiveOnly && !node.visible {
|
|
47
|
+
include = false
|
|
48
|
+
} else {
|
|
49
|
+
include = true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return FlatSnapshotFilterDecision(include: include, insideMatchedScope: nowInsideScope)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func privateAXInteractiveCandidate(rawElementType: Int) -> Bool {
|
|
56
|
+
guard let type = flatSnapshotElementType(rawElementType: rawElementType) else {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
return interactiveTypes.contains(type) || Self.scrollContainerTypes.contains(type)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func flatSnapshotElementType(rawElementType: Int) -> XCUIElement.ElementType? {
|
|
63
|
+
guard let raw = UInt(exactly: rawElementType),
|
|
64
|
+
let type = XCUIElement.ElementType(rawValue: raw)
|
|
65
|
+
else {
|
|
66
|
+
return nil
|
|
67
|
+
}
|
|
68
|
+
return type
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func testFlatSnapshotFilterDecisionMatrixCoversOptions() {
|
|
72
|
+
let visibleContent = FlatSnapshotFilterNode(
|
|
73
|
+
isRoot: false,
|
|
74
|
+
label: "Welcome back",
|
|
75
|
+
identifier: "",
|
|
76
|
+
valueText: nil,
|
|
77
|
+
visible: true
|
|
78
|
+
)
|
|
79
|
+
let hiddenInteractive = FlatSnapshotFilterNode(
|
|
80
|
+
isRoot: false,
|
|
81
|
+
label: "Hidden menu",
|
|
82
|
+
identifier: "",
|
|
83
|
+
valueText: nil,
|
|
84
|
+
visible: false
|
|
85
|
+
)
|
|
86
|
+
let decorative = FlatSnapshotFilterNode(
|
|
87
|
+
isRoot: false,
|
|
88
|
+
label: "",
|
|
89
|
+
identifier: "",
|
|
90
|
+
valueText: nil,
|
|
91
|
+
visible: true
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
XCTAssertTrue(
|
|
95
|
+
flatSnapshotFilterDecision(
|
|
96
|
+
visibleContent,
|
|
97
|
+
options: SnapshotOptions(interactiveOnly: false, depth: nil, scope: nil, raw: false),
|
|
98
|
+
insideMatchedScope: false
|
|
99
|
+
).include
|
|
100
|
+
)
|
|
101
|
+
XCTAssertFalse(
|
|
102
|
+
flatSnapshotFilterDecision(
|
|
103
|
+
hiddenInteractive,
|
|
104
|
+
options: SnapshotOptions(interactiveOnly: true, depth: nil, scope: nil, raw: false),
|
|
105
|
+
insideMatchedScope: false
|
|
106
|
+
).include
|
|
107
|
+
)
|
|
108
|
+
XCTAssertTrue(
|
|
109
|
+
flatSnapshotFilterDecision(
|
|
110
|
+
decorative,
|
|
111
|
+
options: SnapshotOptions(interactiveOnly: false, depth: nil, scope: nil, raw: false),
|
|
112
|
+
insideMatchedScope: false
|
|
113
|
+
).include
|
|
114
|
+
)
|
|
115
|
+
XCTAssertTrue(
|
|
116
|
+
flatSnapshotFilterDecision(
|
|
117
|
+
decorative,
|
|
118
|
+
options: SnapshotOptions(interactiveOnly: false, depth: nil, scope: nil, raw: false),
|
|
119
|
+
insideMatchedScope: false
|
|
120
|
+
).include
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func testFlatSnapshotFilterDecisionCarriesSubtreeScopeState() {
|
|
125
|
+
let scopeRoot = FlatSnapshotFilterNode(
|
|
126
|
+
isRoot: false,
|
|
127
|
+
label: "",
|
|
128
|
+
identifier: "homeScreen",
|
|
129
|
+
valueText: nil,
|
|
130
|
+
visible: true
|
|
131
|
+
)
|
|
132
|
+
let unmatchedDescendant = FlatSnapshotFilterNode(
|
|
133
|
+
isRoot: false,
|
|
134
|
+
label: "Post body without the scope text",
|
|
135
|
+
identifier: "",
|
|
136
|
+
valueText: nil,
|
|
137
|
+
visible: true
|
|
138
|
+
)
|
|
139
|
+
let options = SnapshotOptions(interactiveOnly: false, depth: nil, scope: "homeScreen", raw: false)
|
|
140
|
+
|
|
141
|
+
let rootDecision = flatSnapshotFilterDecision(
|
|
142
|
+
scopeRoot,
|
|
143
|
+
options: options,
|
|
144
|
+
insideMatchedScope: false
|
|
145
|
+
)
|
|
146
|
+
XCTAssertTrue(rootDecision.include)
|
|
147
|
+
XCTAssertTrue(rootDecision.insideMatchedScope)
|
|
148
|
+
|
|
149
|
+
XCTAssertTrue(
|
|
150
|
+
flatSnapshotFilterDecision(
|
|
151
|
+
unmatchedDescendant,
|
|
152
|
+
options: options,
|
|
153
|
+
insideMatchedScope: rootDecision.insideMatchedScope
|
|
154
|
+
).include
|
|
155
|
+
)
|
|
156
|
+
XCTAssertFalse(
|
|
157
|
+
flatSnapshotFilterDecision(
|
|
158
|
+
unmatchedDescendant,
|
|
159
|
+
options: options,
|
|
160
|
+
insideMatchedScope: false
|
|
161
|
+
).include
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
func testPrivateAXInteractiveCandidatesPreserveBackendInputs() {
|
|
166
|
+
XCTAssertTrue(
|
|
167
|
+
privateAXInteractiveCandidate(rawElementType: Int(XCUIElement.ElementType.scrollView.rawValue)),
|
|
168
|
+
"private AX marks scroll containers as interactive candidates"
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -841,16 +841,6 @@ extension RunnerTests {
|
|
|
841
841
|
#endif
|
|
842
842
|
}
|
|
843
843
|
|
|
844
|
-
func runSeries(count: Int, pauseMs: Double, operation: (Int) -> Void) {
|
|
845
|
-
let total = max(count, 1)
|
|
846
|
-
let pause = max(pauseMs, 0)
|
|
847
|
-
for idx in 0..<total {
|
|
848
|
-
operation(idx)
|
|
849
|
-
if idx < total - 1 && pause > 0 {
|
|
850
|
-
sleepFor(pause / 1000.0)
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
844
|
|
|
855
845
|
func swipe(app: XCUIApplication, direction: String) -> DragVisualizationFrame? {
|
|
856
846
|
if performTvRemoteSwipeIfAvailable(direction: direction) {
|
|
@@ -3,14 +3,12 @@
|
|
|
3
3
|
enum CommandType: String, Codable {
|
|
4
4
|
case tap
|
|
5
5
|
case mouseClick
|
|
6
|
-
case tapSeries
|
|
7
6
|
case longPress
|
|
8
|
-
case interactionFrame
|
|
9
7
|
case drag
|
|
10
|
-
case dragSeries
|
|
11
8
|
case remotePress
|
|
12
9
|
case type
|
|
13
10
|
case swipe
|
|
11
|
+
case scroll
|
|
14
12
|
case findText
|
|
15
13
|
case querySelector
|
|
16
14
|
case readText
|
|
@@ -26,6 +24,7 @@ enum CommandType: String, Codable {
|
|
|
26
24
|
case keyboardReturn
|
|
27
25
|
case alert
|
|
28
26
|
case pinch
|
|
27
|
+
case sequence
|
|
29
28
|
case rotateGesture
|
|
30
29
|
case transformGesture
|
|
31
30
|
case recordStart
|
|
@@ -68,16 +67,16 @@ extension CommandType {
|
|
|
68
67
|
var traits: CommandTraits {
|
|
69
68
|
switch self {
|
|
70
69
|
// Interaction commands: require the foreground-guard + stabilization preflight.
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
case .tap, .
|
|
70
|
+
// keyboardReturn is the sibling of keyboardDismiss (missing from the historical switch —
|
|
71
|
+
// drift the table now prevents). .scroll is the fused frame-resolve + drag scroll; same
|
|
72
|
+
// classification as .drag. .sequence is the fused multi-step gesture batch.
|
|
73
|
+
case .tap, .longPress, .drag, .remotePress, .type, .swipe, .scroll,
|
|
75
74
|
.back, .backInApp, .backSystem, .rotate, .appSwitcher,
|
|
76
|
-
.keyboardDismiss, .keyboardReturn, .pinch, .rotateGesture, .transformGesture:
|
|
75
|
+
.keyboardDismiss, .keyboardReturn, .pinch, .sequence, .rotateGesture, .transformGesture:
|
|
77
76
|
return CommandTraits(isInteraction: true, readOnly: .never, isLifecycle: false)
|
|
78
77
|
|
|
79
78
|
// Read-only reads: eligible for the session-invalidating retry.
|
|
80
|
-
case .
|
|
79
|
+
case .findText, .readText, .snapshot:
|
|
81
80
|
return CommandTraits(isInteraction: false, readOnly: .always, isLifecycle: false)
|
|
82
81
|
|
|
83
82
|
// Screenshot is both a read and a runner-lifecycle command (skips app-activation preflight).
|
|
@@ -123,17 +122,14 @@ struct Command: Codable {
|
|
|
123
122
|
let y: Double?
|
|
124
123
|
let button: String?
|
|
125
124
|
let remoteButton: String?
|
|
126
|
-
let count: Double?
|
|
127
|
-
let intervalMs: Double?
|
|
128
|
-
let doubleTap: Bool?
|
|
129
|
-
let pauseMs: Double?
|
|
130
|
-
let pattern: String?
|
|
131
125
|
let x2: Double?
|
|
132
126
|
let y2: Double?
|
|
133
127
|
let dx: Double?
|
|
134
128
|
let dy: Double?
|
|
135
129
|
let durationMs: Double?
|
|
136
130
|
let direction: String?
|
|
131
|
+
let amount: Double?
|
|
132
|
+
let pixels: Double?
|
|
137
133
|
let orientation: String?
|
|
138
134
|
let scale: Double?
|
|
139
135
|
let degrees: Double?
|
|
@@ -142,12 +138,39 @@ struct Command: Codable {
|
|
|
142
138
|
let fps: Int?
|
|
143
139
|
let quality: Int?
|
|
144
140
|
let interactiveOnly: Bool?
|
|
145
|
-
let compact: Bool?
|
|
146
141
|
let depth: Int?
|
|
147
142
|
let scope: String?
|
|
148
143
|
let raw: Bool?
|
|
149
144
|
let fullscreen: Bool?
|
|
150
145
|
let synthesized: Bool?
|
|
146
|
+
let steps: [SequenceStep]?
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/// One allowlisted coordinate gesture step inside a fused `sequence` command.
|
|
150
|
+
/// `kind` is decoded as a raw String (not an enum) so the runner can return a clear
|
|
151
|
+
/// INVALID_ARGS for an unknown kind instead of a generic decode failure.
|
|
152
|
+
struct SequenceStep: Codable {
|
|
153
|
+
let kind: String
|
|
154
|
+
let x: Double?
|
|
155
|
+
let y: Double?
|
|
156
|
+
let x2: Double?
|
|
157
|
+
let y2: Double?
|
|
158
|
+
let durationMs: Double?
|
|
159
|
+
let pauseMs: Double?
|
|
160
|
+
/// For `tap`/`drag` steps on iOS non-tv: use the synthesized HID fast path instead of the
|
|
161
|
+
/// drag-based XCUICoordinate path, matching the individual command behavior.
|
|
162
|
+
let synthesized: Bool?
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// Per-step result for a `sequence` response. `ok:false` carries the failing step's
|
|
166
|
+
/// errorCode/errorMessage; execution stops at the first failed step.
|
|
167
|
+
struct SequenceStepResult: Codable {
|
|
168
|
+
let ok: Bool
|
|
169
|
+
let kind: String
|
|
170
|
+
let errorCode: String?
|
|
171
|
+
let errorMessage: String?
|
|
172
|
+
let gestureStartUptimeMs: Double?
|
|
173
|
+
let gestureEndUptimeMs: Double?
|
|
151
174
|
}
|
|
152
175
|
|
|
153
176
|
struct Response: Codable {
|
|
@@ -162,6 +185,18 @@ struct Response: Codable {
|
|
|
162
185
|
}
|
|
163
186
|
}
|
|
164
187
|
|
|
188
|
+
extension Response {
|
|
189
|
+
// The daemon pairs this gesture-clock anchor with its own receipt time to map
|
|
190
|
+
// gesture uptimes onto wall-clock for the recording touch overlay. Error responses
|
|
191
|
+
// carry no anchor so the daemon falls back instead of pairing a stale value.
|
|
192
|
+
func stampingCurrentUptimeMs(_ value: Double) -> Response {
|
|
193
|
+
guard ok else { return self }
|
|
194
|
+
var payload = data ?? DataPayload()
|
|
195
|
+
payload.currentUptimeMs = value
|
|
196
|
+
return Response(ok: ok, data: payload, error: error)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
165
200
|
struct DataPayload: Codable {
|
|
166
201
|
let message: String?
|
|
167
202
|
let text: String?
|
|
@@ -169,6 +204,7 @@ struct DataPayload: Codable {
|
|
|
169
204
|
let items: [String]?
|
|
170
205
|
let nodes: [SnapshotNode]?
|
|
171
206
|
let truncated: Bool?
|
|
207
|
+
let snapshotQuality: SnapshotQuality?
|
|
172
208
|
let gestureStartUptimeMs: Double?
|
|
173
209
|
let gestureEndUptimeMs: Double?
|
|
174
210
|
let x: Double?
|
|
@@ -177,7 +213,7 @@ struct DataPayload: Codable {
|
|
|
177
213
|
let y2: Double?
|
|
178
214
|
let referenceWidth: Double?
|
|
179
215
|
let referenceHeight: Double?
|
|
180
|
-
|
|
216
|
+
var currentUptimeMs: Double?
|
|
181
217
|
let commandId: String?
|
|
182
218
|
let lifecycleState: String?
|
|
183
219
|
let lifecycleCommand: String?
|
|
@@ -195,6 +231,9 @@ struct DataPayload: Codable {
|
|
|
195
231
|
let gestureFallbackHint: String?
|
|
196
232
|
let runnerFatal: Bool?
|
|
197
233
|
let runnerFatalReason: String?
|
|
234
|
+
let completedSteps: Int?
|
|
235
|
+
let failedStepIndex: Int?
|
|
236
|
+
let sequenceResults: [SequenceStepResult]?
|
|
198
237
|
|
|
199
238
|
init(
|
|
200
239
|
message: String? = nil,
|
|
@@ -203,6 +242,7 @@ struct DataPayload: Codable {
|
|
|
203
242
|
items: [String]? = nil,
|
|
204
243
|
nodes: [SnapshotNode]? = nil,
|
|
205
244
|
truncated: Bool? = nil,
|
|
245
|
+
snapshotQuality: SnapshotQuality? = nil,
|
|
206
246
|
gestureStartUptimeMs: Double? = nil,
|
|
207
247
|
gestureEndUptimeMs: Double? = nil,
|
|
208
248
|
x: Double? = nil,
|
|
@@ -228,7 +268,10 @@ struct DataPayload: Codable {
|
|
|
228
268
|
gestureFallbackMessage: String? = nil,
|
|
229
269
|
gestureFallbackHint: String? = nil,
|
|
230
270
|
runnerFatal: Bool? = nil,
|
|
231
|
-
runnerFatalReason: String? = nil
|
|
271
|
+
runnerFatalReason: String? = nil,
|
|
272
|
+
completedSteps: Int? = nil,
|
|
273
|
+
failedStepIndex: Int? = nil,
|
|
274
|
+
sequenceResults: [SequenceStepResult]? = nil
|
|
232
275
|
) {
|
|
233
276
|
self.message = message
|
|
234
277
|
self.text = text
|
|
@@ -236,6 +279,7 @@ struct DataPayload: Codable {
|
|
|
236
279
|
self.items = items
|
|
237
280
|
self.nodes = nodes
|
|
238
281
|
self.truncated = truncated
|
|
282
|
+
self.snapshotQuality = snapshotQuality
|
|
239
283
|
self.gestureStartUptimeMs = gestureStartUptimeMs
|
|
240
284
|
self.gestureEndUptimeMs = gestureEndUptimeMs
|
|
241
285
|
self.x = x
|
|
@@ -262,6 +306,9 @@ struct DataPayload: Codable {
|
|
|
262
306
|
self.gestureFallbackHint = gestureFallbackHint
|
|
263
307
|
self.runnerFatal = runnerFatal
|
|
264
308
|
self.runnerFatalReason = runnerFatalReason
|
|
309
|
+
self.completedSteps = completedSteps
|
|
310
|
+
self.failedStepIndex = failedStepIndex
|
|
311
|
+
self.sequenceResults = sequenceResults
|
|
265
312
|
}
|
|
266
313
|
}
|
|
267
314
|
|
|
@@ -303,7 +350,6 @@ struct SnapshotNode: Codable {
|
|
|
303
350
|
|
|
304
351
|
struct SnapshotOptions {
|
|
305
352
|
let interactiveOnly: Bool
|
|
306
|
-
let compact: Bool
|
|
307
353
|
let depth: Int?
|
|
308
354
|
let scope: String?
|
|
309
355
|
let raw: Bool
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
|
|
3
|
+
// Swift port of buildScrollGesturePlan from src/core/scroll-gesture.ts.
|
|
4
|
+
//
|
|
5
|
+
// This is a deliberate two-place invariant: the daemon keeps the TS implementation (for Android,
|
|
6
|
+
// recording, and reported-pixels), and the runner places the gesture with this Swift copy. The
|
|
7
|
+
// parity test vectors at the bottom of this file mirror src/core/__tests__/scroll-gesture.test.ts —
|
|
8
|
+
// if you change the math in either language, update the other and both vector sets.
|
|
9
|
+
//
|
|
10
|
+
// All inputs here are positive (reference dims, travel, center), so Swift's `.rounded()`
|
|
11
|
+
// (half away from zero) matches JS `Math.round` (half up) on every value computed below.
|
|
12
|
+
|
|
13
|
+
struct RunnerScrollGesturePlan {
|
|
14
|
+
let x1: Double
|
|
15
|
+
let y1: Double
|
|
16
|
+
let x2: Double
|
|
17
|
+
let y2: Double
|
|
18
|
+
let travelPixels: Double
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private let runnerDefaultScrollAmount = 0.6
|
|
22
|
+
private let runnerDefaultEdgePaddingFraction = 0.05
|
|
23
|
+
|
|
24
|
+
func runnerScrollGesturePlan(
|
|
25
|
+
direction: String,
|
|
26
|
+
amount: Double?,
|
|
27
|
+
pixels: Double?,
|
|
28
|
+
referenceWidth: Double,
|
|
29
|
+
referenceHeight: Double
|
|
30
|
+
) -> RunnerScrollGesturePlan? {
|
|
31
|
+
// Mirror the TS INVALID_ARGS contract: non-positive or non-finite amount/pixels are rejected
|
|
32
|
+
// rather than clamped into a journaled 1px scroll. The daemon validates before sending, so
|
|
33
|
+
// this only triggers for non-daemon wire clients.
|
|
34
|
+
if let amount, !(amount.isFinite && amount > 0) { return nil }
|
|
35
|
+
if let pixels, !(pixels.isFinite && pixels > 0) { return nil }
|
|
36
|
+
let axisLength = (direction == "up" || direction == "down") ? referenceHeight : referenceWidth
|
|
37
|
+
let requestedAmount = amount ?? runnerDefaultScrollAmount
|
|
38
|
+
let requestedPixels: Double =
|
|
39
|
+
pixels.map { max(1, $0.rounded()) } ?? (axisLength * requestedAmount).rounded()
|
|
40
|
+
let edgePadding = max(1, (axisLength * runnerDefaultEdgePaddingFraction).rounded())
|
|
41
|
+
let maxTravelPixels = max(1, axisLength - edgePadding * 2)
|
|
42
|
+
let travelPixels = max(1, min(requestedPixels, maxTravelPixels))
|
|
43
|
+
let halfTravel = (travelPixels / 2).rounded()
|
|
44
|
+
let centerX = (referenceWidth / 2).rounded()
|
|
45
|
+
let centerY = (referenceHeight / 2).rounded()
|
|
46
|
+
|
|
47
|
+
func plan(_ x1: Double, _ y1: Double, _ x2: Double, _ y2: Double) -> RunnerScrollGesturePlan {
|
|
48
|
+
RunnerScrollGesturePlan(x1: x1, y1: y1, x2: x2, y2: y2, travelPixels: travelPixels)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
switch direction {
|
|
52
|
+
case "up":
|
|
53
|
+
return plan(centerX, centerY - halfTravel, centerX, centerY + halfTravel)
|
|
54
|
+
case "down":
|
|
55
|
+
return plan(centerX, centerY + halfTravel, centerX, centerY - halfTravel)
|
|
56
|
+
case "left":
|
|
57
|
+
return plan(centerX - halfTravel, centerY, centerX + halfTravel, centerY)
|
|
58
|
+
case "right":
|
|
59
|
+
return plan(centerX + halfTravel, centerY, centerX - halfTravel, centerY)
|
|
60
|
+
default:
|
|
61
|
+
return nil
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
extension RunnerTests {
|
|
66
|
+
// Cross-language parity vectors mirroring src/core/__tests__/scroll-gesture.test.ts. Keep these
|
|
67
|
+
// in sync with the vitest vectors so the two buildScrollGesturePlan implementations cannot drift.
|
|
68
|
+
|
|
69
|
+
func testRunnerScrollGesturePlanMapsRelativeAmount() throws {
|
|
70
|
+
let plan = try XCTUnwrap(
|
|
71
|
+
runnerScrollGesturePlan(
|
|
72
|
+
direction: "down",
|
|
73
|
+
amount: 0.5,
|
|
74
|
+
pixels: nil,
|
|
75
|
+
referenceWidth: 400,
|
|
76
|
+
referenceHeight: 800
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
XCTAssertEqual(plan.x1, 200)
|
|
80
|
+
XCTAssertEqual(plan.y1, 600)
|
|
81
|
+
XCTAssertEqual(plan.x2, 200)
|
|
82
|
+
XCTAssertEqual(plan.y2, 200)
|
|
83
|
+
XCTAssertEqual(plan.travelPixels, 400)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func testRunnerScrollGesturePlanPixelsDown() throws {
|
|
87
|
+
// 300x600, down, pixels 120 -> (150,360)->(150,240), travel 120.
|
|
88
|
+
let plan = try XCTUnwrap(
|
|
89
|
+
runnerScrollGesturePlan(
|
|
90
|
+
direction: "down",
|
|
91
|
+
amount: nil,
|
|
92
|
+
pixels: 120,
|
|
93
|
+
referenceWidth: 300,
|
|
94
|
+
referenceHeight: 600
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
XCTAssertEqual(plan.x1, 150)
|
|
98
|
+
XCTAssertEqual(plan.y1, 360)
|
|
99
|
+
XCTAssertEqual(plan.x2, 150)
|
|
100
|
+
XCTAssertEqual(plan.y2, 240)
|
|
101
|
+
XCTAssertEqual(plan.travelPixels, 120)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
func testRunnerScrollGesturePlanClampsAmountAboveOne() throws {
|
|
105
|
+
// 400x800, down, amount 2 -> requested 1600 clamps to the safe band (720): (200,760)->(200,40).
|
|
106
|
+
let plan = try XCTUnwrap(
|
|
107
|
+
runnerScrollGesturePlan(
|
|
108
|
+
direction: "down",
|
|
109
|
+
amount: 2,
|
|
110
|
+
pixels: nil,
|
|
111
|
+
referenceWidth: 400,
|
|
112
|
+
referenceHeight: 800
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
XCTAssertEqual(plan.x1, 200)
|
|
116
|
+
XCTAssertEqual(plan.y1, 760)
|
|
117
|
+
XCTAssertEqual(plan.x2, 200)
|
|
118
|
+
XCTAssertEqual(plan.y2, 40)
|
|
119
|
+
XCTAssertEqual(plan.travelPixels, 720)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
func testRunnerScrollGesturePlanFloorsTinyFrames() throws {
|
|
123
|
+
// 2x2, down, pixels 10 engages every max(1, ...) floor and the .5 rounding cases the two
|
|
124
|
+
// ports must agree on (halfTravel 0.5 -> 1, center 1 from 2/2): (1,2)->(1,0), travel 1.
|
|
125
|
+
let plan = try XCTUnwrap(
|
|
126
|
+
runnerScrollGesturePlan(
|
|
127
|
+
direction: "down",
|
|
128
|
+
amount: nil,
|
|
129
|
+
pixels: 10,
|
|
130
|
+
referenceWidth: 2,
|
|
131
|
+
referenceHeight: 2
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
XCTAssertEqual(plan.x1, 1)
|
|
135
|
+
XCTAssertEqual(plan.y1, 2)
|
|
136
|
+
XCTAssertEqual(plan.x2, 1)
|
|
137
|
+
XCTAssertEqual(plan.y2, 0)
|
|
138
|
+
XCTAssertEqual(plan.travelPixels, 1)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
func testRunnerScrollGesturePlanClampsToSafeBand() throws {
|
|
142
|
+
// 300x600, right, pixels 500 clamps travel to the safe band (270).
|
|
143
|
+
let plan = try XCTUnwrap(
|
|
144
|
+
runnerScrollGesturePlan(
|
|
145
|
+
direction: "right",
|
|
146
|
+
amount: nil,
|
|
147
|
+
pixels: 500,
|
|
148
|
+
referenceWidth: 300,
|
|
149
|
+
referenceHeight: 600
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
XCTAssertEqual(plan.x1, 285)
|
|
153
|
+
XCTAssertEqual(plan.x2, 15)
|
|
154
|
+
XCTAssertEqual(plan.y1, 300)
|
|
155
|
+
XCTAssertEqual(plan.y2, 300)
|
|
156
|
+
XCTAssertEqual(plan.travelPixels, 270)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
func testRunnerScrollGesturePlanRejectsUnknownDirection() {
|
|
160
|
+
XCTAssertNil(
|
|
161
|
+
runnerScrollGesturePlan(
|
|
162
|
+
direction: "sideways",
|
|
163
|
+
amount: nil,
|
|
164
|
+
pixels: 100,
|
|
165
|
+
referenceWidth: 300,
|
|
166
|
+
referenceHeight: 600
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
func testRunnerScrollGesturePlanRejectsInvalidAmountAndPixels() {
|
|
172
|
+
XCTAssertNil(
|
|
173
|
+
runnerScrollGesturePlan(
|
|
174
|
+
direction: "down",
|
|
175
|
+
amount: 0,
|
|
176
|
+
pixels: nil,
|
|
177
|
+
referenceWidth: 300,
|
|
178
|
+
referenceHeight: 600
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
XCTAssertNil(
|
|
182
|
+
runnerScrollGesturePlan(
|
|
183
|
+
direction: "down",
|
|
184
|
+
amount: nil,
|
|
185
|
+
pixels: -10,
|
|
186
|
+
referenceWidth: 300,
|
|
187
|
+
referenceHeight: 600
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
XCTAssertNil(
|
|
191
|
+
runnerScrollGesturePlan(
|
|
192
|
+
direction: "down",
|
|
193
|
+
amount: .infinity,
|
|
194
|
+
pixels: nil,
|
|
195
|
+
referenceWidth: 300,
|
|
196
|
+
referenceHeight: 600
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
}
|