agent-device 0.20.5 → 0.20.7
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 +5 -3
- package/android/ime-helper/dist/{agent-device-android-ime-helper-0.20.5.apk → agent-device-android-ime-helper-0.20.7.apk} +0 -0
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.7.apk.sha256 +1 -0
- package/android/ime-helper/dist/{agent-device-android-ime-helper-0.20.5.manifest.json → agent-device-android-ime-helper-0.20.7.manifest.json} +4 -4
- package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.20.5.apk → agent-device-android-snapshot-helper-0.20.7.apk} +0 -0
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.7.apk.sha256 +1 -0
- package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.20.5.manifest.json → agent-device-android-snapshot-helper-0.20.7.manifest.json} +6 -6
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +48 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +1 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +108 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +630 -89
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSelectorMatchPolicy.swift +78 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +126 -187
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedTextEntry.h +32 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedTextEntry.m +212 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +149 -10
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +161 -32
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CoordinateTextEntryTests.swift +4 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +36 -28
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Keyboard.swift +23 -5
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +10 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+LifecycleCacheTests.swift +0 -67
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +18 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift +2 -22
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SelectorMatchPolicyTests.swift +4 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +2 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +145 -20
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotTraversalIdentityTests.swift +0 -20
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +4 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedTextEntry.swift +234 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +106 -448
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntryPolicyTests.swift +4 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextTyping.swift +602 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +18 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.h +72 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.m +89 -0
- package/dist/src/agent-browser-tool.js +1 -1
- package/dist/src/agent-device-client.js +2 -2
- package/dist/src/android-adb.js +1 -1
- package/dist/src/android.js +1 -1
- package/dist/src/app-helpers.js +1 -1
- package/dist/src/app-lifecycle.js +7 -7
- package/dist/src/app-lifecycle2.js +2 -0
- package/dist/src/app-log-files.js +1 -0
- package/dist/src/app-log-reconnect.js +1 -0
- package/dist/src/app-log-runtime.js +4 -0
- package/dist/src/app-resolution.js +2 -1
- package/dist/src/apps.js +5 -5
- package/dist/src/audio-probe.js +1 -1
- package/dist/src/auth-session.js +1 -0
- package/dist/src/auth.js +2 -0
- package/dist/src/boot-diagnostics.js +2 -2
- package/dist/src/cli-config.js +1 -1
- package/dist/src/cli-grammar.js +1 -0
- package/dist/src/cli-help.js +204 -254
- package/dist/src/cli-output.js +1 -0
- package/dist/src/cli-runner.js +1 -2
- package/dist/src/cli.js +6 -16
- package/dist/src/client-metro.js +1 -1
- package/dist/src/cloud-artifacts.d.ts +2 -137
- package/dist/src/command-metadata.js +1 -1
- package/dist/src/command-schema.js +3 -3
- package/dist/src/connection-runtime.js +1 -0
- package/dist/src/connection.js +5 -0
- package/dist/src/context.js +1 -1
- package/dist/src/daemon-shutdown-report.js +1 -0
- package/dist/src/daemon.js +2 -0
- package/dist/src/debug-symbols.js +3 -3
- package/dist/src/device-claim-inspection.js +1 -0
- package/dist/src/device-input-state.js +2 -2
- package/dist/src/device.js +1 -1
- package/dist/src/device2.js +2 -0
- package/dist/src/find.js +1 -1
- package/dist/src/finders.js +1 -1
- package/dist/src/generic-settle.js +1 -0
- package/dist/src/generic.js +1 -1
- package/dist/src/harmonyos.js +1 -0
- package/dist/src/helper.js +1 -1
- package/dist/src/host-process.js +2 -2
- package/dist/src/ime-lifecycle.js +7 -7
- package/dist/src/index.d.ts +69 -18
- package/dist/src/input-actions.js +4 -4
- package/dist/src/input-actions2.js +1 -1
- package/dist/src/input-actions3.js +1 -0
- package/dist/src/install-artifact.js +1 -5
- package/dist/src/install-artifact2.js +1 -1
- package/dist/src/install-source2.js +1 -1
- package/dist/src/interaction-runtime.js +1 -0
- package/dist/src/interaction-snapshot.js +1 -1
- package/dist/src/interaction-touch-reference-frame.js +1 -0
- package/dist/src/interaction.js +1 -1
- package/dist/src/interactor.js +2 -2
- package/dist/src/interactor2.js +1 -1
- package/dist/src/internal/bin.js +2 -2
- package/dist/src/internal/daemon.js +9 -16
- package/dist/src/inventory.js +5 -0
- package/dist/src/inventory2.js +1 -0
- package/dist/src/inventory3.js +2 -0
- package/dist/src/inventory4.js +1 -0
- package/dist/src/inventory5.js +1 -0
- package/dist/src/inventory6.js +1 -0
- package/dist/src/lease-context.js +1 -1
- package/dist/src/lease.js +1 -1
- package/dist/src/limrun-runtime-dependencies.js +1 -0
- package/dist/src/limrun.d.ts +10 -104
- package/dist/src/limrun.js +1 -1
- package/dist/src/linux.js +1 -1
- package/dist/src/logcat.js +1 -1
- package/dist/src/manifest.js +1 -1
- package/dist/src/owner-identity.js +1 -0
- package/dist/src/physical-device-control.js +1 -1
- package/dist/src/platform-runtime-app-log-android-transport.js +1 -0
- package/dist/src/platform-runtime-app-log-process.js +1 -0
- package/dist/src/platform-runtime-host.js +1 -0
- package/dist/src/platform-runtime-network-web-transport.js +1 -0
- package/dist/src/platform-runtime-operation-host.js +5 -0
- package/dist/src/platform-runtime-toolchain-host.js +1 -0
- package/dist/src/platform-runtime.js +1 -0
- package/dist/src/prepare-kind.js +15 -0
- package/dist/src/process-entry.js +1 -0
- package/dist/src/process-exit.js +1 -0
- package/dist/src/process-lock.js +1 -1
- package/dist/src/provider-webdriver.js +1 -0
- package/dist/src/provider.js +1 -0
- package/dist/src/proxy.js +2 -0
- package/dist/src/react-native.js +1 -1
- package/dist/src/record-trace.js +1 -1
- package/dist/src/registry.js +25 -24
- package/dist/src/remote-config.js +1 -1
- package/dist/src/remote-config2.js +1 -1
- package/dist/src/replay.js +3 -0
- package/dist/src/reporting.js +4 -4
- package/dist/src/runner-client.js +1 -1
- package/dist/src/runner-disposal.js +1 -1
- package/dist/src/runner-lease.js +1 -1
- package/dist/src/runtime.js +3 -3
- package/dist/src/runtime2.js +4 -0
- package/dist/src/runtime3.js +3 -0
- package/dist/src/runtime4.js +1 -0
- package/dist/src/runtime5.js +1 -0
- package/dist/src/runtime6.js +1 -0
- package/dist/src/runtime7.js +1 -0
- package/dist/src/screenshot-result.js +4 -4
- package/dist/src/screenshot.js +1 -0
- package/dist/src/sdk-android-adb.d.ts +20 -2
- package/dist/src/sdk-android-adb.js +2 -2
- package/dist/src/sdk-batch-runner.js +3 -3
- package/dist/src/sdk-contracts.d.ts +47 -6
- package/dist/src/sdk-contracts.js +1 -1
- package/dist/src/sdk-device.js +1 -1
- package/dist/src/sdk-finders.d.ts +4 -20
- package/dist/src/sdk-finders.js +1 -1
- package/dist/src/sdk-remote-config.js +1 -1
- package/dist/src/sdk-selectors.d.ts +601 -0
- package/dist/src/sdk-selectors.js +1 -1
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/selector-vocabulary.js +1 -1
- package/dist/src/selectors.d.ts +1 -46
- package/dist/src/selectors.js +1 -1
- package/dist/src/server.js +3 -3
- package/dist/src/session-target-evidence.js +1 -1
- package/dist/src/session.js +10 -9
- package/dist/src/shared.js +2 -0
- package/dist/src/shell-quote.js +1 -0
- package/dist/src/simulator.js +1 -1
- package/dist/src/snapshot-runtime.js +1 -0
- package/dist/src/snapshot-session.js +1 -0
- package/dist/src/snapshot.js +1 -1
- package/dist/src/snapshot2.js +1 -1
- package/dist/src/src.js +1 -1
- package/dist/src/src2.js +2 -0
- package/dist/src/src3.js +1 -0
- package/dist/src/text-surface.js +1 -0
- package/dist/src/timing-safe-equal.js +1 -0
- package/dist/src/tool-provider.js +3 -3
- package/dist/src/tool-provider2.js +1 -1
- package/dist/src/tool-provider3.js +1 -1
- package/dist/src/update-check.js +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/web.js +1 -1
- package/package.json +72 -50
- package/server.json +3 -3
- package/skills/agent-device/SKILL.md +6 -31
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.5.apk.sha256 +0 -1
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.5.apk.sha256 +0 -1
- package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/recording-resize.swift +0 -225
- package/dist/src/app-log-request-scope.js +0 -1
- package/dist/src/devices.js +0 -1
- package/dist/src/devices2.js +0 -1
- package/dist/src/devices3.js +0 -1
- package/dist/src/devices4.js +0 -1
- package/dist/src/interaction.d.ts +0 -230
- package/dist/src/provider-limrun-runtime.js +0 -1
- package/dist/src/register-builtins.js +0 -1
- package/dist/src/screenshot-geometry.js +0 -1
- package/dist/src/viewport-dimension.js +0 -14
package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift
CHANGED
|
@@ -4,6 +4,29 @@ import XCTest
|
|
|
4
4
|
// pipeline, readiness polling, and field clearing. Behavior-preserving extraction from
|
|
5
5
|
// RunnerTests+Interaction.swift (no logic changes) to keep that file navigable.
|
|
6
6
|
extension RunnerTests {
|
|
7
|
+
enum TextEntryFailure: String {
|
|
8
|
+
case notFocused = "TEXT_INPUT_NOT_FOCUSED"
|
|
9
|
+
case synthesisUnavailable = "TEXT_INPUT_SYNTHESIS_UNAVAILABLE"
|
|
10
|
+
|
|
11
|
+
var message: String {
|
|
12
|
+
switch self {
|
|
13
|
+
case .notFocused:
|
|
14
|
+
return "No focused text input was available for typing."
|
|
15
|
+
case .synthesisUnavailable:
|
|
16
|
+
return "Reliable text synthesis is unavailable while the software keyboard is hidden."
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
var hint: String {
|
|
21
|
+
switch self {
|
|
22
|
+
case .notFocused:
|
|
23
|
+
return "Focus a visible text input, then retry type or fill. If the input is not exposed by accessibility, use a coordinate focus command before typing."
|
|
24
|
+
case .synthesisUnavailable:
|
|
25
|
+
return "Show the software keyboard, then retry type or fill."
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
7
30
|
enum TextTypingRepairMode {
|
|
8
31
|
case none
|
|
9
32
|
case append
|
|
@@ -17,6 +40,7 @@ extension RunnerTests {
|
|
|
17
40
|
static let pollInterval: TimeInterval = 0.02
|
|
18
41
|
static let warmupValueTimeout: TimeInterval = 0.4
|
|
19
42
|
static let verificationStabilityWindow: TimeInterval = 0.2
|
|
43
|
+
static let synthesizedCommitTimeout: TimeInterval = 3.0
|
|
20
44
|
}
|
|
21
45
|
|
|
22
46
|
struct TextEntryResult {
|
|
@@ -24,12 +48,27 @@ extension RunnerTests {
|
|
|
24
48
|
let repaired: Bool
|
|
25
49
|
let expectedText: String?
|
|
26
50
|
let observedText: String?
|
|
51
|
+
var textEntryRoute: String? = nil
|
|
52
|
+
var failure: TextEntryFailure? = nil
|
|
27
53
|
}
|
|
28
54
|
|
|
29
55
|
struct TextEntryTarget {
|
|
30
56
|
let element: XCUIElement?
|
|
31
57
|
let refreshPoint: CGPoint?
|
|
32
58
|
let prefersFocusedElement: Bool
|
|
59
|
+
let fromTapWitness: Bool
|
|
60
|
+
|
|
61
|
+
init(
|
|
62
|
+
element: XCUIElement?,
|
|
63
|
+
refreshPoint: CGPoint?,
|
|
64
|
+
prefersFocusedElement: Bool,
|
|
65
|
+
fromTapWitness: Bool = false
|
|
66
|
+
) {
|
|
67
|
+
self.element = element
|
|
68
|
+
self.refreshPoint = refreshPoint
|
|
69
|
+
self.prefersFocusedElement = prefersFocusedElement
|
|
70
|
+
self.fromTapWitness = fromTapWitness
|
|
71
|
+
}
|
|
33
72
|
|
|
34
73
|
func withElement(_ nextElement: XCUIElement?) -> TextEntryTarget {
|
|
35
74
|
guard let nextElement else {
|
|
@@ -40,7 +79,8 @@ extension RunnerTests {
|
|
|
40
79
|
return TextEntryTarget(
|
|
41
80
|
element: nextElement,
|
|
42
81
|
refreshPoint: point,
|
|
43
|
-
prefersFocusedElement: prefersFocusedElement
|
|
82
|
+
prefersFocusedElement: prefersFocusedElement,
|
|
83
|
+
fromTapWitness: fromTapWitness
|
|
44
84
|
)
|
|
45
85
|
}
|
|
46
86
|
}
|
|
@@ -50,6 +90,16 @@ extension RunnerTests {
|
|
|
50
90
|
let focusConfirmed: Bool
|
|
51
91
|
}
|
|
52
92
|
|
|
93
|
+
struct TextEntryTapWitness {
|
|
94
|
+
let element: XCUIElement
|
|
95
|
+
let bundleId: String?
|
|
96
|
+
let processIdentifier: Int?
|
|
97
|
+
|
|
98
|
+
func matches(bundleId: String?, processIdentifier: Int?) -> Bool {
|
|
99
|
+
self.bundleId == bundleId && self.processIdentifier == processIdentifier
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
53
103
|
func clearTextInput(_ element: XCUIElement) {
|
|
54
104
|
// Skip the clear (delete burst + moveCaretToEnd edge-tap) ONLY when we can confirm the
|
|
55
105
|
// field is empty. Why skip: the edge-tap computes a point from the element frame, which can
|
|
@@ -95,6 +145,54 @@ extension RunnerTests {
|
|
|
95
145
|
#endif
|
|
96
146
|
}
|
|
97
147
|
|
|
148
|
+
func rememberTextEntryTap(_ element: XCUIElement?) {
|
|
149
|
+
guard let element, isTextEntryElement(element) else {
|
|
150
|
+
clearRememberedTextEntryTap()
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
textEntryTapWitness = TextEntryTapWitness(
|
|
154
|
+
element: element,
|
|
155
|
+
bundleId: currentBundleId,
|
|
156
|
+
processIdentifier: currentAppProcessIdentifier
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
func clearRememberedTextEntryTap() {
|
|
161
|
+
textEntryTapWitness = nil
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private func rememberedTextEntryTarget() -> TextEntryTarget? {
|
|
165
|
+
guard let witness = textEntryTapWitness else {
|
|
166
|
+
return nil
|
|
167
|
+
}
|
|
168
|
+
// The tap is proof for one immediately-following bare type only. Consume it before checking
|
|
169
|
+
// the element so a failed or interrupted type cannot reuse stale focus evidence.
|
|
170
|
+
clearRememberedTextEntryTap()
|
|
171
|
+
guard witness.matches(
|
|
172
|
+
bundleId: currentBundleId,
|
|
173
|
+
processIdentifier: currentAppProcessIdentifier
|
|
174
|
+
) else {
|
|
175
|
+
return nil
|
|
176
|
+
}
|
|
177
|
+
let element = witness.element
|
|
178
|
+
// XCUIElement is query-backed rather than a stable node identity. A same-identifier field
|
|
179
|
+
// introduced by app-side navigation between tap and this immediate type can therefore
|
|
180
|
+
// re-resolve here; keep the witness one-shot and fail closed on every observable identity
|
|
181
|
+
// boundary instead of using frame equality, which would reject legitimate layout changes.
|
|
182
|
+
guard safely("LAST_TAPPED_TEXT_INPUT_EXISTS", false, { element.exists }) else {
|
|
183
|
+
return nil
|
|
184
|
+
}
|
|
185
|
+
// Keep the target scoped to the element that the preceding tap actually selected. Do not
|
|
186
|
+
// attach a refresh point: if that element disappeared, bare type must fail closed rather
|
|
187
|
+
// than rediscovering a different field or dispatching unscoped app.typeText.
|
|
188
|
+
return TextEntryTarget(
|
|
189
|
+
element: element,
|
|
190
|
+
refreshPoint: nil,
|
|
191
|
+
prefersFocusedElement: false,
|
|
192
|
+
fromTapWitness: true
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
98
196
|
func stabilizeTextInputBeforeTyping(
|
|
99
197
|
app: XCUIApplication,
|
|
100
198
|
target: XCUIElement?,
|
|
@@ -123,11 +221,15 @@ extension RunnerTests {
|
|
|
123
221
|
|
|
124
222
|
func focusTextInputForTextEntry(app: XCUIApplication, x: Double?, y: Double?) -> TextEntryTarget {
|
|
125
223
|
guard let x, let y else {
|
|
224
|
+
let softwareKeyboardVisible = isKeyboardVisible(app: app)
|
|
225
|
+
if !softwareKeyboardVisible, let rememberedTarget = rememberedTextEntryTarget() {
|
|
226
|
+
return rememberedTarget
|
|
227
|
+
}
|
|
126
228
|
// Bare `type` targets the current first responder. On iOS we intentionally do not trust
|
|
127
229
|
// `hasKeyboardFocus`, but an already-visible software keyboard is sufficient evidence that
|
|
128
230
|
// app.typeText has a receiver; waiting the full readiness timeout cannot prove a stronger
|
|
129
231
|
// target because there is no selector/coordinate focus move to validate.
|
|
130
|
-
if
|
|
232
|
+
if softwareKeyboardVisible {
|
|
131
233
|
return TextEntryTarget(
|
|
132
234
|
element: focusedTextInput(app: app),
|
|
133
235
|
refreshPoint: nil,
|
|
@@ -244,451 +346,7 @@ extension RunnerTests {
|
|
|
244
346
|
}
|
|
245
347
|
}
|
|
246
348
|
|
|
247
|
-
func
|
|
248
|
-
app: XCUIApplication,
|
|
249
|
-
target: TextEntryTarget,
|
|
250
|
-
text: String,
|
|
251
|
-
delaySeconds: Double,
|
|
252
|
-
repairMode: TextTypingRepairMode = .none,
|
|
253
|
-
commandId: String? = nil
|
|
254
|
-
) -> TextEntryResult {
|
|
255
|
-
let totalStartedAt = Date()
|
|
256
|
-
guard !text.isEmpty else {
|
|
257
|
-
logTextEntryPhase(commandId: commandId, phase: "total", startedAt: totalStartedAt, chars: 0, mode: repairMode)
|
|
258
|
-
return TextEntryResult(verified: true, repaired: false, expectedText: "", observedText: "")
|
|
259
|
-
}
|
|
260
|
-
var activeTarget = target
|
|
261
|
-
let initialResolveStartedAt = Date()
|
|
262
|
-
let initialTarget = resolveTextEntryElement(app: app, target: activeTarget)
|
|
263
|
-
activeTarget = activeTarget.withElement(initialTarget)
|
|
264
|
-
let currentText = editableTextValue(for: initialTarget, treatingPlaceholderAsEmpty: true)
|
|
265
|
-
let initialText = repairMode == .append ? currentText : nil
|
|
266
|
-
let expectedText = expectedTextEntryValue(typedText: text, mode: repairMode, initialText: initialText)
|
|
267
|
-
logTextEntryPhase(
|
|
268
|
-
commandId: commandId,
|
|
269
|
-
phase: "initial-resolve",
|
|
270
|
-
startedAt: initialResolveStartedAt,
|
|
271
|
-
chars: text.count,
|
|
272
|
-
mode: repairMode
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
if repairMode == .replacement {
|
|
276
|
-
guard let replacementTarget = initialTarget else {
|
|
277
|
-
logTextEntryPhase(commandId: commandId, phase: "total", startedAt: totalStartedAt, chars: text.count, mode: repairMode)
|
|
278
|
-
return TextEntryResult(verified: nil, repaired: false, expectedText: expectedText, observedText: nil)
|
|
279
|
-
}
|
|
280
|
-
if currentText == nil || currentText?.isEmpty == false {
|
|
281
|
-
let clearStartedAt = Date()
|
|
282
|
-
clearTextInput(replacementTarget)
|
|
283
|
-
activeTarget = activeTarget.withElement(replacementTarget)
|
|
284
|
-
logTextEntryPhase(
|
|
285
|
-
commandId: commandId,
|
|
286
|
-
phase: "clear",
|
|
287
|
-
startedAt: clearStartedAt,
|
|
288
|
-
chars: text.count,
|
|
289
|
-
mode: repairMode
|
|
290
|
-
)
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
func typeIntoCurrentTarget(_ value: String) -> XCUIElement? {
|
|
295
|
-
if let currentTarget = resolveTextEntryElement(app: app, target: activeTarget) {
|
|
296
|
-
currentTarget.typeText(value)
|
|
297
|
-
return currentTarget
|
|
298
|
-
} else {
|
|
299
|
-
app.typeText(value)
|
|
300
|
-
return resolveTextEntryElement(app: app, target: activeTarget)
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
func waitForWarmupValue(_ expectedValue: String?, target: TextEntryTarget) {
|
|
305
|
-
guard let expectedValue else {
|
|
306
|
-
sleepFor(TextEntryTiming.pollInterval)
|
|
307
|
-
return
|
|
308
|
-
}
|
|
309
|
-
let deadline = Date().addingTimeInterval(TextEntryTiming.warmupValueTimeout)
|
|
310
|
-
while Date() < deadline {
|
|
311
|
-
if editableTextValue(for: resolveTextEntryElement(app: app, target: target)) == expectedValue {
|
|
312
|
-
return
|
|
313
|
-
}
|
|
314
|
-
sleepFor(TextEntryTiming.pollInterval)
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
let characters = Array(text)
|
|
319
|
-
if delaySeconds > 0 && characters.count > 1 {
|
|
320
|
-
var typedTarget: XCUIElement?
|
|
321
|
-
let delayedTypeStartedAt = Date()
|
|
322
|
-
for (index, character) in characters.enumerated() {
|
|
323
|
-
typedTarget = typeIntoCurrentTarget(String(character)) ?? typedTarget
|
|
324
|
-
if index + 1 < characters.count {
|
|
325
|
-
sleepFor(delaySeconds)
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
logTextEntryPhase(
|
|
329
|
-
commandId: commandId,
|
|
330
|
-
phase: "type-delayed",
|
|
331
|
-
startedAt: delayedTypeStartedAt,
|
|
332
|
-
chars: characters.count,
|
|
333
|
-
mode: repairMode
|
|
334
|
-
)
|
|
335
|
-
if repairMode == .none {
|
|
336
|
-
logTextEntryPhase(commandId: commandId, phase: "total", startedAt: totalStartedAt, chars: text.count, mode: repairMode)
|
|
337
|
-
return TextEntryResult(verified: nil, repaired: false, expectedText: nil, observedText: nil)
|
|
338
|
-
}
|
|
339
|
-
let verifyStartedAt = Date()
|
|
340
|
-
let result = verifyTextEntryWithRepairIfNeeded(
|
|
341
|
-
app: app,
|
|
342
|
-
target: activeTarget.withElement(typedTarget),
|
|
343
|
-
expectedText: expectedText,
|
|
344
|
-
repairMode: repairMode
|
|
345
|
-
)
|
|
346
|
-
logTextEntryPhase(
|
|
347
|
-
commandId: commandId,
|
|
348
|
-
phase: "verify",
|
|
349
|
-
startedAt: verifyStartedAt,
|
|
350
|
-
chars: characters.count,
|
|
351
|
-
mode: repairMode
|
|
352
|
-
)
|
|
353
|
-
logTextEntryPhase(commandId: commandId, phase: "total", startedAt: totalStartedAt, chars: text.count, mode: repairMode)
|
|
354
|
-
return result
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
let typedTarget: XCUIElement?
|
|
358
|
-
if repairMode != .none && characters.count > 1 {
|
|
359
|
-
let firstCharacter = String(characters[0])
|
|
360
|
-
let firstStartedAt = Date()
|
|
361
|
-
var firstTypedTarget = typeIntoCurrentTarget(firstCharacter)
|
|
362
|
-
logTextEntryPhase(
|
|
363
|
-
commandId: commandId,
|
|
364
|
-
phase: "type-first",
|
|
365
|
-
startedAt: firstStartedAt,
|
|
366
|
-
chars: 1,
|
|
367
|
-
mode: repairMode
|
|
368
|
-
)
|
|
369
|
-
activeTarget = activeTarget.withElement(firstTypedTarget)
|
|
370
|
-
let warmupExpectedText = expectedTextEntryValue(
|
|
371
|
-
typedText: firstCharacter,
|
|
372
|
-
mode: repairMode,
|
|
373
|
-
initialText: initialText
|
|
374
|
-
)
|
|
375
|
-
let warmupStartedAt = Date()
|
|
376
|
-
waitForWarmupValue(warmupExpectedText, target: activeTarget)
|
|
377
|
-
logTextEntryPhase(
|
|
378
|
-
commandId: commandId,
|
|
379
|
-
phase: "warmup",
|
|
380
|
-
startedAt: warmupStartedAt,
|
|
381
|
-
chars: 1,
|
|
382
|
-
mode: repairMode
|
|
383
|
-
)
|
|
384
|
-
let remainingText = String(characters.dropFirst())
|
|
385
|
-
let remainingStartedAt = Date()
|
|
386
|
-
firstTypedTarget = typeIntoCurrentTarget(remainingText) ?? firstTypedTarget
|
|
387
|
-
logTextEntryPhase(
|
|
388
|
-
commandId: commandId,
|
|
389
|
-
phase: "type-remaining",
|
|
390
|
-
startedAt: remainingStartedAt,
|
|
391
|
-
chars: characters.count - 1,
|
|
392
|
-
mode: repairMode
|
|
393
|
-
)
|
|
394
|
-
typedTarget = firstTypedTarget
|
|
395
|
-
} else {
|
|
396
|
-
let typeStartedAt = Date()
|
|
397
|
-
typedTarget = typeIntoCurrentTarget(text)
|
|
398
|
-
logTextEntryPhase(
|
|
399
|
-
commandId: commandId,
|
|
400
|
-
phase: "type-all",
|
|
401
|
-
startedAt: typeStartedAt,
|
|
402
|
-
chars: characters.count,
|
|
403
|
-
mode: repairMode
|
|
404
|
-
)
|
|
405
|
-
}
|
|
406
|
-
if repairMode == .none {
|
|
407
|
-
logTextEntryPhase(commandId: commandId, phase: "total", startedAt: totalStartedAt, chars: text.count, mode: repairMode)
|
|
408
|
-
return TextEntryResult(verified: nil, repaired: false, expectedText: nil, observedText: nil)
|
|
409
|
-
}
|
|
410
|
-
let verifyStartedAt = Date()
|
|
411
|
-
let result = verifyTextEntryWithRepairIfNeeded(
|
|
412
|
-
app: app,
|
|
413
|
-
target: activeTarget.withElement(typedTarget),
|
|
414
|
-
expectedText: expectedText,
|
|
415
|
-
repairMode: repairMode
|
|
416
|
-
)
|
|
417
|
-
logTextEntryPhase(
|
|
418
|
-
commandId: commandId,
|
|
419
|
-
phase: "verify",
|
|
420
|
-
startedAt: verifyStartedAt,
|
|
421
|
-
chars: characters.count,
|
|
422
|
-
mode: repairMode
|
|
423
|
-
)
|
|
424
|
-
logTextEntryPhase(commandId: commandId, phase: "total", startedAt: totalStartedAt, chars: text.count, mode: repairMode)
|
|
425
|
-
return result
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
private func logTextEntryPhase(
|
|
429
|
-
commandId: String?,
|
|
430
|
-
phase: String,
|
|
431
|
-
startedAt: Date,
|
|
432
|
-
chars: Int,
|
|
433
|
-
mode: TextTypingRepairMode
|
|
434
|
-
) {
|
|
435
|
-
NSLog(
|
|
436
|
-
"AGENT_DEVICE_RUNNER_TEXT_ENTRY_PHASE commandId=%@ phase=%@ durationMs=%.1f chars=%d mode=%@",
|
|
437
|
-
commandId ?? "",
|
|
438
|
-
phase,
|
|
439
|
-
Date().timeIntervalSince(startedAt) * 1000.0,
|
|
440
|
-
chars,
|
|
441
|
-
textEntryModeName(mode)
|
|
442
|
-
)
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
func textEntryModeName(_ mode: TextTypingRepairMode) -> String {
|
|
446
|
-
switch mode {
|
|
447
|
-
case .none:
|
|
448
|
-
return "none"
|
|
449
|
-
case .append:
|
|
450
|
-
return "append"
|
|
451
|
-
case .replacement:
|
|
452
|
-
return "replacement"
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
private func verifyTextEntryWithRepairIfNeeded(
|
|
457
|
-
app: XCUIApplication,
|
|
458
|
-
target: TextEntryTarget,
|
|
459
|
-
expectedText: String?,
|
|
460
|
-
repairMode: TextTypingRepairMode
|
|
461
|
-
) -> TextEntryResult {
|
|
462
|
-
let initialResult = verifyTextEntry(
|
|
463
|
-
app: app,
|
|
464
|
-
target: target,
|
|
465
|
-
expectedText: expectedText,
|
|
466
|
-
repaired: false
|
|
467
|
-
)
|
|
468
|
-
#if os(iOS)
|
|
469
|
-
guard initialResult.verified == false,
|
|
470
|
-
let expectedText = initialResult.expectedText
|
|
471
|
-
else {
|
|
472
|
-
return initialResult
|
|
473
|
-
}
|
|
474
|
-
guard shouldRepairTextEntry(
|
|
475
|
-
app: app,
|
|
476
|
-
target: target,
|
|
477
|
-
expectedText: expectedText,
|
|
478
|
-
repairMode: repairMode
|
|
479
|
-
) else {
|
|
480
|
-
return verifyTextEntry(
|
|
481
|
-
app: app,
|
|
482
|
-
target: target,
|
|
483
|
-
expectedText: expectedText,
|
|
484
|
-
repaired: false
|
|
485
|
-
)
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
guard let repairTarget = resolveTextEntryElement(app: app, target: target) else {
|
|
489
|
-
return initialResult
|
|
490
|
-
}
|
|
491
|
-
let observedText = editableTextValue(for: repairTarget) ?? ""
|
|
492
|
-
NSLog(
|
|
493
|
-
"AGENT_DEVICE_RUNNER_REPAIR_TEXT_ENTRY expectedLength=%d observedLength=%d",
|
|
494
|
-
expectedText.count,
|
|
495
|
-
observedText.count
|
|
496
|
-
)
|
|
497
|
-
clearTextInput(repairTarget)
|
|
498
|
-
repairTarget.typeText(expectedText)
|
|
499
|
-
return verifyTextEntry(
|
|
500
|
-
app: app,
|
|
501
|
-
target: target.withElement(repairTarget),
|
|
502
|
-
expectedText: expectedText,
|
|
503
|
-
repaired: true
|
|
504
|
-
)
|
|
505
|
-
#else
|
|
506
|
-
return initialResult
|
|
507
|
-
#endif
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
private func verifyTextEntry(
|
|
511
|
-
app: XCUIApplication,
|
|
512
|
-
target: TextEntryTarget,
|
|
513
|
-
expectedText: String?,
|
|
514
|
-
repaired: Bool
|
|
515
|
-
) -> TextEntryResult {
|
|
516
|
-
let targetElement = resolveTextEntryElement(app: app, target: target)
|
|
517
|
-
guard let expectedText else {
|
|
518
|
-
return TextEntryResult(
|
|
519
|
-
verified: nil,
|
|
520
|
-
repaired: repaired,
|
|
521
|
-
expectedText: nil,
|
|
522
|
-
observedText: editableTextValue(for: targetElement)
|
|
523
|
-
)
|
|
524
|
-
}
|
|
525
|
-
guard let observedText = editableTextValue(for: targetElement) else {
|
|
526
|
-
return TextEntryResult(verified: nil, repaired: repaired, expectedText: expectedText, observedText: nil)
|
|
527
|
-
}
|
|
528
|
-
guard textEntryValueMatchesExpected(targetElement, observedText: observedText, expectedText: expectedText) else {
|
|
529
|
-
return TextEntryResult(
|
|
530
|
-
verified: false,
|
|
531
|
-
repaired: repaired,
|
|
532
|
-
expectedText: expectedText,
|
|
533
|
-
observedText: observedText
|
|
534
|
-
)
|
|
535
|
-
}
|
|
536
|
-
let stableDeadline = Date().addingTimeInterval(TextEntryTiming.verificationStabilityWindow)
|
|
537
|
-
var latestObservedText = observedText
|
|
538
|
-
while Date() < stableDeadline {
|
|
539
|
-
sleepFor(TextEntryTiming.pollInterval)
|
|
540
|
-
guard let nextObservedText = editableTextValue(for: resolveTextEntryElement(app: app, target: target)) else {
|
|
541
|
-
return TextEntryResult(verified: nil, repaired: repaired, expectedText: expectedText, observedText: nil)
|
|
542
|
-
}
|
|
543
|
-
latestObservedText = nextObservedText
|
|
544
|
-
guard textEntryValueMatchesExpected(
|
|
545
|
-
resolveTextEntryElement(app: app, target: target),
|
|
546
|
-
observedText: nextObservedText,
|
|
547
|
-
expectedText: expectedText
|
|
548
|
-
) else {
|
|
549
|
-
return TextEntryResult(
|
|
550
|
-
verified: false,
|
|
551
|
-
repaired: repaired,
|
|
552
|
-
expectedText: expectedText,
|
|
553
|
-
observedText: nextObservedText
|
|
554
|
-
)
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
return TextEntryResult(
|
|
558
|
-
verified: true,
|
|
559
|
-
repaired: repaired,
|
|
560
|
-
expectedText: expectedText,
|
|
561
|
-
observedText: latestObservedText
|
|
562
|
-
)
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
private func textEntryValueMatchesExpected(
|
|
566
|
-
_ element: XCUIElement?,
|
|
567
|
-
observedText: String,
|
|
568
|
-
expectedText: String
|
|
569
|
-
) -> Bool {
|
|
570
|
-
if observedText == expectedText {
|
|
571
|
-
return true
|
|
572
|
-
}
|
|
573
|
-
guard hasTextEntrySubmitSuffix(expectedText), element?.elementType != .textView else {
|
|
574
|
-
return false
|
|
575
|
-
}
|
|
576
|
-
var submittedText = expectedText
|
|
577
|
-
while hasTextEntrySubmitSuffix(submittedText) {
|
|
578
|
-
submittedText.removeLast()
|
|
579
|
-
}
|
|
580
|
-
return observedText == submittedText
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
private func hasTextEntrySubmitSuffix(_ text: String) -> Bool {
|
|
584
|
-
text.hasSuffix("\n") || text.hasSuffix("\r")
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
private func expectedTextEntryValue(
|
|
588
|
-
typedText: String,
|
|
589
|
-
mode: TextTypingRepairMode,
|
|
590
|
-
initialText: String?
|
|
591
|
-
) -> String? {
|
|
592
|
-
switch mode {
|
|
593
|
-
case .none:
|
|
594
|
-
return nil
|
|
595
|
-
case .append:
|
|
596
|
-
guard let initialText else {
|
|
597
|
-
return nil
|
|
598
|
-
}
|
|
599
|
-
return initialText + typedText
|
|
600
|
-
case .replacement:
|
|
601
|
-
return typedText
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
private func shouldRepairTextEntry(
|
|
606
|
-
app: XCUIApplication,
|
|
607
|
-
target: TextEntryTarget,
|
|
608
|
-
expectedText: String,
|
|
609
|
-
repairMode: TextTypingRepairMode
|
|
610
|
-
) -> Bool {
|
|
611
|
-
#if os(iOS)
|
|
612
|
-
var latestObservedText: String?
|
|
613
|
-
let deadline = Date().addingTimeInterval(TextEntryTiming.verificationStabilityWindow)
|
|
614
|
-
repeat {
|
|
615
|
-
guard let observedText = editableTextValue(for: resolveTextEntryElement(app: app, target: target)) else {
|
|
616
|
-
return false
|
|
617
|
-
}
|
|
618
|
-
if textEntryValueMatchesExpected(
|
|
619
|
-
resolveTextEntryElement(app: app, target: target),
|
|
620
|
-
observedText: observedText,
|
|
621
|
-
expectedText: expectedText
|
|
622
|
-
) {
|
|
623
|
-
return false
|
|
624
|
-
}
|
|
625
|
-
latestObservedText = observedText
|
|
626
|
-
if !isRepairableTextEntryMismatch(
|
|
627
|
-
observedText: observedText,
|
|
628
|
-
expectedText: expectedText,
|
|
629
|
-
repairMode: repairMode
|
|
630
|
-
) {
|
|
631
|
-
return false
|
|
632
|
-
}
|
|
633
|
-
sleepFor(TextEntryTiming.pollInterval)
|
|
634
|
-
} while Date() < deadline
|
|
635
|
-
|
|
636
|
-
guard let latestObservedText else {
|
|
637
|
-
return false
|
|
638
|
-
}
|
|
639
|
-
guard !textEntryValueMatchesExpected(
|
|
640
|
-
resolveTextEntryElement(app: app, target: target),
|
|
641
|
-
observedText: latestObservedText,
|
|
642
|
-
expectedText: expectedText
|
|
643
|
-
) else {
|
|
644
|
-
return false
|
|
645
|
-
}
|
|
646
|
-
return isRepairableTextEntryMismatch(
|
|
647
|
-
observedText: latestObservedText,
|
|
648
|
-
expectedText: expectedText,
|
|
649
|
-
repairMode: repairMode
|
|
650
|
-
)
|
|
651
|
-
#else
|
|
652
|
-
return false
|
|
653
|
-
#endif
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
private func isRepairableTextEntryMismatch(
|
|
657
|
-
observedText: String,
|
|
658
|
-
expectedText: String,
|
|
659
|
-
repairMode: TextTypingRepairMode
|
|
660
|
-
) -> Bool {
|
|
661
|
-
guard observedText != expectedText else {
|
|
662
|
-
return false
|
|
663
|
-
}
|
|
664
|
-
if repairMode == .replacement {
|
|
665
|
-
return true
|
|
666
|
-
}
|
|
667
|
-
return observedText.isEmpty || isLikelyDroppedCharacterTextEntryMismatch(
|
|
668
|
-
observedText: observedText,
|
|
669
|
-
expectedText: expectedText
|
|
670
|
-
)
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
private func isLikelyDroppedCharacterTextEntryMismatch(observedText: String, expectedText: String) -> Bool {
|
|
674
|
-
guard observedText.count < expectedText.count else {
|
|
675
|
-
return false
|
|
676
|
-
}
|
|
677
|
-
let missingCharacterCount = expectedText.count - observedText.count
|
|
678
|
-
guard missingCharacterCount <= max(2, expectedText.count / 4) else {
|
|
679
|
-
return false
|
|
680
|
-
}
|
|
681
|
-
var expectedIndex = expectedText.startIndex
|
|
682
|
-
for character in observedText {
|
|
683
|
-
guard let matchIndex = expectedText[expectedIndex...].firstIndex(of: character) else {
|
|
684
|
-
return false
|
|
685
|
-
}
|
|
686
|
-
expectedIndex = expectedText.index(after: matchIndex)
|
|
687
|
-
}
|
|
688
|
-
return true
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
private func resolveTextEntryElement(app: XCUIApplication, target: TextEntryTarget) -> XCUIElement? {
|
|
349
|
+
func resolveTextEntryElement(app: XCUIApplication, target: TextEntryTarget) -> XCUIElement? {
|
|
692
350
|
if target.prefersFocusedElement {
|
|
693
351
|
if let focused = focusedTextInput(app: app) {
|
|
694
352
|
return focused
|
|
@@ -836,7 +494,7 @@ extension RunnerTests {
|
|
|
836
494
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
837
495
|
}
|
|
838
496
|
|
|
839
|
-
|
|
497
|
+
func editableTextValue(
|
|
840
498
|
for element: XCUIElement?,
|
|
841
499
|
treatingPlaceholderAsEmpty: Bool = false
|
|
842
500
|
) -> String? {
|