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
package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
|
|
3
|
+
extension RunnerTests {
|
|
4
|
+
// MARK: - Sequence command
|
|
5
|
+
|
|
6
|
+
/// Hard cap mirrored from the daemon (MAX_RUNNER_SEQUENCE_STEPS). Keeps the worst-case
|
|
7
|
+
/// retained journal response well under the 16KB cap and bounds the lost-response window.
|
|
8
|
+
var maxSequenceSteps: Int { 20 }
|
|
9
|
+
|
|
10
|
+
/// Allowlisted step kinds. Validated on both sides so an unsupported kind is rejected with a
|
|
11
|
+
/// clear INVALID_ARGS naming the step index, executing nothing.
|
|
12
|
+
private var sequenceableStepKinds: Set<String> { ["tap", "doubleTap", "longPress", "drag"] }
|
|
13
|
+
|
|
14
|
+
/// Per-step outcome carried by `assembleSequenceExecution`. The timing is captured by the
|
|
15
|
+
/// executor closure (via performGesture) so ordering/stop-on-failure stay device-free testable.
|
|
16
|
+
struct SequenceStepOutcome {
|
|
17
|
+
let outcome: RunnerInteractionOutcome
|
|
18
|
+
let gestureStartUptimeMs: Double
|
|
19
|
+
let gestureEndUptimeMs: Double
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func executeSequence(command: Command, activeApp: XCUIApplication) -> Response {
|
|
23
|
+
guard let steps = command.steps, !steps.isEmpty else {
|
|
24
|
+
return sequenceInvalidArgs("sequence requires at least one step")
|
|
25
|
+
}
|
|
26
|
+
guard steps.count <= maxSequenceSteps else {
|
|
27
|
+
return sequenceInvalidArgs(
|
|
28
|
+
"sequence accepts at most \(maxSequenceSteps) steps, received \(steps.count)"
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
for (index, step) in steps.enumerated() {
|
|
32
|
+
if let error = validateSequenceStep(step, index: index) {
|
|
33
|
+
return error
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Touch frame resolves from the first step's coords so recording-gestures works unchanged.
|
|
38
|
+
let firstStep = steps[0]
|
|
39
|
+
let firstFrame = (firstStep.x != nil && firstStep.y != nil)
|
|
40
|
+
? resolvedTouchVisualizationFrame(app: activeApp, x: firstStep.x!, y: firstStep.y!)
|
|
41
|
+
: nil
|
|
42
|
+
|
|
43
|
+
let execution = assembleSequenceExecution(steps: steps) { _, step in
|
|
44
|
+
performSequenceStep(step, activeApp: activeApp)
|
|
45
|
+
}
|
|
46
|
+
return sequenceResponse(execution: execution, touchFrame: firstFrame)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Pure, device-free assembler: runs each step in order via `perform`, stops at the first
|
|
50
|
+
/// `.unsupported` outcome, and assembles the DataPayload (completedSteps, optional
|
|
51
|
+
/// failedStepIndex, per-step results, top-level gesture timing spanning first..last executed).
|
|
52
|
+
/// Steps after the failed index are never invoked and produce no result entries, so
|
|
53
|
+
/// results.count == completedSteps + (failedStepIndex != nil ? 1 : 0).
|
|
54
|
+
func assembleSequenceExecution(
|
|
55
|
+
steps: [SequenceStep],
|
|
56
|
+
perform: (Int, SequenceStep) -> SequenceStepOutcome
|
|
57
|
+
) -> SequenceExecutionResult {
|
|
58
|
+
var results: [SequenceStepResult] = []
|
|
59
|
+
var completedSteps = 0
|
|
60
|
+
var failedStepIndex: Int?
|
|
61
|
+
var gestureStartUptimeMs: Double?
|
|
62
|
+
var gestureEndUptimeMs: Double?
|
|
63
|
+
|
|
64
|
+
for (index, step) in steps.enumerated() {
|
|
65
|
+
let stepOutcome = perform(index, step)
|
|
66
|
+
if gestureStartUptimeMs == nil {
|
|
67
|
+
gestureStartUptimeMs = stepOutcome.gestureStartUptimeMs
|
|
68
|
+
}
|
|
69
|
+
gestureEndUptimeMs = stepOutcome.gestureEndUptimeMs
|
|
70
|
+
|
|
71
|
+
switch stepOutcome.outcome {
|
|
72
|
+
case .performed:
|
|
73
|
+
results.append(
|
|
74
|
+
SequenceStepResult(
|
|
75
|
+
ok: true,
|
|
76
|
+
kind: step.kind,
|
|
77
|
+
errorCode: nil,
|
|
78
|
+
errorMessage: nil,
|
|
79
|
+
gestureStartUptimeMs: stepOutcome.gestureStartUptimeMs,
|
|
80
|
+
gestureEndUptimeMs: stepOutcome.gestureEndUptimeMs
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
completedSteps += 1
|
|
84
|
+
case .unsupported(let message, _):
|
|
85
|
+
results.append(
|
|
86
|
+
SequenceStepResult(
|
|
87
|
+
ok: false,
|
|
88
|
+
kind: step.kind,
|
|
89
|
+
errorCode: "UNSUPPORTED_OPERATION",
|
|
90
|
+
errorMessage: message,
|
|
91
|
+
gestureStartUptimeMs: stepOutcome.gestureStartUptimeMs,
|
|
92
|
+
gestureEndUptimeMs: stepOutcome.gestureEndUptimeMs
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
failedStepIndex = index
|
|
96
|
+
return SequenceExecutionResult(
|
|
97
|
+
results: results,
|
|
98
|
+
completedSteps: completedSteps,
|
|
99
|
+
failedStepIndex: failedStepIndex,
|
|
100
|
+
gestureStartUptimeMs: gestureStartUptimeMs,
|
|
101
|
+
gestureEndUptimeMs: gestureEndUptimeMs
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return SequenceExecutionResult(
|
|
107
|
+
results: results,
|
|
108
|
+
completedSteps: completedSteps,
|
|
109
|
+
failedStepIndex: nil,
|
|
110
|
+
gestureStartUptimeMs: gestureStartUptimeMs,
|
|
111
|
+
gestureEndUptimeMs: gestureEndUptimeMs
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
struct SequenceExecutionResult {
|
|
116
|
+
let results: [SequenceStepResult]
|
|
117
|
+
let completedSteps: Int
|
|
118
|
+
let failedStepIndex: Int?
|
|
119
|
+
let gestureStartUptimeMs: Double?
|
|
120
|
+
let gestureEndUptimeMs: Double?
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// MARK: - Step validation / execution
|
|
124
|
+
|
|
125
|
+
private func validateSequenceStep(_ step: SequenceStep, index: Int) -> Response? {
|
|
126
|
+
guard sequenceableStepKinds.contains(step.kind) else {
|
|
127
|
+
return sequenceInvalidArgs(
|
|
128
|
+
"sequence step \(index) has unsupported kind \"\(step.kind)\"; allowed: tap, doubleTap, longPress, drag"
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
guard let x = step.x, let y = step.y, x.isFinite, y.isFinite else {
|
|
132
|
+
return sequenceInvalidArgs("sequence step \(index) (\(step.kind)) requires finite x and y")
|
|
133
|
+
}
|
|
134
|
+
if step.kind == "drag" {
|
|
135
|
+
guard let x2 = step.x2, let y2 = step.y2, x2.isFinite, y2.isFinite else {
|
|
136
|
+
return sequenceInvalidArgs("sequence step \(index) (drag) requires finite x2 and y2")
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return nil
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private func performSequenceStep(
|
|
143
|
+
_ step: SequenceStep,
|
|
144
|
+
activeApp: XCUIApplication
|
|
145
|
+
) -> SequenceStepOutcome {
|
|
146
|
+
let x = step.x ?? 0
|
|
147
|
+
let y = step.y ?? 0
|
|
148
|
+
// Synthesized HID tap fast path mirrors the individual `tap` command (idleTimeout:false, with
|
|
149
|
+
// a tapAt fallback when synthesis is unsupported), so fusing a jittered tap series does not
|
|
150
|
+
// change the touch mechanism for these inputs.
|
|
151
|
+
if step.kind == "tap", step.synthesized == true {
|
|
152
|
+
let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
|
|
153
|
+
synthesizedTapAt(app: activeApp, x: x, y: y)
|
|
154
|
+
}
|
|
155
|
+
if case .performed = outcome {
|
|
156
|
+
if let pauseMs = step.pauseMs, pauseMs > 0 {
|
|
157
|
+
sleepFor(min(max(pauseMs, 0), 10000) / 1000.0)
|
|
158
|
+
}
|
|
159
|
+
return SequenceStepOutcome(
|
|
160
|
+
outcome: outcome,
|
|
161
|
+
gestureStartUptimeMs: timing.gestureStartUptimeMs,
|
|
162
|
+
gestureEndUptimeMs: timing.gestureEndUptimeMs
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
// Synthesis unsupported (e.g. macOS) — fall through to the drag-based tapAt below.
|
|
166
|
+
}
|
|
167
|
+
if step.kind == "drag", step.synthesized == true {
|
|
168
|
+
let dragPoints = keyboardAvoidingDragPoints(
|
|
169
|
+
app: activeApp, x: x, y: y, x2: step.x2 ?? x, y2: step.y2 ?? y)
|
|
170
|
+
let durationMs = min(max(step.durationMs ?? 250, 16), 10000)
|
|
171
|
+
let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
|
|
172
|
+
synthesizedDragAt(
|
|
173
|
+
app: activeApp,
|
|
174
|
+
x: dragPoints.x,
|
|
175
|
+
y: dragPoints.y,
|
|
176
|
+
x2: dragPoints.x2,
|
|
177
|
+
y2: dragPoints.y2,
|
|
178
|
+
durationMs: durationMs
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
if case .performed = outcome {
|
|
182
|
+
if let pauseMs = step.pauseMs, pauseMs > 0 {
|
|
183
|
+
sleepFor(min(max(pauseMs, 0), 10000) / 1000.0)
|
|
184
|
+
}
|
|
185
|
+
return SequenceStepOutcome(
|
|
186
|
+
outcome: outcome,
|
|
187
|
+
gestureStartUptimeMs: timing.gestureStartUptimeMs,
|
|
188
|
+
gestureEndUptimeMs: timing.gestureEndUptimeMs
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
let fallbackHoldDuration = synthesizedSwipeFallbackHoldDuration(durationMs: step.durationMs ?? 250)
|
|
192
|
+
let (fallbackTiming, fallbackOutcome) = performGesture(activeApp) {
|
|
193
|
+
dragAt(
|
|
194
|
+
app: activeApp,
|
|
195
|
+
x: dragPoints.x,
|
|
196
|
+
y: dragPoints.y,
|
|
197
|
+
x2: dragPoints.x2,
|
|
198
|
+
y2: dragPoints.y2,
|
|
199
|
+
holdDuration: fallbackHoldDuration
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
if case .performed = fallbackOutcome, let pauseMs = step.pauseMs, pauseMs > 0 {
|
|
203
|
+
sleepFor(min(max(pauseMs, 0), 10000) / 1000.0)
|
|
204
|
+
}
|
|
205
|
+
return SequenceStepOutcome(
|
|
206
|
+
outcome: fallbackOutcome,
|
|
207
|
+
gestureStartUptimeMs: fallbackTiming.gestureStartUptimeMs,
|
|
208
|
+
gestureEndUptimeMs: fallbackTiming.gestureEndUptimeMs
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
let (timing, outcome) = performGesture(activeApp) {
|
|
212
|
+
switch step.kind {
|
|
213
|
+
case "doubleTap":
|
|
214
|
+
// doubleTapAt per step, matching the behavior of the retired tapSeries doubleTap path.
|
|
215
|
+
return doubleTapAt(app: activeApp, x: x, y: y)
|
|
216
|
+
case "longPress":
|
|
217
|
+
let duration = min(max(step.durationMs ?? 800, 16), 10000) / 1000.0
|
|
218
|
+
return longPressAt(app: activeApp, x: x, y: y, duration: duration)
|
|
219
|
+
case "drag":
|
|
220
|
+
// Route through keyboardAvoidingDragPoints for parity with the individual `.drag` command.
|
|
221
|
+
// The non-synthesized coordinate-drag path ignores durationMs, matching that command's
|
|
222
|
+
// non-synthesized branch.
|
|
223
|
+
let dragPoints = keyboardAvoidingDragPoints(
|
|
224
|
+
app: activeApp, x: x, y: y, x2: step.x2 ?? x, y2: step.y2 ?? y)
|
|
225
|
+
return dragAt(
|
|
226
|
+
app: activeApp,
|
|
227
|
+
x: dragPoints.x,
|
|
228
|
+
y: dragPoints.y,
|
|
229
|
+
x2: dragPoints.x2,
|
|
230
|
+
y2: dragPoints.y2,
|
|
231
|
+
holdDuration: coordinateDragHoldDuration()
|
|
232
|
+
)
|
|
233
|
+
default:
|
|
234
|
+
return tapAt(app: activeApp, x: x, y: y)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Sleep AFTER the step — pauseMs is the inter-step gap — but only when the step performed.
|
|
238
|
+
// assembleSequenceExecution stops at the first unsupported outcome, so pausing after a failed
|
|
239
|
+
// step would burn up to 10s of watchdog budget with no following step to separate from.
|
|
240
|
+
if case .performed = outcome, let pauseMs = step.pauseMs, pauseMs > 0 {
|
|
241
|
+
sleepFor(min(max(pauseMs, 0), 10000) / 1000.0)
|
|
242
|
+
}
|
|
243
|
+
return SequenceStepOutcome(
|
|
244
|
+
outcome: outcome,
|
|
245
|
+
gestureStartUptimeMs: timing.gestureStartUptimeMs,
|
|
246
|
+
gestureEndUptimeMs: timing.gestureEndUptimeMs
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private func sequenceResponse(
|
|
251
|
+
execution: SequenceExecutionResult,
|
|
252
|
+
touchFrame: TouchVisualizationFrame?
|
|
253
|
+
) -> Response {
|
|
254
|
+
return Response(
|
|
255
|
+
ok: true,
|
|
256
|
+
data: DataPayload(
|
|
257
|
+
message: "sequence",
|
|
258
|
+
gestureStartUptimeMs: execution.gestureStartUptimeMs,
|
|
259
|
+
gestureEndUptimeMs: execution.gestureEndUptimeMs,
|
|
260
|
+
x: touchFrame?.x,
|
|
261
|
+
y: touchFrame?.y,
|
|
262
|
+
referenceWidth: touchFrame?.referenceWidth,
|
|
263
|
+
referenceHeight: touchFrame?.referenceHeight,
|
|
264
|
+
completedSteps: execution.completedSteps,
|
|
265
|
+
failedStepIndex: execution.failedStepIndex,
|
|
266
|
+
sequenceResults: execution.results
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private func sequenceInvalidArgs(_ message: String) -> Response {
|
|
272
|
+
Response(ok: false, error: ErrorPayload(code: "INVALID_ARGS", message: message))
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// MARK: - In-bundle unit tests (device-free)
|
|
277
|
+
|
|
278
|
+
extension RunnerTests {
|
|
279
|
+
func testSequenceDecodesStepsFromWire() throws {
|
|
280
|
+
let json = """
|
|
281
|
+
{"command":"sequence","commandId":"seq-1","steps":[
|
|
282
|
+
{"kind":"tap","x":100,"y":200},
|
|
283
|
+
{"kind":"doubleTap","x":101,"y":200},
|
|
284
|
+
{"kind":"longPress","x":102,"y":200,"durationMs":300},
|
|
285
|
+
{"kind":"drag","x":10,"y":600,"x2":10,"y2":200,"durationMs":250,"pauseMs":50}
|
|
286
|
+
]}
|
|
287
|
+
"""
|
|
288
|
+
let command = try JSONDecoder().decode(Command.self, from: Data(json.utf8))
|
|
289
|
+
XCTAssertEqual(command.command, .sequence)
|
|
290
|
+
XCTAssertEqual(command.steps?.count, 4)
|
|
291
|
+
XCTAssertEqual(command.steps?[0].kind, "tap")
|
|
292
|
+
XCTAssertEqual(command.steps?[1].kind, "doubleTap")
|
|
293
|
+
XCTAssertEqual(command.steps?[3].x2, 10)
|
|
294
|
+
XCTAssertEqual(command.steps?[3].pauseMs, 50)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
func testSequenceAcceptsDoubleTapKind() {
|
|
298
|
+
// A doubleTap step missing coords must fail on the coords check, not the kind allowlist —
|
|
299
|
+
// proving "doubleTap" passes validateSequenceStep without needing a device to execute on.
|
|
300
|
+
let response = executeSequenceForTest(steps: [
|
|
301
|
+
sequenceStep(kind: "doubleTap", x: nil)
|
|
302
|
+
])
|
|
303
|
+
XCTAssertEqual(response.ok, false)
|
|
304
|
+
XCTAssertEqual(response.error?.code, "INVALID_ARGS")
|
|
305
|
+
XCTAssertTrue(response.error?.message.contains("requires finite x and y") ?? false)
|
|
306
|
+
XCTAssertFalse(response.error?.message.contains("unsupported kind") ?? true)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
func testSequenceRejectsUnknownKind() throws {
|
|
310
|
+
let response = executeSequenceForTest(steps: [
|
|
311
|
+
sequenceStep(kind: "tap", x: 1, y: 2),
|
|
312
|
+
sequenceStep(kind: "pinch", x: 3, y: 4),
|
|
313
|
+
])
|
|
314
|
+
XCTAssertEqual(response.ok, false)
|
|
315
|
+
XCTAssertEqual(response.error?.code, "INVALID_ARGS")
|
|
316
|
+
XCTAssertTrue(response.error?.message.contains("step 1") ?? false)
|
|
317
|
+
XCTAssertTrue(response.error?.message.contains("pinch") ?? false)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
func testSequenceRejectsEmpty() {
|
|
321
|
+
let response = executeSequenceForTest(steps: [])
|
|
322
|
+
XCTAssertEqual(response.ok, false)
|
|
323
|
+
XCTAssertEqual(response.error?.code, "INVALID_ARGS")
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
func testSequenceRejectsTooManySteps() {
|
|
327
|
+
let steps = (0..<21).map { _ in sequenceStep(kind: "tap", x: 1, y: 2) }
|
|
328
|
+
let response = executeSequenceForTest(steps: steps)
|
|
329
|
+
XCTAssertEqual(response.ok, false)
|
|
330
|
+
XCTAssertEqual(response.error?.code, "INVALID_ARGS")
|
|
331
|
+
XCTAssertTrue(response.error?.message.contains("at most 20") ?? false)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
func testSequenceRejectsDragMissingSecondPoint() {
|
|
335
|
+
let response = executeSequenceForTest(steps: [
|
|
336
|
+
sequenceStep(kind: "tap", x: 1, y: 2),
|
|
337
|
+
sequenceStep(kind: "drag", x: 3, y: 4),
|
|
338
|
+
])
|
|
339
|
+
XCTAssertEqual(response.ok, false)
|
|
340
|
+
XCTAssertEqual(response.error?.code, "INVALID_ARGS")
|
|
341
|
+
XCTAssertTrue(response.error?.message.contains("step 1") ?? false)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
func testAssembleSequencePreservesOrderOnSuccess() {
|
|
345
|
+
let steps = [
|
|
346
|
+
sequenceStep(kind: "tap", x: 1, y: 1),
|
|
347
|
+
sequenceStep(kind: "longPress", x: 2, y: 2),
|
|
348
|
+
sequenceStep(kind: "tap", x: 3, y: 3),
|
|
349
|
+
]
|
|
350
|
+
var calls: [Int] = []
|
|
351
|
+
let execution = assembleSequenceExecution(steps: steps) { index, _ in
|
|
352
|
+
calls.append(index)
|
|
353
|
+
return SequenceStepOutcome(
|
|
354
|
+
outcome: .performed,
|
|
355
|
+
gestureStartUptimeMs: Double(index * 10),
|
|
356
|
+
gestureEndUptimeMs: Double(index * 10 + 5)
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
XCTAssertEqual(calls, [0, 1, 2])
|
|
360
|
+
XCTAssertEqual(execution.completedSteps, 3)
|
|
361
|
+
XCTAssertNil(execution.failedStepIndex)
|
|
362
|
+
XCTAssertEqual(execution.results.map { $0.kind }, ["tap", "longPress", "tap"])
|
|
363
|
+
XCTAssertEqual(execution.gestureStartUptimeMs, 0)
|
|
364
|
+
XCTAssertEqual(execution.gestureEndUptimeMs, 25)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
func testAssembleSequenceStopsAtFirstFailure() {
|
|
368
|
+
let steps = [
|
|
369
|
+
sequenceStep(kind: "tap", x: 1, y: 1),
|
|
370
|
+
sequenceStep(kind: "drag", x: 2, y: 2),
|
|
371
|
+
sequenceStep(kind: "tap", x: 3, y: 3),
|
|
372
|
+
]
|
|
373
|
+
var calls: [Int] = []
|
|
374
|
+
let execution = assembleSequenceExecution(steps: steps) { index, _ in
|
|
375
|
+
calls.append(index)
|
|
376
|
+
if index == 1 {
|
|
377
|
+
return SequenceStepOutcome(
|
|
378
|
+
outcome: .unsupported(message: "drag unsupported", hint: nil),
|
|
379
|
+
gestureStartUptimeMs: 10,
|
|
380
|
+
gestureEndUptimeMs: 15
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
return SequenceStepOutcome(outcome: .performed, gestureStartUptimeMs: 0, gestureEndUptimeMs: 5)
|
|
384
|
+
}
|
|
385
|
+
// Step 2 is never invoked.
|
|
386
|
+
XCTAssertEqual(calls, [0, 1])
|
|
387
|
+
XCTAssertEqual(execution.completedSteps, 1)
|
|
388
|
+
XCTAssertEqual(execution.failedStepIndex, 1)
|
|
389
|
+
// results.count == completedSteps + 1 (the failed step).
|
|
390
|
+
XCTAssertEqual(execution.results.count, 2)
|
|
391
|
+
XCTAssertEqual(execution.results[1].ok, false)
|
|
392
|
+
XCTAssertEqual(execution.results[1].errorCode, "UNSUPPORTED_OPERATION")
|
|
393
|
+
XCTAssertEqual(execution.results[1].errorMessage, "drag unsupported")
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
func testSequenceWorstCaseResponseStaysUnderJournalCap() throws {
|
|
397
|
+
let longMessage = String(repeating: "e", count: 200)
|
|
398
|
+
let results = (0..<20).map { index in
|
|
399
|
+
SequenceStepResult(
|
|
400
|
+
ok: index < 19,
|
|
401
|
+
kind: "drag",
|
|
402
|
+
errorCode: index < 19 ? nil : "UNSUPPORTED_OPERATION",
|
|
403
|
+
errorMessage: index < 19 ? nil : longMessage,
|
|
404
|
+
gestureStartUptimeMs: 123456.789,
|
|
405
|
+
gestureEndUptimeMs: 123466.789
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
let response = Response(
|
|
409
|
+
ok: true,
|
|
410
|
+
data: DataPayload(
|
|
411
|
+
message: "sequence",
|
|
412
|
+
completedSteps: 19,
|
|
413
|
+
failedStepIndex: 19,
|
|
414
|
+
sequenceResults: results
|
|
415
|
+
)
|
|
416
|
+
)
|
|
417
|
+
let encoded = try JSONEncoder().encode(response)
|
|
418
|
+
XCTAssertLessThan(encoded.count, 16 * 1024)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private func sequenceStep(
|
|
422
|
+
kind: String,
|
|
423
|
+
x: Double?,
|
|
424
|
+
y: Double? = nil,
|
|
425
|
+
x2: Double? = nil,
|
|
426
|
+
y2: Double? = nil
|
|
427
|
+
) -> SequenceStep {
|
|
428
|
+
SequenceStep(
|
|
429
|
+
kind: kind, x: x, y: y, x2: x2, y2: y2, durationMs: nil, pauseMs: nil, synthesized: nil)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/// Validation runs before any executor call, so the INVALID_ARGS paths are exercised without
|
|
433
|
+
/// reaching the device executor (which is never invoked when validation rejects).
|
|
434
|
+
private func executeSequenceForTest(steps: [SequenceStep]) -> Response {
|
|
435
|
+
let command = makeSequenceCommand(steps: steps)
|
|
436
|
+
return executeSequence(command: command, activeApp: app)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/// Build a sequence Command via JSON so the test does not depend on the memberwise init's
|
|
440
|
+
/// parameter order.
|
|
441
|
+
private func makeSequenceCommand(steps: [SequenceStep]) -> Command {
|
|
442
|
+
struct SequenceCommandFixture: Encodable {
|
|
443
|
+
let command = "sequence"
|
|
444
|
+
let commandId = "seq-test"
|
|
445
|
+
let steps: [SequenceStep]
|
|
446
|
+
}
|
|
447
|
+
let data = try! JSONEncoder().encode(SequenceCommandFixture(steps: steps))
|
|
448
|
+
return try! JSONDecoder().decode(Command.self, from: data)
|
|
449
|
+
}
|
|
450
|
+
}
|