agent-device 0.18.3 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.18.3.apk → agent-device-android-multitouch-helper-0.19.1.apk} +0 -0
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.1.apk.sha256 +1 -0
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.18.3.manifest.json → agent-device-android-multitouch-helper-0.19.1.manifest.json} +4 -4
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.18.3.apk → agent-device-android-snapshot-helper-0.19.1.apk} +0 -0
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.1.apk.sha256 +1 -0
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.18.3.manifest.json → agent-device-android-snapshot-helper-0.19.1.manifest.json} +6 -6
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTapPointPolicy.swift +28 -0
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +33 -127
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +630 -177
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +155 -0
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +67 -0
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +173 -122
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift +172 -0
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +64 -0
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +76 -182
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +92 -51
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +213 -137
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +142 -0
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +208 -46
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +82 -11
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +74 -0
- package/dist/src/agent-browser-tool.js +1 -0
- package/dist/src/android-adb.d.ts +63 -234
- package/dist/src/android-adb.js +1 -1
- package/dist/src/android-input-ownership.js +6 -0
- package/dist/src/android.js +1 -5
- package/dist/src/app-lifecycle.js +7 -0
- package/dist/src/apple-runner-options.js +1 -0
- package/dist/src/apps.js +5 -11
- package/dist/src/artifacts.d.ts +4 -3
- package/dist/src/artifacts.js +1 -1
- package/dist/src/audio-probe.js +1 -1
- package/dist/src/batch.d.ts +2 -177
- package/dist/src/batch.js +1 -1
- package/dist/src/cli-help.js +178 -60
- package/dist/src/cli-output.js +1 -1
- package/dist/src/cli-runner.js +2 -0
- package/dist/src/cli.js +12 -41
- package/dist/src/client-metro-companion.js +1 -0
- package/dist/src/client.js +3 -0
- package/dist/src/command-metadata.js +1 -0
- package/dist/src/command-schema.js +22 -0
- package/dist/src/command-surface.js +1 -1
- package/dist/src/contracts.d.ts +2 -213
- package/dist/src/contracts.js +1 -1
- package/dist/src/debug-symbols.js +5 -3
- package/dist/src/device-input-state.js +2 -0
- package/dist/src/devices.js +1 -1
- package/dist/src/devices2.js +1 -0
- package/dist/src/devices3.js +1 -0
- package/dist/src/find.js +1 -1
- package/dist/src/finders.d.ts +2 -81
- package/dist/src/finders.js +1 -1
- package/dist/src/generic.js +1 -1
- package/dist/src/handler-utils.js +1 -1
- package/dist/src/helper.js +2 -0
- package/dist/src/host-process.js +2 -0
- package/dist/src/index.d.ts +1444 -1786
- package/dist/src/index.js +1 -1
- package/dist/src/input-actions.js +3 -1
- package/dist/src/input-actions2.js +1 -0
- package/dist/src/install-source.d.ts +13 -14
- package/dist/src/install-source.js +1 -1
- package/dist/src/install-source2.js +1 -0
- package/dist/src/interaction-snapshot.js +1 -0
- package/dist/src/interaction.js +1 -1
- package/dist/src/interactor.js +2 -1
- package/dist/src/internal/bin.d.ts +1 -1
- package/dist/src/internal/bin.js +2 -5
- package/dist/src/internal/companion-tunnel.d.ts +1 -1
- package/dist/src/internal/companion-tunnel.js +1 -1
- package/dist/src/internal/daemon.d.ts +1 -1
- package/dist/src/internal/daemon.js +18 -1
- package/dist/src/internal/png-worker.d.ts +29 -26
- package/dist/src/internal/png-worker.js +1 -1
- package/dist/src/internal/update-check-entry.d.ts +1 -1
- package/dist/src/internal/update-check-entry.js +1 -1
- package/dist/src/io.d.ts +2 -85
- package/dist/src/io.js +1 -1
- package/dist/src/lease.js +1 -1
- package/dist/src/linux.js +1 -1
- package/dist/src/manifest.js +1 -0
- package/dist/src/metro.d.ts +69 -113
- package/dist/src/metro.js +1 -1
- package/dist/src/metro2.js +1 -0
- package/dist/src/notifications.js +1 -1
- package/dist/src/output-path.js +1 -0
- package/dist/src/path-resolution.js +1 -0
- package/dist/src/png-worker-contract.js +1 -0
- package/dist/src/points.js +4 -0
- package/dist/src/project-runtime.js +1 -0
- package/dist/src/provider.js +1 -1
- package/dist/src/providers.js +1 -0
- package/dist/src/react-native.js +1 -1
- package/dist/src/record-trace-recording.js +2 -5
- package/dist/src/record-trace.js +1 -1
- package/dist/src/recording-provider.js +1 -1
- package/dist/src/registry.js +38 -0
- package/dist/src/remote-config.d.ts +2 -83
- package/dist/src/reporting.js +9 -0
- package/dist/src/rolldown-runtime.js +1 -0
- package/dist/src/runner-client.js +1 -0
- package/dist/src/runner-provider.js +1 -0
- package/dist/src/runner-sequence.js +1 -1
- package/dist/src/runtime-transport.js +1 -0
- package/dist/src/runtime.js +3 -0
- package/dist/src/scoped-provider.js +1 -0
- package/dist/src/sdk-android-adb.d.ts +144 -0
- package/dist/src/sdk-android-adb.js +4 -0
- package/dist/src/sdk-app-inventory.d.ts +4 -0
- package/dist/src/sdk-app-inventory.js +1 -0
- package/dist/src/sdk-batch-runner.d.ts +39 -0
- package/dist/src/sdk-batch-runner.js +2 -0
- package/dist/src/sdk-batch.js +2 -0
- package/dist/src/sdk-contracts.d.ts +386 -0
- package/dist/src/sdk-contracts.js +2 -0
- package/dist/src/sdk-finders.d.ts +60 -0
- package/dist/src/sdk-finders.js +1 -0
- package/dist/src/sdk-io.d.ts +78 -0
- package/dist/src/sdk-io.js +1 -0
- package/dist/src/sdk-remote-config.d.ts +139 -0
- package/dist/src/sdk-remote-config.js +1 -0
- package/dist/src/sdk-selectors.js +1 -0
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/selectors.d.ts +38 -137
- package/dist/src/selectors.js +1 -1
- package/dist/src/selectors2.js +1 -0
- package/dist/src/server.js +3 -2
- package/dist/src/session.js +10 -11
- package/dist/src/setting-state.js +1 -0
- package/dist/src/simctl.js +1 -0
- package/dist/src/simulator.js +1 -2
- package/dist/src/snapshot-diagnostics.js +3 -0
- package/dist/src/snapshot-helper.js +5 -0
- package/dist/src/snapshot-quality.js +1 -0
- package/dist/src/snapshot.js +1 -2
- package/dist/src/snapshot2.js +2 -0
- package/dist/src/{5560.js → timeouts.js} +1 -1
- package/dist/src/tool-provider.js +3 -0
- package/dist/src/tool-provider2.js +1 -0
- package/dist/src/ttl-memo.js +1 -0
- package/dist/src/tv-remote.js +14 -0
- package/dist/src/update-check.js +1 -0
- package/dist/src/url.js +1 -0
- package/dist/src/version.js +1 -0
- package/dist/src/web.js +1 -1
- package/dist/src/xml.js +2 -0
- package/package.json +13 -19
- package/server.json +2 -2
- package/skills/agent-device/SKILL.md +8 -7
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.18.3.apk.sha256 +0 -1
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.18.3.apk.sha256 +0 -1
- package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +0 -443
- package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +0 -169
- package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +0 -220
- package/dist/src/1404.js +0 -1
- package/dist/src/1986.js +0 -1
- package/dist/src/2748.js +0 -1
- package/dist/src/2948.js +0 -30
- package/dist/src/3006.js +0 -1
- package/dist/src/314.js +0 -1
- package/dist/src/3340.js +0 -8
- package/dist/src/4418.js +0 -1
- package/dist/src/4428.js +0 -3
- package/dist/src/485.js +0 -1
- package/dist/src/4917.js +0 -1
- package/dist/src/495.js +0 -5
- package/dist/src/5448.js +0 -3
- package/dist/src/5484.js +0 -1
- package/dist/src/5643.js +0 -1
- package/dist/src/587.js +0 -1
- package/dist/src/5913.js +0 -4
- package/dist/src/6849.js +0 -1
- package/dist/src/6918.js +0 -1
- package/dist/src/6967.js +0 -1
- package/dist/src/760.js +0 -1
- package/dist/src/7871.js +0 -1
- package/dist/src/8184.js +0 -1
- package/dist/src/8225.js +0 -1
- package/dist/src/8391.js +0 -1
- package/dist/src/8404.js +0 -2
- package/dist/src/8414.js +0 -2
- package/dist/src/8875.js +0 -4
- package/dist/src/893.js +0 -1
- package/dist/src/8936.js +0 -1
- package/dist/src/9414.js +0 -1
- package/dist/src/9581.js +0 -22
- package/dist/src/9612.js +0 -1
- package/dist/src/9624.js +0 -1
- package/dist/src/9722.js +0 -39
- package/dist/src/devices~1.js +0 -1
- package/dist/src/devices~2.js +0 -1
- package/dist/src/input-actions~1.js +0 -1
- package/dist/src/rslib-runtime.js +0 -1
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcshareddata/xcschemes/AgentDeviceRunner.xcscheme +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests.entitlements +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.h +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.m +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Environment.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Exceptions.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TvRemote.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests.xctestplan +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-overlay.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-resize.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-trim.swift +0 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
enum RunnerCommandLifecycleState: String {
|
|
5
|
+
case notAccepted
|
|
6
|
+
case accepted
|
|
7
|
+
case started
|
|
8
|
+
case completed
|
|
9
|
+
case failed
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
struct RunnerCommandJournalEntry {
|
|
13
|
+
let commandId: String
|
|
14
|
+
let command: String
|
|
15
|
+
var state: RunnerCommandLifecycleState
|
|
16
|
+
var responseOk: Bool?
|
|
17
|
+
var responseJson: String?
|
|
18
|
+
var error: ErrorPayload?
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
final class RunnerCommandJournal {
|
|
22
|
+
private let lock = NSLock()
|
|
23
|
+
private let maxEntries = 64
|
|
24
|
+
private let maxResponseJsonBytes = 16 * 1024
|
|
25
|
+
private var entries: [String: RunnerCommandJournalEntry] = [:]
|
|
26
|
+
private var order: [String] = []
|
|
27
|
+
|
|
28
|
+
func accept(command: Command) {
|
|
29
|
+
guard let commandId = normalizedCommandId(command.commandId) else { return }
|
|
30
|
+
lock.lock()
|
|
31
|
+
defer { lock.unlock() }
|
|
32
|
+
entries[commandId] = RunnerCommandJournalEntry(
|
|
33
|
+
commandId: commandId,
|
|
34
|
+
command: command.command.rawValue,
|
|
35
|
+
state: .accepted,
|
|
36
|
+
responseOk: nil,
|
|
37
|
+
responseJson: nil,
|
|
38
|
+
error: nil
|
|
39
|
+
)
|
|
40
|
+
order.removeAll { $0 == commandId }
|
|
41
|
+
order.append(commandId)
|
|
42
|
+
pruneIfNeeded()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func start(command: Command) {
|
|
46
|
+
update(command: command, state: .started, responseOk: nil, responseJson: nil, error: nil)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func finish(command: Command, response: Response) {
|
|
50
|
+
update(
|
|
51
|
+
command: command,
|
|
52
|
+
state: response.ok ? .completed : .failed,
|
|
53
|
+
responseOk: response.ok,
|
|
54
|
+
responseJson: encodeResponseJson(command: command, response: response),
|
|
55
|
+
error: response.error
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func fail(command: Command, error: Error) {
|
|
60
|
+
update(
|
|
61
|
+
command: command,
|
|
62
|
+
state: .failed,
|
|
63
|
+
responseOk: nil,
|
|
64
|
+
responseJson: nil,
|
|
65
|
+
error: ErrorPayload(message: error.localizedDescription)
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func status(commandId: String) -> DataPayload {
|
|
70
|
+
guard let normalized = normalizedCommandId(commandId) else {
|
|
71
|
+
return DataPayload(lifecycleState: RunnerCommandLifecycleState.notAccepted.rawValue)
|
|
72
|
+
}
|
|
73
|
+
lock.lock()
|
|
74
|
+
let entry = entries[normalized]
|
|
75
|
+
lock.unlock()
|
|
76
|
+
guard let entry else {
|
|
77
|
+
return DataPayload(
|
|
78
|
+
commandId: normalized,
|
|
79
|
+
lifecycleState: RunnerCommandLifecycleState.notAccepted.rawValue
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
return DataPayload(
|
|
83
|
+
commandId: entry.commandId,
|
|
84
|
+
lifecycleState: entry.state.rawValue,
|
|
85
|
+
lifecycleCommand: entry.command,
|
|
86
|
+
lifecycleResponseOk: entry.responseOk,
|
|
87
|
+
lifecycleResponseJson: entry.responseJson,
|
|
88
|
+
lifecycleErrorCode: entry.error?.code,
|
|
89
|
+
lifecycleErrorMessage: entry.error?.message,
|
|
90
|
+
lifecycleErrorHint: entry.error?.hint
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private func update(
|
|
95
|
+
command: Command,
|
|
96
|
+
state: RunnerCommandLifecycleState,
|
|
97
|
+
responseOk: Bool?,
|
|
98
|
+
responseJson: String?,
|
|
99
|
+
error: ErrorPayload?
|
|
100
|
+
) {
|
|
101
|
+
guard let commandId = normalizedCommandId(command.commandId) else { return }
|
|
102
|
+
lock.lock()
|
|
103
|
+
defer { lock.unlock() }
|
|
104
|
+
var entry = entries[commandId] ?? RunnerCommandJournalEntry(
|
|
105
|
+
commandId: commandId,
|
|
106
|
+
command: command.command.rawValue,
|
|
107
|
+
state: .accepted,
|
|
108
|
+
responseOk: nil,
|
|
109
|
+
responseJson: nil,
|
|
110
|
+
error: nil
|
|
111
|
+
)
|
|
112
|
+
entry.state = state
|
|
113
|
+
entry.responseOk = responseOk
|
|
114
|
+
entry.responseJson = responseJson
|
|
115
|
+
entry.error = error
|
|
116
|
+
entries[commandId] = entry
|
|
117
|
+
order.removeAll { $0 == commandId }
|
|
118
|
+
order.append(commandId)
|
|
119
|
+
pruneIfNeeded()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private func pruneIfNeeded() {
|
|
123
|
+
while order.count > maxEntries {
|
|
124
|
+
let removed = order.removeFirst()
|
|
125
|
+
entries.removeValue(forKey: removed)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private func normalizedCommandId(_ value: String?) -> String? {
|
|
130
|
+
guard let value else { return nil }
|
|
131
|
+
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
132
|
+
return trimmed.isEmpty ? nil : trimmed
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private func encodeResponseJson(command: Command, response: Response) -> String? {
|
|
136
|
+
guard shouldRetainResponseJson(command: command) else { return nil }
|
|
137
|
+
guard let data = try? JSONEncoder().encode(response) else { return nil }
|
|
138
|
+
guard data.count <= maxResponseJsonBytes else { return nil }
|
|
139
|
+
return String(data: data, encoding: .utf8)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private func shouldRetainResponseJson(command: Command) -> Bool {
|
|
143
|
+
switch command.command {
|
|
144
|
+
case .snapshot, .screenshot:
|
|
145
|
+
return false
|
|
146
|
+
case .tap, .mouseClick, .longPress, .drag,
|
|
147
|
+
.remotePress, .type, .swipe, .scroll, .desktopScroll, .findText, .querySelector, .readText, .back,
|
|
148
|
+
.backInApp, .backSystem, .home, .rotate, .appSwitcher, .keyboardDismiss, .keyboardReturn,
|
|
149
|
+
.alert, .pinch, .sequence, .rotateGesture, .transformGesture, .recordStart, .recordStop,
|
|
150
|
+
.status, .uptime, .shutdown:
|
|
151
|
+
return true
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
|
|
3
|
+
struct FlatSnapshotFilterNode {
|
|
4
|
+
let isRoot: Bool
|
|
5
|
+
let label: String
|
|
6
|
+
let identifier: String
|
|
7
|
+
let valueText: String?
|
|
8
|
+
let visible: Bool
|
|
9
|
+
|
|
10
|
+
func matchesScope(_ scope: String) -> Bool {
|
|
11
|
+
let haystack = [label, identifier, valueText ?? ""].joined(separator: "\n")
|
|
12
|
+
return haystack.localizedCaseInsensitiveContains(scope)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
struct FlatSnapshotFilterDecision {
|
|
17
|
+
let include: Bool
|
|
18
|
+
let insideMatchedScope: Bool
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
extension RunnerTests {
|
|
22
|
+
func flatSnapshotFilterDecision(
|
|
23
|
+
_ node: FlatSnapshotFilterNode,
|
|
24
|
+
options: SnapshotOptions,
|
|
25
|
+
insideMatchedScope: Bool
|
|
26
|
+
) -> FlatSnapshotFilterDecision {
|
|
27
|
+
let scope = options.scope?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
28
|
+
let scopeActive = scope?.isEmpty == false
|
|
29
|
+
let matchesScope: Bool
|
|
30
|
+
if scopeActive, let scope {
|
|
31
|
+
matchesScope = node.matchesScope(scope)
|
|
32
|
+
} else {
|
|
33
|
+
matchesScope = false
|
|
34
|
+
}
|
|
35
|
+
let nowInsideScope = insideMatchedScope || matchesScope
|
|
36
|
+
|
|
37
|
+
let include: Bool
|
|
38
|
+
if node.isRoot {
|
|
39
|
+
include = true
|
|
40
|
+
} else if scopeActive && !nowInsideScope {
|
|
41
|
+
include = false
|
|
42
|
+
} else if options.interactiveOnly && !node.visible {
|
|
43
|
+
include = false
|
|
44
|
+
} else {
|
|
45
|
+
include = true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return FlatSnapshotFilterDecision(include: include, insideMatchedScope: nowInsideScope)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func privateAXInteractiveCandidate(rawElementType: Int) -> Bool {
|
|
52
|
+
guard let type = flatSnapshotElementType(rawElementType: rawElementType) else {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
return interactiveTypes.contains(type) || Self.scrollContainerTypes.contains(type)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func flatSnapshotElementType(rawElementType: Int) -> XCUIElement.ElementType? {
|
|
59
|
+
guard let raw = UInt(exactly: rawElementType),
|
|
60
|
+
let type = XCUIElement.ElementType(rawValue: raw)
|
|
61
|
+
else {
|
|
62
|
+
return nil
|
|
63
|
+
}
|
|
64
|
+
return type
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
}
|
|
@@ -36,34 +36,21 @@ extension RunnerTests {
|
|
|
36
36
|
let y2: Double
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
struct SynthesizedDragPlan {
|
|
40
|
+
let points: DragPoints
|
|
41
|
+
let context: SynthesizedCoordinateContext
|
|
42
|
+
|
|
43
|
+
var referenceFrame: CGRect {
|
|
44
|
+
context.referenceFrame
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
39
48
|
struct SelectorElementMatch {
|
|
40
49
|
let element: XCUIElement?
|
|
41
50
|
let isAmbiguous: Bool
|
|
42
51
|
let usedNonHittableFallback: Bool
|
|
43
52
|
}
|
|
44
53
|
|
|
45
|
-
// MARK: - Navigation Gestures
|
|
46
|
-
|
|
47
|
-
func tapInAppBackControl(app: XCUIApplication) -> Bool {
|
|
48
|
-
#if os(macOS)
|
|
49
|
-
if let back = macOSNavigationBackElement(app: app) {
|
|
50
|
-
tapElementCenter(app: app, element: back)
|
|
51
|
-
return true
|
|
52
|
-
}
|
|
53
|
-
return false
|
|
54
|
-
#elseif os(tvOS)
|
|
55
|
-
_ = pressTvRemote(.menu)
|
|
56
|
-
return true
|
|
57
|
-
#else
|
|
58
|
-
let buttons = app.navigationBars.buttons.allElementsBoundByIndex
|
|
59
|
-
if let back = buttons.first(where: { $0.isHittable }) {
|
|
60
|
-
back.tap()
|
|
61
|
-
return true
|
|
62
|
-
}
|
|
63
|
-
return false
|
|
64
|
-
#endif
|
|
65
|
-
}
|
|
66
|
-
|
|
67
54
|
func performBackGesture(app: XCUIApplication) {
|
|
68
55
|
if pressTvRemote(.menu) {
|
|
69
56
|
return
|
|
@@ -200,16 +187,32 @@ extension RunnerTests {
|
|
|
200
187
|
)
|
|
201
188
|
}
|
|
202
189
|
|
|
190
|
+
// Maestro-compat gate for the non-hittable coordinate fallback: an element
|
|
191
|
+
// with no frame at all cannot be coordinate-tapped, otherwise the decision
|
|
192
|
+
// is the shared TapPointPolicy center-in-frame rule (golden parity table
|
|
193
|
+
// with the TS twin). app.frame is the frame source here — replay taps
|
|
194
|
+
// resolved bounds Maestro-style, so the union frame is intentional.
|
|
203
195
|
private func hasTappableFrame(app: XCUIApplication, element: XCUIElement) -> Bool {
|
|
204
196
|
let frame = element.frame
|
|
205
197
|
if frame.isEmpty {
|
|
206
198
|
return false
|
|
207
199
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
200
|
+
return TapPointPolicy.isAllowed(elementFrame: frame, windowFrame: app.frame)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// The tappable on-screen viewport. app.frame is unsuitable: it unions
|
|
204
|
+
// transformed subtrees, so a closed drawer at negative x inflates it and
|
|
205
|
+
// out-of-window coordinates still pass containment. Falls back to app.frame
|
|
206
|
+
// when no window frame is readable.
|
|
207
|
+
func onScreenWindowFrame(app: XCUIApplication) -> CGRect {
|
|
208
|
+
let window = app.windows.element(boundBy: 0)
|
|
209
|
+
if window.exists {
|
|
210
|
+
let frame = window.frame
|
|
211
|
+
if !frame.isEmpty {
|
|
212
|
+
return frame
|
|
213
|
+
}
|
|
211
214
|
}
|
|
212
|
-
return
|
|
215
|
+
return app.frame
|
|
213
216
|
}
|
|
214
217
|
|
|
215
218
|
func queryElement(app: XCUIApplication, selectorKey: String, selectorValue: String) -> Response {
|
|
@@ -771,11 +774,24 @@ extension RunnerTests {
|
|
|
771
774
|
y: Double,
|
|
772
775
|
x2: Double,
|
|
773
776
|
y2: Double,
|
|
774
|
-
durationMs: Double
|
|
777
|
+
durationMs: Double,
|
|
778
|
+
context: SynthesizedCoordinateContext? = nil
|
|
775
779
|
) -> RunnerInteractionOutcome {
|
|
776
780
|
#if os(iOS)
|
|
781
|
+
guard x.isFinite, y.isFinite, x2.isFinite, y2.isFinite else {
|
|
782
|
+
return .unsupported(
|
|
783
|
+
message: "synthesized coordinate drag requires finite coordinates",
|
|
784
|
+
hint: "Retry with finite x, y, x2, and y2 values."
|
|
785
|
+
)
|
|
786
|
+
}
|
|
777
787
|
let orientation = Int(RunnerSynthesizedGesture.interfaceOrientation(forApplication: app))
|
|
778
|
-
let
|
|
788
|
+
guard let context = context ?? synthesizedCoordinateContext(policy: synthesizedGesturePolicy(.synthesizedDrag)) else {
|
|
789
|
+
return .unsupported(
|
|
790
|
+
message: "synthesized coordinate drag could not resolve a finite screen frame",
|
|
791
|
+
hint: "Retry after the app is foregrounded, or use a plain screenshot to choose coordinates."
|
|
792
|
+
)
|
|
793
|
+
}
|
|
794
|
+
let frame = context.referenceFrame
|
|
779
795
|
let start = nativeSynthesizedPoint(orientedX: x, orientedY: y, in: frame, interfaceOrientation: orientation)
|
|
780
796
|
let end = nativeSynthesizedPoint(orientedX: x2, orientedY: y2, in: frame, interfaceOrientation: orientation)
|
|
781
797
|
if let message = RunnerSynthesizedGesture.synthesizeSwipe(
|
|
@@ -788,7 +804,7 @@ extension RunnerTests {
|
|
|
788
804
|
) {
|
|
789
805
|
return .unsupported(
|
|
790
806
|
message: message,
|
|
791
|
-
hint: "
|
|
807
|
+
hint: "Private XCTest event synthesis is required for AX-free coordinate drag on iOS; update Xcode if this persists."
|
|
792
808
|
)
|
|
793
809
|
}
|
|
794
810
|
return .performed
|
|
@@ -805,10 +821,28 @@ extension RunnerTests {
|
|
|
805
821
|
#endif
|
|
806
822
|
}
|
|
807
823
|
|
|
808
|
-
func synthesizedTapAt(
|
|
824
|
+
func synthesizedTapAt(
|
|
825
|
+
app: XCUIApplication,
|
|
826
|
+
x: Double,
|
|
827
|
+
y: Double,
|
|
828
|
+
context: SynthesizedCoordinateContext? = nil
|
|
829
|
+
) -> RunnerInteractionOutcome {
|
|
809
830
|
#if os(iOS)
|
|
831
|
+
guard x.isFinite, y.isFinite else {
|
|
832
|
+
return .unsupported(
|
|
833
|
+
message: "synthesized coordinate tap requires finite coordinates",
|
|
834
|
+
hint: "Retry with finite x and y values."
|
|
835
|
+
)
|
|
836
|
+
}
|
|
810
837
|
let orientation = Int(RunnerSynthesizedGesture.interfaceOrientation(forApplication: app))
|
|
811
|
-
let
|
|
838
|
+
guard let context = context ?? synthesizedCoordinateContext(policy: synthesizedGesturePolicy(.coordinateTap)) else {
|
|
839
|
+
return .unsupported(
|
|
840
|
+
message: "synthesized coordinate tap could not resolve a finite screen frame",
|
|
841
|
+
hint: "Retry after the app is foregrounded, or use a plain screenshot to choose coordinates."
|
|
842
|
+
)
|
|
843
|
+
}
|
|
844
|
+
let frame = context.referenceFrame
|
|
845
|
+
let point = nativeSynthesizedPoint(orientedX: x, orientedY: y, in: frame, interfaceOrientation: orientation)
|
|
812
846
|
if let message = RunnerSynthesizedGesture.synthesizeTap(
|
|
813
847
|
withApplication: app,
|
|
814
848
|
x: Double(point.x),
|
|
@@ -978,6 +1012,114 @@ extension RunnerTests {
|
|
|
978
1012
|
#endif
|
|
979
1013
|
}
|
|
980
1014
|
|
|
1015
|
+
func axFreeSynthesizedDragPlan(
|
|
1016
|
+
app: XCUIApplication,
|
|
1017
|
+
x: Double,
|
|
1018
|
+
y: Double,
|
|
1019
|
+
x2: Double,
|
|
1020
|
+
y2: Double,
|
|
1021
|
+
context: SynthesizedCoordinateContext? = nil
|
|
1022
|
+
) -> SynthesizedDragPlan? {
|
|
1023
|
+
#if os(iOS)
|
|
1024
|
+
let context = context ?? synthesizedCoordinateContext(policy: synthesizedGesturePolicy(.synthesizedDrag))
|
|
1025
|
+
guard x.isFinite, y.isFinite, x2.isFinite, y2.isFinite,
|
|
1026
|
+
let context
|
|
1027
|
+
else {
|
|
1028
|
+
return nil
|
|
1029
|
+
}
|
|
1030
|
+
let points = keyboardAvoidingSynthesizedDragPoints(
|
|
1031
|
+
app: app,
|
|
1032
|
+
x: x,
|
|
1033
|
+
y: y,
|
|
1034
|
+
x2: x2,
|
|
1035
|
+
y2: y2,
|
|
1036
|
+
context: context
|
|
1037
|
+
)
|
|
1038
|
+
return SynthesizedDragPlan(
|
|
1039
|
+
points: points,
|
|
1040
|
+
context: context
|
|
1041
|
+
)
|
|
1042
|
+
#else
|
|
1043
|
+
return nil
|
|
1044
|
+
#endif
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
func axFreeDragVisualizationFrame(
|
|
1048
|
+
x: Double,
|
|
1049
|
+
y: Double,
|
|
1050
|
+
x2: Double,
|
|
1051
|
+
y2: Double,
|
|
1052
|
+
referenceFrame: CGRect
|
|
1053
|
+
) -> DragVisualizationFrame {
|
|
1054
|
+
return DragVisualizationFrame(
|
|
1055
|
+
x: x,
|
|
1056
|
+
y: y,
|
|
1057
|
+
x2: x2,
|
|
1058
|
+
y2: y2,
|
|
1059
|
+
referenceWidth: Double(referenceFrame.width),
|
|
1060
|
+
referenceHeight: Double(referenceFrame.height)
|
|
1061
|
+
)
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
func synthesizedCoordinateContext(policy: SynthesizedGesturePolicy) -> SynthesizedCoordinateContext? {
|
|
1065
|
+
#if os(iOS)
|
|
1066
|
+
let health = runnerAccessibilityHealth
|
|
1067
|
+
guard let referenceFrame = synthesizedScreenshotReferenceFrame(
|
|
1068
|
+
screenshotSize: { XCUIScreen.main.screenshot().image.size }
|
|
1069
|
+
) else {
|
|
1070
|
+
return nil
|
|
1071
|
+
}
|
|
1072
|
+
return SynthesizedCoordinateContext(
|
|
1073
|
+
referenceFrame: referenceFrame,
|
|
1074
|
+
keyboardPolicy: policy.keyboardPolicy,
|
|
1075
|
+
fallbackPolicy: policy.fallbackPolicy,
|
|
1076
|
+
accessibilityHealth: health
|
|
1077
|
+
)
|
|
1078
|
+
#else
|
|
1079
|
+
return nil
|
|
1080
|
+
#endif
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
func synthesizedScreenshotReferenceFrame(screenshotSize: () -> CGSize) -> CGRect? {
|
|
1084
|
+
let screenshotSize = screenshotSize()
|
|
1085
|
+
guard screenshotSize.width.isFinite, screenshotSize.height.isFinite,
|
|
1086
|
+
screenshotSize.width > 0,
|
|
1087
|
+
screenshotSize.height > 0
|
|
1088
|
+
else {
|
|
1089
|
+
return nil
|
|
1090
|
+
}
|
|
1091
|
+
return CGRect(x: 0, y: 0, width: screenshotSize.width, height: screenshotSize.height)
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
func synthesizedFrameAvoidingKeyboardWhenAllowed(
|
|
1095
|
+
app: XCUIApplication,
|
|
1096
|
+
context: SynthesizedCoordinateContext
|
|
1097
|
+
) -> CGRect {
|
|
1098
|
+
#if os(iOS)
|
|
1099
|
+
guard context.allowsKeyboardProbe else { return context.referenceFrame }
|
|
1100
|
+
return frameAvoidingKeyboard(app: app, frame: context.referenceFrame)
|
|
1101
|
+
#else
|
|
1102
|
+
return context.referenceFrame
|
|
1103
|
+
#endif
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
func keyboardAvoidingSynthesizedDragPoints(
|
|
1107
|
+
app: XCUIApplication,
|
|
1108
|
+
x: Double,
|
|
1109
|
+
y: Double,
|
|
1110
|
+
x2: Double,
|
|
1111
|
+
y2: Double,
|
|
1112
|
+
context: SynthesizedCoordinateContext
|
|
1113
|
+
) -> DragPoints {
|
|
1114
|
+
#if os(iOS)
|
|
1115
|
+
guard context.allowsKeyboardProbe else {
|
|
1116
|
+
return DragPoints(x: x, y: y, x2: x2, y2: y2)
|
|
1117
|
+
}
|
|
1118
|
+
return keyboardAvoidingDragPoints(app: app, x: x, y: y, x2: x2, y2: y2)
|
|
1119
|
+
#else
|
|
1120
|
+
return DragPoints(x: x, y: y, x2: x2, y2: y2)
|
|
1121
|
+
#endif
|
|
1122
|
+
}
|
|
981
1123
|
|
|
982
1124
|
func swipe(app: XCUIApplication, direction: String) -> DragVisualizationFrame? {
|
|
983
1125
|
if performTvRemoteSwipeIfAvailable(direction: direction) {
|
|
@@ -1211,95 +1353,4 @@ extension RunnerTests {
|
|
|
1211
1353
|
}
|
|
1212
1354
|
#endif
|
|
1213
1355
|
|
|
1214
|
-
private func tapElementCenter(app: XCUIApplication, element: XCUIElement) {
|
|
1215
|
-
let frame = element.frame
|
|
1216
|
-
if !frame.isEmpty {
|
|
1217
|
-
_ = tapAt(app: app, x: frame.midX, y: frame.midY)
|
|
1218
|
-
return
|
|
1219
|
-
}
|
|
1220
|
-
#if !os(tvOS)
|
|
1221
|
-
element.tap()
|
|
1222
|
-
#endif
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
private func macOSNavigationBackElement(app: XCUIApplication) -> XCUIElement? {
|
|
1226
|
-
let predicate = NSPredicate(
|
|
1227
|
-
format: "identifier == %@ OR label == %@",
|
|
1228
|
-
"go back",
|
|
1229
|
-
"Back"
|
|
1230
|
-
)
|
|
1231
|
-
let element = app.descendants(matching: .any).matching(predicate).firstMatch
|
|
1232
|
-
return element.exists ? element : nil
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
#if AGENT_DEVICE_RUNNER_UNIT_TESTS
|
|
1236
|
-
// Identity in portrait/unknown, 90° per landscape, 180° upside-down.
|
|
1237
|
-
func testNativeSynthesizedPointRotatesByInterfaceOrientation() {
|
|
1238
|
-
let portrait = CGRect(x: 0, y: 0, width: 834, height: 1210)
|
|
1239
|
-
let landscape = CGRect(x: 0, y: 0, width: 1210, height: 834)
|
|
1240
|
-
let offsetLandscape = CGRect(x: 10, y: 20, width: 1210, height: 834)
|
|
1241
|
-
// (frame, UIInterfaceOrientation, expected native point) for a tap at (170, 268).
|
|
1242
|
-
let cases: [(CGRect, Int, CGPoint)] = [
|
|
1243
|
-
(portrait, RunnerInterfaceOrientation.portrait, CGPoint(x: 170, y: 268)),
|
|
1244
|
-
(landscape, RunnerInterfaceOrientation.landscapeRight, CGPoint(x: 566, y: 170)),
|
|
1245
|
-
(landscape, RunnerInterfaceOrientation.landscapeLeft, CGPoint(x: 268, y: 1040)),
|
|
1246
|
-
(portrait, RunnerInterfaceOrientation.portraitUpsideDown, CGPoint(x: 664, y: 942)),
|
|
1247
|
-
(portrait, RunnerInterfaceOrientation.unknown, CGPoint(x: 170, y: 268)),
|
|
1248
|
-
]
|
|
1249
|
-
for (frame, orientation, expected) in cases {
|
|
1250
|
-
XCTAssertEqual(
|
|
1251
|
-
nativeSynthesizedPoint(orientedX: 170, orientedY: 268, in: frame, interfaceOrientation: orientation),
|
|
1252
|
-
expected,
|
|
1253
|
-
"interfaceOrientation \(orientation)"
|
|
1254
|
-
)
|
|
1255
|
-
}
|
|
1256
|
-
XCTAssertEqual(
|
|
1257
|
-
nativeSynthesizedPoint(
|
|
1258
|
-
orientedX: 180,
|
|
1259
|
-
orientedY: 288,
|
|
1260
|
-
in: offsetLandscape,
|
|
1261
|
-
interfaceOrientation: RunnerInterfaceOrientation.landscapeLeft
|
|
1262
|
-
),
|
|
1263
|
-
CGPoint(x: 268, y: 1040),
|
|
1264
|
-
"non-zero frame origin is localized before rotation"
|
|
1265
|
-
)
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
func testNativeSynthesizedVectorRotatesByInterfaceOrientation() {
|
|
1269
|
-
let cases: [(Int, CGVector)] = [
|
|
1270
|
-
(RunnerInterfaceOrientation.portrait, CGVector(dx: 40, dy: -20)),
|
|
1271
|
-
(RunnerInterfaceOrientation.landscapeRight, CGVector(dx: 20, dy: 40)),
|
|
1272
|
-
(RunnerInterfaceOrientation.landscapeLeft, CGVector(dx: -20, dy: -40)),
|
|
1273
|
-
(RunnerInterfaceOrientation.portraitUpsideDown, CGVector(dx: -40, dy: 20)),
|
|
1274
|
-
(RunnerInterfaceOrientation.unknown, CGVector(dx: 40, dy: -20)),
|
|
1275
|
-
]
|
|
1276
|
-
for (orientation, expected) in cases {
|
|
1277
|
-
let vector = nativeSynthesizedVector(orientedDx: 40, orientedDy: -20, interfaceOrientation: orientation)
|
|
1278
|
-
XCTAssertEqual(vector.dx, expected.dx, "dx interfaceOrientation \(orientation)")
|
|
1279
|
-
XCTAssertEqual(vector.dy, expected.dy, "dy interfaceOrientation \(orientation)")
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
func testDesktopScrollWheelDeltasMapDirections() throws {
|
|
1284
|
-
XCTAssertEqual(try XCTUnwrap(desktopScrollWheelDeltas(direction: "up", pixels: 120)).vertical, 120)
|
|
1285
|
-
XCTAssertEqual(try XCTUnwrap(desktopScrollWheelDeltas(direction: "down", pixels: 120)).vertical, -120)
|
|
1286
|
-
XCTAssertEqual(try XCTUnwrap(desktopScrollWheelDeltas(direction: "left", pixels: 120)).horizontal, 120)
|
|
1287
|
-
XCTAssertEqual(try XCTUnwrap(desktopScrollWheelDeltas(direction: "right", pixels: 120)).horizontal, -120)
|
|
1288
|
-
XCTAssertNil(desktopScrollWheelDeltas(direction: "diagonal", pixels: 120))
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
func testDesktopScrollWheelDeltaEventsHonorDurationAndPreservePixels() throws {
|
|
1292
|
-
let events = try XCTUnwrap(desktopScrollWheelDeltaEvents(direction: "down", pixels: 200, durationMs: 50))
|
|
1293
|
-
XCTAssertEqual(events.count, 4)
|
|
1294
|
-
XCTAssertEqual(events.map(\.vertical).reduce(0, +), -200)
|
|
1295
|
-
XCTAssertEqual(events.map(\.horizontal).reduce(0, +), 0)
|
|
1296
|
-
XCTAssertEqual(desktopScrollEventIntervalSeconds(durationMs: 50, eventCount: events.count), 0.05 / 3.0)
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
func testDesktopScrollWheelDeltaEventsKeepInstantScrollSingleEvent() throws {
|
|
1300
|
-
let events = try XCTUnwrap(desktopScrollWheelDeltaEvents(direction: "down", pixels: 200, durationMs: 0))
|
|
1301
|
-
XCTAssertEqual(events.count, 1)
|
|
1302
|
-
XCTAssertEqual(events.first?.vertical, -200)
|
|
1303
|
-
}
|
|
1304
|
-
#endif
|
|
1305
1356
|
}
|