agent-device 0.16.8 → 0.16.10

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.
Files changed (27) hide show
  1. package/README.md +1 -0
  2. package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.16.8.apk → agent-device-android-multitouch-helper-0.16.10.apk} +0 -0
  3. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.16.10.apk.sha256 +1 -0
  4. package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.16.8.manifest.json → agent-device-android-multitouch-helper-0.16.10.manifest.json} +4 -4
  5. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.16.8.apk → agent-device-android-snapshot-helper-0.16.10.apk} +0 -0
  6. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.16.10.apk.sha256 +1 -0
  7. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.16.8.manifest.json → agent-device-android-snapshot-helper-0.16.10.manifest.json} +6 -6
  8. package/dist/src/2415.js +19 -19
  9. package/dist/src/8114.js +3 -3
  10. package/dist/src/apps.js +2 -2
  11. package/dist/src/generic.js +4 -3
  12. package/dist/src/input-actions.js +1 -1
  13. package/dist/src/session.js +2 -2
  14. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +197 -232
  15. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +282 -0
  16. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Exceptions.swift +29 -0
  17. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +8 -771
  18. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +30 -0
  19. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +2 -20
  20. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +10 -50
  21. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +723 -0
  22. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +64 -22
  23. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +7 -4
  24. package/package.json +1 -1
  25. package/server.json +2 -2
  26. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.16.8.apk.sha256 +0 -1
  27. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.16.8.apk.sha256 +0 -1
