agent-device 0.20.6 → 0.20.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/android/ime-helper/dist/{agent-device-android-ime-helper-0.20.6.apk → agent-device-android-ime-helper-0.20.7.apk} +0 -0
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.7.apk.sha256 +1 -0
- package/android/ime-helper/dist/{agent-device-android-ime-helper-0.20.6.manifest.json → agent-device-android-ime-helper-0.20.7.manifest.json} +4 -4
- package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.20.6.apk → agent-device-android-snapshot-helper-0.20.7.apk} +0 -0
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.7.apk.sha256 +1 -0
- package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.20.6.manifest.json → agent-device-android-snapshot-helper-0.20.7.manifest.json} +6 -6
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +20 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +67 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +339 -4
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSelectorMatchPolicy.swift +78 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +31 -2
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +29 -7
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CoordinateTextEntryTests.swift +4 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +36 -28
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +7 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SelectorMatchPolicyTests.swift +4 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +2 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +100 -12
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +4 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedTextEntry.swift +57 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +1 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextTyping.swift +18 -3
- package/dist/src/agent-browser-tool.js +1 -1
- package/dist/src/agent-device-client.js +2 -2
- package/dist/src/android-adb.js +1 -1
- package/dist/src/android.js +1 -1
- package/dist/src/app-helpers.js +1 -1
- package/dist/src/app-lifecycle.js +7 -7
- package/dist/src/app-lifecycle2.js +2 -0
- package/dist/src/app-log-files.js +1 -0
- package/dist/src/app-log-reconnect.js +1 -0
- package/dist/src/app-log-runtime.js +4 -0
- package/dist/src/app-resolution.js +2 -1
- package/dist/src/apps.js +5 -5
- package/dist/src/audio-probe.js +1 -1
- package/dist/src/auth-session.js +1 -0
- package/dist/src/auth.js +2 -0
- package/dist/src/boot-diagnostics.js +2 -2
- package/dist/src/cli-config.js +1 -1
- package/dist/src/cli-grammar.js +1 -0
- package/dist/src/cli-help.js +199 -259
- package/dist/src/cli-output.js +1 -0
- package/dist/src/cli-runner.js +1 -2
- package/dist/src/cli.js +6 -16
- package/dist/src/client-metro.js +1 -1
- package/dist/src/cloud-artifacts.d.ts +2 -137
- package/dist/src/command-schema.js +3 -3
- package/dist/src/connection-runtime.js +1 -0
- package/dist/src/connection.js +5 -0
- package/dist/src/context.js +1 -1
- package/dist/src/daemon-shutdown-report.js +1 -0
- package/dist/src/daemon.js +2 -0
- package/dist/src/debug-symbols.js +3 -3
- package/dist/src/device-claim-inspection.js +1 -0
- package/dist/src/device-input-state.js +2 -2
- package/dist/src/device.js +1 -1
- package/dist/src/device2.js +2 -0
- package/dist/src/find.js +1 -1
- package/dist/src/finders.js +1 -1
- package/dist/src/generic-settle.js +1 -1
- package/dist/src/generic.js +1 -1
- package/dist/src/harmonyos.js +1 -0
- package/dist/src/helper.js +1 -1
- package/dist/src/host-process.js +2 -2
- package/dist/src/ime-lifecycle.js +7 -7
- package/dist/src/index.d.ts +43 -12
- package/dist/src/input-actions.js +4 -4
- package/dist/src/input-actions2.js +1 -1
- package/dist/src/input-actions3.js +1 -0
- package/dist/src/install-artifact.js +1 -5
- package/dist/src/install-artifact2.js +1 -1
- package/dist/src/install-source2.js +1 -1
- package/dist/src/interaction-runtime.js +1 -1
- package/dist/src/interaction-snapshot.js +1 -1
- package/dist/src/interaction-touch-reference-frame.js +1 -1
- package/dist/src/interaction.js +1 -1
- package/dist/src/interactor.js +2 -2
- package/dist/src/interactor2.js +1 -1
- package/dist/src/internal/bin.js +2 -2
- package/dist/src/internal/daemon.js +8 -15
- package/dist/src/inventory.js +5 -0
- package/dist/src/inventory2.js +1 -0
- package/dist/src/inventory3.js +2 -0
- package/dist/src/inventory4.js +1 -0
- package/dist/src/inventory5.js +1 -0
- package/dist/src/inventory6.js +1 -0
- package/dist/src/lease-context.js +1 -1
- package/dist/src/lease.js +1 -1
- package/dist/src/limrun-runtime-dependencies.js +1 -0
- package/dist/src/limrun.d.ts +10 -125
- package/dist/src/limrun.js +1 -1
- package/dist/src/linux.js +1 -1
- package/dist/src/logcat.js +1 -1
- package/dist/src/manifest.js +1 -1
- package/dist/src/owner-identity.js +1 -0
- package/dist/src/physical-device-control.js +1 -1
- package/dist/src/platform-runtime-app-log-android-transport.js +1 -0
- package/dist/src/platform-runtime-app-log-process.js +1 -0
- package/dist/src/platform-runtime-host.js +1 -0
- package/dist/src/platform-runtime-network-web-transport.js +1 -0
- package/dist/src/platform-runtime-operation-host.js +5 -0
- package/dist/src/platform-runtime-toolchain-host.js +1 -0
- package/dist/src/platform-runtime.js +1 -0
- package/dist/src/prepare-kind.js +15 -0
- package/dist/src/process-entry.js +1 -0
- package/dist/src/process-lock.js +1 -1
- package/dist/src/provider-webdriver.js +1 -0
- package/dist/src/provider.js +1 -0
- package/dist/src/proxy.js +2 -0
- package/dist/src/react-native.js +1 -1
- package/dist/src/record-trace.js +1 -1
- package/dist/src/registry.js +24 -24
- package/dist/src/remote-config2.js +1 -1
- package/dist/src/replay.js +3 -0
- package/dist/src/reporting.js +3 -3
- package/dist/src/runner-client.js +1 -1
- package/dist/src/runner-disposal.js +1 -1
- package/dist/src/runner-lease.js +1 -1
- package/dist/src/runtime.js +2 -2
- package/dist/src/runtime2.js +4 -0
- package/dist/src/runtime3.js +3 -0
- package/dist/src/runtime4.js +1 -0
- package/dist/src/runtime5.js +1 -0
- package/dist/src/runtime6.js +1 -0
- package/dist/src/runtime7.js +1 -0
- package/dist/src/screenshot-result.js +4 -4
- package/dist/src/screenshot.js +1 -0
- package/dist/src/sdk-android-adb.js +2 -2
- package/dist/src/sdk-batch-runner.js +3 -3
- package/dist/src/sdk-contracts.d.ts +31 -5
- package/dist/src/sdk-contracts.js +1 -1
- package/dist/src/sdk-device.js +1 -1
- package/dist/src/sdk-finders.js +1 -1
- package/dist/src/sdk-remote-config.js +1 -1
- package/dist/src/sdk-selectors.d.ts +280 -4
- package/dist/src/sdk-selectors.js +1 -1
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/selector-vocabulary.js +1 -1
- package/dist/src/selectors.js +1 -1
- package/dist/src/server.js +3 -3
- package/dist/src/session-target-evidence.js +1 -1
- package/dist/src/session.js +10 -9
- package/dist/src/shared.js +2 -0
- package/dist/src/shell-quote.js +1 -0
- package/dist/src/simulator.js +1 -1
- package/dist/src/snapshot-runtime.js +1 -0
- package/dist/src/snapshot-session.js +1 -0
- package/dist/src/snapshot.js +1 -1
- package/dist/src/snapshot2.js +1 -1
- package/dist/src/src.js +1 -1
- package/dist/src/src2.js +2 -0
- package/dist/src/src3.js +1 -0
- package/dist/src/text-surface.js +1 -1
- package/dist/src/timing-safe-equal.js +1 -0
- package/dist/src/tool-provider.js +3 -3
- package/dist/src/tool-provider2.js +1 -1
- package/dist/src/tool-provider3.js +1 -1
- package/dist/src/update-check.js +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/web.js +1 -1
- package/package.json +36 -20
- package/server.json +2 -2
- package/skills/agent-device/SKILL.md +6 -31
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.6.apk.sha256 +0 -1
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.6.apk.sha256 +0 -1
- package/dist/src/app-log-request-scope.js +0 -1
- package/dist/src/devices.js +0 -1
- package/dist/src/devices2.js +0 -1
- package/dist/src/devices3.js +0 -1
- package/dist/src/devices4.js +0 -1
- package/dist/src/provider-limrun-runtime.js +0 -1
- package/dist/src/register-builtins.js +0 -1
- package/dist/src/screenshot-geometry.js +0 -1
- package/dist/src/viewport-dimension.js +0 -15
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
Let your coding agent verify its changes in the running app.
|
|
17
17
|
|
|
18
|
-
`agent-device` lets coding agents inspect, control, and verify apps on iOS, Android, tvOS, Android TV, Amazon Vega OS TV through the Vega Virtual Device (VVD), web, macOS, and Linux. Agents can read token-efficient accessibility snapshots where supported, find elements by ref or selector, run device actions, and save evidence for review. Initial Vega OS support is VVD-only and covers discovery, app lifecycle, and complete TV-remote control; physical Fire TV, capture, and selector backends remain unsupported.
|
|
18
|
+
`agent-device` lets coding agents inspect, control, and verify apps on iOS, Android, HarmonyOS, tvOS, Android TV, Amazon Vega OS TV through the Vega Virtual Device (VVD), web, macOS, and Linux. Agents can read token-efficient accessibility snapshots where supported, find elements by ref or selector, run device actions, and save evidence for review. HarmonyOS support uses HDC and ArkUI `uitest` on connected devices and DevEco emulators; use `capabilities --platform harmonyos` to inspect its evidence-backed command subset. Initial Vega OS support is VVD-only and covers discovery, app lifecycle, and complete TV-remote control; physical Fire TV, capture, and selector backends remain unsupported.
|
|
19
19
|
|
|
20
20
|
Your coding agent or QA tool reads each result and chooses the next command. `agent-device` runs the command and saves evidence when asked.
|
|
21
21
|
|
|
@@ -109,7 +109,7 @@ See [Commands](https://oss.callstack.com/agent-device/docs/commands) for the com
|
|
|
109
109
|
|
|
110
110
|
## How it works
|
|
111
111
|
|
|
112
|
-
`agent-device` keeps device state in sessions. It sends commands to XCTest on iOS and tvOS, ADB and the snapshot helper on Android, Vega CLI/VDA on the Vega Virtual Device, a local helper on macOS, and AT-SPI on Linux.
|
|
112
|
+
`agent-device` keeps device state in sessions. It sends commands to XCTest on iOS and tvOS, ADB and the snapshot helper on Android, HDC and ArkUI `uitest` on HarmonyOS, Vega CLI/VDA on the Vega Virtual Device, a local helper on macOS, and AT-SPI on Linux.
|
|
113
113
|
|
|
114
114
|
Node.js apps can use the typed client or public subpaths. `agent-device/android-adb` provides the Android ADB provider interface, helpers for logcat, the clipboard, the keyboard, and apps, and port reverse management.
|
|
115
115
|
|
|
@@ -117,7 +117,7 @@ Node.js apps can use the typed client or public subpaths. `agent-device/android-
|
|
|
117
117
|
|
|
118
118
|
### What is agent-device?
|
|
119
119
|
|
|
120
|
-
`agent-device` is a command-line tool that lets coding agents inspect, control, and verify apps and save evidence for review. It supports iOS, Android, TV, web, macOS, and Linux.
|
|
120
|
+
`agent-device` is a command-line tool that lets coding agents inspect, control, and verify apps and save evidence for review. It supports iOS, Android, HarmonyOS, TV, web, macOS, and Linux.
|
|
121
121
|
|
|
122
122
|
### Does it work with React Native, Expo, Flutter, and native apps?
|
|
123
123
|
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
7311d6a527bdf8be33b752e59e73f4a527585654f8d530d3680ab15ccf271cb0 agent-device-android-ime-helper-0.20.7.apk
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "android-ime-helper",
|
|
3
|
-
"version": "0.20.
|
|
4
|
-
"assetName": "agent-device-android-ime-helper-0.20.
|
|
5
|
-
"sha256": "
|
|
3
|
+
"version": "0.20.7",
|
|
4
|
+
"assetName": "agent-device-android-ime-helper-0.20.7.apk",
|
|
5
|
+
"sha256": "7311d6a527bdf8be33b752e59e73f4a527585654f8d530d3680ab15ccf271cb0",
|
|
6
6
|
"packageName": "com.callstack.agentdevice.imehelper",
|
|
7
|
-
"versionCode":
|
|
7
|
+
"versionCode": 20007,
|
|
8
8
|
"serviceComponent": "com.callstack.agentdevice.imehelper/.TestInputMethodService",
|
|
9
9
|
"broadcastProtocol": "android-ime-helper-v1"
|
|
10
10
|
}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
c45f72eea93c19b4a17fedb4b25f880d5e225c9ab41be4017db8613fdd276e09 agent-device-android-snapshot-helper-0.20.7.apk
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "android-snapshot-helper",
|
|
3
|
-
"version": "0.20.
|
|
4
|
-
"releaseTag": "v0.20.
|
|
5
|
-
"assetName": "agent-device-android-snapshot-helper-0.20.
|
|
3
|
+
"version": "0.20.7",
|
|
4
|
+
"releaseTag": "v0.20.7",
|
|
5
|
+
"assetName": "agent-device-android-snapshot-helper-0.20.7.apk",
|
|
6
6
|
"apkUrl": null,
|
|
7
|
-
"sha256": "
|
|
8
|
-
"checksumName": "agent-device-android-snapshot-helper-0.20.
|
|
7
|
+
"sha256": "c45f72eea93c19b4a17fedb4b25f880d5e225c9ab41be4017db8613fdd276e09",
|
|
8
|
+
"checksumName": "agent-device-android-snapshot-helper-0.20.7.apk.sha256",
|
|
9
9
|
"packageName": "com.callstack.agentdevice.snapshothelper",
|
|
10
|
-
"versionCode":
|
|
10
|
+
"versionCode": 20007,
|
|
11
11
|
"instrumentationRunner": "com.callstack.agentdevice.snapshothelper/.SnapshotInstrumentation",
|
|
12
12
|
"minSdk": 23,
|
|
13
13
|
"targetSdk": 36,
|
|
@@ -105,6 +105,26 @@ int main(int argc, const char *argv[]) {
|
|
|
105
105
|
[textField.heightAnchor constraintEqualToConstant:44],
|
|
106
106
|
]];
|
|
107
107
|
}
|
|
108
|
+
|
|
109
|
+
if ([NSProcessInfo.processInfo.arguments containsObject:@"--agent-device-selector-read-regression"]) {
|
|
110
|
+
NSString *const duplicateIdentifier = @"agent-device-selector-read-duplicate";
|
|
111
|
+
|
|
112
|
+
UIButton *visibleButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
|
113
|
+
visibleButton.accessibilityIdentifier = duplicateIdentifier;
|
|
114
|
+
[visibleButton setTitle:@"Readable target" forState:UIControlStateNormal];
|
|
115
|
+
visibleButton.translatesAutoresizingMaskIntoConstraints = NO;
|
|
116
|
+
[self.view addSubview:visibleButton];
|
|
117
|
+
|
|
118
|
+
UILabel *offscreenLabel = [[UILabel alloc] initWithFrame:CGRectMake(-200, -200, 100, 40)];
|
|
119
|
+
offscreenLabel.accessibilityIdentifier = duplicateIdentifier;
|
|
120
|
+
offscreenLabel.text = @"Decorative duplicate";
|
|
121
|
+
[self.view addSubview:offscreenLabel];
|
|
122
|
+
|
|
123
|
+
[NSLayoutConstraint activateConstraints:@[
|
|
124
|
+
[visibleButton.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
|
|
125
|
+
[visibleButton.topAnchor constraintEqualToAnchor:label.bottomAnchor constant:24],
|
|
126
|
+
]];
|
|
127
|
+
}
|
|
108
128
|
#endif
|
|
109
129
|
}
|
|
110
130
|
|
package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#import <Foundation/Foundation.h>
|
|
2
|
+
#import <CoreGraphics/CoreGraphics.h>
|
|
2
3
|
#import <XCTest/XCTest.h>
|
|
3
4
|
|
|
4
5
|
NS_ASSUME_NONNULL_BEGIN
|
|
@@ -11,6 +12,15 @@ FOUNDATION_EXPORT NSString *const RunnerAXSnapshotDeepExtensionNodesAddedKey;
|
|
|
11
12
|
FOUNDATION_EXPORT NSString *const RunnerAXSnapshotDeepExtensionPendingKey;
|
|
12
13
|
FOUNDATION_EXPORT NSString *const RunnerAXSnapshotDeepExtensionMissedKey;
|
|
13
14
|
|
|
15
|
+
/// Keys of the `customActions` dictionary in the snapshot response: how many
|
|
16
|
+
/// merged elements were eligible for an action read, and how many the bounded
|
|
17
|
+
/// pass actually reached. Present only when the capture asked for actions.
|
|
18
|
+
FOUNDATION_EXPORT NSString *const RunnerAXSnapshotCustomActionsKey;
|
|
19
|
+
FOUNDATION_EXPORT NSString *const RunnerAXSnapshotCustomActionsReadKey;
|
|
20
|
+
FOUNDATION_EXPORT NSString *const RunnerAXSnapshotCustomActionsCandidatesKey;
|
|
21
|
+
FOUNDATION_EXPORT NSString *const RunnerAXSnapshotCustomActionsTruncatedKey;
|
|
22
|
+
FOUNDATION_EXPORT NSString *const RunnerAXSnapshotCustomActionsBlockedKey;
|
|
23
|
+
|
|
14
24
|
/// A depth-capped childless node awaiting an element-rooted follow-up request.
|
|
15
25
|
/// Public so the runner unit bundle can drive the extension's miss paths with
|
|
16
26
|
/// fabricated snapshots — the executed contract that missed frontiers are
|
|
@@ -29,12 +39,68 @@ FOUNDATION_EXPORT NSString *const RunnerAXSnapshotDeepExtensionMissedKey;
|
|
|
29
39
|
/// not). The response gains a `deepExtension` {calls, nodesAdded,
|
|
30
40
|
/// pendingFrontiers, missedFrontiers} dictionary when any frontier existed and
|
|
31
41
|
/// the call limit allowed extension. Pass 0 to disable extension entirely.
|
|
42
|
+
///
|
|
43
|
+
/// `customActionLimit` bounds the per-element custom-action reads described on
|
|
44
|
+
/// `customActionNamesForElement:axClient:`; pass 0 to skip them entirely.
|
|
32
45
|
+ (NSDictionary<NSString *, id> *)snapshotTreeForApplication:(XCUIApplication *)application
|
|
33
46
|
maxDepth:(NSInteger)maxDepth
|
|
34
47
|
maxNodes:(NSInteger)maxNodes
|
|
35
48
|
deepExtensionCallLimit:(NSInteger)deepExtensionCallLimit
|
|
49
|
+
customActionLimit:(NSInteger)customActionLimit
|
|
36
50
|
deadline:(nullable NSDate *)deadline;
|
|
37
51
|
|
|
52
|
+
/// Names of the element's UIAccessibilityCustomActions, or nil when it has
|
|
53
|
+
/// none.
|
|
54
|
+
///
|
|
55
|
+
/// One element per call on purpose. The bulk snapshot request cannot carry this
|
|
56
|
+
/// attribute: adding it makes testmanagerd's reply decoder reject the nested
|
|
57
|
+
/// arrays a custom action serializes into ("value for key 'NS.objects' was of
|
|
58
|
+
/// unexpected class 'NSArray'"), drop the reply, and time the request out after
|
|
59
|
+
/// ~65s instead of the usual ~110ms. Per-element reads use a different XPC
|
|
60
|
+
/// message and answer normally.
|
|
61
|
+
///
|
|
62
|
+
/// Reading is all we can do: the actions are not invocable from the runner.
|
|
63
|
+
/// XCUIElement exposes no custom-action API; XCAXClient_iOS's
|
|
64
|
+
/// performAction:onElement:value: answers kAXErrorCannotComplete for every AX
|
|
65
|
+
/// action number; setAttribute: on the custom-actions attribute reports success
|
|
66
|
+
/// and does nothing; parameterizedAttribute: answers kAXErrorNoValue; and the
|
|
67
|
+
/// raw AXUIElement C API cannot be aimed at app elements because
|
|
68
|
+
/// XCAccessibilityElement is token-based and answers nil for AXUIElement.
|
|
69
|
+
+ (nullable NSArray<NSString *> *)customActionNamesForElement:(id)element
|
|
70
|
+
axClient:(id)axClient
|
|
71
|
+
completed:(BOOL *)completed;
|
|
72
|
+
|
|
73
|
+
/// Bounds one element's contribution to the response (max actions, max name
|
|
74
|
+
/// length). Reports whether anything was clipped so truncation is disclosed
|
|
75
|
+
/// rather than silently presented as a complete list.
|
|
76
|
+
+ (NSArray<NSString *> *)cappedActionNames:(NSArray<NSString *> *)names
|
|
77
|
+
truncated:(BOOL *)truncated;
|
|
78
|
+
|
|
79
|
+
/// The bounded read pass over merged leaves, exposed for the runner unit
|
|
80
|
+
/// bundle: the containment contract (a hung read blocks the pass and is
|
|
81
|
+
/// disclosed, and adds no further dispatches) is only executable from here.
|
|
82
|
+
/// Returns {read, candidates, truncated, blocked}.
|
|
83
|
+
+ (NSDictionary<NSString *, NSNumber *> *)
|
|
84
|
+
annotateCustomActionsOnMergedLeaves:(nullable NSArray<RunnerAXSnapshotFrontier *> *)leaves
|
|
85
|
+
axClient:(id)axClient
|
|
86
|
+
limit:(NSInteger)limit
|
|
87
|
+
rootFrame:(CGRect)rootFrame
|
|
88
|
+
deadline:(nullable NSDate *)deadline;
|
|
89
|
+
|
|
90
|
+
/// Reads dispatched but not yet returned. The AX call cannot be cancelled, so a
|
|
91
|
+
/// read that blew its deadline stays counted here until it finally returns —
|
|
92
|
+
/// which is precisely the state in which further reads must be refused rather
|
|
93
|
+
/// than queued behind it. Consumed by the read pass to disclose the skip.
|
|
94
|
+
+ (NSInteger)customActionReadsInFlight;
|
|
95
|
+
|
|
96
|
+
/// Total reads ever dispatched. The containment invariant is "a blocked capture
|
|
97
|
+
/// adds no work", which is only observable as this counter standing still.
|
|
98
|
+
+ (NSInteger)customActionReadDispatchCount;
|
|
99
|
+
|
|
100
|
+
/// The shared AX client (`XCUIDevice.accessibilityInterface`), or nil when the
|
|
101
|
+
/// private interface is unavailable.
|
|
102
|
+
+ (nullable id)accessibilityClient;
|
|
103
|
+
|
|
38
104
|
+ (NSInteger)processIdentifierForApplication:(XCUIApplication *)application;
|
|
39
105
|
|
|
40
106
|
/// The frontier-extension loop, exposed for the runner unit bundle: a frontier
|
|
@@ -49,6 +115,7 @@ FOUNDATION_EXPORT NSString *const RunnerAXSnapshotDeepExtensionMissedKey;
|
|
|
49
115
|
nodeCount:(NSInteger *)nodeCount
|
|
50
116
|
truncated:(BOOL *)truncated
|
|
51
117
|
callsAllowed:(NSInteger)callsAllowed
|
|
118
|
+
mergedLeaves:(nullable NSMutableArray<RunnerAXSnapshotFrontier *> *)mergedLeaves
|
|
52
119
|
deadline:(nullable NSDate *)deadline;
|
|
53
120
|
|
|
54
121
|
@end
|
package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
#import <CoreGraphics/CoreGraphics.h>
|
|
4
4
|
#import <objc/message.h>
|
|
5
|
+
#import <stdatomic.h>
|
|
5
6
|
|
|
6
7
|
static NSString *const RunnerAXSnapshotOkKey = @"ok";
|
|
7
8
|
static NSString *const RunnerAXSnapshotErrorKey = @"error";
|
|
@@ -14,6 +15,60 @@ NSString *const RunnerAXSnapshotDeepExtensionNodesAddedKey = @"nodesAdded";
|
|
|
14
15
|
NSString *const RunnerAXSnapshotDeepExtensionPendingKey = @"pendingFrontiers";
|
|
15
16
|
NSString *const RunnerAXSnapshotDeepExtensionMissedKey = @"missedFrontiers";
|
|
16
17
|
|
|
18
|
+
NSString *const RunnerAXSnapshotCustomActionsKey = @"customActions";
|
|
19
|
+
NSString *const RunnerAXSnapshotCustomActionsReadKey = @"read";
|
|
20
|
+
NSString *const RunnerAXSnapshotCustomActionsCandidatesKey = @"candidates";
|
|
21
|
+
NSString *const RunnerAXSnapshotCustomActionsTruncatedKey = @"truncated";
|
|
22
|
+
NSString *const RunnerAXSnapshotCustomActionsBlockedKey = @"blocked";
|
|
23
|
+
|
|
24
|
+
/// The AX server's name for UIAccessibilityCustomActions (React Native's
|
|
25
|
+
/// `accessibilityActions` prop lands here too).
|
|
26
|
+
static NSString *const RunnerAXCustomActionsAttribute = @"XC_kAXXCAttributeCustomActions";
|
|
27
|
+
|
|
28
|
+
/// A single read costs ~100ms against a responsive app. This bound exists for
|
|
29
|
+
/// the unresponsive case: without it one wedged element would swallow the whole
|
|
30
|
+
/// capture's budget and starve every remaining candidate, turning a bounded
|
|
31
|
+
/// enrichment into a capture-length stall.
|
|
32
|
+
static const NSTimeInterval RunnerAXCustomActionReadTimeout = 1.0;
|
|
33
|
+
|
|
34
|
+
/// The AX call is a synchronous XPC round trip that cannot be cancelled once
|
|
35
|
+
/// issued, so the deadline above only frees the CALLER — the call itself keeps
|
|
36
|
+
/// running. Containment therefore has two parts, and both are needed:
|
|
37
|
+
///
|
|
38
|
+
/// 1. every read runs on this one serial queue, so a wedged call can never be
|
|
39
|
+
/// joined by a second concurrent user of the shared XCAXClient;
|
|
40
|
+
/// 2. a single-flight guard (below) refuses to dispatch while an abandoned
|
|
41
|
+
/// read is still outstanding, so repeated captures cannot pile up work
|
|
42
|
+
/// behind it.
|
|
43
|
+
///
|
|
44
|
+
/// Reads resume by themselves once the hung call returns (or the runner
|
|
45
|
+
/// restarts); nothing has to be reset by hand.
|
|
46
|
+
static dispatch_queue_t RunnerAXCustomActionReadQueue(void)
|
|
47
|
+
{
|
|
48
|
+
static dispatch_queue_t queue;
|
|
49
|
+
static dispatch_once_t onceToken;
|
|
50
|
+
dispatch_once(&onceToken, ^{
|
|
51
|
+
queue = dispatch_queue_create("com.callstack.agentdevice.runner.ax-custom-actions",
|
|
52
|
+
DISPATCH_QUEUE_SERIAL);
|
|
53
|
+
});
|
|
54
|
+
return queue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Reads dispatched but not yet returned. Non-zero after a read blew its
|
|
58
|
+
/// deadline, which is exactly the condition the single-flight guard refuses on.
|
|
59
|
+
static atomic_int RunnerAXCustomActionReadsInFlight = 0;
|
|
60
|
+
|
|
61
|
+
/// Total dispatches. The invariant the containment fix exists to protect is
|
|
62
|
+
/// "a blocked capture adds no work", which is only observable as this counter
|
|
63
|
+
/// staying put, so it is recorded rather than inferred.
|
|
64
|
+
static atomic_long RunnerAXCustomActionReadDispatches = 0;
|
|
65
|
+
|
|
66
|
+
/// Output caps. The element budget bounds how many elements we read; these
|
|
67
|
+
/// bound what any ONE element can put in the response, so a pathological app
|
|
68
|
+
/// cannot spend the whole snapshot on one node's action list.
|
|
69
|
+
static const NSUInteger RunnerAXCustomActionsMaxPerElement = 8;
|
|
70
|
+
static const NSUInteger RunnerAXCustomActionNameMaxLength = 80;
|
|
71
|
+
|
|
17
72
|
typedef id (*RunnerAXObjectMsgSend)(id, SEL);
|
|
18
73
|
typedef NSInteger (*RunnerAXIntegerMsgSend)(id, SEL);
|
|
19
74
|
typedef id (*RunnerAXSnapshotMsgSend)(id, SEL, id, id, id, NSError **);
|
|
@@ -33,10 +88,11 @@ typedef id (*RunnerAXSnapshotMsgSend)(id, SEL, id, id, id, NSError **);
|
|
|
33
88
|
maxDepth:(NSInteger)maxDepth
|
|
34
89
|
maxNodes:(NSInteger)maxNodes
|
|
35
90
|
deepExtensionCallLimit:(NSInteger)deepExtensionCallLimit
|
|
91
|
+
customActionLimit:(NSInteger)customActionLimit
|
|
36
92
|
deadline:(nullable NSDate *)deadline
|
|
37
93
|
{
|
|
38
94
|
@try {
|
|
39
|
-
id axClient = [self
|
|
95
|
+
id axClient = [self accessibilityClient];
|
|
40
96
|
if (nil == axClient) {
|
|
41
97
|
return [self failure:@"XCUIDevice accessibilityInterface is unavailable"];
|
|
42
98
|
}
|
|
@@ -64,13 +120,16 @@ typedef id (*RunnerAXSnapshotMsgSend)(id, SEL, id, id, id, NSError **);
|
|
|
64
120
|
// zero extension bookkeeping.
|
|
65
121
|
NSMutableArray<RunnerAXSnapshotFrontier *> *candidates =
|
|
66
122
|
deepExtensionCallLimit > 0 ? [NSMutableArray array] : nil;
|
|
123
|
+
NSMutableArray<RunnerAXSnapshotFrontier *> *merged =
|
|
124
|
+
customActionLimit > 0 ? [NSMutableArray array] : nil;
|
|
67
125
|
NSMutableDictionary *rootNode = [self dictionaryForSnapshot:root
|
|
68
126
|
depth:0
|
|
69
127
|
maxDepth:maxDepth
|
|
70
128
|
maxNodes:maxNodes
|
|
71
129
|
nodeCount:&nodeCount
|
|
72
130
|
truncated:&truncated
|
|
73
|
-
frontiers:candidates
|
|
131
|
+
frontiers:candidates
|
|
132
|
+
mergedLeaves:merged];
|
|
74
133
|
if (nil == rootNode) {
|
|
75
134
|
return [self failure:@"AX snapshot root could not be serialized"];
|
|
76
135
|
}
|
|
@@ -85,7 +144,20 @@ typedef id (*RunnerAXSnapshotMsgSend)(id, SEL, id, id, id, NSError **);
|
|
|
85
144
|
nodeCount:&nodeCount
|
|
86
145
|
truncated:&truncated
|
|
87
146
|
callsAllowed:deepExtensionCallLimit
|
|
147
|
+
mergedLeaves:merged
|
|
88
148
|
deadline:deadline];
|
|
149
|
+
CGRect rootFrame = CGRectZero;
|
|
150
|
+
NSDictionary *rootFrameValue = rootNode[@"frame"];
|
|
151
|
+
if ([rootFrameValue isKindOfClass:NSDictionary.class]) {
|
|
152
|
+
rootFrame = CGRectMake(
|
|
153
|
+
[rootFrameValue[@"x"] doubleValue], [rootFrameValue[@"y"] doubleValue],
|
|
154
|
+
[rootFrameValue[@"width"] doubleValue], [rootFrameValue[@"height"] doubleValue]);
|
|
155
|
+
}
|
|
156
|
+
NSDictionary *customActions = [self annotateCustomActionsOnMergedLeaves:merged
|
|
157
|
+
axClient:axClient
|
|
158
|
+
limit:customActionLimit
|
|
159
|
+
rootFrame:rootFrame
|
|
160
|
+
deadline:deadline];
|
|
89
161
|
|
|
90
162
|
NSMutableDictionary *response = [NSMutableDictionary dictionaryWithDictionary:@{
|
|
91
163
|
RunnerAXSnapshotOkKey: @YES,
|
|
@@ -95,6 +167,9 @@ typedef id (*RunnerAXSnapshotMsgSend)(id, SEL, id, id, id, NSError **);
|
|
|
95
167
|
if (nil != deepExtension) {
|
|
96
168
|
response[RunnerAXSnapshotDeepExtensionKey] = deepExtension;
|
|
97
169
|
}
|
|
170
|
+
if (customActions.count > 0) {
|
|
171
|
+
response[RunnerAXSnapshotCustomActionsKey] = customActions;
|
|
172
|
+
}
|
|
98
173
|
return response.copy;
|
|
99
174
|
} @catch (NSException *exception) {
|
|
100
175
|
return [self failure:exception.reason ?: exception.name ?: @"AX snapshot bridge exception"];
|
|
@@ -115,6 +190,7 @@ typedef id (*RunnerAXSnapshotMsgSend)(id, SEL, id, id, id, NSError **);
|
|
|
115
190
|
nodeCount:(NSInteger *)nodeCount
|
|
116
191
|
truncated:(BOOL *)truncated
|
|
117
192
|
callsAllowed:(NSInteger)callsAllowed
|
|
193
|
+
mergedLeaves:(nullable NSMutableArray<RunnerAXSnapshotFrontier *> *)mergedLeaves
|
|
118
194
|
deadline:(nullable NSDate *)deadline
|
|
119
195
|
{
|
|
120
196
|
if (callsAllowed <= 0 || frontiers.count == 0) {
|
|
@@ -167,7 +243,8 @@ typedef id (*RunnerAXSnapshotMsgSend)(id, SEL, id, id, id, NSError **);
|
|
|
167
243
|
maxNodes:maxNodes
|
|
168
244
|
nodeCount:nodeCount
|
|
169
245
|
truncated:truncated
|
|
170
|
-
frontiers:subCandidates
|
|
246
|
+
frontiers:subCandidates
|
|
247
|
+
mergedLeaves:mergedLeaves];
|
|
171
248
|
if (nil != childNode) {
|
|
172
249
|
[children addObject:childNode];
|
|
173
250
|
}
|
|
@@ -306,6 +383,250 @@ typedef id (*RunnerAXSnapshotMsgSend)(id, SEL, id, id, id, NSError **);
|
|
|
306
383
|
return attributes;
|
|
307
384
|
}
|
|
308
385
|
|
|
386
|
+
/// Reads custom actions for the merged leaves in traversal order until the
|
|
387
|
+
/// limit or the capture deadline is spent. Each read is its own AX round trip
|
|
388
|
+
/// (~100ms on an idle simulator), so this is strictly opt-in and strictly
|
|
389
|
+
/// bounded.
|
|
390
|
+
///
|
|
391
|
+
/// Returns {read, candidates} so the response can DISCLOSE an incomplete pass.
|
|
392
|
+
/// An unread element is byte-identical to one that genuinely has no actions, so
|
|
393
|
+
/// silence here would teach a reader that later cards have no affordances —
|
|
394
|
+
/// exactly the mis-inference this capture exists to prevent.
|
|
395
|
+
///
|
|
396
|
+
/// `candidates` counts only elements still eligible at read time: a leaf that
|
|
397
|
+
/// deep extension filled in since serialization is not a merged element, so
|
|
398
|
+
/// counting it would inflate the denominator with nodes that never needed a
|
|
399
|
+
/// read. A leaf whose live element vanished stays counted and unread, because
|
|
400
|
+
/// that one really is an element we failed to answer for.
|
|
401
|
+
///
|
|
402
|
+
/// On-screen candidates are read first. The budget is smaller than a long
|
|
403
|
+
/// feed's card count, so the order decides which elements an agent can act on
|
|
404
|
+
/// at all — and it also makes the disclosure's remedy true: scrolling changes
|
|
405
|
+
/// the on-screen set, so a re-run reads elements the previous pass could not.
|
|
406
|
+
/// Note that `--scope` deliberately is NOT the ordering key: scope is applied
|
|
407
|
+
/// when the Swift walk builds nodes, long after this pass, so it cannot narrow
|
|
408
|
+
/// the read budget without duplicating the scope predicate here.
|
|
409
|
+
+ (NSDictionary<NSString *, NSNumber *> *)
|
|
410
|
+
annotateCustomActionsOnMergedLeaves:(nullable NSArray<RunnerAXSnapshotFrontier *> *)leaves
|
|
411
|
+
axClient:(id)axClient
|
|
412
|
+
limit:(NSInteger)limit
|
|
413
|
+
rootFrame:(CGRect)rootFrame
|
|
414
|
+
deadline:(nullable NSDate *)deadline
|
|
415
|
+
{
|
|
416
|
+
if (limit <= 0 || leaves.count == 0) {
|
|
417
|
+
return @{};
|
|
418
|
+
}
|
|
419
|
+
NSMutableArray<RunnerAXSnapshotFrontier *> *onScreen = [NSMutableArray array];
|
|
420
|
+
NSMutableArray<RunnerAXSnapshotFrontier *> *offScreen = [NSMutableArray array];
|
|
421
|
+
for (RunnerAXSnapshotFrontier *leaf in leaves) {
|
|
422
|
+
if ([leaf.node[@"children"] isKindOfClass:NSArray.class]
|
|
423
|
+
&& [(NSArray *)leaf.node[@"children"] count] > 0) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
[([self isFrameOnScreen:leaf.node[@"frame"] rootFrame:rootFrame] ? onScreen : offScreen)
|
|
427
|
+
addObject:leaf];
|
|
428
|
+
}
|
|
429
|
+
NSInteger candidates = onScreen.count + offScreen.count;
|
|
430
|
+
NSInteger attempts = 0;
|
|
431
|
+
NSInteger reads = 0;
|
|
432
|
+
NSInteger annotated = 0;
|
|
433
|
+
NSInteger truncatedElements = 0;
|
|
434
|
+
BOOL blocked = NO;
|
|
435
|
+
for (RunnerAXSnapshotFrontier *leaf in [onScreen arrayByAddingObjectsFromArray:offScreen]) {
|
|
436
|
+
if (attempts >= limit || (nil != deadline && deadline.timeIntervalSinceNow <= 0)) {
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
// An abandoned read from an earlier capture is still outstanding. Stop the
|
|
440
|
+
// pass here rather than spending a deadline per element on reads that will
|
|
441
|
+
// all be refused, and record why so the response does not present the
|
|
442
|
+
// unread elements as "has no actions".
|
|
443
|
+
if ([self customActionReadsInFlight] > 0) {
|
|
444
|
+
blocked = YES;
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
id element = [self accessibilityElementForSnapshot:leaf.snapshot];
|
|
448
|
+
if (nil == element) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
// The attempt is what the budget buys; only a completed read counts as
|
|
452
|
+
// read, so a timed-out element stays in the undisclosed remainder instead
|
|
453
|
+
// of being reported as "read, and it has no actions".
|
|
454
|
+
attempts += 1;
|
|
455
|
+
BOOL completed = NO;
|
|
456
|
+
NSArray<NSString *> *names = [self customActionNamesForElement:element
|
|
457
|
+
axClient:axClient
|
|
458
|
+
completed:&completed];
|
|
459
|
+
if (!completed) {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
reads += 1;
|
|
463
|
+
BOOL truncated = NO;
|
|
464
|
+
NSArray<NSString *> *capped = [self cappedActionNames:names ?: @[] truncated:&truncated];
|
|
465
|
+
if (truncated) {
|
|
466
|
+
truncatedElements += 1;
|
|
467
|
+
}
|
|
468
|
+
if (capped.count > 0) {
|
|
469
|
+
leaf.node[@"actions"] = capped;
|
|
470
|
+
annotated += 1;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
NSLog(
|
|
474
|
+
@"AGENT_DEVICE_RUNNER_PRIVATE_AX_CUSTOM_ACTIONS reads=%ld attempts=%ld annotated=%ld "
|
|
475
|
+
"candidates=%ld onScreen=%ld truncated=%ld blocked=%d dispatches=%ld",
|
|
476
|
+
(long)reads, (long)attempts, (long)annotated, (long)candidates, (long)onScreen.count,
|
|
477
|
+
(long)truncatedElements, (int)blocked, (long)[self customActionReadDispatchCount]);
|
|
478
|
+
return @{
|
|
479
|
+
RunnerAXSnapshotCustomActionsReadKey: @(reads),
|
|
480
|
+
RunnerAXSnapshotCustomActionsCandidatesKey: @(candidates),
|
|
481
|
+
RunnerAXSnapshotCustomActionsTruncatedKey: @(truncatedElements),
|
|
482
|
+
RunnerAXSnapshotCustomActionsBlockedKey: @(blocked),
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/// An empty root frame means the capture has no viewport to judge against, so
|
|
487
|
+
/// every candidate counts as on-screen and the order stays document order.
|
|
488
|
+
+ (BOOL)isFrameOnScreen:(id)frameValue rootFrame:(CGRect)rootFrame
|
|
489
|
+
{
|
|
490
|
+
if (CGRectIsEmpty(rootFrame)) {
|
|
491
|
+
return YES;
|
|
492
|
+
}
|
|
493
|
+
if (![frameValue isKindOfClass:NSDictionary.class]) {
|
|
494
|
+
return NO;
|
|
495
|
+
}
|
|
496
|
+
NSDictionary *frame = (NSDictionary *)frameValue;
|
|
497
|
+
CGRect rect = CGRectMake(
|
|
498
|
+
[frame[@"x"] doubleValue], [frame[@"y"] doubleValue],
|
|
499
|
+
[frame[@"width"] doubleValue], [frame[@"height"] doubleValue]);
|
|
500
|
+
return !CGRectIsEmpty(rect) && CGRectIntersectsRect(rect, rootFrame);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
+ (nullable NSArray<NSString *> *)customActionNamesForElement:(id)element
|
|
504
|
+
axClient:(id)axClient
|
|
505
|
+
completed:(BOOL *)completed
|
|
506
|
+
{
|
|
507
|
+
if (NULL != completed) {
|
|
508
|
+
*completed = NO;
|
|
509
|
+
}
|
|
510
|
+
if (nil == element || nil == axClient) {
|
|
511
|
+
return nil;
|
|
512
|
+
}
|
|
513
|
+
SEL selector = NSSelectorFromString(@"attributesForElement:attributes:error:");
|
|
514
|
+
if (![axClient respondsToSelector:selector]) {
|
|
515
|
+
return nil;
|
|
516
|
+
}
|
|
517
|
+
// Single flight. An abandoned read still owns the serial queue, so
|
|
518
|
+
// dispatching here would only queue work behind it — and repeating the
|
|
519
|
+
// capture would keep queueing more. Refuse without dispatching instead.
|
|
520
|
+
if (atomic_load(&RunnerAXCustomActionReadsInFlight) > 0) {
|
|
521
|
+
NSLog(@"AGENT_DEVICE_RUNNER_PRIVATE_AX_CUSTOM_ACTIONS_READ_BLOCKED");
|
|
522
|
+
return nil;
|
|
523
|
+
}
|
|
524
|
+
// The AX call is a synchronous XPC round trip with no timeout of its own, so
|
|
525
|
+
// it runs off-thread behind a bounded wait. On timeout the result box is
|
|
526
|
+
// never read, so a late-returning wedged call cannot race this thread.
|
|
527
|
+
atomic_fetch_add(&RunnerAXCustomActionReadsInFlight, 1);
|
|
528
|
+
atomic_fetch_add(&RunnerAXCustomActionReadDispatches, 1);
|
|
529
|
+
NSMutableArray *box = [NSMutableArray array];
|
|
530
|
+
dispatch_semaphore_t finished = dispatch_semaphore_create(0);
|
|
531
|
+
dispatch_async(RunnerAXCustomActionReadQueue(), ^{
|
|
532
|
+
typedef id (*RunnerAXAttributesMsgSend)(id, SEL, id, id, NSError **);
|
|
533
|
+
RunnerAXAttributesMsgSend send = (RunnerAXAttributesMsgSend)objc_msgSend;
|
|
534
|
+
NSError *error = nil;
|
|
535
|
+
id result = nil;
|
|
536
|
+
@try {
|
|
537
|
+
result = send(axClient, selector, element, @[ RunnerAXCustomActionsAttribute ], &error);
|
|
538
|
+
} @catch (NSException *exception) {
|
|
539
|
+
result = nil;
|
|
540
|
+
}
|
|
541
|
+
if (nil != result) {
|
|
542
|
+
[box addObject:result];
|
|
543
|
+
}
|
|
544
|
+
// Clear in-flight BEFORE signalling, so a waiter that wakes on this signal
|
|
545
|
+
// never observes its own completed read as still outstanding.
|
|
546
|
+
atomic_fetch_sub(&RunnerAXCustomActionReadsInFlight, 1);
|
|
547
|
+
dispatch_semaphore_signal(finished);
|
|
548
|
+
});
|
|
549
|
+
long waited = dispatch_semaphore_wait(
|
|
550
|
+
finished,
|
|
551
|
+
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(RunnerAXCustomActionReadTimeout * NSEC_PER_SEC)));
|
|
552
|
+
if (waited != 0) {
|
|
553
|
+
NSLog(@"AGENT_DEVICE_RUNNER_PRIVATE_AX_CUSTOM_ACTIONS_READ_TIMEOUT");
|
|
554
|
+
return nil;
|
|
555
|
+
}
|
|
556
|
+
if (NULL != completed) {
|
|
557
|
+
*completed = YES;
|
|
558
|
+
}
|
|
559
|
+
id values = box.firstObject;
|
|
560
|
+
if (![values isKindOfClass:NSDictionary.class]) {
|
|
561
|
+
return nil;
|
|
562
|
+
}
|
|
563
|
+
id actions = ((NSDictionary *)values)[RunnerAXCustomActionsAttribute];
|
|
564
|
+
if (![actions isKindOfClass:NSArray.class]) {
|
|
565
|
+
return nil;
|
|
566
|
+
}
|
|
567
|
+
NSMutableArray<NSString *> *names = [NSMutableArray array];
|
|
568
|
+
for (id action in (NSArray *)actions) {
|
|
569
|
+
if (![action isKindOfClass:NSDictionary.class]) {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
id name = ((NSDictionary *)action)[@"CustomActionName"];
|
|
573
|
+
if ([name isKindOfClass:NSString.class] && [(NSString *)name length] > 0) {
|
|
574
|
+
[names addObject:name];
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return names.count > 0 ? names.copy : nil;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/// Bounds what one element can contribute to the response. The names come from
|
|
581
|
+
/// the app, so their count and length are the app's choice, not ours — a list
|
|
582
|
+
/// long enough to dominate a snapshot is a plausible accident (generated rows)
|
|
583
|
+
/// as well as a hostile case. Truncation is reported, never silent: a clipped
|
|
584
|
+
/// list is exactly as misleading as an unread element if it looks complete.
|
|
585
|
+
+ (NSArray<NSString *> *)cappedActionNames:(NSArray<NSString *> *)names
|
|
586
|
+
truncated:(BOOL *)truncated
|
|
587
|
+
{
|
|
588
|
+
if (NULL != truncated) {
|
|
589
|
+
*truncated = NO;
|
|
590
|
+
}
|
|
591
|
+
NSMutableArray<NSString *> *capped = [NSMutableArray array];
|
|
592
|
+
for (NSString *name in names) {
|
|
593
|
+
if (capped.count >= RunnerAXCustomActionsMaxPerElement) {
|
|
594
|
+
if (NULL != truncated) {
|
|
595
|
+
*truncated = YES;
|
|
596
|
+
}
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
if (name.length > RunnerAXCustomActionNameMaxLength) {
|
|
600
|
+
// Clip on composed character boundaries so a truncated name stays a
|
|
601
|
+
// valid string rather than a split grapheme.
|
|
602
|
+
NSRange clip = [name rangeOfComposedCharacterSequencesForRange:
|
|
603
|
+
NSMakeRange(0, RunnerAXCustomActionNameMaxLength)];
|
|
604
|
+
[capped addObject:[[name substringWithRange:clip] stringByAppendingString:@"…"]];
|
|
605
|
+
if (NULL != truncated) {
|
|
606
|
+
*truncated = YES;
|
|
607
|
+
}
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
[capped addObject:name];
|
|
611
|
+
}
|
|
612
|
+
return capped.copy;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
+ (NSInteger)customActionReadsInFlight
|
|
616
|
+
{
|
|
617
|
+
return atomic_load(&RunnerAXCustomActionReadsInFlight);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
+ (NSInteger)customActionReadDispatchCount
|
|
621
|
+
{
|
|
622
|
+
return (NSInteger)atomic_load(&RunnerAXCustomActionReadDispatches);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
+ (nullable id)accessibilityClient
|
|
626
|
+
{
|
|
627
|
+
return [self objectFrom:XCUIDevice.sharedDevice selectorName:@"accessibilityInterface"];
|
|
628
|
+
}
|
|
629
|
+
|
|
309
630
|
+ (nullable id)accessibilityElementForSnapshot:(id)snapshot
|
|
310
631
|
{
|
|
311
632
|
id element = nil;
|
|
@@ -384,6 +705,7 @@ typedef id (*RunnerAXSnapshotMsgSend)(id, SEL, id, id, id, NSError **);
|
|
|
384
705
|
nodeCount:(NSInteger *)nodeCount
|
|
385
706
|
truncated:(BOOL *)truncated
|
|
386
707
|
frontiers:(NSMutableArray<RunnerAXSnapshotFrontier *> *)frontiers
|
|
708
|
+
mergedLeaves:(NSMutableArray<RunnerAXSnapshotFrontier *> *)mergedLeaves
|
|
387
709
|
{
|
|
388
710
|
if (nil == snapshot || *nodeCount >= maxNodes) {
|
|
389
711
|
*truncated = YES;
|
|
@@ -410,7 +732,8 @@ typedef id (*RunnerAXSnapshotMsgSend)(id, SEL, id, id, id, NSError **);
|
|
|
410
732
|
maxNodes:maxNodes
|
|
411
733
|
nodeCount:nodeCount
|
|
412
734
|
truncated:truncated
|
|
413
|
-
frontiers:frontiers
|
|
735
|
+
frontiers:frontiers
|
|
736
|
+
mergedLeaves:mergedLeaves];
|
|
414
737
|
if (nil != childNode) {
|
|
415
738
|
[children addObject:childNode];
|
|
416
739
|
}
|
|
@@ -420,6 +743,18 @@ typedef id (*RunnerAXSnapshotMsgSend)(id, SEL, id, id, id, NSError **);
|
|
|
420
743
|
}
|
|
421
744
|
}
|
|
422
745
|
}
|
|
746
|
+
if (children.count == 0 && nil != mergedLeaves
|
|
747
|
+
&& [result[@"label"] isKindOfClass:NSString.class]
|
|
748
|
+
&& [(NSString *)result[@"label"] length] > 0) {
|
|
749
|
+
// A labelled childless node is the shape a merged card takes: the container
|
|
750
|
+
// is marked accessible, so its real controls never appear as children. Only
|
|
751
|
+
// these are worth an action read — unlabelled leaves are decoration.
|
|
752
|
+
RunnerAXSnapshotFrontier *candidate = [[RunnerAXSnapshotFrontier alloc] init];
|
|
753
|
+
candidate.snapshot = snapshot;
|
|
754
|
+
candidate.node = result;
|
|
755
|
+
candidate.depth = depth;
|
|
756
|
+
[mergedLeaves addObject:candidate];
|
|
757
|
+
}
|
|
423
758
|
if (children.count == 0 && nil != frontiers && depth >= maxDepth - 1) {
|
|
424
759
|
// Childless node at the cap boundary: either a real leaf or a branch whose
|
|
425
760
|
// children the AX server withheld. The two are indistinguishable here, so
|