agent-device 0.18.2 → 0.19.0
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.18.2.apk → agent-device-android-multitouch-helper-0.19.0.apk} +0 -0
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.0.apk.sha256 +1 -0
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.18.2.manifest.json → agent-device-android-multitouch-helper-0.19.0.manifest.json} +4 -4
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.18.2.apk → agent-device-android-snapshot-helper-0.19.0.apk} +0 -0
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.0.apk.sha256 +1 -0
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.18.2.manifest.json → agent-device-android-snapshot-helper-0.19.0.manifest.json} +6 -6
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTapPointPolicy.swift +28 -0
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +9 -120
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +132 -66
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +155 -0
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +67 -0
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +20 -74
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +64 -0
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +0 -177
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +81 -48
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +102 -129
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +208 -46
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +82 -11
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +72 -0
- package/dist/src/agent-browser-tool.js +1 -0
- package/dist/src/android-adb.d.ts +64 -234
- package/dist/src/android-adb.js +1 -1
- package/dist/src/android.js +1 -5
- package/dist/src/app-lifecycle.js +7 -0
- package/dist/src/apple-runner-options.js +1 -0
- package/dist/src/apps.js +5 -11
- package/dist/src/artifacts.d.ts +4 -3
- package/dist/src/artifacts.js +1 -1
- package/dist/src/audio-probe.js +1 -1
- package/dist/src/batch.d.ts +2 -177
- package/dist/src/batch.js +1 -1
- package/dist/src/cli-help.js +64 -38
- package/dist/src/cli-output.js +1 -1
- package/dist/src/cli-runner.js +2 -0
- package/dist/src/cli.js +12 -41
- package/dist/src/client-metro-companion.js +1 -0
- package/dist/src/client.js +3 -0
- package/dist/src/command-metadata.js +1 -0
- package/dist/src/command-schema.js +22 -0
- package/dist/src/command-surface.js +1 -1
- package/dist/src/contracts.d.ts +2 -213
- package/dist/src/contracts.js +1 -1
- package/dist/src/debug-symbols.js +5 -3
- package/dist/src/device-input-state.js +2 -0
- package/dist/src/device-rotation.js +14 -0
- package/dist/src/devices.js +1 -1
- package/dist/src/devices2.js +1 -0
- package/dist/src/devices3.js +1 -0
- package/dist/src/find.js +1 -1
- package/dist/src/finders.d.ts +2 -81
- package/dist/src/finders.js +1 -1
- package/dist/src/generic.js +1 -1
- package/dist/src/handler-utils.js +1 -1
- package/dist/src/helper.js +2 -0
- package/dist/src/host-process.js +2 -0
- package/dist/src/index.d.ts +1265 -1768
- package/dist/src/index.js +1 -1
- package/dist/src/input-actions.js +3 -1
- package/dist/src/input-actions2.js +1 -0
- package/dist/src/install-source.d.ts +13 -14
- package/dist/src/install-source.js +1 -1
- package/dist/src/install-source2.js +1 -0
- package/dist/src/interaction-snapshot.js +1 -0
- package/dist/src/interaction.js +1 -1
- package/dist/src/interactor.js +2 -1
- package/dist/src/internal/bin.d.ts +1 -1
- package/dist/src/internal/bin.js +2 -5
- package/dist/src/internal/companion-tunnel.d.ts +1 -1
- package/dist/src/internal/companion-tunnel.js +1 -1
- package/dist/src/internal/daemon.d.ts +1 -1
- package/dist/src/internal/daemon.js +16 -1
- package/dist/src/internal/png-worker.d.ts +29 -26
- package/dist/src/internal/png-worker.js +1 -1
- package/dist/src/internal/update-check-entry.d.ts +1 -1
- package/dist/src/internal/update-check-entry.js +1 -1
- package/dist/src/io.d.ts +2 -85
- package/dist/src/io.js +1 -1
- package/dist/src/lease.js +1 -1
- package/dist/src/linux.js +1 -1
- package/dist/src/logcat.js +6 -0
- package/dist/src/manifest.js +1 -0
- package/dist/src/metro.d.ts +70 -113
- package/dist/src/metro.js +1 -1
- package/dist/src/metro2.js +1 -0
- package/dist/src/notifications.js +1 -1
- package/dist/src/output-path.js +1 -0
- package/dist/src/path-resolution.js +1 -0
- package/dist/src/png-worker-contract.js +1 -0
- package/dist/src/points.js +4 -0
- package/dist/src/project-runtime.js +1 -0
- package/dist/src/provider.js +1 -1
- package/dist/src/providers.js +1 -0
- package/dist/src/react-native.js +1 -1
- package/dist/src/record-trace-recording.js +2 -5
- package/dist/src/record-trace.js +1 -1
- package/dist/src/recording-provider.js +1 -1
- package/dist/src/registry.js +37 -0
- package/dist/src/remote-config.d.ts +2 -83
- package/dist/src/reporting.js +9 -0
- package/dist/src/rolldown-runtime.js +1 -0
- package/dist/src/runner-client.js +1 -0
- package/dist/src/runner-provider.js +1 -0
- package/dist/src/runner-sequence.js +1 -1
- package/dist/src/runtime-transport.js +1 -0
- package/dist/src/runtime.js +3 -0
- package/dist/src/scoped-provider.js +1 -0
- package/dist/src/sdk-android-adb.d.ts +144 -0
- package/dist/src/sdk-android-adb.js +4 -0
- package/dist/src/sdk-app-inventory.d.ts +4 -0
- package/dist/src/sdk-app-inventory.js +1 -0
- package/dist/src/sdk-batch-runner.d.ts +40 -0
- package/dist/src/sdk-batch-runner.js +2 -0
- package/dist/src/sdk-batch.js +2 -0
- package/dist/src/sdk-contracts.d.ts +386 -0
- package/dist/src/sdk-contracts.js +2 -0
- package/dist/src/sdk-finders.d.ts +61 -0
- package/dist/src/sdk-finders.js +1 -0
- package/dist/src/sdk-io.d.ts +79 -0
- package/dist/src/sdk-io.js +1 -0
- package/dist/src/sdk-remote-config.d.ts +140 -0
- package/dist/src/sdk-remote-config.js +1 -0
- package/dist/src/sdk-selectors.js +1 -0
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/selectors.d.ts +39 -137
- package/dist/src/selectors.js +1 -1
- package/dist/src/selectors2.js +1 -0
- package/dist/src/server.js +3 -2
- package/dist/src/session.js +10 -11
- package/dist/src/setting-state.js +1 -0
- package/dist/src/simctl.js +1 -0
- package/dist/src/simulator.js +1 -2
- package/dist/src/snapshot-diagnostics.js +3 -0
- package/dist/src/snapshot-helper.js +5 -0
- package/dist/src/snapshot-quality.js +1 -0
- package/dist/src/snapshot.js +1 -2
- package/dist/src/snapshot2.js +2 -0
- package/dist/src/{5560.js → timeouts.js} +1 -1
- package/dist/src/tool-provider.js +3 -0
- package/dist/src/tool-provider2.js +1 -0
- package/dist/src/ttl-memo.js +1 -0
- package/dist/src/update-check.js +1 -0
- package/dist/src/url.js +1 -0
- package/dist/src/version.js +1 -0
- package/dist/src/web.js +1 -1
- package/dist/src/xml.js +2 -0
- package/package.json +12 -19
- package/server.json +2 -2
- package/skills/agent-device/SKILL.md +2 -0
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.18.2.apk.sha256 +0 -1
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.18.2.apk.sha256 +0 -1
- package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +0 -443
- package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +0 -169
- package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +0 -220
- package/dist/src/1404.js +0 -1
- package/dist/src/1986.js +0 -1
- package/dist/src/2748.js +0 -1
- package/dist/src/2948.js +0 -30
- package/dist/src/3006.js +0 -1
- package/dist/src/314.js +0 -1
- package/dist/src/3340.js +0 -8
- package/dist/src/4418.js +0 -1
- package/dist/src/4428.js +0 -3
- package/dist/src/485.js +0 -1
- package/dist/src/4917.js +0 -1
- package/dist/src/495.js +0 -5
- package/dist/src/5448.js +0 -3
- package/dist/src/5484.js +0 -1
- package/dist/src/5643.js +0 -1
- package/dist/src/587.js +0 -1
- package/dist/src/5913.js +0 -4
- package/dist/src/6849.js +0 -1
- package/dist/src/6918.js +0 -1
- package/dist/src/6967.js +0 -1
- package/dist/src/760.js +0 -1
- package/dist/src/7871.js +0 -1
- package/dist/src/8184.js +0 -1
- package/dist/src/8225.js +0 -1
- package/dist/src/8391.js +0 -1
- package/dist/src/8404.js +0 -2
- package/dist/src/8414.js +0 -2
- package/dist/src/8875.js +0 -4
- package/dist/src/893.js +0 -1
- package/dist/src/8936.js +0 -1
- package/dist/src/9414.js +0 -1
- package/dist/src/9581.js +0 -22
- package/dist/src/9612.js +0 -1
- package/dist/src/9624.js +0 -1
- package/dist/src/9722.js +0 -38
- package/dist/src/devices~1.js +0 -1
- package/dist/src/devices~2.js +0 -1
- package/dist/src/input-actions~1.js +0 -1
- package/dist/src/rslib-runtime.js +0 -1
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcshareddata/xcschemes/AgentDeviceRunner.xcscheme +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests.entitlements +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.h +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.m +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Environment.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Exceptions.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TvRemote.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests.xctestplan +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-overlay.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-resize.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-trim.swift +0 -0
|
Binary file
|
package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.0.apk.sha256
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fbcaeff3de2f16c64ffe2fef8a4736b4392a08f11e3c957d826b40009195cf2e agent-device-android-multitouch-helper-0.19.0.apk
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "android-multitouch-helper",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"assetName": "agent-device-android-multitouch-helper-0.
|
|
5
|
-
"sha256": "
|
|
3
|
+
"version": "0.19.0",
|
|
4
|
+
"assetName": "agent-device-android-multitouch-helper-0.19.0.apk",
|
|
5
|
+
"sha256": "fbcaeff3de2f16c64ffe2fef8a4736b4392a08f11e3c957d826b40009195cf2e",
|
|
6
6
|
"packageName": "com.callstack.agentdevice.multitouchhelper",
|
|
7
|
-
"versionCode":
|
|
7
|
+
"versionCode": 19000,
|
|
8
8
|
"instrumentationRunner": "com.callstack.agentdevice.multitouchhelper/.MultiTouchInstrumentation",
|
|
9
9
|
"statusProtocol": "android-multitouch-helper-v1"
|
|
10
10
|
}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
56bb052ba4868491bdace904dc41f905cdde3a589e4f4f314d2a7d57c6f8d76c agent-device-android-snapshot-helper-0.19.0.apk
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "android-snapshot-helper",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"releaseTag": "v0.
|
|
5
|
-
"assetName": "agent-device-android-snapshot-helper-0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
|
+
"releaseTag": "v0.19.0",
|
|
5
|
+
"assetName": "agent-device-android-snapshot-helper-0.19.0.apk",
|
|
6
6
|
"apkUrl": null,
|
|
7
|
-
"sha256": "
|
|
8
|
-
"checksumName": "agent-device-android-snapshot-helper-0.
|
|
7
|
+
"sha256": "56bb052ba4868491bdace904dc41f905cdde3a589e4f4f314d2a7d57c6f8d76c",
|
|
8
|
+
"checksumName": "agent-device-android-snapshot-helper-0.19.0.apk.sha256",
|
|
9
9
|
"packageName": "com.callstack.agentdevice.snapshothelper",
|
|
10
|
-
"versionCode":
|
|
10
|
+
"versionCode": 19000,
|
|
11
11
|
"instrumentationRunner": "com.callstack.agentdevice.snapshothelper/.SnapshotInstrumentation",
|
|
12
12
|
"minSdk": 23,
|
|
13
13
|
"targetSdk": 36,
|
package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTapPointPolicy.swift
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
|
|
3
|
+
// The tap-point rule the runner shares with the TS runtime (ADR 0011 Layer 2).
|
|
4
|
+
//
|
|
5
|
+
// RULE: the tap point is the element frame's CENTER; a tap is allowed iff that
|
|
6
|
+
// center lies inside the window frame, edges inclusive. An empty or invalid
|
|
7
|
+
// window frame fails open (allowed): resolving the best available frame is the
|
|
8
|
+
// impure caller's job (onScreenWindowFrame / app.frame), and the policy must
|
|
9
|
+
// not turn a missing frame into a refusal.
|
|
10
|
+
//
|
|
11
|
+
// This is pure geometry on purpose — no XCUIElement — so the exact decision
|
|
12
|
+
// can be proven against the golden fixture table shared with the TS twin:
|
|
13
|
+
// table: contracts/fixtures/tap-point-policy.json
|
|
14
|
+
// TS twin: src/snapshot/mobile-snapshot-semantics.ts#isTapPointInsideViewport
|
|
15
|
+
// TS test: src/snapshot/__tests__/tap-point-policy-parity.test.ts
|
|
16
|
+
// Drift on either side turns CI red without needing a simulator.
|
|
17
|
+
enum TapPointPolicy {
|
|
18
|
+
static func isAllowed(elementFrame: CGRect, windowFrame: CGRect) -> Bool {
|
|
19
|
+
if windowFrame.isNull || windowFrame.isEmpty || windowFrame.isInfinite {
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
let centerX = elementFrame.midX
|
|
23
|
+
let centerY = elementFrame.midY
|
|
24
|
+
return centerX >= windowFrame.minX && centerX <= windowFrame.maxX
|
|
25
|
+
&& centerY >= windowFrame.minY && centerY <= windowFrame.maxY
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
@@ -12,7 +12,8 @@ extension RunnerTests {
|
|
|
12
12
|
|
|
13
13
|
func privateAXSnapshotCapture(
|
|
14
14
|
app: XCUIApplication,
|
|
15
|
-
options: SnapshotOptions
|
|
15
|
+
options: SnapshotOptions,
|
|
16
|
+
deadline: Date = .distantFuture
|
|
16
17
|
) -> SnapshotBackendCapture? {
|
|
17
18
|
#if os(iOS) && targetEnvironment(simulator)
|
|
18
19
|
let requestedDepth = options.depth ?? 64
|
|
@@ -24,6 +25,13 @@ extension RunnerTests {
|
|
|
24
25
|
var effectiveDepth = requestedDepth
|
|
25
26
|
var lastError = "unknown private AX snapshot failure"
|
|
26
27
|
for depth in attemptDepths {
|
|
28
|
+
// The first rung always runs (the plan gated entry on its own budget); later rungs
|
|
29
|
+
// stop when the capture-plan deadline is spent so ladder retries can never stack
|
|
30
|
+
// past the runner's main-thread watchdog (#1105).
|
|
31
|
+
if depth != attemptDepths.first, Date() >= deadline {
|
|
32
|
+
NSLog("AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_BUDGET_EXHAUSTED depth=%ld", depth)
|
|
33
|
+
break
|
|
34
|
+
}
|
|
27
35
|
response = RunnerAXSnapshotBridge.snapshotTree(
|
|
28
36
|
for: app,
|
|
29
37
|
maxDepth: depth,
|
|
@@ -208,122 +216,3 @@ extension RunnerTests {
|
|
|
208
216
|
}
|
|
209
217
|
}
|
|
210
218
|
|
|
211
|
-
#if AGENT_DEVICE_RUNNER_UNIT_TESTS
|
|
212
|
-
// MARK: - In-bundle unit tests
|
|
213
|
-
|
|
214
|
-
extension RunnerTests {
|
|
215
|
-
func testPrivateAXScopeSelectsSubtreeNotMatchingLabels() {
|
|
216
|
-
let tree: [String: Any] = [
|
|
217
|
-
"type": 1, "label": "App",
|
|
218
|
-
"children": [
|
|
219
|
-
[
|
|
220
|
-
"type": 9, "identifier": "homeScreen",
|
|
221
|
-
"children": [
|
|
222
|
-
["type": 48, "label": "Post body without the scope text", "children": []]
|
|
223
|
-
],
|
|
224
|
-
],
|
|
225
|
-
["type": 9, "label": "unrelated sibling", "children": []],
|
|
226
|
-
],
|
|
227
|
-
]
|
|
228
|
-
var nodes: [SnapshotNode] = []
|
|
229
|
-
appendPrivateAXNode(
|
|
230
|
-
tree,
|
|
231
|
-
to: &nodes,
|
|
232
|
-
options: SnapshotOptions(interactiveOnly: false, depth: nil, scope: "homeScreen", raw: false),
|
|
233
|
-
viewport: .infinite,
|
|
234
|
-
depth: 0,
|
|
235
|
-
parentIndex: nil,
|
|
236
|
-
insideMatchedScope: false
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
let labels = nodes.compactMap { $0.label ?? $0.identifier }
|
|
240
|
-
XCTAssertTrue(labels.contains("homeScreen"))
|
|
241
|
-
// Descendants of the matched scope are included even when they do not contain the text.
|
|
242
|
-
XCTAssertTrue(labels.contains("Post body without the scope text"))
|
|
243
|
-
XCTAssertFalse(labels.contains("unrelated sibling"))
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
func testPrivateAXInteractiveFiltersLoginLikeHiddenDrawer() {
|
|
247
|
-
let tree: [String: Any] = [
|
|
248
|
-
"type": Int(XCUIElement.ElementType.application.rawValue),
|
|
249
|
-
"label": "Blue Sky",
|
|
250
|
-
"frame": ["x": 0, "y": 0, "width": 390, "height": 844],
|
|
251
|
-
"children": [
|
|
252
|
-
[
|
|
253
|
-
"type": Int(XCUIElement.ElementType.scrollView.rawValue),
|
|
254
|
-
"frame": ["x": 0, "y": 0, "width": 390, "height": 844],
|
|
255
|
-
"children": [
|
|
256
|
-
[
|
|
257
|
-
"type": Int(XCUIElement.ElementType.image.rawValue),
|
|
258
|
-
"label": "Callstack",
|
|
259
|
-
"frame": ["x": 145, "y": 104, "width": 100, "height": 100],
|
|
260
|
-
"children": [],
|
|
261
|
-
],
|
|
262
|
-
[
|
|
263
|
-
"type": Int(XCUIElement.ElementType.staticText.rawValue),
|
|
264
|
-
"label": "Welcome back",
|
|
265
|
-
"frame": ["x": 32, "y": 260, "width": 326, "height": 32],
|
|
266
|
-
"children": [],
|
|
267
|
-
],
|
|
268
|
-
[
|
|
269
|
-
"type": Int(XCUIElement.ElementType.textField.rawValue),
|
|
270
|
-
"label": "Email",
|
|
271
|
-
"identifier": "login.email",
|
|
272
|
-
"frame": ["x": 32, "y": 348, "width": 326, "height": 48],
|
|
273
|
-
"children": [],
|
|
274
|
-
],
|
|
275
|
-
[
|
|
276
|
-
"type": Int(XCUIElement.ElementType.secureTextField.rawValue),
|
|
277
|
-
"label": "Password",
|
|
278
|
-
"identifier": "login.password",
|
|
279
|
-
"frame": ["x": 32, "y": 412, "width": 326, "height": 48],
|
|
280
|
-
"children": [],
|
|
281
|
-
],
|
|
282
|
-
[
|
|
283
|
-
"type": Int(XCUIElement.ElementType.button.rawValue),
|
|
284
|
-
"label": "Sign in",
|
|
285
|
-
"identifier": "login.submit",
|
|
286
|
-
"frame": ["x": 32, "y": 492, "width": 326, "height": 52],
|
|
287
|
-
"children": [],
|
|
288
|
-
],
|
|
289
|
-
[
|
|
290
|
-
"type": Int(XCUIElement.ElementType.link.rawValue),
|
|
291
|
-
"label": "Forgot password?",
|
|
292
|
-
"frame": ["x": 128, "y": 568, "width": 134, "height": 32],
|
|
293
|
-
"children": [],
|
|
294
|
-
],
|
|
295
|
-
[
|
|
296
|
-
"type": Int(XCUIElement.ElementType.button.rawValue),
|
|
297
|
-
"label": "Admin settings",
|
|
298
|
-
"frame": ["x": -260, "y": 184, "width": 220, "height": 44],
|
|
299
|
-
"children": [],
|
|
300
|
-
],
|
|
301
|
-
[
|
|
302
|
-
"type": Int(XCUIElement.ElementType.other.rawValue),
|
|
303
|
-
"frame": ["x": 16, "y": 184, "width": 220, "height": 44],
|
|
304
|
-
"children": [],
|
|
305
|
-
],
|
|
306
|
-
],
|
|
307
|
-
]
|
|
308
|
-
],
|
|
309
|
-
]
|
|
310
|
-
var nodes: [SnapshotNode] = []
|
|
311
|
-
appendPrivateAXNode(
|
|
312
|
-
tree,
|
|
313
|
-
to: &nodes,
|
|
314
|
-
options: SnapshotOptions(interactiveOnly: true, depth: nil, scope: nil, raw: false),
|
|
315
|
-
viewport: CGRect(x: 0, y: 0, width: 390, height: 844),
|
|
316
|
-
depth: 0,
|
|
317
|
-
parentIndex: nil,
|
|
318
|
-
insideMatchedScope: false
|
|
319
|
-
)
|
|
320
|
-
|
|
321
|
-
let labels = nodes.compactMap { $0.label }
|
|
322
|
-
XCTAssertEqual(
|
|
323
|
-
labels,
|
|
324
|
-
["Blue Sky", "Callstack", "Welcome back", "Email", "Password", "Sign in", "Forgot password?"]
|
|
325
|
-
)
|
|
326
|
-
XCTAssertFalse(labels.contains("Admin settings"))
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
#endif
|
|
@@ -130,58 +130,6 @@ extension RunnerTests {
|
|
|
130
130
|
return Response(ok: true, data: data)
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
#if AGENT_DEVICE_RUNNER_UNIT_TESTS
|
|
134
|
-
func testGestureResponseIncludesSynthesizedTapFallbackDiagnostics() {
|
|
135
|
-
let response = gestureResponse(
|
|
136
|
-
message: "tapped",
|
|
137
|
-
timing: (gestureStartUptimeMs: 1, gestureEndUptimeMs: 2),
|
|
138
|
-
fallback: GestureFallback(
|
|
139
|
-
strategy: "xctest-coordinate-tap",
|
|
140
|
-
message: "Runner synthesized coordinate tap is unavailable",
|
|
141
|
-
hint: "Using XCTest coordinate tap fallback."
|
|
142
|
-
)
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
XCTAssertEqual(response.ok, true)
|
|
146
|
-
XCTAssertEqual(response.data?.gestureFallback, "xctest-coordinate-tap")
|
|
147
|
-
XCTAssertEqual(
|
|
148
|
-
response.data?.gestureFallbackMessage,
|
|
149
|
-
"Runner synthesized coordinate tap is unavailable"
|
|
150
|
-
)
|
|
151
|
-
XCTAssertEqual(response.data?.gestureFallbackHint, "Using XCTest coordinate tap fallback.")
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
func testXCTestRecordedFailureResponseFailsMutatingSuccesses() throws {
|
|
155
|
-
let command = try runnerCommandFixture(#"{"command":"tap","commandId":"tap-1"}"#)
|
|
156
|
-
let response = Response(ok: true, data: DataPayload(message: "tapped"))
|
|
157
|
-
|
|
158
|
-
let failureResponse = xctestRecordedFailureResponse(command: command, response: response)
|
|
159
|
-
|
|
160
|
-
XCTAssertEqual(failureResponse?.ok, false)
|
|
161
|
-
XCTAssertEqual(failureResponse?.error?.code, "XCTEST_RECORDED_FAILURE")
|
|
162
|
-
XCTAssertEqual(
|
|
163
|
-
failureResponse?.error?.message,
|
|
164
|
-
"XCTest recorded a failure while executing tap; the action may not have been performed."
|
|
165
|
-
)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
func testXCTestRecordedFailureResponseDoesNotWrapReadOnlyOrRunnerFatalResponses() throws {
|
|
169
|
-
let snapshotCommand = try runnerCommandFixture(#"{"command":"snapshot","commandId":"snapshot-1"}"#)
|
|
170
|
-
let tapCommand = try runnerCommandFixture(#"{"command":"tap","commandId":"tap-1"}"#)
|
|
171
|
-
let runnerFatalResponse = Response(
|
|
172
|
-
ok: true,
|
|
173
|
-
data: DataPayload(runnerFatal: true, runnerFatalReason: "ax_snapshot_unavailable")
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
XCTAssertNil(
|
|
177
|
-
xctestRecordedFailureResponse(
|
|
178
|
-
command: snapshotCommand,
|
|
179
|
-
response: Response(ok: true, data: DataPayload(nodes: [], truncated: false))
|
|
180
|
-
)
|
|
181
|
-
)
|
|
182
|
-
XCTAssertNil(xctestRecordedFailureResponse(command: tapCommand, response: runnerFatalResponse))
|
|
183
|
-
}
|
|
184
|
-
#endif
|
|
185
133
|
|
|
186
134
|
func execute(command: Command) throws -> Response {
|
|
187
135
|
if command.command == .status {
|
|
@@ -233,23 +181,126 @@ extension RunnerTests {
|
|
|
233
181
|
)
|
|
234
182
|
}
|
|
235
183
|
|
|
184
|
+
/// Tracks one main-queue dispatch so the watchdog and the dispatched block can agree —
|
|
185
|
+
/// under `mainThreadWorkLock` — on exactly one of: finished in time, or abandoned.
|
|
186
|
+
private final class MainThreadWorkState {
|
|
187
|
+
var finished = false
|
|
188
|
+
var abandoned = false
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
enum MainThreadBusyState {
|
|
192
|
+
case idle
|
|
193
|
+
case busy(abandonedForSeconds: TimeInterval)
|
|
194
|
+
case wedged(abandonedForSeconds: TimeInterval)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
func currentMainThreadBusyState() -> MainThreadBusyState {
|
|
198
|
+
mainThreadWorkLock.lock()
|
|
199
|
+
defer { mainThreadWorkLock.unlock() }
|
|
200
|
+
guard abandonedMainThreadWorkCount > 0 else { return .idle }
|
|
201
|
+
let abandonedFor = abandonedMainThreadWorkSince.map { Date().timeIntervalSince($0) } ?? 0
|
|
202
|
+
if abandonedFor > mainThreadWedgeThreshold {
|
|
203
|
+
return .wedged(abandonedForSeconds: abandonedFor)
|
|
204
|
+
}
|
|
205
|
+
return .busy(abandonedForSeconds: abandonedFor)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private func runnerBusyResponse(command: Command, abandonedForSeconds: TimeInterval) -> Response {
|
|
209
|
+
NSLog(
|
|
210
|
+
"AGENT_DEVICE_RUNNER_BUSY command=%@ commandId=%@ abandonedForSeconds=%.1f",
|
|
211
|
+
command.command.rawValue,
|
|
212
|
+
command.commandId ?? "",
|
|
213
|
+
abandonedForSeconds
|
|
214
|
+
)
|
|
215
|
+
return Response(
|
|
216
|
+
ok: false,
|
|
217
|
+
error: ErrorPayload(
|
|
218
|
+
code: "RUNNER_BUSY",
|
|
219
|
+
message:
|
|
220
|
+
"The iOS runner is still finishing a previous command that exceeded its execution watchdog (usually an accessibility capture on a heavy or animating screen).",
|
|
221
|
+
hint:
|
|
222
|
+
"Wait a few seconds and retry. If snapshots keep failing on this screen, use screenshot as visual truth and interact by coordinates, or navigate to another screen."
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private func runnerWedgedResponse(command: Command, abandonedForSeconds: TimeInterval) -> Response {
|
|
228
|
+
NSLog(
|
|
229
|
+
"AGENT_DEVICE_RUNNER_WEDGED command=%@ commandId=%@ abandonedForSeconds=%.1f",
|
|
230
|
+
command.command.rawValue,
|
|
231
|
+
command.commandId ?? "",
|
|
232
|
+
abandonedForSeconds
|
|
233
|
+
)
|
|
234
|
+
return Response(
|
|
235
|
+
ok: false,
|
|
236
|
+
error: ErrorPayload(
|
|
237
|
+
code: "RUNNER_WEDGED",
|
|
238
|
+
message:
|
|
239
|
+
"The iOS runner main thread has been stuck in abandoned work for \(Int(abandonedForSeconds)) seconds and cannot recover on its own.",
|
|
240
|
+
hint:
|
|
241
|
+
"The runner session will be restarted. Retry the command after the restart; if this screen keeps wedging captures, use screenshot as visual truth and interact by coordinates."
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
236
246
|
private func executeDispatched(command: Command) throws -> Response {
|
|
247
|
+
// XCTest work cannot be cancelled mid-flight: once the watchdog abandons a main-queue
|
|
248
|
+
// block, queueing more main-thread commands behind it only buries the runner deeper.
|
|
249
|
+
// Refuse fast instead so the daemon backs off while the abandoned work drains; past the
|
|
250
|
+
// wedge threshold, escalate so the daemon recycles this runner (#1105).
|
|
251
|
+
switch currentMainThreadBusyState() {
|
|
252
|
+
case .idle:
|
|
253
|
+
break
|
|
254
|
+
case .busy(let abandonedForSeconds):
|
|
255
|
+
return runnerBusyResponse(command: command, abandonedForSeconds: abandonedForSeconds)
|
|
256
|
+
case .wedged(let abandonedForSeconds):
|
|
257
|
+
return runnerWedgedResponse(command: command, abandonedForSeconds: abandonedForSeconds)
|
|
258
|
+
}
|
|
237
259
|
if Thread.isMainThread {
|
|
238
260
|
return try executeOnMainSafely(command: command)
|
|
239
261
|
}
|
|
240
262
|
var result: Result<Response, Error>?
|
|
241
263
|
let semaphore = DispatchSemaphore(value: 0)
|
|
264
|
+
let workState = MainThreadWorkState()
|
|
242
265
|
DispatchQueue.main.async {
|
|
243
266
|
do {
|
|
244
267
|
result = .success(try self.executeOnMainSafely(command: command))
|
|
245
268
|
} catch {
|
|
246
269
|
result = .failure(error)
|
|
247
270
|
}
|
|
271
|
+
self.mainThreadWorkLock.lock()
|
|
272
|
+
if workState.abandoned {
|
|
273
|
+
self.abandonedMainThreadWorkCount -= 1
|
|
274
|
+
if self.abandonedMainThreadWorkCount == 0 {
|
|
275
|
+
self.abandonedMainThreadWorkSince = nil
|
|
276
|
+
NSLog("AGENT_DEVICE_RUNNER_ABANDONED_WORK_DRAINED")
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
workState.finished = true
|
|
280
|
+
}
|
|
281
|
+
self.mainThreadWorkLock.unlock()
|
|
248
282
|
semaphore.signal()
|
|
249
283
|
}
|
|
250
284
|
let waitResult = semaphore.wait(timeout: .now() + mainThreadExecutionTimeout)
|
|
251
285
|
if waitResult == .timedOut {
|
|
252
286
|
// The main queue work may still be running; we stop waiting and report timeout.
|
|
287
|
+
mainThreadWorkLock.lock()
|
|
288
|
+
let stillRunning = !workState.finished
|
|
289
|
+
if stillRunning {
|
|
290
|
+
workState.abandoned = true
|
|
291
|
+
abandonedMainThreadWorkCount += 1
|
|
292
|
+
if abandonedMainThreadWorkSince == nil {
|
|
293
|
+
abandonedMainThreadWorkSince = Date()
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
mainThreadWorkLock.unlock()
|
|
297
|
+
if stillRunning && command.command == .snapshot {
|
|
298
|
+
// The next capture on this screen must not re-grind the tree backend.
|
|
299
|
+
penalizeSnapshotTreeBackend(
|
|
300
|
+
bundleId: command.appBundleId,
|
|
301
|
+
reason: "main_thread_watchdog"
|
|
302
|
+
)
|
|
303
|
+
}
|
|
253
304
|
throw NSError(
|
|
254
305
|
domain: RunnerErrorDomain.general,
|
|
255
306
|
code: RunnerErrorCode.mainThreadExecutionTimedOut,
|
|
@@ -477,6 +528,24 @@ extension RunnerTests {
|
|
|
477
528
|
}
|
|
478
529
|
if let element = match.element {
|
|
479
530
|
let frame = element.frame
|
|
531
|
+
// XCTest reports closed-drawer/off-viewport items as hittable, then
|
|
532
|
+
// "taps" coordinates outside the visible window as a silent no-op.
|
|
533
|
+
// Refuse instead; the daemon falls back to tree-based resolution,
|
|
534
|
+
// which can prefer an on-screen candidate or explain the off-screen
|
|
535
|
+
// state. The check uses the main window frame, not app.frame: on RN
|
|
536
|
+
// apps app.frame unions transformed subtrees (a closed drawer at
|
|
537
|
+
// negative x), so it happily "contains" unreachable coordinates.
|
|
538
|
+
// The DECISION lives in TapPointPolicy (golden parity table with the
|
|
539
|
+
// TS twin); onScreenWindowFrame only supplies the frame.
|
|
540
|
+
if !match.usedNonHittableFallback
|
|
541
|
+
&& !TapPointPolicy.isAllowed(
|
|
542
|
+
elementFrame: frame,
|
|
543
|
+
windowFrame: onScreenWindowFrame(app: activeApp)
|
|
544
|
+
) {
|
|
545
|
+
return Response(ok: false, error: ErrorPayload(
|
|
546
|
+
code: "ELEMENT_OFFSCREEN",
|
|
547
|
+
message: "element resolved off-screen at (\(Int(frame.midX)), \(Int(frame.midY)))"))
|
|
548
|
+
}
|
|
480
549
|
let isTextEntry = isTextEntryElement(element)
|
|
481
550
|
let touchFrame = frame.isEmpty
|
|
482
551
|
? nil
|
|
@@ -504,18 +573,6 @@ extension RunnerTests {
|
|
|
504
573
|
}
|
|
505
574
|
return Response(ok: false, error: ErrorPayload(code: "ELEMENT_NOT_FOUND", message: "element not found"))
|
|
506
575
|
}
|
|
507
|
-
if let text = command.text {
|
|
508
|
-
if let element = findElement(app: activeApp, text: text) {
|
|
509
|
-
let (timing, outcome) = performGesture(activeApp) {
|
|
510
|
-
activateElement(app: activeApp, element: element, action: "tap by text")
|
|
511
|
-
}
|
|
512
|
-
if let response = unsupportedResponse(for: outcome) {
|
|
513
|
-
return response
|
|
514
|
-
}
|
|
515
|
-
return gestureResponse(message: "tapped", timing: timing)
|
|
516
|
-
}
|
|
517
|
-
return Response(ok: false, error: ErrorPayload(message: "element not found"))
|
|
518
|
-
}
|
|
519
576
|
if let x = command.x, let y = command.y {
|
|
520
577
|
var fallback: GestureFallback?
|
|
521
578
|
if command.synthesized == true {
|
|
@@ -539,7 +596,7 @@ extension RunnerTests {
|
|
|
539
596
|
fallback: fallback
|
|
540
597
|
)
|
|
541
598
|
}
|
|
542
|
-
return Response(ok: false, error: ErrorPayload(message: "tap requires
|
|
599
|
+
return Response(ok: false, error: ErrorPayload(message: "tap requires a selector or x/y"))
|
|
543
600
|
case .mouseClick:
|
|
544
601
|
guard let x = command.x, let y = command.y else {
|
|
545
602
|
return Response(ok: false, error: ErrorPayload(message: "mouseClick requires x and y"))
|
|
@@ -1128,6 +1185,7 @@ extension RunnerTests {
|
|
|
1128
1185
|
let delaySeconds = Double(max(command.delayMs ?? 0, 0)) / 1000.0
|
|
1129
1186
|
let textEntryMode = resolveTextEntryMode(command)
|
|
1130
1187
|
let target: TextEntryTarget
|
|
1188
|
+
let focusStartedAt = Date()
|
|
1131
1189
|
if let selectorKey = command.selectorKey, let selectorValue = command.selectorValue {
|
|
1132
1190
|
let match = findElement(
|
|
1133
1191
|
app: activeApp,
|
|
@@ -1148,6 +1206,13 @@ extension RunnerTests {
|
|
|
1148
1206
|
} else {
|
|
1149
1207
|
target = focusTextInputForTextEntry(app: activeApp, x: command.x, y: command.y)
|
|
1150
1208
|
}
|
|
1209
|
+
NSLog(
|
|
1210
|
+
"AGENT_DEVICE_RUNNER_TEXT_ENTRY_PHASE commandId=%@ phase=focus durationMs=%.1f chars=%d mode=%@",
|
|
1211
|
+
command.commandId ?? "",
|
|
1212
|
+
Date().timeIntervalSince(focusStartedAt) * 1000.0,
|
|
1213
|
+
text.count,
|
|
1214
|
+
textEntryModeName(textEntryMode)
|
|
1215
|
+
)
|
|
1151
1216
|
if textEntryMode == .replacement {
|
|
1152
1217
|
guard target.element != nil else {
|
|
1153
1218
|
let message =
|
|
@@ -1162,7 +1227,8 @@ extension RunnerTests {
|
|
|
1162
1227
|
target: target,
|
|
1163
1228
|
text: text,
|
|
1164
1229
|
delaySeconds: delaySeconds,
|
|
1165
|
-
repairMode: textEntryMode
|
|
1230
|
+
repairMode: textEntryMode,
|
|
1231
|
+
commandId: command.commandId
|
|
1166
1232
|
)
|
|
1167
1233
|
if textResult.verified == false {
|
|
1168
1234
|
let expected = textResult.expectedText ?? ""
|
|
@@ -0,0 +1,155 @@
|
|
|
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, .longPress, .drag,
|
|
147
|
+
.remotePress, .type, .swipe, .scroll, .desktopScroll, .findText, .querySelector, .readText, .back,
|
|
148
|
+
.backInApp, .backSystem, .home, .rotate, .appSwitcher, .keyboardDismiss, .keyboardReturn,
|
|
149
|
+
.alert, .pinch, .sequence, .rotateGesture, .transformGesture, .recordStart, .recordStop,
|
|
150
|
+
.status, .uptime, .shutdown:
|
|
151
|
+
return true
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|