agent-device 0.15.2 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +51 -155
  2. package/android-multitouch-helper/README.md +41 -0
  3. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.16.0.apk +0 -0
  4. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.16.0.apk.sha256 +1 -0
  5. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.16.0.manifest.json +10 -0
  6. package/android-snapshot-helper/README.md +2 -0
  7. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.16.0.apk +0 -0
  8. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.16.0.apk.sha256 +1 -0
  9. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.15.2.manifest.json → agent-device-android-snapshot-helper-0.16.0.manifest.json} +6 -6
  10. package/dist/src/1231.js +1 -1
  11. package/dist/src/1769.js +7 -7
  12. package/dist/src/2099.js +1 -0
  13. package/dist/src/221.js +4 -4
  14. package/dist/src/3622.js +3 -0
  15. package/dist/src/7519.js +1 -0
  16. package/dist/src/7556.js +1 -1
  17. package/dist/src/89.js +1 -0
  18. package/dist/src/940.js +1 -1
  19. package/dist/src/9542.js +2 -2
  20. package/dist/src/9639.js +2 -2
  21. package/dist/src/989.js +1 -1
  22. package/dist/src/android-adb.d.ts +26 -0
  23. package/dist/src/android-adb.js +1 -1
  24. package/dist/src/android-snapshot-helper.d.ts +30 -0
  25. package/dist/src/batch.d.ts +9 -9
  26. package/dist/src/cli.js +494 -76
  27. package/dist/src/index.d.ts +47 -5
  28. package/dist/src/internal/daemon.js +69 -45
  29. package/dist/src/server.js +2 -2
  30. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +1 -0
  31. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +19 -0
  32. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +297 -0
  33. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +144 -5
  34. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +328 -23
  35. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +3 -1
  36. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +8 -0
  37. package/package.json +9 -3
  38. package/server.json +2 -2
  39. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.15.2.apk +0 -0
  40. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.15.2.apk.sha256 +0 -1
  41. package/dist/src/1393.js +0 -1
  42. package/dist/src/2151.js +0 -438
  43. package/dist/src/3572.js +0 -1
  44. package/dist/src/7599.js +0 -3
  45. package/dist/src/9671.js +0 -1
