agent-device 0.16.9 → 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.
- package/README.md +1 -0
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.16.9.apk → agent-device-android-multitouch-helper-0.16.10.apk} +0 -0
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.16.10.apk.sha256 +1 -0
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.16.9.manifest.json → agent-device-android-multitouch-helper-0.16.10.manifest.json} +4 -4
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.16.9.apk → agent-device-android-snapshot-helper-0.16.10.apk} +0 -0
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.16.10.apk.sha256 +1 -0
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.16.9.manifest.json → agent-device-android-snapshot-helper-0.16.10.manifest.json} +6 -6
- package/dist/src/2415.js +19 -19
- package/dist/src/apps.js +2 -2
- package/dist/src/input-actions.js +1 -1
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +197 -232
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +282 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Exceptions.swift +29 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +8 -34
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +30 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +2 -20
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +10 -50
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +3 -23
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +64 -22
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +7 -4
- package/package.json +1 -1
- package/server.json +2 -2
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.16.9.apk.sha256 +0 -1
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.16.9.apk.sha256 +0 -1
package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift
ADDED
|
@@ -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
|
+
}
|
|
@@ -285,14 +285,13 @@ extension RunnerTests {
|
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
private func textInputCandidatesAt(app: XCUIApplication, point: CGPoint) -> [XCUIElement] {
|
|
288
|
-
|
|
289
|
-
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
288
|
+
safely("TEXT_INPUT_AT_POINT", []) {
|
|
290
289
|
// Query the text-input element types directly instead of enumerating the entire tree
|
|
291
290
|
// (app.descendants(.any).allElementsBoundByIndex snapshots every element and is ~10x
|
|
292
291
|
// slower — it dominated fill latency because resolveTextEntryElement re-runs this on
|
|
293
292
|
// each verify/repair poll once the focused field reference goes stale).
|
|
294
293
|
// Prefer the smallest matching field so nested editable controls win over large containers.
|
|
295
|
-
|
|
294
|
+
[
|
|
296
295
|
app.textFields,
|
|
297
296
|
app.secureTextFields,
|
|
298
297
|
app.searchFields,
|
|
@@ -318,15 +317,7 @@ extension RunnerTests {
|
|
|
318
317
|
}
|
|
319
318
|
return left.elementType.rawValue < right.elementType.rawValue
|
|
320
319
|
}
|
|
321
|
-
})
|
|
322
|
-
if let exceptionMessage {
|
|
323
|
-
NSLog(
|
|
324
|
-
"AGENT_DEVICE_RUNNER_TEXT_INPUT_AT_POINT_IGNORED_EXCEPTION=%@",
|
|
325
|
-
exceptionMessage
|
|
326
|
-
)
|
|
327
|
-
return []
|
|
328
320
|
}
|
|
329
|
-
return candidates
|
|
330
321
|
}
|
|
331
322
|
|
|
332
323
|
private func frameContainsPoint(_ frame: CGRect, _ point: CGPoint, tolerance: CGFloat) -> Bool {
|
|
@@ -431,9 +422,8 @@ extension RunnerTests {
|
|
|
431
422
|
|
|
432
423
|
private func singleTextEntryElement(app: XCUIApplication) -> XCUIElement? {
|
|
433
424
|
#if os(iOS)
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
matches = app.descendants(matching: .any).allElementsBoundByIndex.filter { element in
|
|
425
|
+
let matches = safely("KEYBOARD_RETURN_TEXT_ENTRY_QUERY", []) {
|
|
426
|
+
app.descendants(matching: .any).allElementsBoundByIndex.filter { element in
|
|
437
427
|
guard element.exists else { return false }
|
|
438
428
|
switch element.elementType {
|
|
439
429
|
case .textField, .secureTextField, .searchField, .textView:
|
|
@@ -442,13 +432,6 @@ extension RunnerTests {
|
|
|
442
432
|
return false
|
|
443
433
|
}
|
|
444
434
|
}
|
|
445
|
-
})
|
|
446
|
-
if let exceptionMessage {
|
|
447
|
-
NSLog(
|
|
448
|
-
"AGENT_DEVICE_RUNNER_KEYBOARD_RETURN_TEXT_ENTRY_QUERY_IGNORED_EXCEPTION=%@",
|
|
449
|
-
exceptionMessage
|
|
450
|
-
)
|
|
451
|
-
return nil
|
|
452
435
|
}
|
|
453
436
|
return matches.count == 1 ? matches[0] : nil
|
|
454
437
|
#else
|
|
@@ -784,22 +767,13 @@ extension RunnerTests {
|
|
|
784
767
|
|
|
785
768
|
private func visibleKeyboardFrame(app: XCUIApplication) -> CGRect? {
|
|
786
769
|
#if os(iOS)
|
|
787
|
-
|
|
788
|
-
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
770
|
+
return safely("KEYBOARD_FRAME") {
|
|
789
771
|
let keyboard = app.keyboards.firstMatch
|
|
790
|
-
guard keyboard.exists else { return }
|
|
772
|
+
guard keyboard.exists else { return nil }
|
|
791
773
|
let keyboardFrame = keyboard.frame
|
|
792
|
-
guard !keyboardFrame.isEmpty else { return }
|
|
793
|
-
|
|
794
|
-
})
|
|
795
|
-
if let exceptionMessage {
|
|
796
|
-
NSLog(
|
|
797
|
-
"AGENT_DEVICE_RUNNER_KEYBOARD_FRAME_IGNORED_EXCEPTION=%@",
|
|
798
|
-
exceptionMessage
|
|
799
|
-
)
|
|
800
|
-
return nil
|
|
774
|
+
guard !keyboardFrame.isEmpty else { return nil }
|
|
775
|
+
return keyboardFrame
|
|
801
776
|
}
|
|
802
|
-
return frame
|
|
803
777
|
#else
|
|
804
778
|
return nil
|
|
805
779
|
#endif
|
|
@@ -30,6 +30,7 @@ enum CommandType: String, Codable {
|
|
|
30
30
|
case transformGesture
|
|
31
31
|
case recordStart
|
|
32
32
|
case recordStop
|
|
33
|
+
case status
|
|
33
34
|
case uptime
|
|
34
35
|
case shutdown
|
|
35
36
|
}
|
|
@@ -91,6 +92,9 @@ extension CommandType {
|
|
|
91
92
|
case .recordStop, .uptime, .shutdown:
|
|
92
93
|
return CommandTraits(isInteraction: false, readOnly: .never, isLifecycle: true)
|
|
93
94
|
|
|
95
|
+
case .status:
|
|
96
|
+
return CommandTraits(isInteraction: false, readOnly: .always, isLifecycle: true)
|
|
97
|
+
|
|
94
98
|
// Normal preflight, not retried.
|
|
95
99
|
// NOTE: mouseClick stays non-interaction for now — it is macOS-only and the foreground
|
|
96
100
|
// guard interacts with bespoke macOS activation, so classifying it needs a macOS smoke
|
|
@@ -104,6 +108,8 @@ extension CommandType {
|
|
|
104
108
|
|
|
105
109
|
struct Command: Codable {
|
|
106
110
|
let command: CommandType
|
|
111
|
+
let commandId: String?
|
|
112
|
+
let statusCommandId: String?
|
|
107
113
|
let appBundleId: String?
|
|
108
114
|
let text: String?
|
|
109
115
|
let selectorKey: String?
|
|
@@ -171,6 +177,14 @@ struct DataPayload: Codable {
|
|
|
171
177
|
let referenceWidth: Double?
|
|
172
178
|
let referenceHeight: Double?
|
|
173
179
|
let currentUptimeMs: Double?
|
|
180
|
+
let commandId: String?
|
|
181
|
+
let lifecycleState: String?
|
|
182
|
+
let lifecycleCommand: String?
|
|
183
|
+
let lifecycleResponseOk: Bool?
|
|
184
|
+
let lifecycleResponseJson: String?
|
|
185
|
+
let lifecycleErrorCode: String?
|
|
186
|
+
let lifecycleErrorMessage: String?
|
|
187
|
+
let lifecycleErrorHint: String?
|
|
174
188
|
let visible: Bool?
|
|
175
189
|
let wasVisible: Bool?
|
|
176
190
|
let dismissed: Bool?
|
|
@@ -192,6 +206,14 @@ struct DataPayload: Codable {
|
|
|
192
206
|
referenceWidth: Double? = nil,
|
|
193
207
|
referenceHeight: Double? = nil,
|
|
194
208
|
currentUptimeMs: Double? = nil,
|
|
209
|
+
commandId: String? = nil,
|
|
210
|
+
lifecycleState: String? = nil,
|
|
211
|
+
lifecycleCommand: String? = nil,
|
|
212
|
+
lifecycleResponseOk: Bool? = nil,
|
|
213
|
+
lifecycleResponseJson: String? = nil,
|
|
214
|
+
lifecycleErrorCode: String? = nil,
|
|
215
|
+
lifecycleErrorMessage: String? = nil,
|
|
216
|
+
lifecycleErrorHint: String? = nil,
|
|
195
217
|
visible: Bool? = nil,
|
|
196
218
|
wasVisible: Bool? = nil,
|
|
197
219
|
dismissed: Bool? = nil,
|
|
@@ -212,6 +234,14 @@ struct DataPayload: Codable {
|
|
|
212
234
|
self.referenceWidth = referenceWidth
|
|
213
235
|
self.referenceHeight = referenceHeight
|
|
214
236
|
self.currentUptimeMs = currentUptimeMs
|
|
237
|
+
self.commandId = commandId
|
|
238
|
+
self.lifecycleState = lifecycleState
|
|
239
|
+
self.lifecycleCommand = lifecycleCommand
|
|
240
|
+
self.lifecycleResponseOk = lifecycleResponseOk
|
|
241
|
+
self.lifecycleResponseJson = lifecycleResponseJson
|
|
242
|
+
self.lifecycleErrorCode = lifecycleErrorCode
|
|
243
|
+
self.lifecycleErrorMessage = lifecycleErrorMessage
|
|
244
|
+
self.lifecycleErrorHint = lifecycleErrorHint
|
|
215
245
|
self.visible = visible
|
|
216
246
|
self.wasVisible = wasVisible
|
|
217
247
|
self.dismissed = dismissed
|
|
@@ -354,14 +354,7 @@ extension RunnerTests {
|
|
|
354
354
|
}
|
|
355
355
|
|
|
356
356
|
private func safeSnapshotViewport(app: XCUIApplication) -> CGRect {
|
|
357
|
-
|
|
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
|
|
357
|
+
safely("SNAPSHOT_VIEWPORT", CGRect.infinite) { snapshotViewport(app: app) }
|
|
365
358
|
}
|
|
366
359
|
|
|
367
360
|
private func describeSnapshotError(_ error: Error) -> String {
|
|
@@ -718,18 +711,7 @@ extension RunnerTests {
|
|
|
718
711
|
}
|
|
719
712
|
|
|
720
713
|
private func safeSnapshotElementsQuery(_ fetch: () -> [XCUIElement]) -> [XCUIElement] {
|
|
721
|
-
|
|
722
|
-
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
723
|
-
elements = fetch()
|
|
724
|
-
})
|
|
725
|
-
if let exceptionMessage {
|
|
726
|
-
NSLog(
|
|
727
|
-
"AGENT_DEVICE_RUNNER_SNAPSHOT_QUERY_IGNORED_EXCEPTION=%@",
|
|
728
|
-
exceptionMessage
|
|
729
|
-
)
|
|
730
|
-
return []
|
|
731
|
-
}
|
|
732
|
-
return elements
|
|
714
|
+
safely("SNAPSHOT_QUERY", [], fetch)
|
|
733
715
|
}
|
|
734
716
|
|
|
735
717
|
private func isScrollableContainer(_ snapshot: XCUIElementSnapshot, visible: Bool) -> Bool {
|
|
@@ -77,33 +77,11 @@ extension RunnerTests {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
func safeElementsQuery(_ fetch: () -> [XCUIElement]) -> [XCUIElement] {
|
|
80
|
-
|
|
81
|
-
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
82
|
-
elements = fetch()
|
|
83
|
-
})
|
|
84
|
-
if let exceptionMessage {
|
|
85
|
-
NSLog(
|
|
86
|
-
"AGENT_DEVICE_RUNNER_MODAL_QUERY_IGNORED_EXCEPTION=%@",
|
|
87
|
-
exceptionMessage
|
|
88
|
-
)
|
|
89
|
-
return []
|
|
90
|
-
}
|
|
91
|
-
return elements
|
|
80
|
+
safely("MODAL_QUERY", [], fetch)
|
|
92
81
|
}
|
|
93
82
|
|
|
94
83
|
private func safeIsBlockingSystemModal(_ element: XCUIElement, in springboard: XCUIApplication) -> Bool {
|
|
95
|
-
|
|
96
|
-
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
97
|
-
isBlocking = isBlockingSystemModal(element, in: springboard)
|
|
98
|
-
})
|
|
99
|
-
if let exceptionMessage {
|
|
100
|
-
NSLog(
|
|
101
|
-
"AGENT_DEVICE_RUNNER_MODAL_CHECK_IGNORED_EXCEPTION=%@",
|
|
102
|
-
exceptionMessage
|
|
103
|
-
)
|
|
104
|
-
return false
|
|
105
|
-
}
|
|
106
|
-
return isBlocking
|
|
84
|
+
safely("MODAL_CHECK", false) { isBlockingSystemModal(element, in: springboard) }
|
|
107
85
|
}
|
|
108
86
|
|
|
109
87
|
private func isBlockingSystemModal(_ element: XCUIElement, in springboard: XCUIApplication) -> Bool {
|
|
@@ -134,25 +112,16 @@ extension RunnerTests {
|
|
|
134
112
|
}
|
|
135
113
|
|
|
136
114
|
private func safeIsActionableCandidate(_ candidate: XCUIElement, seen: inout Set<String>) -> Bool {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if !
|
|
140
|
-
if !actionableTypes.contains(candidate.elementType) { return }
|
|
115
|
+
safely("MODAL_ACTION", false) {
|
|
116
|
+
if !candidate.exists || !candidate.isHittable { return false }
|
|
117
|
+
if !actionableTypes.contains(candidate.elementType) { return false }
|
|
141
118
|
let frame = candidate.frame
|
|
142
|
-
if frame.isNull || frame.isEmpty { return }
|
|
119
|
+
if frame.isNull || frame.isEmpty { return false }
|
|
143
120
|
let key = "\(candidate.elementType.rawValue)-\(frame.origin.x)-\(frame.origin.y)-\(frame.size.width)-\(frame.size.height)-\(candidate.label)"
|
|
144
|
-
if seen.contains(key) { return }
|
|
121
|
+
if seen.contains(key) { return false }
|
|
145
122
|
seen.insert(key)
|
|
146
|
-
|
|
147
|
-
})
|
|
148
|
-
if let exceptionMessage {
|
|
149
|
-
NSLog(
|
|
150
|
-
"AGENT_DEVICE_RUNNER_MODAL_ACTION_IGNORED_EXCEPTION=%@",
|
|
151
|
-
exceptionMessage
|
|
152
|
-
)
|
|
153
|
-
return false
|
|
123
|
+
return true
|
|
154
124
|
}
|
|
155
|
-
return include
|
|
156
125
|
}
|
|
157
126
|
|
|
158
127
|
private func preferredSystemModalTitle(_ element: XCUIElement) -> String {
|
|
@@ -205,9 +174,8 @@ extension RunnerTests {
|
|
|
205
174
|
depth: Int,
|
|
206
175
|
hittableOverride: Bool? = nil
|
|
207
176
|
) -> SnapshotNode? {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
node = makeSnapshotNode(
|
|
177
|
+
safely("MODAL_NODE") {
|
|
178
|
+
makeSnapshotNode(
|
|
211
179
|
element: element,
|
|
212
180
|
index: index,
|
|
213
181
|
type: type,
|
|
@@ -216,14 +184,6 @@ extension RunnerTests {
|
|
|
216
184
|
depth: depth,
|
|
217
185
|
hittableOverride: hittableOverride
|
|
218
186
|
)
|
|
219
|
-
})
|
|
220
|
-
if let exceptionMessage {
|
|
221
|
-
NSLog(
|
|
222
|
-
"AGENT_DEVICE_RUNNER_MODAL_NODE_IGNORED_EXCEPTION=%@",
|
|
223
|
-
exceptionMessage
|
|
224
|
-
)
|
|
225
|
-
return nil
|
|
226
187
|
}
|
|
227
|
-
return node
|
|
228
188
|
}
|
|
229
189
|
}
|
|
@@ -73,8 +73,7 @@ extension RunnerTests {
|
|
|
73
73
|
// under XCUITest, so text entry readiness is driven by tap/keyboard state.
|
|
74
74
|
return nil
|
|
75
75
|
#else
|
|
76
|
-
|
|
77
|
-
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
76
|
+
return safely("FOCUSED_INPUT_QUERY") {
|
|
78
77
|
let candidates = app
|
|
79
78
|
.descendants(matching: .any)
|
|
80
79
|
.matching(NSPredicate(format: "hasKeyboardFocus == 1"))
|
|
@@ -82,21 +81,13 @@ extension RunnerTests {
|
|
|
82
81
|
for candidate in candidates where candidate.exists {
|
|
83
82
|
switch candidate.elementType {
|
|
84
83
|
case .textField, .secureTextField, .searchField, .textView:
|
|
85
|
-
|
|
86
|
-
return
|
|
84
|
+
return candidate
|
|
87
85
|
default:
|
|
88
86
|
continue
|
|
89
87
|
}
|
|
90
88
|
}
|
|
91
|
-
})
|
|
92
|
-
if let exceptionMessage {
|
|
93
|
-
NSLog(
|
|
94
|
-
"AGENT_DEVICE_RUNNER_FOCUSED_INPUT_QUERY_IGNORED_EXCEPTION=%@",
|
|
95
|
-
exceptionMessage
|
|
96
|
-
)
|
|
97
89
|
return nil
|
|
98
90
|
}
|
|
99
|
-
return focused
|
|
100
91
|
#endif
|
|
101
92
|
}
|
|
102
93
|
|
|
@@ -650,18 +641,7 @@ extension RunnerTests {
|
|
|
650
641
|
|
|
651
642
|
private func keyboardElementExists(app: XCUIApplication) -> Bool {
|
|
652
643
|
#if os(iOS)
|
|
653
|
-
|
|
654
|
-
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
655
|
-
exists = app.keyboards.firstMatch.exists
|
|
656
|
-
})
|
|
657
|
-
if let exceptionMessage {
|
|
658
|
-
NSLog(
|
|
659
|
-
"AGENT_DEVICE_RUNNER_KEYBOARD_EXISTS_IGNORED_EXCEPTION=%@",
|
|
660
|
-
exceptionMessage
|
|
661
|
-
)
|
|
662
|
-
return false
|
|
663
|
-
}
|
|
664
|
-
return exists
|
|
644
|
+
return safely("KEYBOARD_EXISTS", false) { app.keyboards.firstMatch.exists }
|
|
665
645
|
#else
|
|
666
646
|
return false
|
|
667
647
|
#endif
|