agent-device 0.21.20 → 0.21.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (357) hide show
  1. package/android/ime-helper/dist/{agent-device-android-ime-helper-0.21.20.apk → agent-device-android-ime-helper-0.21.22.apk} +0 -0
  2. package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.22.apk.sha256 +1 -0
  3. package/android/ime-helper/dist/{agent-device-android-ime-helper-0.21.20.manifest.json → agent-device-android-ime-helper-0.21.22.manifest.json} +4 -4
  4. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.22.apk +0 -0
  5. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.22.apk.sha256 +1 -0
  6. package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.21.20.manifest.json → agent-device-android-snapshot-helper-0.21.22.manifest.json} +6 -6
  7. package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/AppWindowScreenshot.swift +73 -0
  8. package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/BackgroundInteraction.swift +557 -0
  9. package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/GhostCursor.swift +141 -0
  10. package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/SnapshotTraversal.swift +107 -3
  11. package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/main.swift +136 -22
  12. package/apple/macos-helper/Tests/AgentDeviceMacOSHelperTests/BackgroundInteractionTests.swift +88 -0
  13. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +121 -4
  14. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +10 -2
  15. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +36 -9
  16. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +28 -0
  17. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +27 -0
  18. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedCommitDeadline.swift +6 -1
  19. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +7 -2
  20. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedInteraction.swift +21 -7
  21. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedTextEntry.swift +121 -17
  22. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +179 -6
  23. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntryConfirmation.swift +119 -0
  24. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntryFocus.swift +8 -1
  25. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntryReadiness.swift +23 -11
  26. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextInputProbe.swift +59 -6
  27. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextTyping.swift +112 -23
  28. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TypeExecution.swift +25 -12
  29. package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/RecordingExportSupport.swift +6 -1
  30. package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/recording-overlay.swift +43 -13
  31. package/dist/src/adapter.js +1 -1
  32. package/dist/src/adb-executor.js +2 -2
  33. package/dist/src/adb-failure.js +1 -1
  34. package/dist/src/adb-transport.js +1 -1
  35. package/dist/src/agent-browser-lifecycle.js +1 -1
  36. package/dist/src/agent-browser-process-record.js +1 -1
  37. package/dist/src/agent-browser-provider.js +1 -1
  38. package/dist/src/agent-browser-tool.js +1 -1
  39. package/dist/src/agent-device-client.js +2 -2
  40. package/dist/src/ai-sdk.js +1 -1
  41. package/dist/src/alert-runtime.js +1 -1
  42. package/dist/src/alert.js +1 -1
  43. package/dist/src/android-adb.d.ts +12 -0
  44. package/dist/src/android-input-ownership.js +1 -1
  45. package/dist/src/app-backend.js +1 -0
  46. package/dist/src/app-catalog.js +1 -1
  47. package/dist/src/app-deployment-resolution.js +1 -1
  48. package/dist/src/app-inventory.js +1 -1
  49. package/dist/src/app-launch.js +1 -1
  50. package/dist/src/app-lifecycle.js +2 -2
  51. package/dist/src/app-lifecycle2.js +1 -1
  52. package/dist/src/app-log-files.js +1 -1
  53. package/dist/src/app-log-reconnect.js +1 -1
  54. package/dist/src/app-log-runtime2.js +3 -3
  55. package/dist/src/app-log-session-resource.js +1 -0
  56. package/dist/src/app-resolution.js +2 -2
  57. package/dist/src/apple-runner-platform.js +1 -1
  58. package/dist/src/application-lifecycle-interaction.js +1 -1
  59. package/dist/src/archive.js +2 -2
  60. package/dist/src/artifact-download.js +1 -1
  61. package/dist/src/auth-session.js +1 -1
  62. package/dist/src/auth.js +1 -1
  63. package/dist/src/back-mode.js +1 -1
  64. package/dist/src/backend-snapshot-options.js +1 -1
  65. package/dist/src/boot-diagnostics.js +1 -1
  66. package/dist/src/cache.js +1 -1
  67. package/dist/src/capabilities.js +1 -1
  68. package/dist/src/capture.js +1 -1
  69. package/dist/src/cli-config.js +1 -1
  70. package/dist/src/cli-help.js +13 -5
  71. package/dist/src/cli.js +4 -4
  72. package/dist/src/click-button.js +1 -1
  73. package/dist/src/client-metro-companion.js +1 -1
  74. package/dist/src/client-metro.js +1 -1
  75. package/dist/src/client-options.js +3 -0
  76. package/dist/src/client-types.d.ts +88 -5
  77. package/dist/src/clipboard-shell-response.js +1 -1
  78. package/dist/src/cloud-artifacts.d.ts +7 -2
  79. package/dist/src/code-signature-cache.js +1 -1
  80. package/dist/src/code-signature.js +1 -1
  81. package/dist/src/command-schema.js +1 -1
  82. package/dist/src/command-tools.js +2 -2
  83. package/dist/src/connection-runtime.js +1 -1
  84. package/dist/src/connection.js +5 -5
  85. package/dist/src/contracts.d.ts +1 -1
  86. package/dist/src/contracts.js +1 -1
  87. package/dist/src/daemon-artifacts.js +1 -0
  88. package/dist/src/daemon-client-lease-beat.js +1 -1
  89. package/dist/src/daemon-client-lifecycle.js +1 -1
  90. package/dist/src/daemon-client-liveness-probe.js +1 -0
  91. package/dist/src/daemon-client-transport.js +1 -1
  92. package/dist/src/daemon-http.js +1 -0
  93. package/dist/src/daemon-policy-file.js +1 -1
  94. package/dist/src/daemon-registration-owner.js +1 -0
  95. package/dist/src/daemon-registration.js +1 -1
  96. package/dist/src/daemon-resolution.js +1 -1
  97. package/dist/src/daemon-shutdown-report.js +1 -1
  98. package/dist/src/daemon.js +2 -2
  99. package/dist/src/debug-symbols-facade.js +1 -1
  100. package/dist/src/device-claim-inspection.js +1 -1
  101. package/dist/src/device-claim-rule.js +1 -1
  102. package/dist/src/device-inventory-context.js +1 -1
  103. package/dist/src/device-readiness-runtime.js +1 -1
  104. package/dist/src/device-release.js +1 -1
  105. package/dist/src/device-rotation.js +1 -1
  106. package/dist/src/device-selection-resolver.js +1 -1
  107. package/dist/src/device-session.js +1 -1
  108. package/dist/src/device-shell.js +1 -1
  109. package/dist/src/device.js +1 -1
  110. package/dist/src/device2.js +1 -3
  111. package/dist/src/device3.js +3 -0
  112. package/dist/src/devicectl.js +1 -1
  113. package/dist/src/diagnostics.js +3 -3
  114. package/dist/src/dispatch-resolve.js +4 -1
  115. package/dist/src/display-inventory.js +1 -1
  116. package/dist/src/doctor-facade.js +1 -1
  117. package/dist/src/doctor2.js +1 -1
  118. package/dist/src/durable-capture-resource.js +1 -0
  119. package/dist/src/engine-eval-script.js +1 -1
  120. package/dist/src/error.js +1 -1
  121. package/dist/src/exec.js +1 -1
  122. package/dist/src/file.js +1 -1
  123. package/dist/src/find.js +1 -1
  124. package/dist/src/generic-settle.js +1 -1
  125. package/dist/src/generic.js +1 -1
  126. package/dist/src/gesture-admission.js +1 -1
  127. package/dist/src/gesture-plan.js +1 -1
  128. package/dist/src/gesture-runtime.js +1 -1
  129. package/dist/src/harmonyos.js +1 -1
  130. package/dist/src/hdc.js +1 -1
  131. package/dist/src/helper.js +2 -2
  132. package/dist/src/host-file.js +1 -1
  133. package/dist/src/host.js +1 -1
  134. package/dist/src/human-control-contract.js +1 -1
  135. package/dist/src/human-control.js +1 -1
  136. package/dist/src/index.d.ts +1 -1
  137. package/dist/src/index.js +1 -1
  138. package/dist/src/input-actions.js +1 -1
  139. package/dist/src/input-actions2.js +2 -2
  140. package/dist/src/install-artifact-facade.js +1 -1
  141. package/dist/src/install-source-network-transport.js +1 -1
  142. package/dist/src/install-source.d.ts +5 -0
  143. package/dist/src/install-source2.js +1 -1
  144. package/dist/src/instance-client.js +1 -0
  145. package/dist/src/interaction-positionals.js +1 -1
  146. package/dist/src/interaction-ref-policy.js +1 -1
  147. package/dist/src/interaction.js +1 -1
  148. package/dist/src/interaction2.js +1 -1
  149. package/dist/src/interactor-operation-binding.js +1 -1
  150. package/dist/src/interactor.js +5 -5
  151. package/dist/src/interactor2.js +1 -1
  152. package/dist/src/interactors.js +1 -1
  153. package/dist/src/internal/bin.js +1 -1
  154. package/dist/src/internal/daemon.js +1 -2
  155. package/dist/src/internal/png-worker.js +1 -1
  156. package/dist/src/internal/run-script-http-child.js +1 -1
  157. package/dist/src/inventory.js +4 -4
  158. package/dist/src/inventory2.js +1 -1
  159. package/dist/src/inventory3.js +1 -1
  160. package/dist/src/inventory5.js +1 -1
  161. package/dist/src/ios-client.js +4 -30
  162. package/dist/src/ios-settings.js +1 -0
  163. package/dist/src/ios-snapshot-engine.js +1 -1
  164. package/dist/src/ios-snapshot-runtime.js +1 -1
  165. package/dist/src/launch-confirmation.js +1 -1
  166. package/dist/src/lease-context.js +1 -1
  167. package/dist/src/lease-registry-scope.js +1 -0
  168. package/dist/src/lease-scope.js +1 -1
  169. package/dist/src/lease.js +1 -1
  170. package/dist/src/legacy-xctest-device-set.js +1 -1
  171. package/dist/src/limrun-download-file.js +1 -1
  172. package/dist/src/limrun-runtime-dependencies.js +1 -1
  173. package/dist/src/limrun.d.ts +28 -2
  174. package/dist/src/limrun.js +1 -1
  175. package/dist/src/linux.js +1 -1
  176. package/dist/src/load.js +1 -0
  177. package/dist/src/location-coordinates.js +1 -1
  178. package/dist/src/macos-facade.js +1 -1
  179. package/dist/src/managed-device-scope.js +1 -1
  180. package/dist/src/mechanics.js +16 -16
  181. package/dist/src/observation.js +1 -1
  182. package/dist/src/observe-until.js +1 -1
  183. package/dist/src/overlay.js +2 -2
  184. package/dist/src/owner-identity.js +3 -3
  185. package/dist/src/path-resolution.js +1 -1
  186. package/dist/src/perf-facade.js +1 -1
  187. package/dist/src/perf-runtime-operation-builder.js +1 -1
  188. package/dist/src/perf-runtime-plan.js +1 -1
  189. package/dist/src/perf-target.js +1 -1
  190. package/dist/src/perf.js +1 -1
  191. package/dist/src/physical-device-control.js +1 -1
  192. package/dist/src/platform-request-scope.js +1 -1
  193. package/dist/src/platform-runtime-android-adb-host.js +1 -1
  194. package/dist/src/platform-runtime-app-log-process.js +1 -1
  195. package/dist/src/platform-runtime-host.js +1 -1
  196. package/dist/src/platform-runtime-managed-owner.js +1 -1
  197. package/dist/src/platform-runtime-open-target.js +1 -1
  198. package/dist/src/platform-runtime-operation-host.js +2 -2
  199. package/dist/src/platform-runtime-runtime-hints.js +2 -2
  200. package/dist/src/platform-runtime-screen-recording-android-host.js +1 -1
  201. package/dist/src/platform-runtime-screen-recording-apple-runner-transport.js +1 -1
  202. package/dist/src/platform-runtime-screen-recording-apple-simulator-host.js +1 -1
  203. package/dist/src/platform-runtime-screen-recording-finalizer-host.js +1 -1
  204. package/dist/src/platform-runtime-screen-recording-output-host.js +1 -1
  205. package/dist/src/platform-runtime-screen-recording-process-host.js +1 -1
  206. package/dist/src/platform-runtime-unavailable.js +1 -1
  207. package/dist/src/platform-runtime.js +1 -1
  208. package/dist/src/platform-runtime2.js +1 -1
  209. package/dist/src/plugins.d.ts +9 -0
  210. package/dist/src/plugins.js +1 -0
  211. package/dist/src/plugins2.js +2 -0
  212. package/dist/src/png-crop-bytes.js +1 -1
  213. package/dist/src/png-size.js +1 -1
  214. package/dist/src/png-worker-client.js +1 -1
  215. package/dist/src/png.js +1 -1
  216. package/dist/src/prepare-kind.js +1 -1
  217. package/dist/src/process.js +1 -1
  218. package/dist/src/project-runtime.js +1 -1
  219. package/dist/src/provider-credential-fingerprint.js +1 -0
  220. package/dist/src/provider-device-admission.js +1 -1
  221. package/dist/src/provider-device-runtimes.js +1 -0
  222. package/dist/src/provider-limrun-credentials.js +1 -0
  223. package/dist/src/provider-profile-fields.js +1 -0
  224. package/dist/src/provider-session-ownership.js +2 -2
  225. package/dist/src/provider-webdriver.js +2 -1
  226. package/dist/src/providers.js +1 -1
  227. package/dist/src/proxy.js +1 -1
  228. package/dist/src/quality-warnings.js +1 -1
  229. package/dist/src/react-native.js +1 -1
  230. package/dist/src/record-runtime.js +1 -1
  231. package/dist/src/record-trace.js +1 -1
  232. package/dist/src/recording.js +1 -1
  233. package/dist/src/rect-center.js +1 -1
  234. package/dist/src/register-builtins.js +1 -1
  235. package/dist/src/registry.js +39 -39
  236. package/dist/src/remote-config2.js +1 -1
  237. package/dist/src/replay-divergence.js +1 -1
  238. package/dist/src/replay.js +2 -2
  239. package/dist/src/reporting.js +5 -5
  240. package/dist/src/request-admission.js +1 -1
  241. package/dist/src/request-cancel.js +1 -1
  242. package/dist/src/request-cancellation.js +1 -1
  243. package/dist/src/request-runtime-binding.js +1 -1
  244. package/dist/src/result-serialization.js +1 -1
  245. package/dist/src/retry.js +1 -1
  246. package/dist/src/runner-artifact-manifest.js +1 -1
  247. package/dist/src/runner-cache-metadata.js +3 -3
  248. package/dist/src/runner-client.js +1 -1
  249. package/dist/src/runner-dev-tools-security.js +1 -1
  250. package/dist/src/runner-device-readiness.js +1 -1
  251. package/dist/src/runner-disposal.js +1 -1
  252. package/dist/src/runner-error-classification.js +2 -2
  253. package/dist/src/runner-exchange.js +1 -1
  254. package/dist/src/runner-operations-facade.js +1 -1
  255. package/dist/src/runner-session-types.js +1 -1
  256. package/dist/src/runner-start-budget.js +1 -1
  257. package/dist/src/runner.js +1 -1
  258. package/dist/src/runtime-admission.js +1 -1
  259. package/dist/src/runtime-transport-hints.js +1 -1
  260. package/dist/src/runtime.js +1 -1
  261. package/dist/src/runtime10.js +2 -2
  262. package/dist/src/runtime2.js +3 -3
  263. package/dist/src/runtime3.js +1 -1
  264. package/dist/src/runtime4.js +5 -5
  265. package/dist/src/runtime5.js +1 -1
  266. package/dist/src/runtime6.js +1 -1
  267. package/dist/src/runtime7.js +1 -1
  268. package/dist/src/runtime8.js +1 -1
  269. package/dist/src/runtime9.js +1 -1
  270. package/dist/src/screen-recording-admission-ledger.js +1 -1
  271. package/dist/src/screen-recording-completion.js +1 -1
  272. package/dist/src/screen-recording-options.js +1 -1
  273. package/dist/src/screen-recording-resource-recovery.js +1 -1
  274. package/dist/src/screen-recording-transport.js +1 -1
  275. package/dist/src/screenshot-density.js +1 -1
  276. package/dist/src/screenshot-image.js +1 -1
  277. package/dist/src/screenshot-result.js +1 -1
  278. package/dist/src/screenshot-runtime.js +1 -1
  279. package/dist/src/screenshot.js +1 -1
  280. package/dist/src/screenshot3.js +1 -1
  281. package/dist/src/script.js +1 -1
  282. package/dist/src/scroll-command.js +1 -1
  283. package/dist/src/scroll-gesture.js +1 -1
  284. package/dist/src/sdk-batch-runner.js +2 -2
  285. package/dist/src/sdk-batch.js +2 -2
  286. package/dist/src/sdk-contracts.d.ts +45 -2
  287. package/dist/src/sdk-contracts.js +2 -2
  288. package/dist/src/sdk-device.js +1 -1
  289. package/dist/src/sdk-io.js +1 -1
  290. package/dist/src/sdk-remote-config.js +1 -1
  291. package/dist/src/sdk-selectors.d.ts +33 -4
  292. package/dist/src/selection.js +0 -0
  293. package/dist/src/selector-runtime.js +1 -1
  294. package/dist/src/selectors.d.ts +2 -2
  295. package/dist/src/selectors.js +1 -1
  296. package/dist/src/server.js +1 -1
  297. package/dist/src/session-allocation.js +1 -1
  298. package/dist/src/session-artifact-paths.js +1 -1
  299. package/dist/src/session-repair-tombstone.js +1 -1
  300. package/dist/src/session-routing.js +1 -1
  301. package/dist/src/session-runtime-admission.js +1 -1
  302. package/dist/src/session-snapshot.js +1 -1
  303. package/dist/src/session-stream.js +27 -0
  304. package/dist/src/session.js +1 -1
  305. package/dist/src/session2.js +5 -5
  306. package/dist/src/settings.js +1 -1
  307. package/dist/src/settings2.js +1 -1
  308. package/dist/src/simctl-settings-facade.js +1 -0
  309. package/dist/src/simctl-settings.js +1 -0
  310. package/dist/src/simctl.js +1 -1
  311. package/dist/src/simulator.js +1 -1
  312. package/dist/src/snapshot-diagnostics.js +1 -1
  313. package/dist/src/snapshot-runtime-binding.js +1 -1
  314. package/dist/src/snapshot-runtime.js +1 -1
  315. package/dist/src/snapshot-state.js +1 -1
  316. package/dist/src/snapshot2.js +1 -1
  317. package/dist/src/snapshot3.js +1 -1
  318. package/dist/src/snapshot4.js +1 -1
  319. package/dist/src/src.js +1 -1
  320. package/dist/src/src7.js +1 -1
  321. package/dist/src/src8.js +2 -2
  322. package/dist/src/store.js +1 -0
  323. package/dist/src/string-enum.js +1 -1
  324. package/dist/src/surface-snapshot.js +1 -1
  325. package/dist/src/takeover.js +1 -1
  326. package/dist/src/target-evidence.js +1 -1
  327. package/dist/src/telemetry.js +1 -1
  328. package/dist/src/text-entry-focus.js +1 -1
  329. package/dist/src/tool-provider.js +1 -1
  330. package/dist/src/tool-provider2.js +1 -1
  331. package/dist/src/toolchain-probe.js +2 -2
  332. package/dist/src/touch-runtime.js +1 -1
  333. package/dist/src/touch-runtime2.js +1 -1
  334. package/dist/src/transport.js +2 -2
  335. package/dist/src/tv-remote.js +1 -1
  336. package/dist/src/type-text-runtime.js +1 -1
  337. package/dist/src/update-check.js +1 -1
  338. package/dist/src/vars.js +1 -1
  339. package/dist/src/verified-file.js +1 -1
  340. package/dist/src/version.js +1 -1
  341. package/dist/src/video.js +1 -1
  342. package/dist/src/viewport.js +1 -1
  343. package/dist/src/web.js +1 -1
  344. package/dist/src/web2.js +1 -1
  345. package/dist/src/webdriver-source.js +1 -1
  346. package/package.json +5 -1
  347. package/server.json +2 -2
  348. package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.20.apk.sha256 +0 -1
  349. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.20.apk +0 -0
  350. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.20.apk.sha256 +0 -1
  351. package/dist/src/api.js +0 -3
  352. package/dist/src/daemon-process.js +0 -1
  353. package/dist/src/multitouch-support.js +0 -1
  354. package/dist/src/parameterized-recorded-fill.js +0 -1
  355. package/dist/src/runner-startup-transport.js +0 -1
  356. package/dist/src/runner-xctestrun.js +0 -1
  357. package/dist/src/session-store.js +0 -4