@@ -1,2 +1,2 @@
1
- import{readVersion as e}from"./9671.js";function t(t){if("2.0"!==t.jsonrpc||"string"!=typeof t.method)return r(t.id??null,-32600,"Invalid JSON-RPC request.");if(void 0===t.id)return null;try{var i,s;return i=t.id,s=function(t,r){switch(t){case"initialize":return{protocolVersion:"2025-11-25",capabilities:{tools:{}},serverInfo:{name:"agent-device",version:e()}};case"ping":return{};case"tools/list":return{tools:[{name:"status",description:"Return discovery-only handoff metadata for installing, verifying, and using the agent-device CLI.",inputSchema:{type:"object",properties:{},additionalProperties:!1},outputSchema:{type:"object",properties:{packageName:{type:"string"},installedPackageVersion:{type:"string"},cliCommandName:{type:"string"},installCommand:{type:"string"},verifyCommand:{type:"string"},startingHelpCommand:{type:"string"},supportedTargets:{type:"array",items:{type:"string"}},capabilities:{type:"array",items:{type:"string"}},prerequisites:{type:"array",items:{type:"string"}},docsUrl:{type:"string"},agentDocsUrl:{type:"string"},firstCommands:{type:"array",items:{type:"string"}},automationInterface:{type:"string",const:"cli"},automationNote:{type:"string"},installRequiresHumanApproval:{type:"boolean",const:!0},installSafetyNote:{type:"string"}},required:["packageName","installedPackageVersion","cliCommandName","installCommand","verifyCommand","startingHelpCommand","supportedTargets","capabilities","prerequisites","docsUrl","agentDocsUrl","firstCommands","automationInterface","automationNote","installRequiresHumanApproval","installSafetyNote"],additionalProperties:!1}}]};case"tools/call":var i=r;let s=function(e,t){let r=e[t];if("string"!=typeof r||0===r.length)throw Error(`Expected ${t} to be a non-empty string.`);return r}(function(e){if(!e||"object"!=typeof e||Array.isArray(e))throw Error("Expected object parameters.");return e}(i),"name");try{if("status"===s){let t;return t={packageName:"agent-device",installedPackageVersion:e(),cliCommandName:"agent-device",installCommand:"npm install -g agent-device@latest",verifyCommand:"agent-device --version",startingHelpCommand:"agent-device help workflow",supportedTargets:["ios-simulator","android-emulator","ios-device","android-device","tvos-simulator","macos","linux"],capabilities:["inspect-ui","interact-with-elements","open-apps","install-app","capture-screenshot","accessibility-snapshot","collect-logs","collect-network","collect-performance","record-replay","react-native","expo","android-adb","ios-xcuitest"],prerequisites:["node>=22","xcode-for-ios","android-sdk-adb-for-android","macos-accessibility-permission-for-desktop"],docsUrl:"https://agent-device.dev/",agentDocsUrl:"https://incubator.callstack.com/agent-device/llms-full.txt",firstCommands:["agent-device help workflow","agent-device apps --platform ios","agent-device apps --platform android"],automationInterface:"cli",automationNote:"Device automation happens through the agent-device CLI. This MCP server is discovery-only and does not expose device automation tools.",installRequiresHumanApproval:!0,installSafetyNote:"Agents should not install or update the package unless the human has approved the environment change. If the CLI is missing, ask the human to run the install command, then run the verify command."},{isError:!1,structuredContent:t,content:[{type:"text",text:JSON.stringify(t,null,2)}]}}throw Error(`Unknown tool: ${s}`)}catch(e){return function(e,t=!1){return{isError:t,content:[{type:"text",text:e}]}}(e instanceof Error?e.message:String(e),!0)}return;default:throw new n(`Unsupported MCP method: ${t}`)}}(t.method,t.params),{jsonrpc:"2.0",id:i,result:s}}catch(e){if(e instanceof n)return r(t.id,-32601,e.message);return r(t.id,-32602,e instanceof Error?e.message:String(e))}}function r(e,t,r){return{jsonrpc:"2.0",id:e,error:{code:t,message:r}}}class n extends Error{}async function i(){let e=new s(e=>{let r=function(e){if(Array.isArray(e)){let r=e.flatMap(e=>{var r;return(r=t(e))?[r]:[]});return r.length>0?r:null}return t(e)}(e);r&&a(r)});process.stdin.setEncoding("utf8"),process.stdin.on("data",t=>{try{e.push(t)}catch(e){a({jsonrpc:"2.0",id:null,error:{code:-32700,message:e instanceof Error?e.message:String(e)}})}}),await new Promise(e=>{process.stdin.on("end",e),process.stdin.on("close",e),process.stdin.resume()})}class s{buffer="";sink;constructor(e){this.sink=e}push(e){for(this.buffer+=e;;){let e=this.tryReadLineMessage();if(void 0!==e){this.emit(e);continue}break}}tryReadLineMessage(){let e=this.buffer.indexOf("\n");if(-1===e)return;let t=this.buffer.slice(0,e).trim();return this.buffer=this.buffer.slice(e+1),t.length>0?t:void 0}emit(e){let t=JSON.parse(e);Array.isArray(t),this.sink(t)}}function a(e){process.stdout.write(`${JSON.stringify(e)}
2
- `)}export{i as runAgentDeviceMcpServer};
1
+ import{createAgentDeviceClient as r}from"./9542.js";import{runCommand as e,isCommandName as t,listMcpToolDefinitions as n}from"./89.js";import{readVersion as i}from"./2099.js";let s=function(n={createClient:r,runCommand:e}){return{execute:async(r,e)=>{var i;if(!t(r))throw Error(`Unknown command tool: ${r}`);let s=n.createClient(function(r){if(!r||"object"!=typeof r||Array.isArray(r))return{};let e=r.stateDir;if(void 0===e)return{};if("string"!=typeof e||0===e.length)throw Error("Expected stateDir to be a non-empty string.");return{stateDir:e}}(e)),o=await n.runCommand(s,r,function(r){if(!r||"object"!=typeof r||Array.isArray(r))return r;let{stateDir:e,...t}=r;return t}(e));return{isError:!1,structuredContent:o,content:[{type:"text",text:"string"==typeof(i=o)?i:JSON.stringify(i,null,2)}]}}}}();async function o(r){if("2.0"!==r.jsonrpc||"string"!=typeof r.method)return u(r.id??null,-32600,"Invalid JSON-RPC request.");if(void 0===r.id)return null;try{var e,t;return e=r.id,t=await a(r.method,r.params),{jsonrpc:"2.0",id:e,result:t}}catch(e){if(e instanceof f)return u(r.id,-32601,e.message);return u(r.id,-32602,e instanceof Error?e.message:String(e))}}async function a(r,e){switch(r){case"initialize":return{protocolVersion:"2025-11-25",capabilities:{tools:{}},serverInfo:{name:"agent-device",version:i()}};case"ping":return{};case"tools/list":return{tools:n().map(r=>{var e;return{name:r.name,description:r.description,inputSchema:{...e=r.inputSchema,properties:{...e.properties,stateDir:{type:"string",description:"Agent-device state directory."}}}}})};case"tools/call":return await c(e);default:throw new f(`Unsupported MCP method: ${r}`)}}async function c(r){let e=function(r){if(!r||"object"!=typeof r||Array.isArray(r))throw Error("Expected object parameters.");return r}(r),t=function(r,e){let t=r[e];if("string"!=typeof t||0===t.length)throw Error(`Expected ${e} to be a non-empty string.`);return t}(e,"name");try{return await s.execute(t,e.arguments)}catch(r){return function(r,e=!1){return{isError:e,content:[{type:"text",text:r}]}}(r instanceof Error?r.message:String(r),!0)}}function u(r,e,t){return{jsonrpc:"2.0",id:r,error:{code:e,message:t}}}class f extends Error{}async function l(){let r=function(r={}){let e=r.handlePayload??p,t=r.write??m,n=Promise.resolve();return{push:r=>{var i;let s=Array.isArray(i=r)?1===i.length?i[0]?.id??null:null:i.id??null;n=n.then(async()=>{let n=await e(r);n&&t(n)}).catch(r=>{t({jsonrpc:"2.0",id:s,error:{code:-32603,message:r instanceof Error?r.message:String(r)}})})},idle:async()=>{await n}}}(),e=new y(e=>{r.push(e)});process.stdin.setEncoding("utf8"),process.stdin.on("data",r=>{try{e.push(r)}catch(r){m({jsonrpc:"2.0",id:null,error:{code:-32700,message:r instanceof Error?r.message:String(r)}})}}),await new Promise(r=>{process.stdin.on("end",r),process.stdin.on("close",r),process.stdin.resume()}),await r.idle()}function p(r){return Array.isArray(r)?d(r):o(r)}async function d(r){let e=[];for(let n of r){var t;e.push(...(t=await o(n))?[t]:[])}return e.length>0?e:null}class y{buffer="";sink;constructor(r){this.sink=r}push(r){for(this.buffer+=r;;){let r=this.tryReadLineMessage();if(void 0!==r){this.emit(r);continue}break}}tryReadLineMessage(){let r=this.buffer.indexOf("\n");if(-1===r)return;let e=this.buffer.slice(0,r).trim();return this.buffer=this.buffer.slice(r+1),e.length>0?e:void 0}emit(r){let e=JSON.parse(r);Array.isArray(e),this.sink(e)}}function m(r){process.stdout.write(`${JSON.stringify(r)}
2
+ `)}export{l as runAgentDeviceMcpServer};
@@ -1 +1,2 @@
1
1
  #import "RunnerObjCExceptionCatcher.h"
