agent-device 0.21.7 → 0.21.9
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.7.apk → agent-device-android-ime-helper-0.21.9.apk} +0 -0
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.9.apk.sha256 +1 -0
- package/android/ime-helper/dist/{agent-device-android-ime-helper-0.21.7.manifest.json → agent-device-android-ime-helper-0.21.9.manifest.json} +4 -4
- package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.21.7.apk → agent-device-android-snapshot-helper-0.21.9.apk} +0 -0
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.9.apk.sha256 +1 -0
- package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.21.7.manifest.json → agent-device-android-snapshot-helper-0.21.9.manifest.json} +6 -6
- package/apple/macos-helper/Package.swift +16 -1
- package/apple/macos-helper/Sources/AgentDeviceMacOSDeviceHub/DeviceHubPose.swift +47 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSDeviceHub/DeviceHubWindowInventory.swift +65 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubPose.swift +225 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubSidebar.swift +57 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubWindowDiscovery.swift +108 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/main.swift +66 -13
- package/apple/macos-helper/Sources/AgentDeviceMacOSInput/MouseClickDelivery.swift +116 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSInput/MouseClickSchedule.swift +61 -0
- package/apple/macos-helper/Tests/AgentDeviceMacOSDeviceHubTests/DeviceHubPoseTests.swift +30 -0
- package/apple/macos-helper/Tests/AgentDeviceMacOSDeviceHubTests/DeviceHubWindowInventoryTests.swift +65 -0
- package/apple/macos-helper/Tests/AgentDeviceMacOSInputTests/MouseClickScheduleTests.swift +58 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +1 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAppScreenCapture.swift +175 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +10 -2
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +42 -15
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +8 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +45 -22
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +39 -734
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Keyboard.swift +20 -4
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +63 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +13 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift +25 -10
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +5 -562
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotAcquisition.swift +566 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +5 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedInteraction.swift +594 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.h +35 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.m +160 -0
- package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/RecordingExportSupport.swift +3 -0
- package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/recording-frames.swift +203 -0
- package/dist/apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotCoordinateSpace.swift +5 -0
- package/dist/src/absence-observation-errors.js +1 -1
- package/dist/src/adapter.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-device-client.js +2 -2
- package/dist/src/alert-runtime.js +1 -1
- package/dist/src/android-adb.js +1 -1
- package/dist/src/android-input-ownership.js +1 -1
- package/dist/src/android.js +1 -1
- package/dist/src/app-launch.js +1 -1
- package/dist/src/app-log-runtime2.js +3 -3
- 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/artifact-paths.js +1 -0
- package/dist/src/back-mode.js +1 -1
- package/dist/src/cli-config.js +1 -1
- package/dist/src/cli-help.js +15 -11
- package/dist/src/cli.js +4 -4
- package/dist/src/client-types.d.ts +51 -1
- package/dist/src/command-tools.js +3 -3
- package/dist/src/config.js +1 -1
- package/dist/src/connection-runtime.js +1 -1
- package/dist/src/device-claim-inspection.js +1 -1
- package/dist/src/device-claim-rule.js +2 -2
- package/dist/src/device-input-state.d.ts +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.js +1 -1
- package/dist/src/device2.js +1 -1
- package/dist/src/devicectl.js +1 -1
- package/dist/src/dispatch-resolve.js +1 -1
- package/dist/src/display-inventory.js +1 -1
- package/dist/src/doctor-facade.js +1 -1
- package/dist/src/durable-resource.js +1 -1
- package/dist/src/exec.js +1 -1
- package/dist/src/find.js +1 -1
- package/dist/src/finders.js +1 -1
- package/dist/src/fold-runtime.js +1 -0
- package/dist/src/gesture-admission.js +1 -1
- package/dist/src/gesture-runtime.js +1 -1
- package/dist/src/harmonyos.js +1 -1
- package/dist/src/helper.js +2 -2
- package/dist/src/host.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 -0
- package/dist/src/interactor-operation-catalog.js +1 -1
- package/dist/src/interactor.js +4 -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 +2 -2
- package/dist/src/internal/png-worker.d.ts +5 -0
- package/dist/src/internal/png-worker.js +1 -1
- package/dist/src/inventory2.js +1 -1
- package/dist/src/ios-snapshot-acquisition.js +1 -1
- package/dist/src/ios-snapshot-runtime.js +1 -1
- package/dist/src/lease.js +1 -1
- package/dist/src/linux.js +1 -1
- package/dist/src/local-interactor-operation-set.js +1 -1
- 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 +17 -16
- package/dist/src/mobile-snapshot-semantics.js +1 -1
- package/dist/src/mp4-duration.js +1 -0
- package/dist/src/multitouch-support.js +1 -1
- package/dist/src/owner-identity.js +1 -1
- package/dist/src/parameterized-recorded-fill.js +1 -1
- package/dist/src/perf-facade.js +2 -2
- package/dist/src/perf-runtime-plan.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-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 +1 -1
- package/dist/src/platform-runtime-screen-recording-finalizer-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/png-crop-bytes.js +1 -1
- package/dist/src/png-encode.js +1 -0
- package/dist/src/png-size.js +1 -0
- package/dist/src/png-worker-client.js +1 -1
- package/dist/src/provider-webdriver.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/recording-export-quality.js +1 -1
- package/dist/src/recording-scope.js +1 -0
- package/dist/src/recording.js +3 -0
- package/dist/src/registry.js +35 -35
- package/dist/src/replay.js +1 -1
- package/dist/src/request-admission.js +1 -1
- package/dist/src/request-runtime-binding.js +1 -1
- package/dist/src/result-serialization.js +1 -1
- package/dist/src/runner-cache-metadata.js +2 -2
- package/dist/src/runner-client.js +1 -1
- package/dist/src/runner-contract.js +3 -0
- package/dist/src/runner-dev-tools-security.js +1 -0
- package/dist/src/runner-device-readiness.js +1 -0
- package/dist/src/runner-disposal.js +1 -1
- package/dist/src/runner-operations-facade.js +1 -1
- package/dist/src/runner-presentation.js +1 -1
- package/dist/src/runner.js +1 -1
- package/dist/src/runtime.js +2 -2
- package/dist/src/runtime10.js +2 -1
- package/dist/src/runtime2.js +4 -4
- 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-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 -0
- package/dist/src/screenshot-overlay-draw.js +1 -0
- package/dist/src/screenshot-runtime.js +1 -1
- package/dist/src/script.js +1 -1
- package/dist/src/scroll-gesture.js +1 -1
- package/dist/src/sdk-batch.js +2 -2
- package/dist/src/sdk-contracts.d.ts +7 -5
- package/dist/src/sdk-contracts.js +2 -2
- package/dist/src/sdk-device.js +1 -1
- package/dist/src/sdk-finders.js +1 -1
- package/dist/src/sdk-selectors.d.ts +58 -6
- package/dist/src/sdk-selectors.js +1 -1
- package/dist/src/selection.js +0 -0
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/selectors.js +1 -1
- package/dist/src/semantic-index.js +1 -1
- package/dist/src/session-routing.js +1 -1
- package/dist/src/session-snapshot.js +1 -1
- package/dist/src/session-store.js +3 -3
- package/dist/src/session2.js +3 -3
- package/dist/src/simctl.js +1 -1
- package/dist/src/simulator-boot.js +1 -1
- package/dist/src/simulator-facade.js +1 -1
- package/dist/src/simulator.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/snapshot4.js +1 -1
- package/dist/src/src.js +1 -1
- package/dist/src/src7.js +1 -1
- package/dist/src/src8.js +1 -1
- package/dist/src/stop-sequence.js +1 -1
- package/dist/src/text-entry-focus.js +1 -1
- package/dist/src/touch-runtime.js +1 -1
- package/dist/src/tree.js +1 -1
- package/dist/src/type-text-runtime.js +1 -1
- package/dist/src/video.js +2 -2
- package/dist/src/web2.js +1 -1
- package/package.json +3 -1
- package/server.json +2 -2
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.7.apk.sha256 +0 -1
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.7.apk.sha256 +0 -1
- package/dist/src/png-transcode.js +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import AgentDeviceMacOSInput
|
|
1
2
|
import AppKit
|
|
2
3
|
import ApplicationServices
|
|
3
4
|
import CoreGraphics
|
|
@@ -63,6 +64,10 @@ struct ReadResponse: Encodable {
|
|
|
63
64
|
struct PressResponse: Encodable {
|
|
64
65
|
let x: Double
|
|
65
66
|
let y: Double
|
|
67
|
+
/// Hold actually posted; a short request is raised to the deliverable floor.
|
|
68
|
+
let holdMs: Int
|
|
69
|
+
let clicks: Int
|
|
70
|
+
let doubleClick: Bool
|
|
66
71
|
let bundleId: String?
|
|
67
72
|
let surface: String?
|
|
68
73
|
}
|
|
@@ -122,6 +127,8 @@ struct AgentDeviceMacOSHelper {
|
|
|
122
127
|
return try handleScreenshot(arguments: Array(arguments.dropFirst()))
|
|
123
128
|
case "audio-probe":
|
|
124
129
|
return try handleAudioProbe(arguments: Array(arguments.dropFirst()))
|
|
130
|
+
case "device-hub":
|
|
131
|
+
return try handleDeviceHub(arguments: Array(arguments.dropFirst()))
|
|
125
132
|
default:
|
|
126
133
|
throw HelperError.invalidArgs("unknown command: \(command)")
|
|
127
134
|
}
|
|
@@ -378,10 +385,48 @@ struct AgentDeviceMacOSHelper {
|
|
|
378
385
|
throw HelperError.invalidArgs("press requires --x <number> --y <number>")
|
|
379
386
|
}
|
|
380
387
|
|
|
388
|
+
let holdMs = try validatedPressInt(
|
|
389
|
+
optionValue(arguments: arguments, name: "--hold-ms"),
|
|
390
|
+
name: "--hold-ms",
|
|
391
|
+
minimum: 0,
|
|
392
|
+
default: 0
|
|
393
|
+
)
|
|
394
|
+
let clicks = try validatedPressInt(
|
|
395
|
+
optionValue(arguments: arguments, name: "--clicks"),
|
|
396
|
+
name: "--clicks",
|
|
397
|
+
minimum: 1,
|
|
398
|
+
default: 1
|
|
399
|
+
)
|
|
400
|
+
let intervalMs = try validatedPressInt(
|
|
401
|
+
optionValue(arguments: arguments, name: "--interval-ms"),
|
|
402
|
+
name: "--interval-ms",
|
|
403
|
+
minimum: 0,
|
|
404
|
+
default: 120
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
let doubleClick = arguments.contains("--double-click")
|
|
381
408
|
let bundleId = try optionValue(arguments: arguments, name: "--bundle-id").map(validatedBundleId)
|
|
382
409
|
let surface = optionValue(arguments: arguments, name: "--surface")
|
|
383
|
-
|
|
384
|
-
|
|
410
|
+
let request = MouseClickRequest(
|
|
411
|
+
x: x,
|
|
412
|
+
y: y,
|
|
413
|
+
holdMs: holdMs,
|
|
414
|
+
clicks: clicks,
|
|
415
|
+
doubleClick: doubleClick,
|
|
416
|
+
intervalMs: intervalMs
|
|
417
|
+
)
|
|
418
|
+
try pressAtPosition(request)
|
|
419
|
+
return SuccessEnvelope(
|
|
420
|
+
data: PressResponse(
|
|
421
|
+
x: x,
|
|
422
|
+
y: y,
|
|
423
|
+
holdMs: mouseClickHoldMs(requestedMs: holdMs),
|
|
424
|
+
clicks: clicks,
|
|
425
|
+
doubleClick: doubleClick,
|
|
426
|
+
bundleId: bundleId,
|
|
427
|
+
surface: surface
|
|
428
|
+
)
|
|
429
|
+
)
|
|
385
430
|
}
|
|
386
431
|
|
|
387
432
|
static func handleScreenshot(arguments: [String]) throws -> any Encodable {
|
|
@@ -434,6 +479,21 @@ private func intOption(arguments: [String], name: String) -> Int? {
|
|
|
434
479
|
return Int(value)
|
|
435
480
|
}
|
|
436
481
|
|
|
482
|
+
private func validatedPressInt(
|
|
483
|
+
_ raw: String?,
|
|
484
|
+
name: String,
|
|
485
|
+
minimum: Int,
|
|
486
|
+
default fallback: Int
|
|
487
|
+
) throws -> Int {
|
|
488
|
+
guard let raw else {
|
|
489
|
+
return fallback
|
|
490
|
+
}
|
|
491
|
+
guard let value = Int(raw), value >= minimum else {
|
|
492
|
+
throw HelperError.invalidArgs("press \(name) must be an integer of at least \(minimum)")
|
|
493
|
+
}
|
|
494
|
+
return value
|
|
495
|
+
}
|
|
496
|
+
|
|
437
497
|
private func readTextAtPosition(bundleId: String?, surface: String?, x: Double, y: Double) throws -> String {
|
|
438
498
|
let targetApp: NSRunningApplication?
|
|
439
499
|
if surface == "frontmost-app" || (surface == nil && bundleId != nil) {
|
|
@@ -481,19 +541,12 @@ private func readTextAtPosition(bundleId: String?, surface: String?, x: Double,
|
|
|
481
541
|
throw HelperError.commandFailed("read did not resolve text")
|
|
482
542
|
}
|
|
483
543
|
|
|
484
|
-
private func pressAtPosition(
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
guard let move = CGEvent(mouseEventSource: nil, mouseType: .mouseMoved, mouseCursorPosition: point, mouseButton: .left),
|
|
489
|
-
let down = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown, mouseCursorPosition: point, mouseButton: .left),
|
|
490
|
-
let up = CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp, mouseCursorPosition: point, mouseButton: .left)
|
|
491
|
-
else {
|
|
544
|
+
private func pressAtPosition(_ request: MouseClickRequest) throws {
|
|
545
|
+
do {
|
|
546
|
+
try postMouseClick(request)
|
|
547
|
+
} catch MouseClickDeliveryError.eventCreationFailed {
|
|
492
548
|
throw HelperError.commandFailed("press action failed", details: ["reason": "event_creation_failed"])
|
|
493
549
|
}
|
|
494
|
-
move.post(tap: .cghidEventTap)
|
|
495
|
-
down.post(tap: .cghidEventTap)
|
|
496
|
-
up.post(tap: .cghidEventTap)
|
|
497
550
|
}
|
|
498
551
|
|
|
499
552
|
private func captureSurfaceScreenshot(surface: String?, outPath: String, fullscreen: Bool) throws {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import CoreGraphics
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
public struct MouseClickRequest: Equatable, Sendable {
|
|
5
|
+
public let x: Double
|
|
6
|
+
public let y: Double
|
|
7
|
+
/// How long the button stays down. Zero asks for the default hold.
|
|
8
|
+
public let holdMs: Int
|
|
9
|
+
/// Independent presses, each at click state 1.
|
|
10
|
+
public let clicks: Int
|
|
11
|
+
/// Post every press as a double-click pair with a rising click state.
|
|
12
|
+
public let doubleClick: Bool
|
|
13
|
+
public let intervalMs: Int
|
|
14
|
+
|
|
15
|
+
public init(
|
|
16
|
+
x: Double,
|
|
17
|
+
y: Double,
|
|
18
|
+
holdMs: Int = 0,
|
|
19
|
+
clicks: Int = 1,
|
|
20
|
+
doubleClick: Bool = false,
|
|
21
|
+
intervalMs: Int = 120
|
|
22
|
+
) {
|
|
23
|
+
self.x = x
|
|
24
|
+
self.y = y
|
|
25
|
+
self.holdMs = holdMs
|
|
26
|
+
self.clicks = clicks
|
|
27
|
+
self.doubleClick = doubleClick
|
|
28
|
+
self.intervalMs = intervalMs
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public enum MouseClickDeliveryError: Error, Equatable {
|
|
33
|
+
case eventCreationFailed
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// The button the helper currently holds down, kept where a signal handler can reach it.
|
|
37
|
+
/// A helper killed between a mouse-down and its mouse-up would otherwise leave the system's
|
|
38
|
+
/// primary button stuck down for whatever the user touches next. The host stops the helper
|
|
39
|
+
/// with SIGTERM before SIGKILL (`runMacOsHelper` in `helper.ts`) so that this handler runs
|
|
40
|
+
/// on a deadline and on a cancelled request, not only on a signal sent by hand.
|
|
41
|
+
nonisolated(unsafe) private var heldMouseButton: (point: CGPoint, clickState: Int)?
|
|
42
|
+
|
|
43
|
+
private func releaseHeldMouseButton() {
|
|
44
|
+
guard let held = heldMouseButton else { return }
|
|
45
|
+
heldMouseButton = nil
|
|
46
|
+
let up = CGEvent(
|
|
47
|
+
mouseEventSource: nil,
|
|
48
|
+
mouseType: .leftMouseUp,
|
|
49
|
+
mouseCursorPosition: held.point,
|
|
50
|
+
mouseButton: .left
|
|
51
|
+
)
|
|
52
|
+
up?.setIntegerValueField(.mouseEventClickState, value: Int64(held.clickState))
|
|
53
|
+
up?.post(tap: .cghidEventTap)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private func installMouseReleaseOnTermination() {
|
|
57
|
+
for terminationSignal in [SIGTERM, SIGINT, SIGHUP] {
|
|
58
|
+
signal(terminationSignal) { received in
|
|
59
|
+
releaseHeldMouseButton()
|
|
60
|
+
_exit(128 + received)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Posts a click the way a trackpad would: one motion to the point, then each press held
|
|
66
|
+
/// long enough for the app to accept the release. A termination signal that lands inside a
|
|
67
|
+
/// hold releases the button before the process exits.
|
|
68
|
+
public func postMouseClick(_ request: MouseClickRequest) throws {
|
|
69
|
+
installMouseReleaseOnTermination()
|
|
70
|
+
let point = CGPoint(x: request.x, y: request.y)
|
|
71
|
+
let hold = mouseClickHoldMs(requestedMs: request.holdMs)
|
|
72
|
+
let presses = mouseClickPresses(
|
|
73
|
+
clicks: request.clicks,
|
|
74
|
+
doubleClick: request.doubleClick,
|
|
75
|
+
intervalMs: request.intervalMs
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
guard let move = CGEvent(
|
|
79
|
+
mouseEventSource: nil,
|
|
80
|
+
mouseType: .mouseMoved,
|
|
81
|
+
mouseCursorPosition: point,
|
|
82
|
+
mouseButton: .left
|
|
83
|
+
) else {
|
|
84
|
+
throw MouseClickDeliveryError.eventCreationFailed
|
|
85
|
+
}
|
|
86
|
+
move.post(tap: .cghidEventTap)
|
|
87
|
+
|
|
88
|
+
for press in presses {
|
|
89
|
+
if press.delayBeforeMs > 0 {
|
|
90
|
+
usleep(UInt32(press.delayBeforeMs) * 1000)
|
|
91
|
+
}
|
|
92
|
+
guard let down = CGEvent(
|
|
93
|
+
mouseEventSource: nil,
|
|
94
|
+
mouseType: .leftMouseDown,
|
|
95
|
+
mouseCursorPosition: point,
|
|
96
|
+
mouseButton: .left
|
|
97
|
+
), let up = CGEvent(
|
|
98
|
+
mouseEventSource: nil,
|
|
99
|
+
mouseType: .leftMouseUp,
|
|
100
|
+
mouseCursorPosition: point,
|
|
101
|
+
mouseButton: .left
|
|
102
|
+
) else {
|
|
103
|
+
throw MouseClickDeliveryError.eventCreationFailed
|
|
104
|
+
}
|
|
105
|
+
down.setIntegerValueField(.mouseEventClickState, value: Int64(press.clickState))
|
|
106
|
+
up.setIntegerValueField(.mouseEventClickState, value: Int64(press.clickState))
|
|
107
|
+
heldMouseButton = (point, press.clickState)
|
|
108
|
+
down.post(tap: .cghidEventTap)
|
|
109
|
+
usleep(UInt32(hold) * 1000)
|
|
110
|
+
// The record clears only after the up is posted: a signal that lands between the two
|
|
111
|
+
// would otherwise find nothing to release and exit with the button still down. A signal
|
|
112
|
+
// that lands after the post releases a button already up, which is harmless.
|
|
113
|
+
up.post(tap: .cghidEventTap)
|
|
114
|
+
heldMouseButton = nil
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// AppKit and SwiftUI keep a mouse-down under track long enough to tell a click from a
|
|
4
|
+
/// drag, and a mouse-up posted in the same event tick as its mouse-down is never
|
|
5
|
+
/// delivered to the app. Measured on an `NSButton`: a 0 ms hold delivered 0 of 15
|
|
6
|
+
/// mouse-ups and activated 0 actions, 20 ms activated 14 of 15, and 40 ms and above
|
|
7
|
+
/// activated 15 of 15. Holds shorter than this are raised to it.
|
|
8
|
+
public let minimumMouseClickHoldMs = 40
|
|
9
|
+
|
|
10
|
+
/// Hold used when the caller does not name one. Comfortably above
|
|
11
|
+
/// `minimumMouseClickHoldMs` so a press does not sit on the measured cliff.
|
|
12
|
+
public let defaultMouseClickHoldMs = 60
|
|
13
|
+
|
|
14
|
+
/// The gap between the two presses of one double-click. Well inside the system
|
|
15
|
+
/// double-click interval (500 ms by default), and independent of the caller's repeat
|
|
16
|
+
/// interval, which separates whole presses rather than the halves of one.
|
|
17
|
+
public let mouseClickPairGapMs = 80
|
|
18
|
+
|
|
19
|
+
public func mouseClickHoldMs(requestedMs: Int) -> Int {
|
|
20
|
+
if requestedMs <= 0 {
|
|
21
|
+
return defaultMouseClickHoldMs
|
|
22
|
+
}
|
|
23
|
+
return max(requestedMs, minimumMouseClickHoldMs)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// One press of the button: the click state it is posted with, and how long after the
|
|
27
|
+
/// previous release it starts.
|
|
28
|
+
public struct MouseClickPress: Equatable, Sendable {
|
|
29
|
+
/// `1` for an independent click; `2` for the second half of a double-click.
|
|
30
|
+
public let clickState: Int
|
|
31
|
+
public let delayBeforeMs: Int
|
|
32
|
+
|
|
33
|
+
public init(clickState: Int, delayBeforeMs: Int) {
|
|
34
|
+
self.clickState = clickState
|
|
35
|
+
self.delayBeforeMs = delayBeforeMs
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// The presses one request becomes. `clicks` is always a count of independent presses at
|
|
40
|
+
/// click state 1, which is what every other platform means by `--count`; only
|
|
41
|
+
/// `doubleClick` raises the state, and it does so per press, so `doubleClick` with three
|
|
42
|
+
/// clicks is three double-clicks rather than one triple-click.
|
|
43
|
+
public func mouseClickPresses(clicks: Int, doubleClick: Bool, intervalMs: Int) -> [MouseClickPress] {
|
|
44
|
+
let gap = max(intervalMs, 0)
|
|
45
|
+
var presses: [MouseClickPress] = []
|
|
46
|
+
for index in 0..<max(clicks, 1) {
|
|
47
|
+
presses.append(MouseClickPress(clickState: 1, delayBeforeMs: index == 0 ? 0 : gap))
|
|
48
|
+
if doubleClick {
|
|
49
|
+
presses.append(MouseClickPress(clickState: 2, delayBeforeMs: mouseClickPairGapMs))
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return presses
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// How long the schedule keeps the helper busy: every hold plus every gap. The caller's
|
|
56
|
+
/// process timeout must cover this, or the helper is killed mid-schedule.
|
|
57
|
+
public func mouseClickScheduleMs(holdMs: Int, clicks: Int, doubleClick: Bool, intervalMs: Int) -> Int {
|
|
58
|
+
let hold = mouseClickHoldMs(requestedMs: holdMs)
|
|
59
|
+
return mouseClickPresses(clicks: clicks, doubleClick: doubleClick, intervalMs: intervalMs)
|
|
60
|
+
.reduce(0) { $0 + $1.delayBeforeMs + hold }
|
|
61
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
|
|
3
|
+
@testable import AgentDeviceMacOSDeviceHub
|
|
4
|
+
|
|
5
|
+
final class DeviceHubPoseTests: XCTestCase {
|
|
6
|
+
func testEveryPoseNamesTheActionBarControlDeviceHubDescribes() {
|
|
7
|
+
XCTAssertEqual(DeviceHubPose(argument: "closed")?.controlDescription, "Closed")
|
|
8
|
+
XCTAssertEqual(DeviceHubPose(argument: "book")?.controlDescription, "Book")
|
|
9
|
+
XCTAssertEqual(DeviceHubPose(argument: "OPEN")?.controlDescription, "Open")
|
|
10
|
+
XCTAssertNil(DeviceHubPose(argument: "half-open"), "the helper takes Device Hub's names, not the CLI's")
|
|
11
|
+
XCTAssertEqual(Set(DeviceHubPose.allCases.map(\.controlDescription)), ["Closed", "Book", "Open"])
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Device Hub titles the window `<name> – iOS 27.1`; a simulator whose name merely starts
|
|
15
|
+
// with another's must not claim that window.
|
|
16
|
+
func testWindowTitleMatchesTheDeviceNameAsAPrefixWord() {
|
|
17
|
+
XCTAssertTrue(deviceHubWindowShows(deviceName: "iPhone Duo", title: "iPhone Duo – iOS 27.1"))
|
|
18
|
+
XCTAssertTrue(deviceHubWindowShows(deviceName: "iPhone Duo", title: "iPhone Duo"))
|
|
19
|
+
XCTAssertFalse(deviceHubWindowShows(deviceName: "iPhone Duo", title: "iPhone Duo Lab – iOS 27.1"))
|
|
20
|
+
XCTAssertFalse(deviceHubWindowShows(deviceName: "iPhone Duo", title: "bench-golden-v1 – iOS 27.0"))
|
|
21
|
+
XCTAssertFalse(deviceHubWindowShows(deviceName: "iPhone Duo", title: nil))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func testSidebarRowIsKeyedByUdid() {
|
|
25
|
+
XCTAssertEqual(
|
|
26
|
+
deviceHubDeviceRowIdentifier(udid: "4F879835-4AB3-4046-B033-5AB769209DD4"),
|
|
27
|
+
"TableRow.Device.4F879835-4AB3-4046-B033-5AB769209DD4"
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
}
|
package/apple/macos-helper/Tests/AgentDeviceMacOSDeviceHubTests/DeviceHubWindowInventoryTests.swift
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import ApplicationServices
|
|
2
|
+
import XCTest
|
|
3
|
+
@testable import AgentDeviceMacOSDeviceHub
|
|
4
|
+
|
|
5
|
+
final class DeviceHubWindowInventoryTests: XCTestCase {
|
|
6
|
+
func testLaterWindowKeepsItsProcessAndDoesNotPreventEmptyProcessReopen() {
|
|
7
|
+
var inventory = DeviceHubWindowInventory()
|
|
8
|
+
let window = AXUIElementCreateApplication(20)
|
|
9
|
+
inventory.record(processID: 10, read: .available([]))
|
|
10
|
+
inventory.record(processID: 20, read: .available([window]))
|
|
11
|
+
XCTAssertEqual(inventory.candidates.map(\.processID), [20])
|
|
12
|
+
XCTAssertTrue(CFEqual(inventory.candidates[0].window, window))
|
|
13
|
+
inventory.record(candidate: 0, outcome: .sidebarUnavailable)
|
|
14
|
+
XCTAssertEqual(inventory.processesToReopen(attempted: []), [10])
|
|
15
|
+
XCTAssertEqual(inventory.failureReason(), "device-hub-identity-unconfirmed")
|
|
16
|
+
XCTAssertEqual(inventory.candidateOutcomes, ["20:0:sidebar-unavailable"])
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func testFailedReadsAreNotReopenedOrMisreportedAsMissingWindows() {
|
|
20
|
+
var inventory = DeviceHubWindowInventory()
|
|
21
|
+
inventory.record(processID: 10, read: .failed(-25204))
|
|
22
|
+
inventory.record(processID: 20, read: .available([]))
|
|
23
|
+
XCTAssertEqual(inventory.processesToReopen(attempted: []), [20])
|
|
24
|
+
XCTAssertEqual(inventory.failures, [10: -25204])
|
|
25
|
+
XCTAssertEqual(inventory.failureReason(), "device-hub-window-read-failed")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func testSuccessfulEmptyReadIsMissingAndReopenIsAttemptedOnlyOnce() {
|
|
29
|
+
var inventory = DeviceHubWindowInventory()
|
|
30
|
+
inventory.record(processID: 10, read: .available([]))
|
|
31
|
+
XCTAssertEqual(inventory.failureReason(), "device-hub-window-missing")
|
|
32
|
+
XCTAssertEqual(inventory.processesToReopen(attempted: []), [10])
|
|
33
|
+
XCTAssertEqual(inventory.processesToReopen(attempted: [10]), [])
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func testSharedBudgetExhaustionStopsReopenWithoutClaimingMissingDevice() {
|
|
37
|
+
var inventory = DeviceHubWindowInventory()
|
|
38
|
+
inventory.record(processID: 10, read: .available([]))
|
|
39
|
+
inventory.expireBudget()
|
|
40
|
+
XCTAssertEqual(inventory.processesToReopen(attempted: []), [])
|
|
41
|
+
XCTAssertEqual(inventory.failureReason(), "device-hub-discovery-timeout")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func testCandidateOutcomesRetainUncertaintyAndOwningProcess() {
|
|
45
|
+
var inventory = DeviceHubWindowInventory()
|
|
46
|
+
inventory.record(processID: 10, read: .available([AXUIElementCreateApplication(10)]))
|
|
47
|
+
inventory.record(processID: 20, read: .available([AXUIElementCreateApplication(20)]))
|
|
48
|
+
inventory.record(candidate: 0, outcome: .timedOut)
|
|
49
|
+
inventory.record(candidate: 1, outcome: .unconfirmed)
|
|
50
|
+
XCTAssertEqual(inventory.candidateOutcomes, ["10:0:timed-out", "20:1:row-unconfirmed"])
|
|
51
|
+
XCTAssertEqual(inventory.failureReason(), "device-hub-identity-unconfirmed")
|
|
52
|
+
}
|
|
53
|
+
func testUnrelatedReopenFailureDoesNotMaskCandidateUncertainty() {
|
|
54
|
+
var inventory = DeviceHubWindowInventory()
|
|
55
|
+
inventory.record(processID: 10, read: .available([]))
|
|
56
|
+
let failures: [Int32: String] = [10: "automation-permission"]
|
|
57
|
+
XCTAssertEqual(inventory.failureReason(reopenFailures: failures), "automation-permission")
|
|
58
|
+
inventory.record(processID: 20, read: .available([AXUIElementCreateApplication(20)]))
|
|
59
|
+
inventory.record(candidate: 0, outcome: .timedOut)
|
|
60
|
+
XCTAssertEqual(inventory.failureReason(reopenFailures: failures), "device-hub-identity-unconfirmed")
|
|
61
|
+
inventory.expireBudget()
|
|
62
|
+
XCTAssertEqual(inventory.failureReason(reopenFailures: failures), "device-hub-discovery-timeout")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
|
|
3
|
+
@testable import AgentDeviceMacOSInput
|
|
4
|
+
|
|
5
|
+
final class MouseClickScheduleTests: XCTestCase {
|
|
6
|
+
// The original schedule posted move, down and up back to back. AppKit and SwiftUI
|
|
7
|
+
// never delivered the release, so every surface press opened a tracking session that
|
|
8
|
+
// no control ever completed: 0 of 15 measured clicks activated their control.
|
|
9
|
+
func testHoldShorterThanTheDeliverableFloorIsRaised() {
|
|
10
|
+
XCTAssertEqual(mouseClickHoldMs(requestedMs: 0), defaultMouseClickHoldMs)
|
|
11
|
+
XCTAssertEqual(mouseClickHoldMs(requestedMs: 1), minimumMouseClickHoldMs)
|
|
12
|
+
XCTAssertEqual(mouseClickHoldMs(requestedMs: 39), minimumMouseClickHoldMs)
|
|
13
|
+
XCTAssertGreaterThan(defaultMouseClickHoldMs, minimumMouseClickHoldMs)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
func testNamedLongPressHoldIsKept() {
|
|
17
|
+
XCTAssertEqual(mouseClickHoldMs(requestedMs: 800), 800)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// `--count N` means N independent presses on every platform; a rising click state would
|
|
21
|
+
// turn `--count 2` into a double-click and `--count 3` into a triple-click.
|
|
22
|
+
func testRepeatClicksAreIndependentPressesAtClickStateOne() {
|
|
23
|
+
let presses = mouseClickPresses(clicks: 3, doubleClick: false, intervalMs: 150)
|
|
24
|
+
XCTAssertEqual(presses.map(\.clickState), [1, 1, 1])
|
|
25
|
+
XCTAssertEqual(presses.map(\.delayBeforeMs), [0, 150, 150])
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Only `--double-tap` raises the click state, and it does so per press, so it composes
|
|
29
|
+
// with `--count` into that many double-clicks rather than one triple-click.
|
|
30
|
+
func testDoubleClickRaisesTheStateInsideEachPress() {
|
|
31
|
+
XCTAssertEqual(
|
|
32
|
+
mouseClickPresses(clicks: 1, doubleClick: true, intervalMs: 120).map(\.clickState),
|
|
33
|
+
[1, 2]
|
|
34
|
+
)
|
|
35
|
+
let three = mouseClickPresses(clicks: 3, doubleClick: true, intervalMs: 500)
|
|
36
|
+
XCTAssertEqual(three.map(\.clickState), [1, 2, 1, 2, 1, 2])
|
|
37
|
+
XCTAssertEqual(
|
|
38
|
+
three.map(\.delayBeforeMs),
|
|
39
|
+
[0, mouseClickPairGapMs, 500, mouseClickPairGapMs, 500, mouseClickPairGapMs]
|
|
40
|
+
)
|
|
41
|
+
XCTAssertLessThan(mouseClickPairGapMs, 500, "a pair must land inside the system double-click interval")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func testZeroClicksStillPostsOnePress() {
|
|
45
|
+
XCTAssertEqual(mouseClickPresses(clicks: 0, doubleClick: false, intervalMs: 0).count, 1)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The process timeout the caller sets is derived from this: a schedule the timeout does
|
|
49
|
+
// not cover is killed mid-hold.
|
|
50
|
+
func testScheduleDurationSumsEveryHoldAndGap() {
|
|
51
|
+
XCTAssertEqual(mouseClickScheduleMs(holdMs: 0, clicks: 1, doubleClick: false, intervalMs: 120), defaultMouseClickHoldMs)
|
|
52
|
+
XCTAssertEqual(mouseClickScheduleMs(holdMs: 10_000, clicks: 4, doubleClick: false, intervalMs: 120), 4 * 10_000 + 3 * 120)
|
|
53
|
+
XCTAssertEqual(
|
|
54
|
+
mouseClickScheduleMs(holdMs: 60, clicks: 2, doubleClick: true, intervalMs: 100),
|
|
55
|
+
4 * 60 + 2 * mouseClickPairGapMs + 100
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAppScreenCapture.swift
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
#if canImport(UIKit)
|
|
3
|
+
import UIKit
|
|
4
|
+
#endif
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
struct CapturedAppScreen {
|
|
10
|
+
let image: RunnerImage
|
|
11
|
+
let displayID: UInt
|
|
12
|
+
let pixelWidth: Int
|
|
13
|
+
let pixelHeight: Int
|
|
14
|
+
let pixelsPerPoint: Double
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
enum RunnerAppScreenCaptureFailure: String, Error {
|
|
21
|
+
case unresolvedWindow = "APP_SCREEN_WINDOW_UNRESOLVED"
|
|
22
|
+
case unresolvedScreen = "APP_SCREEN_UNRESOLVED"
|
|
23
|
+
case unrenderableImage = "APP_SCREEN_CAPTURE_UNRENDERABLE"
|
|
24
|
+
|
|
25
|
+
init(_ failure: RunnerApplicationScreenFailure) {
|
|
26
|
+
switch failure {
|
|
27
|
+
case .unresolvedWindow:
|
|
28
|
+
self = .unresolvedWindow
|
|
29
|
+
default:
|
|
30
|
+
self = .unresolvedScreen
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
var message: String {
|
|
35
|
+
switch self {
|
|
36
|
+
case .unresolvedWindow:
|
|
37
|
+
return "Neither the target app nor the system surface resolved a window, so no display could be named."
|
|
38
|
+
case .unresolvedScreen:
|
|
39
|
+
return "The resolved app window did not report the display it is shown on."
|
|
40
|
+
case .unrenderableImage:
|
|
41
|
+
return "The resolved display handed back an image that could not be drawn upright."
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
var hint: String {
|
|
46
|
+
switch self {
|
|
47
|
+
case .unresolvedWindow:
|
|
48
|
+
return "The capture follows the display that owns a window, so this is a runtime that is answering no window for any process; runner.log names the window frame and the query answer it refused."
|
|
49
|
+
case .unresolvedScreen:
|
|
50
|
+
return "Retry the capture. If it persists, the XCTest runtime is not reporting the window's display; runner.log names the window frame it resolved."
|
|
51
|
+
case .unrenderableImage:
|
|
52
|
+
return "Retry the capture. The display was resolved, so this is the capture itself failing, not a panel question."
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#if canImport(UIKit) && os(iOS)
|
|
58
|
+
extension RunnerTests {
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
func captureResolvedAppScreen(
|
|
71
|
+
app: XCUIApplication
|
|
72
|
+
) -> Result<CapturedAppScreen, RunnerAppScreenCaptureFailure> {
|
|
73
|
+
var outcome: Result<CapturedAppScreen, RunnerAppScreenCaptureFailure> = .failure(
|
|
74
|
+
.unresolvedScreen
|
|
75
|
+
)
|
|
76
|
+
let capture = {
|
|
77
|
+
outcome = Self.resolveCapturedAppScreen(app: app)
|
|
78
|
+
}
|
|
79
|
+
if Thread.isMainThread {
|
|
80
|
+
capture()
|
|
81
|
+
} else {
|
|
82
|
+
DispatchQueue.main.sync(execute: capture)
|
|
83
|
+
}
|
|
84
|
+
return outcome
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
func captureObservedScreen(app: XCUIApplication) -> Result<
|
|
98
|
+
CapturedAppScreen, RunnerAppScreenCaptureFailure
|
|
99
|
+
> {
|
|
100
|
+
selectObservedScreenCapture(
|
|
101
|
+
resolving: { captureResolvedAppScreen(app: app) },
|
|
102
|
+
fallingBack: { captureResolvedAppScreen(app: springboard) }
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
func selectObservedScreenCapture(
|
|
109
|
+
resolving: () -> Result<CapturedAppScreen, RunnerAppScreenCaptureFailure>,
|
|
110
|
+
fallingBack: () -> Result<CapturedAppScreen, RunnerAppScreenCaptureFailure>
|
|
111
|
+
) -> Result<CapturedAppScreen, RunnerAppScreenCaptureFailure> {
|
|
112
|
+
let outcome = resolving()
|
|
113
|
+
if case .failure(.unresolvedWindow) = outcome {
|
|
114
|
+
return fallingBack()
|
|
115
|
+
}
|
|
116
|
+
return outcome
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private static func resolveCapturedAppScreen(
|
|
120
|
+
app: XCUIApplication
|
|
121
|
+
) -> Result<CapturedAppScreen, RunnerAppScreenCaptureFailure> {
|
|
122
|
+
var screen: AnyObject?
|
|
123
|
+
var displayID: UInt = 0
|
|
124
|
+
var resolution: RunnerApplicationScreenFailure = .unresolvedScreen
|
|
125
|
+
guard RunnerResolveApplicationScreen(app, &screen, &displayID, &resolution),
|
|
126
|
+
let captureScreen = screen as? XCUIScreen
|
|
127
|
+
else {
|
|
128
|
+
return .failure(RunnerAppScreenCaptureFailure(resolution))
|
|
129
|
+
}
|
|
130
|
+
let image = captureScreen.screenshot().image
|
|
131
|
+
guard let upright = runnerUprightCaptureImage(image) else {
|
|
132
|
+
return .failure(.unrenderableImage)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
guard upright.scale.isFinite, upright.scale > 0 else {
|
|
138
|
+
return .failure(.unrenderableImage)
|
|
139
|
+
}
|
|
140
|
+
guard let cgImage = runnerCGImage(from: upright) else {
|
|
141
|
+
return .failure(.unrenderableImage)
|
|
142
|
+
}
|
|
143
|
+
return .success(
|
|
144
|
+
CapturedAppScreen(
|
|
145
|
+
image: upright,
|
|
146
|
+
displayID: displayID,
|
|
147
|
+
pixelWidth: cgImage.width,
|
|
148
|
+
pixelHeight: cgImage.height,
|
|
149
|
+
pixelsPerPoint: Double(upright.scale)
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
private static func runnerUprightCaptureImage(_ image: UIImage) -> UIImage? {
|
|
161
|
+
if image.imageOrientation == .up {
|
|
162
|
+
return image
|
|
163
|
+
}
|
|
164
|
+
guard image.size.width > 0, image.size.height > 0, image.scale.isFinite, image.scale > 0 else {
|
|
165
|
+
return nil
|
|
166
|
+
}
|
|
167
|
+
let format = UIGraphicsImageRendererFormat()
|
|
168
|
+
format.scale = image.scale
|
|
169
|
+
let renderer = UIGraphicsImageRenderer(size: image.size, format: format)
|
|
170
|
+
return renderer.image { _ in
|
|
171
|
+
image.draw(in: CGRect(origin: .zero, size: image.size))
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
#endif
|
package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h
CHANGED
|
@@ -8,7 +8,11 @@ FOUNDATION_EXPORT double RunnerControlledScrollProgress(double t);
|
|
|
8
8
|
|
|
9
9
|
@interface RunnerSynthesizedGesture : NSObject
|
|
10
10
|
|
|
11
|
+
// `resolvedWindow` is the app window the caller already resolved for the gesture's reference
|
|
12
|
+
// frame. The record's display ID is read from that same window so geometry and routing can never
|
|
13
|
+
// name different windows; pass nil to fall back to the application's first window.
|
|
11
14
|
+ (NSString * _Nullable)synthesizeSwipeWithApplication:(id)application
|
|
15
|
+
resolvedWindow:(id _Nullable)resolvedWindow
|
|
12
16
|
x:(double)x
|
|
13
17
|
y:(double)y
|
|
14
18
|
x2:(double)x2
|
|
@@ -16,26 +20,30 @@ FOUNDATION_EXPORT double RunnerControlledScrollProgress(double t);
|
|
|
16
20
|
durationMs:(double)durationMs;
|
|
17
21
|
|
|
18
22
|
+ (NSString * _Nullable)synthesizeContinuousDragWithApplication:(id)application
|
|
23
|
+
resolvedWindow:(id _Nullable)resolvedWindow
|
|
19
24
|
x:(double)x
|
|
20
25
|
y:(double)y
|
|
21
26
|
x2:(double)x2
|
|
22
|
-
|
|
27
|
+
y2:(double)y2
|
|
23
28
|
durationMs:(double)durationMs;
|
|
24
29
|
|
|
25
30
|
+ (NSString * _Nullable)synthesizeControlledScrollWithApplication:(id)application
|
|
31
|
+
resolvedWindow:(id _Nullable)resolvedWindow
|
|
26
32
|
x:(double)x
|
|
27
33
|
y:(double)y
|
|
28
34
|
x2:(double)x2
|
|
29
|
-
|
|
35
|
+
y2:(double)y2
|
|
30
36
|
durationMs:(double)durationMs;
|
|
31
37
|
|
|
32
38
|
+ (NSString * _Nullable)synthesizeTapWithApplication:(id)application
|
|
39
|
+
resolvedWindow:(id _Nullable)resolvedWindow
|
|
33
40
|
x:(double)x
|
|
34
41
|
y:(double)y;
|
|
35
42
|
|
|
36
43
|
// Each pointer is an ordered array of { x, y, offsetMs } samples. The first sample
|
|
37
44
|
// starts contact; subsequent samples move it; all pointers lift at their final offset.
|
|
38
45
|
+ (NSString * _Nullable)synthesizeGestureWithApplication:(id)application
|
|
46
|
+
resolvedWindow:(id _Nullable)resolvedWindow
|
|
39
47
|
pointerSamples:(NSArray<NSArray<NSDictionary<NSString *, NSNumber *> *> *> *)pointerSamples;
|
|
40
48
|
|
|
41
49
|
// UIInterfaceOrientation of the app (1 portrait, 2 upsideDown, 3 landscapeRight,
|