@@ -0,0 +1,282 @@
1
+ import Foundation
2
+ import XCTest
3
+
4
+ enum RunnerCommandLifecycleState: String {
5
+ case notAccepted
6
+ case accepted
7
+ case started
8
+ case completed
9
+ case failed
10
+ }
11
+
12
+ struct RunnerCommandJournalEntry {
13
+ let commandId: String
14
+ let command: String
15
+ var state: RunnerCommandLifecycleState
16
+ var responseOk: Bool?
17
+ var responseJson: String?
18
+ var error: ErrorPayload?
19
+ }
20
+
21
+ final class RunnerCommandJournal {
22
+ private let lock = NSLock()
23
+ private let maxEntries = 64
24
+ private let maxResponseJsonBytes = 16 * 1024
25
+ private var entries: [String: RunnerCommandJournalEntry] = [:]
26
+ private var order: [String] = []
27
+
28
+ func accept(command: Command) {
29
+ guard let commandId = normalizedCommandId(command.commandId) else { return }
30
+ lock.lock()
31
+ defer { lock.unlock() }
32
+ entries[commandId] = RunnerCommandJournalEntry(
33
+ commandId: commandId,
34
+ command: command.command.rawValue,
35
+ state: .accepted,
36
+ responseOk: nil,
37
+ responseJson: nil,
38
+ error: nil
39
+ )
40
+ order.removeAll { $0 == commandId }
41
+ order.append(commandId)
42
+ pruneIfNeeded()
43
+ }
44
+
45
+ func start(command: Command) {
46
+ update(command: command, state: .started, responseOk: nil, responseJson: nil, error: nil)
47
+ }
48
+
49
+ func finish(command: Command, response: Response) {
50
+ update(
51
+ command: command,
52
+ state: response.ok ? .completed : .failed,
53
+ responseOk: response.ok,
54
+ responseJson: encodeResponseJson(command: command, response: response),
55
+ error: response.error
56
+ )
57
+ }
58
+
59
+ func fail(command: Command, error: Error) {
60
+ update(
61
+ command: command,
62
+ state: .failed,
63
+ responseOk: nil,
64
+ responseJson: nil,
65
+ error: ErrorPayload(message: error.localizedDescription)
66
+ )
67
+ }
68
+
69
+ func status(commandId: String) -> DataPayload {
70
+ guard let normalized = normalizedCommandId(commandId) else {
71
+ return DataPayload(lifecycleState: RunnerCommandLifecycleState.notAccepted.rawValue)
72
+ }
73
+ lock.lock()
74
+ let entry = entries[normalized]
75
+ lock.unlock()
76
+ guard let entry else {
77
+ return DataPayload(
78
+ commandId: normalized,
79
+ lifecycleState: RunnerCommandLifecycleState.notAccepted.rawValue
80
+ )
81
+ }
82
+ return DataPayload(
83
+ commandId: entry.commandId,
84
+ lifecycleState: entry.state.rawValue,
85
+ lifecycleCommand: entry.command,
86
+ lifecycleResponseOk: entry.responseOk,
87
+ lifecycleResponseJson: entry.responseJson,
88
+ lifecycleErrorCode: entry.error?.code,
89
+ lifecycleErrorMessage: entry.error?.message,
90
+ lifecycleErrorHint: entry.error?.hint
91
+ )
92
+ }
93
+
94
+ private func update(
95
+ command: Command,
96
+ state: RunnerCommandLifecycleState,
97
+ responseOk: Bool?,
98
+ responseJson: String?,
99
+ error: ErrorPayload?
100
+ ) {
101
+ guard let commandId = normalizedCommandId(command.commandId) else { return }
102
+ lock.lock()
103
+ defer { lock.unlock() }
104
+ var entry = entries[commandId] ?? RunnerCommandJournalEntry(
105
+ commandId: commandId,
106
+ command: command.command.rawValue,
107
+ state: .accepted,
108
+ responseOk: nil,
109
+ responseJson: nil,
110
+ error: nil
111
+ )
112
+ entry.state = state
113
+ entry.responseOk = responseOk
114
+ entry.responseJson = responseJson
115
+ entry.error = error
116
+ entries[commandId] = entry
117
+ order.removeAll { $0 == commandId }
118
+ order.append(commandId)
119
+ pruneIfNeeded()
120
+ }
121
+
122
+ private func pruneIfNeeded() {
123
+ while order.count > maxEntries {
124
+ let removed = order.removeFirst()
125
+ entries.removeValue(forKey: removed)
126
+ }
127
+ }
128
+
129
+ private func normalizedCommandId(_ value: String?) -> String? {
130
+ guard let value else { return nil }
131
+ let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
132
+ return trimmed.isEmpty ? nil : trimmed
133
+ }
134
+
135
+ private func encodeResponseJson(command: Command, response: Response) -> String? {
136
+ guard shouldRetainResponseJson(command: command) else { return nil }
137
+ guard let data = try? JSONEncoder().encode(response) else { return nil }
138
+ guard data.count <= maxResponseJsonBytes else { return nil }
139
+ return String(data: data, encoding: .utf8)
140
+ }
141
+
142
+ private func shouldRetainResponseJson(command: Command) -> Bool {
143
+ switch command.command {
144
+ case .snapshot, .screenshot:
145
+ return false
146
+ case .tap, .mouseClick, .tapSeries, .longPress, .interactionFrame, .drag, .dragSeries,
147
+ .remotePress, .type, .swipe, .findText, .querySelector, .readText, .back, .backInApp,
148
+ .backSystem, .home, .rotate, .appSwitcher, .keyboardDismiss, .keyboardReturn, .alert,
149
+ .pinch, .rotateGesture, .transformGesture, .recordStart, .recordStop, .status, .uptime,
150
+ .shutdown:
151
+ return true
152
+ }
153
+ }
154
+ }
155
+
156
+ extension RunnerTests {
157
+ func testCommandJournalRetentionPolicy() throws {
158
+ let journal = RunnerCommandJournal()
159
+
160
+ let uptime = runnerJournalCommand("uptime", id: "small-scalar")
161
+ journal.accept(command: uptime)
162
+ journal.finish(
163
+ command: uptime,
164
+ response: Response(ok: true, data: DataPayload(currentUptimeMs: 12.5))
165
+ )
166
+
167
+ let scalarStatus = journal.status(commandId: "small-scalar")
168
+ XCTAssertEqual(scalarStatus.lifecycleState, RunnerCommandLifecycleState.completed.rawValue)
169
+ XCTAssertEqual(scalarStatus.lifecycleResponseOk, true)
170
+ XCTAssertNotNil(scalarStatus.lifecycleResponseJson)
171
+ let scalarResponse = try decodeRunnerJournalResponse(scalarStatus.lifecycleResponseJson)
172
+ XCTAssertEqual(scalarResponse.data?.currentUptimeMs, 12.5)
173
+
174
+ let querySelector = runnerJournalCommand("querySelector", id: "small-object")
175
+ journal.accept(command: querySelector)
176
+ journal.finish(
177
+ command: querySelector,
178
+ response: Response(ok: true, data: DataPayload(found: true, nodes: [runnerJournalNode()]))
179
+ )
180
+
181
+ let objectStatus = journal.status(commandId: "small-object")
182
+ XCTAssertNotNil(objectStatus.lifecycleResponseJson)
183
+ let objectResponse = try decodeRunnerJournalResponse(objectStatus.lifecycleResponseJson)
184
+ XCTAssertEqual(objectResponse.data?.found, true)
185
+ XCTAssertEqual(objectResponse.data?.nodes?.count, 1)
186
+
187
+ let snapshot = runnerJournalCommand("snapshot", id: "snapshot-tree")
188
+ journal.accept(command: snapshot)
189
+ journal.finish(
190
+ command: snapshot,
191
+ response: Response(ok: true, data: DataPayload(nodes: [runnerJournalNode()], truncated: false))
192
+ )
193
+
194
+ let snapshotStatus = journal.status(commandId: "snapshot-tree")
195
+ XCTAssertEqual(snapshotStatus.lifecycleState, RunnerCommandLifecycleState.completed.rawValue)
196
+ XCTAssertEqual(snapshotStatus.lifecycleResponseOk, true)
197
+ XCTAssertNil(snapshotStatus.lifecycleResponseJson)
198
+
199
+ let screenshot = runnerJournalCommand("screenshot", id: "screenshot-artifact")
200
+ journal.accept(command: screenshot)
201
+ journal.finish(
202
+ command: screenshot,
203
+ response: Response(ok: true, data: DataPayload(message: "tmp/screenshot-1.png"))
204
+ )
205
+
206
+ let screenshotStatus = journal.status(commandId: "screenshot-artifact")
207
+ XCTAssertEqual(screenshotStatus.lifecycleState, RunnerCommandLifecycleState.completed.rawValue)
208
+ XCTAssertEqual(screenshotStatus.lifecycleResponseOk, true)
209
+ XCTAssertNil(screenshotStatus.lifecycleResponseJson)
210
+
211
+ let largeRead = runnerJournalCommand("readText", id: "large-read")
212
+ journal.accept(command: largeRead)
213
+ journal.finish(
214
+ command: largeRead,
215
+ response: Response(ok: true, data: DataPayload(text: String(repeating: "x", count: 17 * 1024)))
216
+ )
217
+
218
+ let largeReadStatus = journal.status(commandId: "large-read")
219
+ XCTAssertEqual(largeReadStatus.lifecycleState, RunnerCommandLifecycleState.completed.rawValue)
220
+ XCTAssertEqual(largeReadStatus.lifecycleResponseOk, true)
221
+ XCTAssertNil(largeReadStatus.lifecycleResponseJson)
222
+ }
223
+
224
+ func testCommandJournalKeepsErrorMetadataWhenResponseJsonIsDropped() {
225
+ let journal = RunnerCommandJournal()
226
+ let snapshot = runnerJournalCommand("snapshot", id: "snapshot-error")
227
+ let hint = "Try a smaller read such as snapshot -s <visible label or id> -d 8."
228
+
229
+ journal.accept(command: snapshot)
230
+ journal.finish(
231
+ command: snapshot,
232
+ response: Response(
233
+ ok: false,
234
+ error: ErrorPayload(
235
+ code: "IOS_AX_SNAPSHOT_FAILED",
236
+ message: "iOS XCTest snapshot failed while serializing the accessibility tree.",
237
+ hint: hint
238
+ )
239
+ )
240
+ )
241
+
242
+ let status = journal.status(commandId: "snapshot-error")
243
+ XCTAssertEqual(status.lifecycleState, RunnerCommandLifecycleState.failed.rawValue)
244
+ XCTAssertEqual(status.lifecycleResponseOk, false)
245
+ XCTAssertNil(status.lifecycleResponseJson)
246
+ XCTAssertEqual(status.lifecycleErrorCode, "IOS_AX_SNAPSHOT_FAILED")
247
+ XCTAssertEqual(
248
+ status.lifecycleErrorMessage,
249
+ "iOS XCTest snapshot failed while serializing the accessibility tree."
250
+ )
251
+ XCTAssertEqual(status.lifecycleErrorHint, hint)
252
+ }
253
+
254
+ private func runnerJournalCommand(_ command: String, id: String) -> Command {
255
+ let json = #"{"command":"\#(command)","commandId":"\#(id)"}"#
256
+ return try! JSONDecoder().decode(Command.self, from: Data(json.utf8))
257
+ }
258
+
259
+ private func runnerJournalNode() -> SnapshotNode {
260
+ SnapshotNode(
261
+ index: 0,
262
+ type: "button",
263
+ label: "Continue",
264
+ identifier: "continue",
265
+ value: nil,
266
+ rect: SnapshotRect(x: 10, y: 20, width: 100, height: 44),
267
+ enabled: true,
268
+ focused: nil,
269
+ selected: nil,
270
+ hittable: true,
271
+ depth: 0,
272
+ parentIndex: nil,
273
+ hiddenContentAbove: nil,
274
+ hiddenContentBelow: nil
275
+ )
276
+ }
277
+
278
+ private func decodeRunnerJournalResponse(_ responseJson: String?) throws -> Response {
279
+ let responseJson = try XCTUnwrap(responseJson)
280
+ return try JSONDecoder().decode(Response.self, from: Data(responseJson.utf8))
281
+ }
282
+ }
@@ -0,0 +1,29 @@
1
+ import XCTest
2
+
3
+ extension RunnerTests {
4
+ /// Runs `block` and returns its value. If it raises an Objective-C exception, logs the message
5
+ /// under `AGENT_DEVICE_RUNNER_<tag>_IGNORED_EXCEPTION` and returns `fallback`.
6
+ ///
7
+ /// Consolidates the catch-log-and-default band-aid the runner uses around exception-prone
8
+ /// XCUITest queries (flaky `allElementsBoundByIndex` snapshots, stale element reads), giving the
9
+ /// "silently logged and continued" path one searchable format and one place to add per-tag
10
+ /// exception telemetry later. `RunnerObjCExceptionCatcher.catchException` takes a non-escaping
11
+ /// block, so `block` may capture `inout` state.
12
+ func safely<T>(_ tag: String, _ fallback: T, _ block: () -> T) -> T {
13
+ var result = fallback
14
+ let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
15
+ result = block()
16
+ })
17
+ if let exceptionMessage {
18
+ NSLog("AGENT_DEVICE_RUNNER_%@_IGNORED_EXCEPTION=%@", tag, exceptionMessage)
19
+ return fallback
20
+ }
21
+ return result
22
+ }
23
+
24
+ /// Optional-returning convenience: returns `nil` on exception (matching the common
25
+ /// `var x: T?` + catch-and-return-nil shape).
26
+ func safely<T>(_ tag: String, _ block: () -> T?) -> T? {
27
+ safely(tag, nil, block)
28
+ }
29
+ }