2
+ #import "RunnerSynthesizedGesture.h"
@@ -0,0 +1,19 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ @interface RunnerSynthesizedGesture : NSObject
6
+
7
+ + (NSString * _Nullable)synthesizeTransformWithApplication:(id)application
8
+ x:(double)x
9
+ y:(double)y
10
+ dx:(double)dx
11
+ dy:(double)dy
12
+ scale:(double)scale
13
+ degrees:(double)degrees
14
+ radius:(double)radius
15
+ durationMs:(double)durationMs;
16
+
17
+ @end
18
+
19
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,297 @@
1
+ #import "RunnerSynthesizedGesture.h"
2
+
3
+ #import <CoreGraphics/CoreGraphics.h>
4
+ #import <math.h>
5
+ #import <objc/message.h>
6
+
7
+ typedef NSInteger (*RunnerMsgSendInteger)(id, SEL);
8
+ typedef id (*RunnerMsgSendInitRecord)(id, SEL, NSString *, NSInteger);
9
+ typedef id (*RunnerMsgSendInitPath)(id, SEL, CGPoint, NSTimeInterval);
10
+ typedef void (*RunnerMsgSendPathMove)(id, SEL, CGPoint, NSTimeInterval);
11
+ typedef void (*RunnerMsgSendPathOffset)(id, SEL, NSTimeInterval);
12
+ typedef void (*RunnerMsgSendAddPath)(id, SEL, id);
13
+ typedef void (*RunnerMsgSendSetInteger)(id, SEL, NSInteger);
14
+ typedef BOOL (*RunnerMsgSendSynthesize)(id, SEL, NSError **);
15
+
16
+ typedef struct {
17
+ Class recordClass;
18
+ Class pathClass;
19
+ SEL initRecordSelector;
20
+ SEL addPathSelector;
21
+ SEL setTargetProcessIDSelector;
22
+ SEL synthesizeSelector;
23
+ SEL interfaceOrientationSelector;
24
+ SEL processIDSelector;
25
+ SEL initPathSelector;
26
+ SEL moveSelector;
27
+ SEL liftSelector;
28
+ } RunnerXCTestEventBridge;
29
+
30
+ static NSString * _Nullable RunnerResolveXCTestEventBridge(
31
+ id application,
32
+ RunnerXCTestEventBridge *bridge
33
+ );
34
+ static NSString * _Nullable RunnerRequireClass(Class cls, NSString *className);
35
+ static NSString * _Nullable RunnerRequireSelector(Class cls, SEL selector, NSString *selectorName);
36
+ static NSString * _Nullable RunnerRequireApplicationSelector(id application, SEL selector, NSString *selectorName);
37
+ static id RunnerPointerPath(
38
+ const RunnerXCTestEventBridge *bridge,
39
+ CGPoint start,
40
+ double x,
41
+ double y,
42
+ double dx,
43
+ double dy,
44
+ double scale,
45
+ double degrees,
46
+ double radius,
47
+ double durationMs,
48
+ double side
49
+ );
50
+ static CGPoint RunnerPointerPointAt(
51
+ double x,
52
+ double y,
53
+ double dx,
54
+ double dy,
55
+ double scale,
56
+ double degrees,
57
+ double baseRadius,
58
+ double t,
59
+ double side
60
+ );
61
+
62
+ @implementation RunnerSynthesizedGesture
63
+
64
+ + (NSString * _Nullable)synthesizeTransformWithApplication:(id)application
65
+ x:(double)x
66
+ y:(double)y
67
+ dx:(double)dx
68
+ dy:(double)dy
69
+ scale:(double)scale
70
+ degrees:(double)degrees
71
+ radius:(double)radius
72
+ durationMs:(double)durationMs {
73
+ @try {
74
+ return [self trySynthesizeTransformWithApplication:application
75
+ x:x
76
+ y:y
77
+ dx:dx
78
+ dy:dy
79
+ scale:scale
80
+ degrees:degrees
81
+ radius:radius
82
+ durationMs:durationMs];
83
+ } @catch (NSException *exception) {
84
+ NSString *name = exception.name ?: @"NSException";
85
+ NSString *reason = exception.reason ?: @"private XCTest event synthesis failed";
86
+ return [NSString stringWithFormat:@"%@: %@", name, reason];
87
+ }
88
+ }
89
+
90
+ + (NSString * _Nullable)trySynthesizeTransformWithApplication:(id)application
91
+ x:(double)x
92
+ y:(double)y
93
+ dx:(double)dx
94
+ dy:(double)dy
95
+ scale:(double)scale
96
+ degrees:(double)degrees
97
+ radius:(double)radius
98
+ durationMs:(double)durationMs {
99
+ RunnerXCTestEventBridge bridge;
100
+ NSString *missing = RunnerResolveXCTestEventBridge(application, &bridge);
101
+ if (missing != nil) {
102
+ return missing;
103
+ }
104
+
105
+ NSInteger interfaceOrientation =
106
+ ((RunnerMsgSendInteger)objc_msgSend)(application, bridge.interfaceOrientationSelector);
107
+ NSInteger targetProcessID = ((RunnerMsgSendInteger)objc_msgSend)(application, bridge.processIDSelector);
108
+ if (targetProcessID <= 0) {
109
+ return @"private XCTest event synthesis unavailable: could not resolve target process ID";
110
+ }
111
+
112
+ id record = ((RunnerMsgSendInitRecord)objc_msgSend)(
113
+ [bridge.recordClass alloc],
114
+ bridge.initRecordSelector,
115
+ @"agent-device-transform",
116
+ interfaceOrientation
117
+ );
118
+ if (record == nil) {
119
+ return @"private XCTest event synthesis failed: could not create event record";
120
+ }
121
+ ((RunnerMsgSendSetInteger)objc_msgSend)(record, bridge.setTargetProcessIDSelector, targetProcessID);
122
+
123
+ double sides[] = {1.0, -1.0};
124
+ for (int index = 0; index < 2; index += 1) {
125
+ double side = sides[index];
126
+ id path = RunnerPointerPath(
127
+ &bridge,
128
+ RunnerPointerPointAt(x, y, dx, dy, scale, degrees, radius, 0.0, side),
129
+ x,
130
+ y,
131
+ dx,
132
+ dy,
133
+ scale,
134
+ degrees,
135
+ radius,
136
+ durationMs,
137
+ side
138
+ );
139
+ if (path == nil) {
140
+ return @"private XCTest event synthesis failed: could not create pointer path";
141
+ }
142
+ ((RunnerMsgSendAddPath)objc_msgSend)(record, bridge.addPathSelector, path);
143
+ }
144
+
145
+ NSError *error = nil;
146
+ BOOL ok = ((RunnerMsgSendSynthesize)objc_msgSend)(record, bridge.synthesizeSelector, &error);
147
+ if (!ok) {
148
+ NSString *detail = error.localizedDescription ?: @"synthesizeWithError returned false";
149
+ return [NSString stringWithFormat:@"private XCTest event synthesis failed: %@", detail];
150
+ }
151
+ return nil;
152
+ }
153
+
154
+ static NSString * _Nullable RunnerResolveXCTestEventBridge(
155
+ id application,
156
+ RunnerXCTestEventBridge *bridge
157
+ ) {
158
+ Class recordClass = NSClassFromString(@"XCSynthesizedEventRecord");
159
+ Class pathClass = NSClassFromString(@"XCPointerEventPath");
160
+ SEL initRecordSelector = NSSelectorFromString(@"initWithName:interfaceOrientation:");
161
+ SEL addPathSelector = NSSelectorFromString(@"addPointerEventPath:");
162
+ SEL setTargetProcessIDSelector = NSSelectorFromString(@"setTargetProcessID:");
163
+ SEL synthesizeSelector = NSSelectorFromString(@"synthesizeWithError:");
164
+ SEL interfaceOrientationSelector = NSSelectorFromString(@"interfaceOrientation");
165
+ SEL processIDSelector = NSSelectorFromString(@"processID");
166
+ SEL initPathSelector = NSSelectorFromString(@"initForTouchAtPoint:offset:");
167
+ SEL moveSelector = NSSelectorFromString(@"moveToPoint:atOffset:");
168
+ SEL liftSelector = NSSelectorFromString(@"liftUpAtOffset:");
169
+
170
+ NSString *missing = RunnerRequireClass(recordClass, @"XCSynthesizedEventRecord");
171
+ if (missing != nil) return missing;
172
+ missing = RunnerRequireClass(pathClass, @"XCPointerEventPath");
173
+ if (missing != nil) return missing;
174
+ missing = RunnerRequireSelector(recordClass, initRecordSelector, @"initWithName:interfaceOrientation:");
175
+ if (missing != nil) return missing;
176
+ missing = RunnerRequireSelector(recordClass, addPathSelector, @"addPointerEventPath:");
177
+ if (missing != nil) return missing;
178
+ missing = RunnerRequireSelector(recordClass, setTargetProcessIDSelector, @"setTargetProcessID:");
179
+ if (missing != nil) return missing;
180
+ missing = RunnerRequireSelector(recordClass, synthesizeSelector, @"synthesizeWithError:");
181
+ if (missing != nil) return missing;
182
+ missing = RunnerRequireSelector(pathClass, initPathSelector, @"initForTouchAtPoint:offset:");
183
+ if (missing != nil) return missing;
184
+ missing = RunnerRequireSelector(pathClass, moveSelector, @"moveToPoint:atOffset:");
185
+ if (missing != nil) return missing;
186
+ missing = RunnerRequireSelector(pathClass, liftSelector, @"liftUpAtOffset:");
187
+ if (missing != nil) return missing;
188
+ missing = RunnerRequireApplicationSelector(application, interfaceOrientationSelector, @"interfaceOrientation");
189
+ if (missing != nil) return missing;
190
+ missing = RunnerRequireApplicationSelector(application, processIDSelector, @"processID");
191
+ if (missing != nil) return missing;
192
+
193
+ *bridge = (RunnerXCTestEventBridge){
194
+ .recordClass = recordClass,
195
+ .pathClass = pathClass,
196
+ .initRecordSelector = initRecordSelector,
197
+ .addPathSelector = addPathSelector,
198
+ .setTargetProcessIDSelector = setTargetProcessIDSelector,
199
+ .synthesizeSelector = synthesizeSelector,
200
+ .interfaceOrientationSelector = interfaceOrientationSelector,
201
+ .processIDSelector = processIDSelector,
202
+ .initPathSelector = initPathSelector,
203
+ .moveSelector = moveSelector,
204
+ .liftSelector = liftSelector,
205
+ };
206
+ return nil;
207
+ }
208
+
209
+ static NSString * _Nullable RunnerRequireClass(Class cls, NSString *className) {
210
+ if (cls == Nil) {
211
+ return [NSString stringWithFormat:@"private XCTest event synthesis unavailable: missing %@", className];
212
+ }
213
+ return nil;
214
+ }
215
+
216
+ static NSString * _Nullable RunnerRequireSelector(Class cls, SEL selector, NSString *selectorName) {
217
+ if (![cls instancesRespondToSelector:selector]) {
218
+ return [NSString stringWithFormat:
219
+ @"private XCTest event synthesis unavailable: %@ missing %@",
220
+ NSStringFromClass(cls),
221
+ selectorName
222
+ ];
223
+ }
224
+ return nil;
225
+ }
226
+
227
+ static NSString * _Nullable RunnerRequireApplicationSelector(
228
+ id application,
229
+ SEL selector,
230
+ NSString *selectorName
231
+ ) {
232
+ if (![application respondsToSelector:selector]) {
233
+ return [NSString stringWithFormat:
234
+ @"private XCTest event synthesis unavailable: XCUIApplication missing %@",
235
+ selectorName
236
+ ];
237
+ }
238
+ return nil;
239
+ }
240
+
241
+ static id RunnerPointerPath(
242
+ const RunnerXCTestEventBridge *bridge,
243
+ CGPoint start,
244
+ double x,
245
+ double y,
246
+ double dx,
247
+ double dy,
248
+ double scale,
249
+ double degrees,
250
+ double radius,
251
+ double durationMs,
252
+ double side
253
+ ) {
254
+ id path =
255
+ ((RunnerMsgSendInitPath)objc_msgSend)([bridge->pathClass alloc], bridge->initPathSelector, start, 0.0);
256
+ if (path == nil) {
257
+ return nil;
258
+ }
259
+
260
+ int frameCount = MAX(3, (int)(durationMs / 16.0));
261
+ NSTimeInterval durationSeconds = durationMs / 1000.0;
262
+ for (int index = 1; index <= frameCount; index += 1) {
263
+ double t = (double)index / (double)frameCount;
264
+ CGPoint point = RunnerPointerPointAt(x, y, dx, dy, scale, degrees, radius, t, side);
265
+ NSTimeInterval offset = durationSeconds * t;
266
+ ((RunnerMsgSendPathMove)objc_msgSend)(path, bridge->moveSelector, point, offset);
267
+ }
268
+
269
+ ((RunnerMsgSendPathOffset)objc_msgSend)(path, bridge->liftSelector, durationSeconds);
270
+ return path;
271
+ }
272
+
273
+ static CGPoint RunnerPointerPointAt(
274
+ double x,
275
+ double y,
276
+ double dx,
277
+ double dy,
278
+ double scale,
279
+ double degrees,
280
+ double baseRadius,
281
+ double t,
282
+ double side
283
+ ) {
284
+ double centerX = x + dx * t;
285
+ double centerY = y + dy * t;
286
+ double startRadius = baseRadius / MAX(scale, 1.0);
287
+ double endRadius = baseRadius;
288
+ if (scale < 1.0) {
289
+ startRadius = baseRadius;
290
+ endRadius = baseRadius * scale;
291
+ }
292
+ double radius = startRadius + (endRadius - startRadius) * t;
293
+ double angle = (-M_PI_2) + (degrees * M_PI / 180.0) * t;
294
+ return CGPointMake(centerX + cos(angle) * radius * side, centerY + sin(angle) * radius * side);
295
+ }
296
+
297
+ @end
@@ -252,7 +252,12 @@ extension RunnerTests {
252
252
  )
