agent-device 0.16.7 → 0.16.9
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/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.16.7.apk → agent-device-android-multitouch-helper-0.16.9.apk} +0 -0
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.16.9.apk.sha256 +1 -0
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.16.7.manifest.json → agent-device-android-multitouch-helper-0.16.9.manifest.json} +4 -4
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.16.7.apk → agent-device-android-snapshot-helper-0.16.9.apk} +0 -0
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.16.9.apk.sha256 +1 -0
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.16.7.manifest.json → agent-device-android-snapshot-helper-0.16.9.manifest.json} +6 -6
- package/dist/src/1352.js +1 -1
- package/dist/src/2415.js +29 -29
- package/dist/src/2805.js +1 -1
- package/dist/src/6232.js +1 -0
- package/dist/src/7455.js +1 -0
- package/dist/src/8114.js +3 -3
- package/dist/src/8699.js +1 -1
- package/dist/src/940.js +1 -1
- package/dist/src/9471.js +1 -1
- package/dist/src/9533.js +1 -1
- package/dist/src/9542.js +1 -1
- package/dist/src/9818.js +1 -1
- package/dist/src/android-adb.d.ts +2 -0
- package/dist/src/android-snapshot-helper.d.ts +2 -0
- package/dist/src/args.js +5 -4
- package/dist/src/cli.js +6 -6
- package/dist/src/command-metadata.js +1 -1
- package/dist/src/find.js +1 -1
- package/dist/src/generic.js +11 -7
- package/dist/src/interaction.js +1 -1
- package/dist/src/react-native.js +1 -1
- package/dist/src/record-trace.js +3 -3
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/session.js +9 -9
- package/dist/src/snapshot.js +2 -2
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +20 -6
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +141 -774
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +8 -33
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +71 -1
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +80 -10
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +743 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TvRemote.swift +34 -6
- package/package.json +4 -6
- package/server.json +2 -2
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.16.7.apk.sha256 +0 -1
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.16.7.apk.sha256 +0 -1
- package/dist/src/5186.js +0 -1
|
@@ -216,14 +216,14 @@ extension RunnerTests {
|
|
|
216
216
|
// MARK: - Command Classification
|
|
217
217
|
|
|
218
218
|
func isReadOnlyCommand(_ command: Command) -> Bool {
|
|
219
|
-
switch command.command {
|
|
220
|
-
case .
|
|
219
|
+
switch command.command.traits.readOnly {
|
|
220
|
+
case .always:
|
|
221
221
|
return true
|
|
222
|
-
case .
|
|
223
|
-
let action = (command.action ?? "get").lowercased()
|
|
224
|
-
return action == "get"
|
|
225
|
-
default:
|
|
222
|
+
case .never:
|
|
226
223
|
return false
|
|
224
|
+
case .conditional:
|
|
225
|
+
// Today only `alert` is conditional: read-only when getting, mutating otherwise.
|
|
226
|
+
return (command.action ?? "get").lowercased() == "get"
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
229
|
|
|
@@ -234,36 +234,11 @@ extension RunnerTests {
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
func isInteractionCommand(_ command: CommandType) -> Bool {
|
|
237
|
-
|
|
238
|
-
case
|
|
239
|
-
.tap,
|
|
240
|
-
.longPress,
|
|
241
|
-
.drag,
|
|
242
|
-
.remotePress,
|
|
243
|
-
.type,
|
|
244
|
-
.swipe,
|
|
245
|
-
.back,
|
|
246
|
-
.backInApp,
|
|
247
|
-
.backSystem,
|
|
248
|
-
.rotate,
|
|
249
|
-
.appSwitcher,
|
|
250
|
-
.keyboardDismiss,
|
|
251
|
-
.pinch,
|
|
252
|
-
.rotateGesture,
|
|
253
|
-
.transformGesture:
|
|
254
|
-
return true
|
|
255
|
-
default:
|
|
256
|
-
return false
|
|
257
|
-
}
|
|
237
|
+
return command.traits.isInteraction
|
|
258
238
|
}
|
|
259
239
|
|
|
260
240
|
func isRunnerLifecycleCommand(_ command: CommandType) -> Bool {
|
|
261
|
-
|
|
262
|
-
case .shutdown, .recordStop, .screenshot, .uptime:
|
|
263
|
-
return true
|
|
264
|
-
default:
|
|
265
|
-
return false
|
|
266
|
-
}
|
|
241
|
+
return command.traits.isLifecycle
|
|
267
242
|
}
|
|
268
243
|
|
|
269
244
|
// MARK: - Interaction Stabilization
|
|
@@ -34,6 +34,74 @@ enum CommandType: String, Codable {
|
|
|
34
34
|
case shutdown
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/// Runner command traits — see CONTEXT.md ("Runner command traits").
|
|
38
|
+
///
|
|
39
|
+
/// Single source of truth for how the runner classifies a command across three
|
|
40
|
+
/// independent axes, replacing the three hand-maintained switches that used to live
|
|
41
|
+
/// in RunnerTests+Lifecycle.swift (isInteractionCommand / isReadOnlyCommand /
|
|
42
|
+
/// isRunnerLifecycleCommand). The classification is load-bearing for ADR-0002 session
|
|
43
|
+
/// invalidation: `readOnly` gates the retry that nulls currentApp/currentBundleId.
|
|
44
|
+
struct CommandTraits {
|
|
45
|
+
/// Whether the command needs the foreground-guard + stabilization preflight before running.
|
|
46
|
+
let isInteraction: Bool
|
|
47
|
+
/// Whether the command is eligible for the session-invalidating retry.
|
|
48
|
+
/// `.conditional` is resolved against the request (alert is read-only only for its `get` action).
|
|
49
|
+
let readOnly: ReadOnly
|
|
50
|
+
/// Whether the command skips the app-activation preflight entirely.
|
|
51
|
+
let isLifecycle: Bool
|
|
52
|
+
|
|
53
|
+
enum ReadOnly {
|
|
54
|
+
case always
|
|
55
|
+
case never
|
|
56
|
+
/// Alert-only today. Resolved in `isReadOnlyCommand` with alert's rule (read-only for the
|
|
57
|
+
/// `get` action, mutating otherwise). A new `.conditional` command would inherit that rule
|
|
58
|
+
/// until the resolver is generalized — give it explicit handling there if its semantics differ.
|
|
59
|
+
case conditional
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
extension CommandType {
|
|
64
|
+
/// The classification for this command. Exhaustive by construction: a new CommandType
|
|
65
|
+
/// cannot compile without being classified here, so commands can no longer silently drift
|
|
66
|
+
/// out of classification the way the parallel switches allowed.
|
|
67
|
+
var traits: CommandTraits {
|
|
68
|
+
switch self {
|
|
69
|
+
// Interaction commands: require the foreground-guard + stabilization preflight.
|
|
70
|
+
// tapSeries/dragSeries are the series forms of tap/drag; keyboardReturn is the sibling
|
|
71
|
+
// of keyboardDismiss — all three were missing from the historical switch (drift the
|
|
72
|
+
// table now prevents) and are classified as interactions here.
|
|
73
|
+
case .tap, .tapSeries, .longPress, .drag, .dragSeries, .remotePress, .type, .swipe,
|
|
74
|
+
.back, .backInApp, .backSystem, .rotate, .appSwitcher,
|
|
75
|
+
.keyboardDismiss, .keyboardReturn, .pinch, .rotateGesture, .transformGesture:
|
|
76
|
+
return CommandTraits(isInteraction: true, readOnly: .never, isLifecycle: false)
|
|
77
|
+
|
|
78
|
+
// Read-only reads: eligible for the session-invalidating retry.
|
|
79
|
+
case .interactionFrame, .findText, .readText, .snapshot:
|
|
80
|
+
return CommandTraits(isInteraction: false, readOnly: .always, isLifecycle: false)
|
|
81
|
+
|
|
82
|
+
// Screenshot is both a read and a runner-lifecycle command (skips app-activation preflight).
|
|
83
|
+
case .screenshot:
|
|
84
|
+
return CommandTraits(isInteraction: false, readOnly: .always, isLifecycle: true)
|
|
85
|
+
|
|
86
|
+
// Alert is read-only only for its `get` action (resolved by isReadOnlyCommand).
|
|
87
|
+
case .alert:
|
|
88
|
+
return CommandTraits(isInteraction: false, readOnly: .conditional, isLifecycle: false)
|
|
89
|
+
|
|
90
|
+
// Runner-lifecycle commands: skip the app-activation preflight.
|
|
91
|
+
case .recordStop, .uptime, .shutdown:
|
|
92
|
+
return CommandTraits(isInteraction: false, readOnly: .never, isLifecycle: true)
|
|
93
|
+
|
|
94
|
+
// Normal preflight, not retried.
|
|
95
|
+
// NOTE: mouseClick stays non-interaction for now — it is macOS-only and the foreground
|
|
96
|
+
// guard interacts with bespoke macOS activation, so classifying it needs a macOS smoke
|
|
97
|
+
// check first (tracked as a follow-up). Also preserved: querySelector is NOT read-only;
|
|
98
|
+
// recordStart is NOT a lifecycle command; home/alert remain non-interaction by design.
|
|
99
|
+
case .mouseClick, .querySelector, .home, .recordStart:
|
|
100
|
+
return CommandTraits(isInteraction: false, readOnly: .never, isLifecycle: false)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
37
105
|
struct Command: Codable {
|
|
38
106
|
let command: CommandType
|
|
39
107
|
let appBundleId: String?
|
|
@@ -154,10 +222,12 @@ struct DataPayload: Codable {
|
|
|
154
222
|
struct ErrorPayload: Codable {
|
|
155
223
|
let code: String?
|
|
156
224
|
let message: String
|
|
225
|
+
let hint: String?
|
|
157
226
|
|
|
158
|
-
init(code: String? = nil, message: String) {
|
|
227
|
+
init(code: String? = nil, message: String, hint: String? = nil) {
|
|
159
228
|
self.code = code
|
|
160
229
|
self.message = message
|
|
230
|
+
self.hint = hint
|
|
161
231
|
}
|
|
162
232
|
}
|
|
163
233
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import XCTest
|
|
2
2
|
|
|
3
3
|
extension RunnerTests {
|
|
4
|
+
private static let axSnapshotErrorCode = "IOS_AX_SNAPSHOT_FAILED"
|
|
5
|
+
private static let axSnapshotHint =
|
|
6
|
+
"XCTest could not serialize this iOS accessibility tree. Try a smaller read such as snapshot -s <visible label or id> -d 8, use direct selector commands such as find id <value> click, or use screenshot/logs/appstate in the same session. If you own the app and need full-tree inspection, consider flagging this screen for accessibility-tree simplification: reduce unnecessary accessible wrapper nesting and expose stable ids on actionable controls."
|
|
4
7
|
private static let collapsedTabCandidateTypes: Set<XCUIElement.ElementType> = [
|
|
5
8
|
.button,
|
|
6
9
|
.link,
|
|
@@ -33,6 +36,12 @@ extension RunnerTests {
|
|
|
33
36
|
let visible: Bool
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
struct SnapshotCaptureFailure: Error {
|
|
40
|
+
let code: String
|
|
41
|
+
let message: String
|
|
42
|
+
let hint: String
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
// MARK: - Snapshot Entry
|
|
37
46
|
|
|
38
47
|
func elementTypeName(_ type: XCUIElement.ElementType) -> String {
|
|
@@ -75,12 +84,12 @@ extension RunnerTests {
|
|
|
75
84
|
}
|
|
76
85
|
}
|
|
77
86
|
|
|
78
|
-
func snapshotFast(app: XCUIApplication, options: SnapshotOptions) -> DataPayload {
|
|
87
|
+
func snapshotFast(app: XCUIApplication, options: SnapshotOptions) throws -> DataPayload {
|
|
79
88
|
if let blocking = blockingSystemAlertSnapshot() {
|
|
80
89
|
return blocking
|
|
81
90
|
}
|
|
82
91
|
|
|
83
|
-
guard let context = makeSnapshotTraversalContext(app: app, options: options) else {
|
|
92
|
+
guard let context = try makeSnapshotTraversalContext(app: app, options: options) else {
|
|
84
93
|
return DataPayload(nodes: [], truncated: false)
|
|
85
94
|
}
|
|
86
95
|
|
|
@@ -186,12 +195,12 @@ extension RunnerTests {
|
|
|
186
195
|
return DataPayload(nodes: nodes, truncated: truncated)
|
|
187
196
|
}
|
|
188
197
|
|
|
189
|
-
func snapshotRaw(app: XCUIApplication, options: SnapshotOptions) -> DataPayload {
|
|
198
|
+
func snapshotRaw(app: XCUIApplication, options: SnapshotOptions) throws -> DataPayload {
|
|
190
199
|
if let blocking = blockingSystemAlertSnapshot() {
|
|
191
200
|
return blocking
|
|
192
201
|
}
|
|
193
202
|
|
|
194
|
-
guard let context = makeSnapshotTraversalContext(app: app, options: options) else {
|
|
203
|
+
guard let context = try makeSnapshotTraversalContext(app: app, options: options) else {
|
|
195
204
|
return DataPayload(nodes: [], truncated: false)
|
|
196
205
|
}
|
|
197
206
|
|
|
@@ -304,14 +313,11 @@ extension RunnerTests {
|
|
|
304
313
|
private func makeSnapshotTraversalContext(
|
|
305
314
|
app: XCUIApplication,
|
|
306
315
|
options: SnapshotOptions
|
|
307
|
-
) -> SnapshotTraversalContext? {
|
|
308
|
-
let viewport =
|
|
316
|
+
) throws -> SnapshotTraversalContext? {
|
|
317
|
+
let viewport = safeSnapshotViewport(app: app)
|
|
309
318
|
let queryRoot = options.scope.flatMap { findScopeElement(app: app, scope: $0) } ?? app
|
|
310
319
|
|
|
311
|
-
let rootSnapshot
|
|
312
|
-
do {
|
|
313
|
-
rootSnapshot = try queryRoot.snapshot()
|
|
314
|
-
} catch {
|
|
320
|
+
guard let rootSnapshot = try captureSnapshotRoot(queryRoot) else {
|
|
315
321
|
return nil
|
|
316
322
|
}
|
|
317
323
|
|
|
@@ -326,6 +332,70 @@ extension RunnerTests {
|
|
|
326
332
|
)
|
|
327
333
|
}
|
|
328
334
|
|
|
335
|
+
private func captureSnapshotRoot(_ element: XCUIElement) throws -> XCUIElementSnapshot? {
|
|
336
|
+
var rootSnapshot: XCUIElementSnapshot?
|
|
337
|
+
var swiftErrorMessage: String?
|
|
338
|
+
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
339
|
+
do {
|
|
340
|
+
rootSnapshot = try element.snapshot()
|
|
341
|
+
} catch {
|
|
342
|
+
swiftErrorMessage = describeSnapshotError(error)
|
|
343
|
+
}
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
if let rootSnapshot {
|
|
347
|
+
return rootSnapshot
|
|
348
|
+
}
|
|
349
|
+
let message = exceptionMessage ?? swiftErrorMessage ?? "snapshot returned no root"
|
|
350
|
+
if Self.isAxIllegalArgument(message) {
|
|
351
|
+
throw axSnapshotFailure(message)
|
|
352
|
+
}
|
|
353
|
+
return nil
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private func safeSnapshotViewport(app: XCUIApplication) -> CGRect {
|
|
357
|
+
var viewport = CGRect.infinite
|
|
358
|
+
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
359
|
+
viewport = snapshotViewport(app: app)
|
|
360
|
+
})
|
|
361
|
+
if let exceptionMessage {
|
|
362
|
+
NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_VIEWPORT_IGNORED_EXCEPTION=%@", exceptionMessage)
|
|
363
|
+
}
|
|
364
|
+
return viewport
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private func describeSnapshotError(_ error: Error) -> String {
|
|
368
|
+
let localized = error.localizedDescription
|
|
369
|
+
let debug = String(describing: error)
|
|
370
|
+
if localized.isEmpty { return debug }
|
|
371
|
+
if debug == localized { return localized }
|
|
372
|
+
return "\(localized) (\(debug))"
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private func axSnapshotFailure(_ message: String) -> SnapshotCaptureFailure {
|
|
376
|
+
let failureMessage: String
|
|
377
|
+
if Self.hasAxIllegalArgumentCode(message) {
|
|
378
|
+
failureMessage = "iOS XCTest snapshot failed with kAXErrorIllegalArgument. \(message)"
|
|
379
|
+
} else {
|
|
380
|
+
failureMessage = "iOS XCTest snapshot failed while serializing the accessibility tree. \(message)"
|
|
381
|
+
}
|
|
382
|
+
return SnapshotCaptureFailure(
|
|
383
|
+
code: Self.axSnapshotErrorCode,
|
|
384
|
+
message: failureMessage,
|
|
385
|
+
hint: Self.axSnapshotHint
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private static func isAxIllegalArgument(_ message: String) -> Bool {
|
|
390
|
+
let normalized = message.lowercased()
|
|
391
|
+
return hasAxIllegalArgumentCode(normalized)
|
|
392
|
+
|| (normalized.contains("illegal argument") && normalized.contains("snapshot"))
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private static func hasAxIllegalArgumentCode(_ message: String) -> Bool {
|
|
396
|
+
return message.lowercased().contains("kaxerrorillegalargument")
|
|
397
|
+
}
|
|
398
|
+
|
|
329
399
|
private func evaluateSnapshot(
|
|
330
400
|
_ snapshot: XCUIElementSnapshot,
|
|
331
401
|
in context: SnapshotTraversalContext
|