agent-device 0.21.19 → 0.21.21
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/ime-helper/dist/{agent-device-android-ime-helper-0.21.19.apk → agent-device-android-ime-helper-0.21.21.apk} +0 -0
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.21.apk.sha256 +1 -0
- package/android/ime-helper/dist/{agent-device-android-ime-helper-0.21.19.manifest.json → agent-device-android-ime-helper-0.21.21.manifest.json} +4 -4
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.21.apk +0 -0
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.21.apk.sha256 +1 -0
- package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.21.19.manifest.json → agent-device-android-snapshot-helper-0.21.21.manifest.json} +6 -6
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/AppWindowScreenshot.swift +73 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/BackgroundInteraction.swift +557 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/GhostCursor.swift +141 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/SnapshotTraversal.swift +107 -3
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/main.swift +136 -22
- package/apple/macos-helper/Tests/AgentDeviceMacOSHelperTests/BackgroundInteractionTests.swift +88 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +133 -4
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +10 -2
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +36 -9
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +28 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +1 -14
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +14 -16
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +67 -14
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedCommitDeadline.swift +6 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +7 -2
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedInteraction.swift +21 -7
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedTextEntry.swift +121 -17
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +179 -6
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntryConfirmation.swift +119 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntryFocus.swift +8 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntryReadiness.swift +23 -11
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextInputProbe.swift +59 -6
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextTyping.swift +112 -23
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TypeExecution.swift +25 -12
- package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/RecordingExportSupport.swift +6 -1
- package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/recording-overlay.swift +43 -13
- package/dist/apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotModels.swift +20 -5
- package/dist/apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotPresentationProjection.swift +2 -15
- package/dist/apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotVisibilityFold.swift +10 -25
- package/dist/apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotVisibilityFoldProjection.swift +1 -18
- package/dist/src/adapter.js +1 -1
- package/dist/src/adb-executor.js +2 -2
- package/dist/src/adb-failure.js +1 -1
- package/dist/src/adb-transport.js +1 -1
- package/dist/src/agent-browser-lifecycle.js +1 -1
- package/dist/src/agent-browser-process-record.js +1 -1
- package/dist/src/agent-browser-provider.js +1 -1
- package/dist/src/agent-browser-tool.js +1 -1
- package/dist/src/agent-device-client.js +2 -2
- package/dist/src/ai-sdk.js +1 -1
- package/dist/src/alert-runtime.js +1 -1
- package/dist/src/alert.js +1 -0
- package/dist/src/android-adb.d.ts +12 -0
- package/dist/src/android-input-ownership.js +1 -1
- package/dist/src/app-backend.js +1 -0
- package/dist/src/app-catalog.js +1 -1
- package/dist/src/app-deployment-resolution.js +1 -1
- package/dist/src/app-inventory.js +1 -1
- package/dist/src/app-launch.js +2 -2
- package/dist/src/app-lifecycle.js +2 -2
- package/dist/src/app-lifecycle2.js +1 -1
- package/dist/src/app-log-files.js +1 -1
- package/dist/src/app-log-reconnect.js +1 -1
- package/dist/src/app-log-runtime2.js +3 -3
- package/dist/src/app-log-session-resource.js +1 -0
- package/dist/src/app-resolution.js +2 -2
- package/dist/src/apple-runner-platform.js +1 -1
- package/dist/src/application-lifecycle-interaction.js +1 -1
- package/dist/src/archive.js +2 -2
- package/dist/src/artifact-download.js +1 -1
- package/dist/src/auth-session.js +1 -1
- package/dist/src/auth.js +1 -1
- package/dist/src/back-mode.js +1 -1
- package/dist/src/backend-snapshot-options.js +1 -1
- package/dist/src/boot-diagnostics.js +1 -1
- package/dist/src/browser-launch.js +1 -1
- package/dist/src/cache.js +1 -1
- package/dist/src/capabilities.js +1 -1
- package/dist/src/capture.js +1 -1
- package/dist/src/cli-config.js +1 -1
- package/dist/src/cli-help.js +317 -323
- package/dist/src/cli-runner.js +1 -1
- package/dist/src/cli.js +4 -4
- package/dist/src/click-button.js +1 -1
- package/dist/src/client-metro-companion.js +1 -1
- package/dist/src/client-metro.js +1 -1
- package/dist/src/client-options.js +3 -0
- package/dist/src/client-types.d.ts +101 -6
- package/dist/src/clipboard-shell-response.js +1 -1
- package/dist/src/{scroll-command.d.ts → cloud-artifacts.d.ts} +249 -240
- package/dist/src/code-signature-cache.js +1 -1
- package/dist/src/code-signature.js +1 -1
- package/dist/src/command-schema.js +3 -3
- package/dist/src/command-tools.js +2 -2
- package/dist/src/command.js +1 -1
- package/dist/src/config.js +1 -1
- package/dist/src/connection-runtime.js +1 -1
- package/dist/src/connection.js +5 -5
- package/dist/src/contracts.d.ts +1 -1
- package/dist/src/contracts.js +1 -1
- package/dist/src/daemon-artifacts.js +1 -0
- package/dist/src/daemon-client-lease-beat.js +1 -1
- package/dist/src/daemon-client-lifecycle.js +1 -1
- package/dist/src/daemon-client-liveness-probe.js +1 -0
- package/dist/src/daemon-client-transport.js +1 -1
- package/dist/src/daemon-http.js +1 -0
- package/dist/src/daemon-policy-file.js +1 -1
- package/dist/src/daemon-registration-owner.js +1 -0
- package/dist/src/daemon-registration.js +1 -1
- package/dist/src/daemon-resolution.js +1 -1
- package/dist/src/daemon-shutdown-report.js +1 -1
- package/dist/src/daemon.js +2 -2
- package/dist/src/debug-symbols-facade.js +3 -3
- package/dist/src/device-claim-inspection.js +1 -1
- package/dist/src/device-claim-rule.js +1 -1
- package/dist/src/device-inventory-context.js +1 -1
- package/dist/src/device-readiness-runtime.js +1 -1
- package/dist/src/device-release.js +1 -1
- package/dist/src/device-rotation.js +1 -1
- package/dist/src/device-selection-resolver.js +1 -1
- package/dist/src/device-session.js +1 -1
- package/dist/src/device-shell.js +1 -1
- package/dist/src/device.js +1 -1
- package/dist/src/device2.js +1 -3
- package/dist/src/device3.js +3 -0
- package/dist/src/devicectl.js +1 -1
- package/dist/src/diagnostics.js +3 -3
- package/dist/src/dispatch-resolve.js +4 -1
- package/dist/src/display-inventory.js +1 -1
- package/dist/src/doctor-facade.js +1 -1
- package/dist/src/doctor2.js +1 -1
- package/dist/src/durable-capture-resource.js +1 -0
- package/dist/src/engine-eval-script.js +1 -1
- package/dist/src/error.js +1 -1
- package/dist/src/exec.js +1 -1
- package/dist/src/file.js +1 -1
- package/dist/src/find.js +1 -1
- package/dist/src/generic-settle.js +1 -1
- package/dist/src/generic.js +1 -1
- package/dist/src/gesture-admission.js +1 -1
- package/dist/src/gesture-plan.js +1 -1
- package/dist/src/gesture-runtime.js +1 -1
- package/dist/src/harmonyos.js +1 -1
- package/dist/src/hdc.js +1 -1
- package/dist/src/helper.js +2 -2
- package/dist/src/host-file.js +1 -1
- package/dist/src/host.js +1 -1
- package/dist/src/human-control-contract.js +1 -1
- package/dist/src/human-control.js +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +1 -1
- package/dist/src/input-actions.js +1 -1
- package/dist/src/input-actions2.js +2 -2
- package/dist/src/install-artifact-facade.js +1 -1
- package/dist/src/install-source-network-transport.js +1 -1
- package/dist/src/install-source.d.ts +5 -0
- package/dist/src/install-source2.js +1 -1
- package/dist/src/instance-client.js +1 -0
- package/dist/src/interaction-positionals.js +1 -1
- package/dist/src/interaction-ref-policy.js +1 -1
- package/dist/src/interaction.js +1 -1
- package/dist/src/interaction2.js +1 -1
- package/dist/src/interactor-operation-binding.js +1 -1
- package/dist/src/interactor.js +5 -5
- package/dist/src/interactor2.js +1 -1
- package/dist/src/interactors.js +1 -1
- package/dist/src/internal/bin.js +1 -1
- package/dist/src/internal/daemon.js +1 -2
- package/dist/src/internal/png-worker.js +1 -1
- package/dist/src/internal/run-script-http-child.js +1 -1
- package/dist/src/inventory.js +4 -4
- package/dist/src/inventory2.js +1 -1
- package/dist/src/inventory3.js +1 -1
- package/dist/src/inventory5.js +1 -1
- package/dist/src/ios-client.js +4 -30
- package/dist/src/ios-settings.js +1 -0
- package/dist/src/ios-snapshot-engine.js +1 -1
- package/dist/src/ios-snapshot-runtime.js +1 -1
- package/dist/src/launch-confirmation.js +1 -0
- package/dist/src/lease-context.js +1 -1
- package/dist/src/lease-registry-scope.js +1 -0
- package/dist/src/lease-scope.js +1 -1
- package/dist/src/lease.js +1 -1
- package/dist/src/legacy-xctest-device-set.js +1 -1
- package/dist/src/limrun-download-file.js +1 -1
- package/dist/src/limrun-runtime-dependencies.js +1 -1
- package/dist/src/limrun.d.ts +78 -44
- package/dist/src/limrun.js +1 -1
- package/dist/src/linux.js +1 -1
- package/dist/src/load.js +1 -0
- package/dist/src/location-coordinates.js +1 -1
- package/dist/src/macos-facade.js +1 -1
- package/dist/src/managed-device-scope.js +1 -1
- package/dist/src/mechanics.js +16 -16
- package/dist/src/observation.js +1 -1
- package/dist/src/observe-until.js +1 -1
- package/dist/src/overlay.js +2 -2
- package/dist/src/owner-identity.js +3 -3
- package/dist/src/path-resolution.js +1 -1
- package/dist/src/perf-facade.js +1 -1
- package/dist/src/perf-runtime-operation-builder.js +1 -1
- package/dist/src/perf-runtime-plan.js +1 -1
- package/dist/src/perf-target.js +1 -1
- package/dist/src/perf.js +1 -1
- package/dist/src/physical-device-control.js +1 -1
- package/dist/src/platform-request-scope.js +1 -1
- package/dist/src/platform-runtime-android-adb-host.js +1 -1
- package/dist/src/platform-runtime-app-log-process.js +1 -1
- package/dist/src/platform-runtime-host.js +1 -1
- package/dist/src/platform-runtime-managed-owner.js +1 -1
- package/dist/src/platform-runtime-open-target.js +1 -1
- package/dist/src/platform-runtime-operation-host.js +2 -2
- package/dist/src/platform-runtime-runtime-hints.js +2 -2
- package/dist/src/platform-runtime-screen-recording-android-host.js +1 -1
- package/dist/src/platform-runtime-screen-recording-apple-runner-transport.js +1 -1
- package/dist/src/platform-runtime-screen-recording-apple-simulator-host.js +1 -1
- package/dist/src/platform-runtime-screen-recording-finalizer-host.js +1 -1
- package/dist/src/platform-runtime-screen-recording-output-host.js +1 -1
- package/dist/src/platform-runtime-screen-recording-process-host.js +1 -1
- package/dist/src/platform-runtime-unavailable.js +1 -1
- package/dist/src/platform-runtime.js +1 -1
- package/dist/src/platform-runtime2.js +1 -1
- package/dist/src/plugins.d.ts +9 -0
- package/dist/src/plugins.js +1 -0
- package/dist/src/plugins2.js +2 -0
- package/dist/src/png-crop-bytes.js +1 -1
- package/dist/src/png-size.js +1 -1
- package/dist/src/png-worker-client.js +1 -1
- package/dist/src/png.js +1 -1
- package/dist/src/prepare-kind.js +1 -1
- package/dist/src/process.js +1 -1
- package/dist/src/project-runtime.js +1 -1
- package/dist/src/provider-credential-fingerprint.js +1 -0
- package/dist/src/provider-device-admission.js +1 -1
- package/dist/src/provider-device-runtimes.js +1 -0
- package/dist/src/provider-limrun-credentials.js +1 -0
- package/dist/src/provider-profile-fields.js +1 -0
- package/dist/src/provider-session-ownership.js +2 -2
- package/dist/src/provider-webdriver.js +2 -1
- package/dist/src/providers.js +1 -1
- package/dist/src/proxy.js +1 -1
- package/dist/src/quality-warnings.js +1 -1
- package/dist/src/react-native.js +1 -1
- package/dist/src/record-runtime.js +1 -1
- package/dist/src/record-trace.js +1 -1
- package/dist/src/recording.js +1 -1
- package/dist/src/rect-center.js +1 -1
- package/dist/src/register-builtins.js +1 -1
- package/dist/src/registry.js +52 -36
- package/dist/src/remote-config2.js +1 -1
- package/dist/src/replay-divergence.js +1 -1
- package/dist/src/replay.js +2 -2
- package/dist/src/reporting.js +5 -5
- package/dist/src/request-admission.js +1 -1
- package/dist/src/request-cancel.js +1 -1
- package/dist/src/request-cancellation.js +1 -1
- package/dist/src/request-runtime-binding.js +1 -1
- package/dist/src/result-serialization.js +1 -1
- package/dist/src/retry.js +1 -1
- package/dist/src/runner-artifact-manifest.js +1 -1
- package/dist/src/runner-cache-metadata.js +3 -3
- package/dist/src/runner-client.js +1 -1
- package/dist/src/runner-dev-tools-security.js +1 -1
- package/dist/src/runner-device-readiness.js +1 -1
- package/dist/src/runner-disposal.js +1 -1
- package/dist/src/runner-error-classification.js +2 -2
- package/dist/src/runner-exchange.js +1 -1
- package/dist/src/runner-operations-facade.js +1 -1
- package/dist/src/runner-session-types.js +1 -1
- package/dist/src/runner-start-budget.js +1 -1
- package/dist/src/runner.js +1 -1
- package/dist/src/runtime-admission.js +1 -1
- package/dist/src/runtime-transport-hints.js +1 -1
- package/dist/src/runtime.js +1 -1
- package/dist/src/runtime10.js +2 -2
- package/dist/src/runtime2.js +3 -3
- package/dist/src/runtime3.js +1 -1
- package/dist/src/runtime4.js +5 -5
- package/dist/src/runtime5.js +1 -1
- package/dist/src/runtime6.js +1 -1
- package/dist/src/runtime7.js +1 -1
- package/dist/src/runtime8.js +1 -1
- package/dist/src/runtime9.js +1 -1
- package/dist/src/screen-recording-admission-ledger.js +1 -1
- package/dist/src/screen-recording-completion.js +1 -1
- package/dist/src/screen-recording-options.js +1 -1
- package/dist/src/screen-recording-resource-recovery.js +1 -1
- package/dist/src/screen-recording-transport.js +1 -1
- package/dist/src/screenshot-density.js +1 -1
- package/dist/src/screenshot-image.js +1 -1
- package/dist/src/screenshot-result.js +1 -1
- package/dist/src/screenshot-runtime.js +1 -1
- package/dist/src/screenshot.js +1 -1
- package/dist/src/screenshot3.js +1 -1
- package/dist/src/script.js +1 -1
- package/dist/src/scroll-command.js +1 -1
- package/dist/src/scroll-gesture.js +1 -1
- package/dist/src/sdk-batch-runner.js +2 -2
- package/dist/src/sdk-batch.js +2 -2
- package/dist/src/sdk-contracts.d.ts +47 -2
- package/dist/src/sdk-contracts.js +2 -2
- package/dist/src/sdk-device.js +1 -1
- package/dist/src/sdk-io.js +1 -1
- package/dist/src/sdk-remote-config.js +1 -1
- package/dist/src/sdk-selectors.d.ts +37 -8
- package/dist/src/selection.js +0 -0
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/selectors.d.ts +2 -2
- package/dist/src/selectors.js +1 -1
- package/dist/src/server.js +1 -1
- package/dist/src/session-allocation.js +1 -1
- package/dist/src/session-artifact-paths.js +1 -1
- package/dist/src/session-repair-tombstone.js +1 -1
- package/dist/src/session-routing.js +1 -1
- package/dist/src/session-runtime-admission.js +1 -1
- package/dist/src/session-snapshot.js +1 -1
- package/dist/src/session-stream.js +27 -0
- package/dist/src/session.js +1 -1
- package/dist/src/session2.js +5 -5
- package/dist/src/settings.js +1 -1
- package/dist/src/settings2.js +1 -1
- package/dist/src/simctl-settings-facade.js +1 -0
- package/dist/src/simctl-settings.js +1 -0
- package/dist/src/simctl.js +1 -1
- package/dist/src/simulator-boot.js +1 -1
- package/dist/src/simulator.js +1 -1
- package/dist/src/snapshot-diagnostics.js +1 -1
- package/dist/src/snapshot-runtime-binding.js +1 -1
- package/dist/src/snapshot-runtime.js +1 -1
- package/dist/src/snapshot-state.js +1 -1
- package/dist/src/snapshot2.js +1 -1
- package/dist/src/snapshot3.js +1 -1
- package/dist/src/snapshot4.js +1 -1
- package/dist/src/src.js +1 -1
- package/dist/src/src7.js +1 -1
- package/dist/src/src8.js +2 -2
- package/dist/src/store.js +1 -0
- package/dist/src/string-enum.js +1 -1
- package/dist/src/surface-snapshot.js +1 -1
- package/dist/src/takeover.js +1 -1
- package/dist/src/target-evidence.js +1 -1
- package/dist/src/telemetry.js +1 -1
- package/dist/src/text-entry-focus.js +1 -1
- 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/toolchain-probe.js +2 -2
- package/dist/src/touch-runtime.js +1 -1
- package/dist/src/touch-runtime2.js +1 -1
- package/dist/src/transport.js +2 -2
- package/dist/src/tv-remote.js +1 -1
- package/dist/src/type-text-runtime.js +1 -1
- package/dist/src/update-check.js +1 -1
- package/dist/src/vars.js +1 -1
- package/dist/src/verified-file.js +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/video.js +1 -1
- package/dist/src/viewport.js +1 -1
- package/dist/src/web.js +1 -1
- package/dist/src/web2.js +1 -1
- package/dist/src/webdriver-source.js +1 -1
- package/package.json +5 -1
- package/server.json +2 -2
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.19.apk.sha256 +0 -1
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.19.apk +0 -0
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.19.apk.sha256 +0 -1
- package/dist/src/api.js +0 -3
- package/dist/src/daemon-process.js +0 -1
- package/dist/src/multitouch-support.js +0 -1
- package/dist/src/parameterized-recorded-fill.js +0 -1
- package/dist/src/runner-startup-transport.js +0 -1
- package/dist/src/runner-xctestrun.js +0 -1
- package/dist/src/session-store.js +0 -4
|
@@ -95,8 +95,9 @@ extension RunnerTests {
|
|
|
95
95
|
app: XCUIApplication,
|
|
96
96
|
x: Double,
|
|
97
97
|
y: Double,
|
|
98
|
-
context: SynthesizedCoordinateContext? = nil
|
|
99
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift
CHANGED
|
@@ -8,6 +8,7 @@ extension RunnerTests {
|
|
|
8
8
|
case notFocused = "TEXT_INPUT_NOT_FOCUSED"
|
|
9
9
|
case synthesisUnavailable = "TEXT_INPUT_SYNTHESIS_UNAVAILABLE"
|
|
10
10
|
case commitNotObserved = "TEXT_INPUT_COMMIT_NOT_OBSERVED"
|
|
11
|
+
case clearNotObserved = "TEXT_INPUT_CLEAR_NOT_OBSERVED"
|
|
11
12
|
case synthesisBudgetExceeded = "TEXT_INPUT_SYNTHESIS_BUDGET_EXCEEDED"
|
|
12
13
|
|
|
13
14
|
var message: String {
|
|
@@ -18,6 +19,8 @@ extension RunnerTests {
|
|
|
18
19
|
return "Reliable text synthesis is unavailable while the software keyboard is hidden."
|
|
19
20
|
case .commitNotObserved:
|
|
20
21
|
return "The runner could not confirm the typed text reached the field."
|
|
22
|
+
case .clearNotObserved:
|
|
23
|
+
return "The runner could not clear the field before typing, so it typed nothing."
|
|
21
24
|
case .synthesisBudgetExceeded:
|
|
22
25
|
return "The text is longer than one runner command can type at this pace."
|
|
23
26
|
}
|
|
@@ -31,6 +34,8 @@ extension RunnerTests {
|
|
|
31
34
|
return "Show the software keyboard, then retry type."
|
|
32
35
|
case .commitNotObserved:
|
|
33
36
|
return "The field may hold none, part, or all of the text. Run snapshot -i and inspect the field: if it already matches, continue; otherwise retry fill with the full text quoted and --delay-ms \(TextEntryTiming.recoveryDelayMilliseconds). Do not use type, which appends to whatever committed."
|
|
37
|
+
case .clearNotObserved:
|
|
38
|
+
return "The field may still hold some or all of its old text. Run snapshot -i and inspect the field, then retry fill. Do not use type, which appends to whatever the field holds."
|
|
34
39
|
case .synthesisBudgetExceeded:
|
|
35
40
|
let recoveryDelay = TextEntryTiming.recoveryDelayMilliseconds
|
|
36
41
|
let recoveryBudget = SynthesizedDeliveryBudget.maxTextLength(
|
|
@@ -61,13 +66,24 @@ extension RunnerTests {
|
|
|
61
66
|
|
|
62
67
|
|
|
63
68
|
|
|
69
|
+
static let unconfirmedSettleCeiling: TimeInterval = 1.0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
64
73
|
static let synthesizedCommitStallTimeout: TimeInterval = 3.0
|
|
65
74
|
|
|
66
75
|
|
|
67
76
|
static let synthesizedCommitCeiling: TimeInterval = 10.0
|
|
68
77
|
|
|
69
78
|
|
|
70
|
-
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
static let synthesizedReplacementFocusAllowance: TimeInterval = 4.0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
static let synthesizedClearReadAllowance: TimeInterval = 0.9
|
|
71
87
|
|
|
72
88
|
|
|
73
89
|
|
|
@@ -108,6 +124,27 @@ extension RunnerTests {
|
|
|
108
124
|
let observedText: String?
|
|
109
125
|
var textEntryRoute: String? = nil
|
|
110
126
|
var failure: TextEntryFailure? = nil
|
|
127
|
+
var unconfirmed: TextEntryUnconfirmedEvidence? = nil
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
struct TextEntryInputIdentity: Equatable {
|
|
136
|
+
let elementType: XCUIElement.ElementType
|
|
137
|
+
let identifier: String
|
|
138
|
+
|
|
139
|
+
var isDistinguishable: Bool { !identifier.isEmpty }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
struct TextInputAtPoint {
|
|
146
|
+
let element: XCUIElement
|
|
147
|
+
let identity: TextEntryInputIdentity
|
|
111
148
|
}
|
|
112
149
|
|
|
113
150
|
struct TextEntryTarget {
|
|
@@ -116,33 +153,81 @@ extension RunnerTests {
|
|
|
116
153
|
let prefersFocusedElement: Bool
|
|
117
154
|
let fromTapWitness: Bool
|
|
118
155
|
|
|
156
|
+
let boundIdentity: TextEntryInputIdentity?
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
let inputAtRefreshPoint: TextInputAtPoint?
|
|
163
|
+
|
|
119
164
|
init(
|
|
120
165
|
element: XCUIElement?,
|
|
121
166
|
refreshPoint: CGPoint?,
|
|
122
167
|
prefersFocusedElement: Bool,
|
|
123
|
-
fromTapWitness: Bool = false
|
|
168
|
+
fromTapWitness: Bool = false,
|
|
169
|
+
boundIdentity: TextEntryInputIdentity? = nil,
|
|
170
|
+
inputAtRefreshPoint: TextInputAtPoint? = nil
|
|
124
171
|
) {
|
|
125
172
|
self.element = element
|
|
126
173
|
self.refreshPoint = refreshPoint
|
|
127
174
|
self.prefersFocusedElement = prefersFocusedElement
|
|
128
175
|
self.fromTapWitness = fromTapWitness
|
|
176
|
+
self.boundIdentity = boundIdentity ?? inputAtRefreshPoint?.identity
|
|
177
|
+
self.inputAtRefreshPoint = inputAtRefreshPoint
|
|
129
178
|
}
|
|
130
179
|
|
|
131
180
|
func withElement(_ nextElement: XCUIElement?) -> TextEntryTarget {
|
|
132
181
|
guard let nextElement else {
|
|
133
182
|
return self
|
|
134
183
|
}
|
|
135
|
-
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
let snapshot = nextElement.exists ? try? nextElement.snapshot() : nil
|
|
188
|
+
let frame = snapshot?.frame ?? .zero
|
|
136
189
|
let point = frame.isEmpty ? refreshPoint : CGPoint(x: frame.midX, y: frame.midY)
|
|
137
190
|
return TextEntryTarget(
|
|
138
191
|
element: nextElement,
|
|
139
192
|
refreshPoint: point,
|
|
140
193
|
prefersFocusedElement: prefersFocusedElement,
|
|
141
|
-
fromTapWitness: fromTapWitness
|
|
194
|
+
fromTapWitness: fromTapWitness,
|
|
195
|
+
boundIdentity: boundIdentity ?? snapshot.map {
|
|
196
|
+
TextEntryInputIdentity(elementType: $0.elementType, identifier: $0.identifier)
|
|
197
|
+
},
|
|
198
|
+
inputAtRefreshPoint: inputAtRefreshPoint
|
|
142
199
|
)
|
|
143
200
|
}
|
|
144
201
|
}
|
|
145
202
|
|
|
203
|
+
|
|
204
|
+
enum TextEntryInputProbe: Equatable {
|
|
205
|
+
case input(TextEntryInputIdentity)
|
|
206
|
+
|
|
207
|
+
case noMatch
|
|
208
|
+
|
|
209
|
+
case unavailable
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
static let xCTestUITestingErrorDomain = "com.apple.dt.xctest.ui-testing.error"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
static let xCTestNoMatchesErrorCode = 10008
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
static func textEntryInputProbe(snapshotError error: Error) -> TextEntryInputProbe {
|
|
219
|
+
let error = error as NSError
|
|
220
|
+
return error.domain == xCTestUITestingErrorDomain && error.code == xCTestNoMatchesErrorCode
|
|
221
|
+
? .noMatch
|
|
222
|
+
: .unavailable
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
static func textEntryRepairCanTarget(boundIdentity: TextEntryInputIdentity?) -> Bool {
|
|
228
|
+
boundIdentity?.isDistinguishable ?? true
|
|
229
|
+
}
|
|
230
|
+
|
|
146
231
|
struct TextEntryStabilization {
|
|
147
232
|
let element: XCUIElement?
|
|
148
233
|
let focusConfirmed: Bool
|
|
@@ -199,7 +284,95 @@ extension RunnerTests {
|
|
|
199
284
|
}
|
|
200
285
|
}
|
|
201
286
|
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
|
|
202
290
|
func resolveTextEntryElement(app: XCUIApplication, target: TextEntryTarget) -> XCUIElement? {
|
|
291
|
+
guard let boundIdentity = target.boundIdentity else {
|
|
292
|
+
return resolveUnboundTextEntryElement(app: app, target: target)
|
|
293
|
+
}
|
|
294
|
+
func isBoundInput(_ candidate: XCUIElement) -> Bool {
|
|
295
|
+
candidate.exists && probeTextEntryInput(candidate) == .input(boundIdentity)
|
|
296
|
+
}
|
|
297
|
+
if target.prefersFocusedElement, let focused = focusedTextInput(app: app), isBoundInput(focused) {
|
|
298
|
+
return focused
|
|
299
|
+
}
|
|
300
|
+
if let element = target.element, isBoundInput(element) {
|
|
301
|
+
return element
|
|
302
|
+
}
|
|
303
|
+
if let input = target.inputAtRefreshPoint, textInputStillResolves(input) {
|
|
304
|
+
return input.element
|
|
305
|
+
}
|
|
306
|
+
if target.inputAtRefreshPoint == nil,
|
|
307
|
+
let refreshPoint = target.refreshPoint,
|
|
308
|
+
case .matches(let candidates) = probeTextInputs(app: app, point: refreshPoint),
|
|
309
|
+
let match = candidates.first(where: isBoundInput) {
|
|
310
|
+
return match
|
|
311
|
+
}
|
|
312
|
+
if let focused = focusedTextInput(app: app), isBoundInput(focused) {
|
|
313
|
+
return focused
|
|
314
|
+
}
|
|
315
|
+
if let byIdentifier = boundTextEntryInputQuery(app: app, identity: boundIdentity), isBoundInput(byIdentifier) {
|
|
316
|
+
return byIdentifier
|
|
317
|
+
}
|
|
318
|
+
return nil
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
func boundTextEntryInputIsGone(app: XCUIApplication, target: TextEntryTarget) -> Bool {
|
|
326
|
+
guard let boundIdentity = target.boundIdentity,
|
|
327
|
+
let query = boundTextEntryInputQuery(app: app, identity: boundIdentity) ?? target.element
|
|
328
|
+
else {
|
|
329
|
+
return false
|
|
330
|
+
}
|
|
331
|
+
switch probeTextEntryInput(query) {
|
|
332
|
+
case .input(let identity) where identity != boundIdentity:
|
|
333
|
+
break
|
|
334
|
+
case .noMatch:
|
|
335
|
+
break
|
|
336
|
+
case .input, .unavailable:
|
|
337
|
+
return false
|
|
338
|
+
}
|
|
339
|
+
guard let refreshPoint = target.refreshPoint else {
|
|
340
|
+
return true
|
|
341
|
+
}
|
|
342
|
+
switch probeTextInputs(app: app, point: refreshPoint) {
|
|
343
|
+
case .absent:
|
|
344
|
+
return true
|
|
345
|
+
case .matches(let candidates):
|
|
346
|
+
return candidates.allSatisfy { probeTextEntryInput($0) != .input(boundIdentity) }
|
|
347
|
+
case .unavailable:
|
|
348
|
+
return false
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
private func boundTextEntryInputQuery(app: XCUIApplication, identity: TextEntryInputIdentity) -> XCUIElement? {
|
|
354
|
+
guard identity.isDistinguishable else {
|
|
355
|
+
return nil
|
|
356
|
+
}
|
|
357
|
+
return app.descendants(matching: identity.elementType).matching(identifier: identity.identifier).element
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
func probeTextEntryInput(_ element: XCUIElement) -> TextEntryInputProbe {
|
|
362
|
+
var probe = TextEntryInputProbe.unavailable
|
|
363
|
+
let (_, exception) = catchingObjCException(fallback: ()) {
|
|
364
|
+
do {
|
|
365
|
+
let snapshot = try element.snapshot()
|
|
366
|
+
probe = .input(TextEntryInputIdentity(elementType: snapshot.elementType, identifier: snapshot.identifier))
|
|
367
|
+
} catch {
|
|
368
|
+
probe = Self.textEntryInputProbe(snapshotError: error)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return exception == nil ? probe : .unavailable
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
private func resolveUnboundTextEntryElement(app: XCUIApplication, target: TextEntryTarget) -> XCUIElement? {
|
|
203
376
|
if target.prefersFocusedElement {
|
|
204
377
|
if let focused = focusedTextInput(app: app) {
|
|
205
378
|
return focused
|
|
@@ -251,7 +424,7 @@ extension RunnerTests {
|
|
|
251
424
|
}
|
|
252
425
|
|
|
253
426
|
func editableTextValue(
|
|
254
|
-
for element:
|
|
427
|
+
for element: (any XCUIElementAttributes)?,
|
|
255
428
|
treatingPlaceholderAsEmpty: Bool = false
|
|
256
429
|
) -> String? {
|
|
257
430
|
guard let element else {
|
|
@@ -271,7 +444,7 @@ extension RunnerTests {
|
|
|
271
444
|
}
|
|
272
445
|
}
|
|
273
446
|
|
|
274
|
-
private func isPlaceholderValue(_ value: String, for element:
|
|
447
|
+
private func isPlaceholderValue(_ value: String, for element: any XCUIElementAttributes) -> Bool {
|
|
275
448
|
if Self.textMatchesPlaceholder(value, placeholder: element.placeholderValue) {
|
|
276
449
|
return true
|
|
277
450
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
extension RunnerTests {
|
|
7
|
+
|
|
8
|
+
struct TextEntryElementIdentity: Equatable {
|
|
9
|
+
let identifier: String?
|
|
10
|
+
let elementType: String
|
|
11
|
+
let frame: CGRect
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
func isSameElement(as other: TextEntryElementIdentity) -> Bool {
|
|
16
|
+
if identifier != nil || other.identifier != nil {
|
|
17
|
+
return identifier == other.identifier
|
|
18
|
+
}
|
|
19
|
+
return elementType == other.elementType && frame == other.frame
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
struct TextEntryObservation: Equatable {
|
|
25
|
+
let value: String
|
|
26
|
+
let identity: TextEntryElementIdentity
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
func isSettled(with other: TextEntryObservation) -> Bool {
|
|
30
|
+
value == other.value && identity.isSameElement(as: other.identity)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
struct TextEntryUnconfirmedEvidence: Equatable {
|
|
36
|
+
let requested: String
|
|
37
|
+
let before: String
|
|
38
|
+
let after: String
|
|
39
|
+
let target: TextEntryElementIdentity
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
static func textEntryValueEchoes(observed: String, expected: String, baseline: String) -> Bool {
|
|
48
|
+
let request = textEntryRequestWithoutSubmitKeys(expected)
|
|
49
|
+
let residualAndRequest = baseline + request
|
|
50
|
+
return isOrderedSubsequence(observed, of: residualAndRequest)
|
|
51
|
+
|| (isOrderedSubsequence(request, of: observed) && !isOrderedSubsequence(request, of: baseline))
|
|
52
|
+
|| isOrderedSubsequence(residualAndRequest, of: observed)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
static func unconfirmedTextEntryEvidence(
|
|
59
|
+
requested: String,
|
|
60
|
+
baseline: TextEntryObservation?,
|
|
61
|
+
observed: TextEntryObservation?
|
|
62
|
+
) -> TextEntryUnconfirmedEvidence? {
|
|
63
|
+
guard !textEntryRequestWithoutSubmitKeys(requested).isEmpty,
|
|
64
|
+
let baseline,
|
|
65
|
+
let observed,
|
|
66
|
+
baseline.identity.isSameElement(as: observed.identity),
|
|
67
|
+
observed.value != baseline.value,
|
|
68
|
+
!textEntryValueEchoes(observed: observed.value, expected: requested, baseline: baseline.value)
|
|
69
|
+
else {
|
|
70
|
+
return nil
|
|
71
|
+
}
|
|
72
|
+
return TextEntryUnconfirmedEvidence(
|
|
73
|
+
requested: requested,
|
|
74
|
+
before: baseline.value,
|
|
75
|
+
after: observed.value,
|
|
76
|
+
target: observed.identity
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
func textEntryObservation(for element: XCUIElement?) -> TextEntryObservation? {
|
|
83
|
+
guard let element,
|
|
84
|
+
let snapshot = try? element.snapshot(),
|
|
85
|
+
let value = editableTextValue(for: snapshot, treatingPlaceholderAsEmpty: true)
|
|
86
|
+
else {
|
|
87
|
+
return nil
|
|
88
|
+
}
|
|
89
|
+
return TextEntryObservation(
|
|
90
|
+
value: value,
|
|
91
|
+
identity: TextEntryElementIdentity(
|
|
92
|
+
identifier: snapshot.identifier.isEmpty ? nil : snapshot.identifier,
|
|
93
|
+
elementType: elementTypeName(snapshot.elementType),
|
|
94
|
+
frame: snapshot.frame
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
static func textEntryRequestWithoutSubmitKeys(_ text: String) -> String {
|
|
101
|
+
var request = text
|
|
102
|
+
while request.hasSuffix("\n") || request.hasSuffix("\r") {
|
|
103
|
+
request.removeLast()
|
|
104
|
+
}
|
|
105
|
+
return request
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
static func isOrderedSubsequence(_ candidate: String, of text: String) -> Bool {
|
|
110
|
+
var remaining = text[...]
|
|
111
|
+
for character in candidate {
|
|
112
|
+
guard let match = remaining.firstIndex(of: character) else {
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
115
|
+
remaining = remaining[remaining.index(after: match)...]
|
|
116
|
+
}
|
|
117
|
+
return true
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -6,9 +6,16 @@ import XCTest
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
extension RunnerTests {
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
9
16
|
@MainActor
|
|
10
17
|
func rememberTextEntryTap(_ element: XCUIElement?) {
|
|
11
|
-
guard let element
|
|
18
|
+
guard let element else {
|
|
12
19
|
clearRememberedTextEntryTap()
|
|
13
20
|
return
|
|
14
21
|
}
|
|
@@ -80,21 +80,33 @@ extension RunnerTests {
|
|
|
80
80
|
#endif
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
83
97
|
func waitForTextEntryReadinessAfterTap(app: XCUIApplication, element: XCUIElement) {
|
|
84
98
|
#if os(iOS)
|
|
85
|
-
|
|
86
|
-
case .textField, .secureTextField, .searchField, .textView:
|
|
87
|
-
if waitForFocusedTextInput(app: app, timeout: TextEntryTiming.readinessTimeout) != nil {
|
|
88
|
-
return
|
|
89
|
-
}
|
|
90
|
-
let frame = element.frame
|
|
91
|
-
if !frame.isEmpty {
|
|
92
|
-
_ = tapAt(app: app, x: frame.midX, y: frame.midY)
|
|
93
|
-
_ = waitForFocusedTextInput(app: app, timeout: TextEntryTiming.readinessTimeout)
|
|
94
|
-
}
|
|
95
|
-
default:
|
|
99
|
+
if waitForFocusedTextInput(app: app, timeout: TextEntryTiming.readinessTimeout) != nil {
|
|
96
100
|
return
|
|
97
101
|
}
|
|
102
|
+
guard let settled = safely("TEXT_ENTRY_READINESS_FRAME", { try? element.snapshot() }) else {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
let frame = settled.frame
|
|
106
|
+
if !frame.isEmpty {
|
|
107
|
+
_ = tapAt(app: app, x: frame.midX, y: frame.midY)
|
|
108
|
+
_ = waitForFocusedTextInput(app: app, timeout: TextEntryTiming.readinessTimeout)
|
|
109
|
+
}
|
|
98
110
|
#endif
|
|
99
111
|
}
|
|
100
112
|
|