253
253
  case .tap:
254
254
  if let selectorKey = command.selectorKey, let selectorValue = command.selectorValue {
255
- let match = findElement(app: activeApp, selectorKey: selectorKey, selectorValue: selectorValue)
255
+ let match = findElement(
256
+ app: activeApp,
257
+ selectorKey: selectorKey,
258
+ selectorValue: selectorValue,
259
+ allowNonHittableFallback: command.allowNonHittableCoordinateFallback == true
260
+ )
256
261
  if match.isAmbiguous {
257
262
  return Response(ok: false, error: ErrorPayload(code: "AMBIGUOUS_MATCH", message: "selector matched multiple elements"))
258
263
  }
@@ -264,16 +269,24 @@ extension RunnerTests {
264
269
  var outcome = RunnerInteractionOutcome.performed
265
270
  let timing = measureGesture {
266
271
  withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
267
- outcome = activateElement(app: activeApp, element: element, action: "tap by selector")
272
+ if match.usedNonHittableFallback {
273
+ // Maestro compatibility: RN E2E backdoor controls can be 1x1 and
274
+ // reported non-hittable by XCTest, while Maestro still taps their
275
+ // resolved bounds. Keep this behind the explicit replay-only flag.
276
+ outcome = tapAt(app: activeApp, x: frame.midX, y: frame.midY)
277
+ } else {
278
+ outcome = activateElement(app: activeApp, element: element, action: "tap by selector")
279
+ }
268
280
  }
269
281
  }
270
282
  if let response = unsupportedResponse(for: outcome) {
271
283
  return response
272
284
  }
285
+ waitForTextEntryReadinessAfterTap(app: activeApp, element: element)
273
286
  return Response(
274
287
  ok: true,
275
288
  data: DataPayload(
276
- message: "tapped",
289
+ message: match.usedNonHittableFallback ? "tapped via non-hittable coordinate fallback" : "tapped",
277
290
  gestureStartUptimeMs: timing.gestureStartUptimeMs,
278
291
  gestureEndUptimeMs: timing.gestureEndUptimeMs,
279
292
  x: touchFrame?.x,
@@ -729,6 +742,25 @@ extension RunnerTests {
729
742
  dismissed: result.dismissed
730
743
  )
731
744
  )
745
+ case .keyboardReturn:
746
+ let result = pressKeyboardReturn(app: activeApp)
747
+ if !result.pressed {
748
+ return Response(
749
+ ok: false,
750
+ error: ErrorPayload(
751
+ code: "UNSUPPORTED_OPERATION",
752
+ message: "Unable to press the iOS keyboard return key"
753
+ )
754
+ )
755
+ }
756
+ return Response(
757
+ ok: true,
758
+ data: DataPayload(
759
+ message: "keyboardReturn",
760
+ visible: result.visible,
761
+ wasVisible: result.wasVisible
762
+ )
763
+ )
732
764
  case .alert:
733
765
  let action = (command.action ?? "get").lowercased()
734
766
  guard let alert = resolveAlert(app: activeApp) else {
@@ -754,6 +786,82 @@ extension RunnerTests {
754
786
  gestureEndUptimeMs: timing.gestureEndUptimeMs
755
787
  )
756
788
  )
789
+ case .rotateGesture:
790
+ guard let degrees = command.degrees, degrees.isFinite else {
791
+ return Response(ok: false, error: ErrorPayload(message: "rotateGesture requires degrees"))
792
+ }
793
+ let velocity = command.velocity ?? (degrees >= 0 ? 1.0 : -1.0)
794
+ guard velocity.isFinite && velocity != 0 else {
795
+ return Response(ok: false, error: ErrorPayload(message: "rotateGesture velocity must be non-zero"))
796
+ }
797
+ var outcome = RunnerInteractionOutcome.performed
798
+ let timing = measureGesture {
799
+ outcome = rotateGesture(
800
+ app: activeApp,
801
+ degrees: degrees,
802
+ x: command.x,
803
+ y: command.y,
804
+ velocity: velocity
805
+ )
806
+ }
807
+ if let response = unsupportedResponse(for: outcome) {
808
+ return response
809
+ }
810
+ return Response(
811
+ ok: true,
812
+ data: DataPayload(
813
+ message: "rotatedGesture",
814
+ gestureStartUptimeMs: timing.gestureStartUptimeMs,
815
+ gestureEndUptimeMs: timing.gestureEndUptimeMs
816
+ )
817
+ )
818
+ case .transformGesture:
819
+ guard
820
+ let x = command.x,
821
+ let y = command.y,
822
+ let dx = command.dx,
823
+ let dy = command.dy,
824
+ x.isFinite,
825
+ y.isFinite,
826
+ dx.isFinite,
827
+ dy.isFinite
828
+ else {
829
+ return Response(ok: false, error: ErrorPayload(message: "transformGesture requires finite x y dx dy"))
830
+ }
831
+ guard let scale = command.scale, scale.isFinite, scale > 0 else {
832
+ return Response(ok: false, error: ErrorPayload(message: "transformGesture requires scale > 0"))
833
+ }
834
+ guard let degrees = command.degrees, degrees.isFinite else {
835
+ return Response(ok: false, error: ErrorPayload(message: "transformGesture requires finite degrees"))
836
+ }
837
+ let durationMs = command.durationMs ?? 300
838
+ guard durationMs.isFinite && durationMs >= 16 else {
839
+ return Response(ok: false, error: ErrorPayload(message: "transformGesture durationMs must be >= 16"))
840
+ }
841
+ var outcome = RunnerInteractionOutcome.performed
842
+ let timing = measureGesture {
843
+ outcome = transformGesture(
844
+ app: activeApp,
845
+ x: x,
846
+ y: y,
847
+ dx: dx,
848
+ dy: dy,
849
+ scale: scale,
850
+ degrees: degrees,
851
+ durationMs: durationMs
852
+ )
853
+ }
854
+ if let response = unsupportedResponse(for: outcome) {
855
+ return response
856
+ }
857
+ return Response(
858
+ ok: true,
859
+ data: DataPayload(
860
+ message: "transformedGesture",
861
+ gestureStartUptimeMs: timing.gestureStartUptimeMs,
862
+ gestureEndUptimeMs: timing.gestureEndUptimeMs
863
+ )
864
+ )
757
865
  }
758
866
  }