@@ -66,6 +66,10 @@ int main(int argc, const char *argv[]) {
66
66
  @property(nonatomic, assign) NSUInteger firstAlertActions;
67
67
  @property(nonatomic, assign) NSUInteger replacementAlertActions;
68
68
  @property(nonatomic, strong) UILabel *textEntryWriteBackStatus;
69
+ @property(nonatomic, strong) UILabel *textEntryDigitSlots;
70
+ // Filled from the fixture field's own delegate callbacks so a test can tell that a gesture really
71
+ // took focus from an input that has stopped answering the accessibility query that found it.
72
+ @property(nonatomic, strong) UILabel *textEntryFocusWitness;
69
73
  @property(nonatomic, assign) NSUInteger textEntryWriteBacks;
70
74
  @property(nonatomic, copy, nullable) NSString *textEntryRenderedValue;
71
75
  @property(nonatomic, assign) NSTimeInterval textEntryLastEditTime;
@@ -73,6 +77,7 @@ int main(int argc, const char *argv[]) {
73
77
  @property(nonatomic, assign) NSUInteger textEntryBurstEdits;
74
78
  @property(nonatomic, assign) NSTimeInterval textEntryBurstMinGap;
75
79
  @property(nonatomic, assign) NSTimeInterval textEntryAcknowledgeWindowSeconds;
80
+ @property(nonatomic, strong) NSLayoutConstraint *textEntryFieldTop;
76
81
  @property(nonatomic, assign) BOOL alertFixtureStarted;
77
82
  @property(nonatomic, strong) NSTimer *alertActivationBusyBackstop;
78
83
  @property(nonatomic, strong) NSTimer *alertBannerRepost;
@@ -81,6 +86,17 @@ int main(int argc, const char *argv[]) {
81
86
  #if TARGET_OS_IOS
82
87
  @interface AgentDeviceRunnerViewController () <UNUserNotificationCenterDelegate>
83
88
  @end
89
+
90
+ // A one-time-code field that announces how many digits it holds instead of the digits, the way
91
+ // React Native OTP inputs set `accessibilityValue` on their hidden text input.
92
+ @interface AgentDeviceDigitCountTextField : UITextField
93
+ @end
94
+
95
+ @implementation AgentDeviceDigitCountTextField
96
+ - (NSString *)accessibilityValue {
97
+ return [NSString stringWithFormat:@"%lu of 6 digits", (unsigned long)self.text.length];
98
+ }
99
+ @end
84
100
  #endif
85
101
 
86
102
  @implementation AgentDeviceRunnerViewController
@@ -265,6 +281,11 @@ static NSTimeInterval AgentDeviceTextEntryAcknowledgeWindow(void) {
265
281
  // Edits further apart than this belong to different bursts: one runner command's characters arrive
266
282
  // well inside it, and two commands are separated by at least a commit-wait poll and a status read.
267
283
  static const NSTimeInterval AgentDeviceTextEntryBurstBreakSeconds = 1.0;
284
+ static const NSUInteger AgentDeviceTextEntryAutoSubmitLength = 6;
285
+
286
+ static const CGFloat AgentDeviceTextEntryFieldTopInset = 24;
287
+ static const CGFloat AgentDeviceTextEntryFieldHeight = 44;
288
+ static const CGFloat AgentDeviceTextEntryNeighbourGap = 16;
268
289
 
269
290
  - (void)agentDeviceTextEntryDidChange:(UITextField *)textField {
270
291
  // A field whose app owns its value, the way a controlled React Native `TextInput` does. A burst
@@ -293,10 +314,56 @@ static const NSTimeInterval AgentDeviceTextEntryBurstBreakSeconds = 1.0;
293
314
  }
294
315
  [self updateTextEntryWriteBackStatus];
295
316
  }
317
+ self.textEntryDigitSlots.text = textField.text;
296
318
  if ([NSProcessInfo.processInfo.arguments containsObject:@"--agent-device-text-entry-disappear-after-input"] &&
297
319
  textField.text.length > 0) {
298
320
  [textField removeFromSuperview];
299
321
  }
322
+ // An auto-submitting one-time-code field: the last digit navigates away, to a screen whose own
323
+ // input takes the code field's place or to one without an input. `replace-after-input` is the
324
+ // same navigation landing before the code is complete.
325
+ NSArray<NSString *> *arguments = NSProcessInfo.processInfo.arguments;
326
+ BOOL codeComplete = textField.text.length >= AgentDeviceTextEntryAutoSubmitLength;
327
+ if (textField.superview == nil) {
328
+ return;
329
+ }
330
+ if ((codeComplete && [arguments containsObject:@"--agent-device-text-entry-auto-submit"]) ||
331
+ (textField.text.length > 0 && [arguments containsObject:@"--agent-device-text-entry-replace-after-input"])) {
332
+ UITextField *nextScreenField = [[UITextField alloc] initWithFrame:textField.frame];
333
+ if (![arguments containsObject:@"--agent-device-text-entry-unnamed-input"]) {
334
+ nextScreenField.accessibilityIdentifier = @"agent-device-auto-submit-next-screen-input";
335
+ }
336
+ nextScreenField.borderStyle = UITextBorderStyleRoundedRect;
337
+ [textField.superview addSubview:nextScreenField];
338
+ [textField removeFromSuperview];
339
+ [nextScreenField becomeFirstResponder];
340
+ } else if (codeComplete &&
341
+ [arguments containsObject:@"--agent-device-text-entry-auto-submit-without-successor"]) {
342
+ [textField removeFromSuperview];
343
+ }
344
+ }
345
+
346
+ // Stops answering the element-type query that found it, the moment it takes focus, under
347
+ // `--agent-device-text-entry-unqueryable-on-focus`. #3060's Flutter password field reaches this shape
348
+ // because its two accessibility channels disagree once focused — `TextField` through the legacy
349
+ // attributes, `Other` through the modern ones — and what either channel leaves behind is the same:
350
+ // the tap's element answers the query that resolved it up to the gesture and refuses it after, so a
351
+ // read of it after dispatching records an XCTest failure instead of describing the tap that already
352
+ // landed. The field keeps first-responder, so a test still needs a witness that is not the
353
+ // accessibility tree to tell the gesture landed: `agent-device-text-entry-focus` is a label this
354
+ // method fills in from the field's own delegate callback.
355
+ - (void)agentDeviceTextEntryBecomesUnqueryable:(UITextField *)textField {
356
+ self.textEntryFocusWitness.text = @"focus";
357
+ textField.hidden = YES;
358
+ }
359
+
360
+ // Moves the field up by its own height plus the gap below it when it gains focus, the way keyboard
361
+ // avoidance or a bottom sheet extending above the keyboard does, so the neighbouring field slides
362
+ // into the point the focus tap hit.
363
+ - (void)agentDeviceTextEntryDidBeginEditing:(UITextField *)textField {
364
+ self.textEntryFieldTop.constant =
365
+ AgentDeviceTextEntryFieldTopInset - AgentDeviceTextEntryFieldHeight - AgentDeviceTextEntryNeighbourGap;
366
+ [self.view layoutIfNeeded];
300
367
  }
301
368
  #endif
302
369
 
@@ -339,8 +406,13 @@ static const NSTimeInterval AgentDeviceTextEntryBurstBreakSeconds = 1.0;
339
406
  }
340
407
 
341
408
  if ([NSProcessInfo.processInfo.arguments containsObject:@"--agent-device-text-entry-regression"]) {
342
- UITextField *textField = [[UITextField alloc] init];
343
- textField.accessibilityIdentifier = @"agent-device-hardware-keyboard-input";
409
+ BOOL digitCountValue =
410
+ [NSProcessInfo.processInfo.arguments containsObject:@"--agent-device-text-entry-digit-count-value"];
411
+ UITextField *textField =
412
+ digitCountValue ? [[AgentDeviceDigitCountTextField alloc] init] : [[UITextField alloc] init];
413
+ if (![NSProcessInfo.processInfo.arguments containsObject:@"--agent-device-text-entry-unnamed-input"]) {
414
+ textField.accessibilityIdentifier = @"agent-device-hardware-keyboard-input";
415
+ }
344
416
  textField.borderStyle = UITextBorderStyleRoundedRect;
345
417
  // An empty input view keeps the software keyboard down, which is the hardware-keyboard responder
346
418
  // these routes are addressed to. `--agent-device-text-entry-soft-keyboard` leaves the real input
@@ -354,12 +426,46 @@ static const NSTimeInterval AgentDeviceTextEntryBurstBreakSeconds = 1.0;
354
426
  forControlEvents:UIControlEventEditingChanged];
355
427
  textField.translatesAutoresizingMaskIntoConstraints = NO;
356
428
  [self.view addSubview:textField];
429
+ self.textEntryFieldTop = [textField.topAnchor constraintEqualToAnchor:label.bottomAnchor
430
+ constant:AgentDeviceTextEntryFieldTopInset];
357
431
  [NSLayoutConstraint activateConstraints:@[
358
432
  [textField.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
359
- [textField.topAnchor constraintEqualToAnchor:label.bottomAnchor constant:24],
433
+ self.textEntryFieldTop,
360
434
  [textField.widthAnchor constraintEqualToConstant:240],
361
- [textField.heightAnchor constraintEqualToConstant:44],
435
+ [textField.heightAnchor constraintEqualToConstant:AgentDeviceTextEntryFieldHeight],
362
436
  ]];
437
+ if ([NSProcessInfo.processInfo.arguments containsObject:@"--agent-device-text-entry-unqueryable-on-focus"]) {
438
+ self.textEntryFocusWitness = [[UILabel alloc] init];
439
+ self.textEntryFocusWitness.accessibilityIdentifier = @"agent-device-text-entry-focus";
440
+ self.textEntryFocusWitness.translatesAutoresizingMaskIntoConstraints = NO;
441
+ [self.view addSubview:self.textEntryFocusWitness];
442
+ [NSLayoutConstraint activateConstraints:@[
443
+ [self.textEntryFocusWitness.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
444
+ [self.textEntryFocusWitness.topAnchor constraintEqualToAnchor:textField.bottomAnchor constant:24],
445
+ ]];
446
+ [textField addTarget:self
447
+ action:@selector(agentDeviceTextEntryBecomesUnqueryable:)
448
+ forControlEvents:UIControlEventEditingDidBegin];
449
+ }
450
+ if ([NSProcessInfo.processInfo.arguments containsObject:@"--agent-device-text-entry-moves-on-focus"]) {
451
+ textField.text = @"stale";
452
+ [textField addTarget:self
453
+ action:@selector(agentDeviceTextEntryDidBeginEditing:)
454
+ forControlEvents:UIControlEventEditingDidBegin];
455
+ UITextField *neighbour = [[UITextField alloc] init];
456
+ neighbour.accessibilityIdentifier = @"agent-device-text-entry-neighbour";
457
+ neighbour.borderStyle = UITextBorderStyleRoundedRect;
458
+ neighbour.text = @"neighbour";
459
+ neighbour.translatesAutoresizingMaskIntoConstraints = NO;
460
+ [self.view addSubview:neighbour];
461
+ [NSLayoutConstraint activateConstraints:@[
462
+ [neighbour.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
463
+ [neighbour.topAnchor constraintEqualToAnchor:textField.bottomAnchor
464
+ constant:AgentDeviceTextEntryNeighbourGap],
465
+ [neighbour.widthAnchor constraintEqualToConstant:240],
466
+ [neighbour.heightAnchor constraintEqualToConstant:AgentDeviceTextEntryFieldHeight],
467
+ ]];
468
+ }
363
469
  if ([NSProcessInfo.processInfo.arguments containsObject:@"--agent-device-text-entry-app-owned-value"]) {
364
470
  self.textEntryAcknowledgeWindowSeconds = AgentDeviceTextEntryAcknowledgeWindow();
365
471
  // Reports how many edits this app rendered and how many writes it had to make because a
@@ -375,6 +481,17 @@ static const NSTimeInterval AgentDeviceTextEntryBurstBreakSeconds = 1.0;
375
481
  ]];
376
482
  [self updateTextEntryWriteBackStatus];
377
483
  }
484
+ if (digitCountValue) {
485
+ // The slots an OTP screen renders next to its input: the digits the field really holds.
486
+ self.textEntryDigitSlots = [[UILabel alloc] init];
487
+ self.textEntryDigitSlots.accessibilityIdentifier = @"agent-device-text-entry-digit-slots";
488
+ self.textEntryDigitSlots.translatesAutoresizingMaskIntoConstraints = NO;
489
+ [self.view addSubview:self.textEntryDigitSlots];
490
+ [NSLayoutConstraint activateConstraints:@[
491
+ [self.textEntryDigitSlots.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
492
+ [self.textEntryDigitSlots.topAnchor constraintEqualToAnchor:textField.bottomAnchor constant:12],
493
+ ]];
494
+ }
378
495
  }
379
496
 
380
497
  if ([NSProcessInfo.processInfo.arguments containsObject:@"--agent-device-crowded-screen"]) {
@@ -5,6 +5,12 @@ NS_ASSUME_NONNULL_BEGIN
5
5
  FOUNDATION_EXPORT NSInteger RunnerControlledScrollFrameCount(double durationMs);
6
6
  FOUNDATION_EXPORT double RunnerControlledScrollProgress(double t);
7
7
 
8
+ typedef NS_ENUM(NSInteger, RunnerTapSynthesisStatus) {
9
+ RunnerTapSynthesisStatusSucceeded,
10
+ RunnerTapSynthesisStatusDeadlineExceeded,
11
+ RunnerTapSynthesisStatusFailed,
12
+ };
13
+
8
14
  @interface RunnerSynthesizedGesture : NSObject
9
15
 
10
16
  // `resolvedWindow` is the app window the caller already resolved for the gesture's reference
@@ -26,10 +32,12 @@ FOUNDATION_EXPORT double RunnerControlledScrollProgress(double t);
26
32
  y2:(double)y2
27
33
  durationMs:(double)durationMs;
28
34
 
29
- + (NSString * _Nullable)synthesizeTapWithApplication:(id)application
35
+ + (RunnerTapSynthesisStatus)synthesizeTapWithApplication:(id)application
30
36
  resolvedWindow:(id _Nullable)resolvedWindow
31
37
  x:(double)x
32
- y:(double)y;
38
+ y:(double)y
39
+ deadline:(NSDate * _Nullable)deadline
40
+ errorMessage:(NSString * _Nullable * _Nullable)errorMessage;
33
41
 
34
42
  // Each pointer is an ordered array of { x, y, offsetMs } samples. The first sample
35
43
  // starts contact; subsequent samples move it; all pointers lift at their final offset.
@@ -75,7 +75,13 @@ static NSString * _Nullable RunnerTrySynthesizeDrag(
75
75
  NSString *recordName,
76
76
  RunnerDragPointerPathFactory pathFactory
77
77
  );
78
- static NSString * _Nullable RunnerTrySynthesizeTap(id application, id _Nullable resolvedWindow, CGPoint point);
78
+ static RunnerTapSynthesisStatus RunnerTrySynthesizeTap(
79
+ id application,
80
+ id _Nullable resolvedWindow,
81
+ CGPoint point,
82
+ NSDate * _Nullable deadline,
83
+ NSString * _Nullable * _Nullable errorMessage
84
+ );
79
85
  // XCTest's proven swipe profile reaches the endpoint in 100 ms, then holds for the planned
80
86
  // fling duration. Fast movement is what lets UIKit distinguish a fling from a timed pan.
81
87
  static const NSTimeInterval RunnerSwipeMovementDurationSeconds = 0.1;
@@ -132,14 +138,20 @@ static id RunnerTapPointerPath(
132
138
  }
133
139
  }
134
140
 
135
- + (NSString * _Nullable)synthesizeTapWithApplication:(id)application
141
+ + (RunnerTapSynthesisStatus)synthesizeTapWithApplication:(id)application
136
142
  resolvedWindow:(id _Nullable)resolvedWindow
137
143
  x:(double)x
138
- y:(double)y {
144
+ y:(double)y
145
+ deadline:(NSDate * _Nullable)deadline
146
+ errorMessage:(NSString * _Nullable * _Nullable)errorMessage {
147
+ if (errorMessage != NULL) *errorMessage = nil;
139
148
  @try {
140
- return RunnerTrySynthesizeTap(application, resolvedWindow, CGPointMake(x, y));
149
+ return RunnerTrySynthesizeTap(application, resolvedWindow, CGPointMake(x, y), deadline, errorMessage);
141
150
  } @catch (NSException *exception) {
142
- return RunnerFormatXCTestException(exception, @"private XCTest event synthesis failed");
151
+ if (errorMessage != NULL) {
152
+ *errorMessage = RunnerFormatXCTestException(exception, @"private XCTest event synthesis failed");
153
+ }
154
+ return RunnerTapSynthesisStatusFailed;
143
155
  }
144
156
  }
145
157
 
@@ -227,7 +239,13 @@ static NSString * _Nullable RunnerTrySynthesizeDrag(
227
239
  return RunnerSynthesizeEventRecord(&bridge, record);
228
240
  }
229
241
 
230
- static NSString * _Nullable RunnerTrySynthesizeTap(id application, id _Nullable resolvedWindow, CGPoint point) {
242
+ static RunnerTapSynthesisStatus RunnerTrySynthesizeTap(
243
+ id application,
244
+ id _Nullable resolvedWindow,
245
+ CGPoint point,
246
+ NSDate * _Nullable deadline,
247
+ NSString * _Nullable * _Nullable errorMessage
248
+ ) {
231
249
  RunnerGestureEventBridge bridge;
232
250
  id record = nil;
233
251
  NSString *error = RunnerCreateEventRecord(
@@ -237,14 +255,23 @@ static NSString * _Nullable RunnerTrySynthesizeTap(id application, id _Nullable
237
255
  &bridge,
238
256
  &record
239
257
  );
240
- if (error != nil) return error;
258
+ if (error != nil) {
259
+ if (errorMessage != NULL) *errorMessage = error;
260
+ return RunnerTapSynthesisStatusFailed;
261
+ }
241
262
 
242
263
  id path = RunnerTapPointerPath(&bridge, point);
243
264
  if (path == nil) {
244
- return @"private XCTest event synthesis failed: could not create pointer path";
265
+ if (errorMessage != NULL) *errorMessage = @"private XCTest event synthesis failed: could not create pointer path";
266
+ return RunnerTapSynthesisStatusFailed;
245
267
  }
246
268
  ((RunnerMsgSendAddPath)objc_msgSend)(record, bridge.core.addPathSelector, path);
247
- return RunnerSynthesizeEventRecord(&bridge, record);
269
+ if (deadline != nil && deadline.timeIntervalSinceNow <= 0) {
270
+ return RunnerTapSynthesisStatusDeadlineExceeded;
271
+ }
272
+ error = RunnerSynthesizeEventRecord(&bridge, record);
273
+ if (errorMessage != NULL) *errorMessage = error;
274
+ return error == nil ? RunnerTapSynthesisStatusSucceeded : RunnerTapSynthesisStatusFailed;
248
275
  }
249
276
 
250
277
  static NSString * _Nullable RunnerResolveGestureEventBridge(
@@ -138,12 +138,40 @@ extension RunnerTests {
138
138
  #if !os(tvOS)
139
139
  guard !frame.isEmpty else { return }
140
140
  #endif
141
+ #if os(iOS)
142
+ let postEventWaitSelector = NSSelectorFromString("_waitForQuiescenceAsPreEvent:")
143
+ guard alert.ownerApp.responds(to: postEventWaitSelector) else {
144
+ outcome = .unsupported(
145
+ message: "alert activation could not wait for post-event quiescence",
146
+ hint: "Inspect the current alert before deciding whether to act again."
147
+ )
148
+ return
149
+ }
150
+ switch synthesizedTapAt(app: alert.ownerApp, x: frame.midX, y: frame.midY, deadline: deadline) {
151
+ case .performed?:
152
+ typealias WaitForQuiescence = @convention(c) (NSObject, Selector, Bool) -> Void
153
+ let waitForQuiescence = unsafeBitCast(
154
+ alert.ownerApp.method(for: postEventWaitSelector),
155
+ to: WaitForQuiescence.self
156
+ )
157
+ waitForQuiescence(alert.ownerApp, postEventWaitSelector, false)
158
+ outcome = .performed
159
+ case .unsupported(let message, _)?:
160
+ outcome = .unsupported(
161
+ message: message,
162
+ hint: "Inspect the current alert before deciding whether to act again."
163
+ )
164
+ case nil:
165
+ break
166
+ }
167
+ #else
141
168
  outcome = activateElement(
142
169
  app: alert.ownerApp,
143
170
  element: button,
144
171
  action: "alert \(action)",
145
172
  resolvedFrame: frame
146
173
  )
174
+ #endif
147
175
  }
148
176
  }
149
177
  return outcome
@@ -470,6 +470,13 @@ struct DataPayload: Codable {
470
470
  var keyboardMinY: Double?
471
471
  var maestroNonHittableCoordinateFallbackUsed: Bool?
472
472
  var textEntryRoute: String?
473
+
474
+
475
+ var verification: String?
476
+ var requested: String?
477
+ var before: String?
478
+ var after: String?
479
+ var target: TextEntryVerificationTargetPayload?
473
480
  var runnerFatal: Bool?
474
481
  var runnerFatalReason: String?
475
482
 
@@ -487,6 +494,26 @@ struct DataPayload: Codable {
487
494
  }
488
495
 
489
496
 
497
+ struct TextEntryVerificationTargetPayload: Codable, Equatable {
498
+ let resourceId: String?
499
+ let className: String?
500
+ let packageName: String?
501
+ let rect: SnapshotRect
502
+
503
+ enum CodingKeys: String, CodingKey {
504
+ case resourceId, className, packageName, rect
505
+ }
506
+
507
+ func encode(to encoder: Encoder) throws {
508
+ var container = encoder.container(keyedBy: CodingKeys.self)
509
+ try container.encode(resourceId, forKey: .resourceId)
510
+ try container.encode(className, forKey: .className)
511
+ try container.encode(packageName, forKey: .packageName)
512
+ try container.encode(rect, forKey: .rect)
513
+ }
514
+ }
515
+
516
+
490
517
 
491
518
 
492
519
 
@@ -68,6 +68,7 @@ extension RunnerTests {
68
68
 
69
69
 
70
70
 
71
+
71
72
  func awaitSynthesizedReplacementCommit(
72
73
  app: XCUIApplication,
73
74
  target: TextEntryTarget,
@@ -82,6 +83,7 @@ extension RunnerTests {
82
83
  let outcome = Self.awaitSynthesizedReplacementCommitOutcome(
83
84
  expectedText: expectedText,
84
85
  placeholder: ingredients.placeholder,
86
+ fieldIsSecure: ingredients.fieldIsSecure,
85
87
  now: { Date() },
86
88
  observe: ingredients.observe,
87
89
  waitForNextObservation: ingredients.waitForNextObservation
@@ -132,13 +134,16 @@ extension RunnerTests {
132
134
  expectedText: String
133
135
  ) -> (
134
136
  placeholder: String?,
137
+ fieldIsSecure: Bool,
135
138
  observe: () -> String?,
136
139
  waitForNextObservation: () -> Void
137
140
  ) {
138
- let placeholder = resolveTextEntryElement(app: app, target: target)?.placeholderValue
141
+ let field = resolveTextEntryElement(app: app, target: target)
142
+ let placeholder = field?.placeholderValue
139
143
  let waitStartedAt = Date()
140
144
  return (
141
145
  placeholder: placeholder,
146
+ fieldIsSecure: field?.elementType == .secureTextField,
142
147
  observe: {
143
148
  let observedText = self.editableTextValue(
144
149
  for: self.resolveTextEntryElement(app: app, target: target),
@@ -143,9 +143,14 @@ extension RunnerTests {
143
143
  _ app: XCUIApplication,
144
144
  kind: SynthesizedGesturePolicyKind,
145
145
  context: SynthesizedCoordinateContext?,
146
- synthesize: () -> RunnerInteractionOutcome
146
+ synthesize: () -> RunnerInteractionOutcome?
147
147
  ) -> SynthesizedGestureAttempt {
148
- let (timing, outcome) = performGesture(app, idleTimeout: false, synthesize)
148
+ var outcome: RunnerInteractionOutcome?
149
+ let timing = measureGesture { outcome = synthesize() }
150
+ guard let outcome else {
151
+ logSynthesizedGesturePolicyDecision(kind: kind, context: context, fallbackAttempted: false)
152
+ return .refused(timing: timing, message: "synthesized gesture deadline expired before dispatch", hint: nil)
153
+ }
149
154
  guard case .unsupported(let message, let hint) = outcome else {
150
155
  logSynthesizedGesturePolicyDecision(kind: kind, context: context, fallbackAttempted: false)
151
156
  return .performed(timing: timing)
@@ -95,8 +95,9 @@ extension RunnerTests {
95
95
  app: XCUIApplication,
96
96
  x: Double,
97
97
  y: Double,
98
- context: SynthesizedCoordinateContext? = nil
99
- ) -> RunnerInteractionOutcome {
98
+ context: SynthesizedCoordinateContext? = nil,
99
+ deadline: Date? = nil
100
+ ) -> RunnerInteractionOutcome? {
100
101
  #if os(iOS)
101
102
  guard x.isFinite, y.isFinite else {
102
103
  return .unsupported(
@@ -126,18 +127,31 @@ extension RunnerTests {
126
127
  referenceFrame: context.referenceFrame,
127
128
  orientation: orientation
128
129
  )
129
- if let message = RunnerSynthesizedGesture.synthesizeTap(
130
+ var message: NSString?
131
+ let status = RunnerSynthesizedGesture.synthesizeTap(
130
132
  withApplication: app,
131
133
  resolvedWindow: context.resolvedWindow,
132
134
  x: Double(point.x),
133
- y: Double(point.y)
134
- ) {
135
+ y: Double(point.y),
136
+ deadline: deadline,
137
+ errorMessage: &message
138
+ )
139
+ switch status {
140
+ case .succeeded:
141
+ return .performed
142
+ case .deadlineExceeded:
143
+ return nil
144
+ case .failed:
135
145
  return .unsupported(
136
- message: message,
146
+ message: message as String? ?? "private XCTest event synthesis failed",
137
147
  hint: "Falling back to XCTest coordinate tap may be slower and can still need a healthy accessibility tree."
138
148
  )
149
+ @unknown default:
150
+ return .unsupported(
151
+ message: "private XCTest event synthesis returned an unknown status",
152
+ hint: "Inspect the current app before deciding whether to act again."
153
+ )
139
154
  }
140
- return .performed
141
155
  #elseif os(tvOS)
142
156
  return .unsupported(
143
157
  message: "coordinate tap is not supported on tvOS; move focus with swipe or scroll, then select the focused element",
@@ -101,8 +101,27 @@ extension RunnerTests {
101
101
  var warmsUpField = false
102
102
 
103
103
 
104
+
105
+
106
+
107
+
108
+ static let maxClearPassCount = 4
109
+
110
+ static let unreadableClearPassCount = 2
111
+
112
+
104
113
  var synthesizeCallCount: Int {
105
- replacesExistingText ? 2 : 1
114
+ replacesExistingText ? 2 * Self.maxClearPassCount + 1 : 1
115
+ }
116
+
117
+
118
+ var keystrokeCount: Int {
119
+ characterCount + clearReadCount
120
+ }
121
+
122
+
123
+ var clearReadCount: Int {
124
+ replacesExistingText ? Self.maxClearPassCount : 0
106
125
  }
107
126
  }
108
127
 
@@ -129,8 +148,9 @@ extension RunnerTests {
129
148
  var seconds: TimeInterval {
130
149
  steps.reduce(0) { total, step in
131
150
  total
132
- + Double(step.characterCount) * TextEntryTiming.synthesizedCharacterInterval
151
+ + Double(step.keystrokeCount) * TextEntryTiming.synthesizedCharacterInterval
133
152
  + Double(step.synthesizeCallCount) * TextEntryTiming.synthesizeCallOverhead
153
+ + Double(step.clearReadCount) * TextEntryTiming.synthesizedClearReadAllowance
134
154
  + step.pauseAfterSeconds
135
155
  }
136
156
  }
@@ -226,6 +246,48 @@ extension RunnerTests {
226
246
  case stop
227
247
  }
228
248
 
249
+
250
+ enum ClearedFieldRead: Equatable {
251
+ case text(String)
252
+
253
+ case unreadable
254
+
255
+ case unavailable
256
+ }
257
+
258
+
259
+ enum SynthesizedClearOutcome: Equatable {
260
+ case cleared
261
+
262
+ case unverified
263
+
264
+ case notCleared
265
+
266
+ case stopped
267
+ }
268
+
269
+
270
+
271
+ static func clearForSynthesizedReplacement(
272
+ clearOnce: () -> SynthesizedStepDispatch,
273
+ read: () -> ClearedFieldRead
274
+ ) -> SynthesizedClearOutcome {
275
+ for pass in 1...SynthesizedTextPlan.Step.maxClearPassCount {
276
+ guard clearOnce() == .posted else { return .stopped }
277
+ switch read() {
278
+ case .text(let value) where value.isEmpty:
279
+ return .cleared
280
+ case .text:
281
+ continue
282
+ case .unreadable:
283
+ if pass >= SynthesizedTextPlan.Step.unreadableClearPassCount { return .unverified }
284
+ case .unavailable:
285
+ return .notCleared
286
+ }
287
+ }
288
+ return .notCleared
289
+ }
290
+
229
291
  struct SynthesizedPlanRun {
230
292
  let postedCharacterCount: Int
231
293
 
@@ -298,32 +360,68 @@ extension RunnerTests {
298
360
  )
299
361
  )
300
362
  }
363
+ var clearOutcome: SynthesizedClearOutcome?
301
364
 
302
365
 
303
366
  let synthesisAvailable = runSynthesizedTextPlan(
304
367
  plan,
305
368
  text: request.text,
306
369
  post: { slice, step in
307
- switch request.synthesizer.enterText(
308
- app: request.app,
309
- text: slice,
310
- replacingExistingText: step.replacesExistingText
311
- ) {
312
- case .continueTyping:
313
- return .posted
314
- case .fallback:
315
- return .stop
316
- case .raise(let message):
317
- NSException(
318
- name: NSExceptionName.internalInconsistencyException,
319
- reason: message ?? "private XCTest text synthesis failed"
320
- ).raise()
321
- return .stop
370
+ func enter(_ text: String, replacingExistingText: Bool) -> SynthesizedStepDispatch {
371
+ switch request.synthesizer.enterText(
372
+ app: request.app,
373
+ text: text,
374
+ replacingExistingText: replacingExistingText
375
+ ) {
376
+ case .continueTyping:
377
+ return .posted
378
+ case .fallback:
379
+ return .stop
380
+ case .raise(let message):
381
+ NSException(
382
+ name: NSExceptionName.internalInconsistencyException,
383
+ reason: message ?? "private XCTest text synthesis failed"
384
+ ).raise()
385
+ return .stop
386
+ }
387
+ }
388
+ if step.replacesExistingText {
389
+ var passes = 0
390
+ let outcome = Self.clearForSynthesizedReplacement(
391
+ clearOnce: {
392
+ passes += 1
393
+ return enter(XCUIKeyboardKey.delete.rawValue, replacingExistingText: true)
394
+ },
395
+ read: { self.clearedFieldRead(app: request.app, target: request.target) }
396
+ )
397
+ clearOutcome = outcome
398
+ NSLog("AGENT_DEVICE_RUNNER_TEXT_ENTRY_CLEAR passes=%d outcome=%@", passes, String(describing: outcome))
399
+ guard outcome == .cleared || outcome == .unverified else { return .stop }
322
400
  }
401
+ return enter(slice, replacingExistingText: false)
323
402
  },
324
403
 
325
404
  waitAfterWarmupCharacter: { _ in }
326
405
  )
406
+ if clearOutcome == .notCleared {
407
+ logTextEntryPhase(
408
+ commandId: request.commandId,
409
+ phase: "total",
410
+ startedAt: request.startedAt,
411
+ chars: request.text.count,
412
+ mode: .replacement
413
+ )
414
+ return .completed(
415
+ TextEntryResult(
416
+ verified: nil,
417
+ repaired: false,
418
+ expectedText: request.text,
419
+ observedText: nil,
420
+ textEntryRoute: "synthesized-first-responder-replacement",
421
+ failure: .clearNotObserved
422
+ )
423
+ )
424
+ }
327
425
  if synthesisAvailable.stoppedEarly {
328
426
  NSLog("AGENT_DEVICE_RUNNER_TEXT_ENTRY_ROUTE route=verified-fallback reason=synthesis-unavailable")
329
427
  guard let point = request.target.refreshPoint else { return .notApplicable }
@@ -432,6 +530,7 @@ extension RunnerTests {
432
530
  static func awaitSynthesizedReplacementCommitOutcome(
433
531
  expectedText: String,
434
532
  placeholder: String?,
533
+ fieldIsSecure: Bool = false,
435
534
  stallBudget: TimeInterval = TextEntryTiming.synthesizedCommitStallTimeout,
436
535
  ceiling: TimeInterval = TextEntryTiming.synthesizedCommitCeiling,
437
536
  now: () -> Date = { Date() },
@@ -440,6 +539,11 @@ extension RunnerTests {
440
539
  ) -> SynthesizedTextCommitOutcome {
441
540
 
442
541
 
542
+ if fieldIsSecure {
543
+ return .unobservable
544
+ }
545
+
546
+
443
547
 
444
548
  if Self.textMatchesPlaceholder(expectedText, placeholder: placeholder) {
445
549
  return .notObserved