759
867
 
@@ -763,7 +871,27 @@ extension RunnerTests {
763
871
  }
764
872
  let delaySeconds = Double(max(command.delayMs ?? 0, 0)) / 1000.0
765
873
  let textEntryMode = resolveTextEntryMode(command)
766
- let target = focusTextInputForTextEntry(app: activeApp, x: command.x, y: command.y)
874
+ let target: TextEntryTarget
875
+ if let selectorKey = command.selectorKey, let selectorValue = command.selectorValue {
876
+ let match = findElement(
877
+ app: activeApp,
878
+ selectorKey: selectorKey,
879
+ selectorValue: selectorValue,
880
+ allowNonHittableFallback: command.allowNonHittableCoordinateFallback == true
881
+ )
882
+ if match.isAmbiguous {
883
+ return Response(ok: false, error: ErrorPayload(code: "AMBIGUOUS_MATCH", message: "selector matched multiple elements"))
884
+ }
885
+ guard let element = match.element else {
886
+ return Response(ok: false, error: ErrorPayload(code: "NO_MATCH", message: "selector did not match an element"))
887
+ }
888
+ guard isTextEntryElement(element) else {
889
+ return Response(ok: false, error: ErrorPayload(code: "INVALID_TARGET", message: "selector did not match a text input"))
890
+ }
891
+ target = focusTextInputForTextEntry(app: activeApp, element: element)
892
+ } else {
893
+ target = focusTextInputForTextEntry(app: activeApp, x: command.x, y: command.y)
894
+ }
767
895
  if textEntryMode == .replacement {
768
896
  guard target.element != nil else {
769
897
  let message =
@@ -791,6 +919,17 @@ extension RunnerTests {
791
919
  )
792
920
  )
793
921
  }
794
- return Response(ok: true, data: DataPayload(message: textResult.repaired ? "typed after repair" : "typed"))
922
+ let point = target.refreshPoint
923
+ let frame = activeApp.frame
924
+ return Response(
925
+ ok: true,
926
+ data: DataPayload(
927
+ message: textResult.repaired ? "typed after repair" : "typed",
928
+ x: point.map { Double($0.x) },
929
+ y: point.map { Double($0.y) },
930
+ referenceWidth: frame.isEmpty ? nil : Double(frame.width),
931
+ referenceHeight: frame.isEmpty ? nil : Double(frame.height)
932
+ )
933
+ )
795
934
  }
796
935
  }