Repository navigation
feat: add /wda/absolute-actions endpoint and sessionless /wda/pressButton - #1306
Huseynbala-Gurbanli wants to merge 1 commit into
Conversation
|
|
Huseynbala-Gurbanli
left a comment
There was a problem hiding this comment.
Here is a well-structured PR justification you can paste into the pull request description:
Summary
This PR adds two new WDA endpoints that make it possible to send touch gestures and hardware button events without an active XCUIApplication session. Together they close a long-standing gap for automation clients that need to perform gestures at raw screen coordinates or trigger system buttons (Home, Volume, etc.) independent of any running app.
Changes
1. FBCustomCommands.m — sessionless POST /wda/pressButton
/wda/pressButton previously required a live session. A single .withoutSession route is added alongside the existing session-bound route:
[[FBRoute POST:@"/wda/pressButton"].withoutSession respondWithTarget:self action:@selector(handlePressButtonCommand:)],
[[FBRoute POST:@"/wda/pressButton"] respondWithTarget:self action:@selector(handlePressButtonCommand:)],Why: Hardware button presses (Home, lock, volume) are frequently needed at moments when no app session exists — for example, to wake a locked device, dismiss a crash dialog, or press Home after a session has been torn down. Requiring a session to do this forces automation clients to maintain session state just to send a single system-level event, which is fragile and unnecessary.
2. FBTouchActionCommands.m — new POST /wda/absolute-actions endpoint
A new handler handleAbsoluteTouchActions: is registered both with and without a session:
[[FBRoute POST:@"/wda/absolute-actions"].withoutSession respondWithTarget:self action:@selector(handleAbsoluteTouchActions:)],
[[FBRoute POST:@"/wda/absolute-actions"] respondWithTarget:self action:@selector(handleAbsoluteTouchActions:)],The handler accepts the same W3C actions JSON structure that /actions uses, but synthesises gestures via XCSynthesizedEventRecord / FBXCTestDaemonsProxy at absolute screen-point coordinates, completely bypassing the accessibility element tree.
Why the existing /actions endpoint is not sufficient:
/actions |
/wda/absolute-actions |
|
|---|---|---|
| Session required | Yes | No |
| Coordinate space | Relative to app bounds | Absolute screen points |
| Element tree lookup | Yes (blocks until tree is ready) | Never |
| Works on lock screen / SpringBoard | No | Yes |
| Latency on complex layouts | High (accessibility query) | Constant |
The existing W3C actions handler works well for per-element interactions inside a running app, but it fails in three common real-device scenarios:
- No foreground app (lock screen, SpringBoard, system dialogs) — there is no XCUIApplication to attach to, so the session-based handler has nothing to resolve coordinates against.
- Coordinate-based automation — device-farm tools and computer-vision-driven scripts compute tap/swipe positions directly from screen geometry. Forcing them through the element tree adds latency and introduces non-deterministic failures when the accessibility hierarchy is slow to load.
- Multi-device fleet health checks — keepalive supervisors that periodically verify device responsiveness need to issue a tap or swipe without spinning up a full session.
XCSynthesizedEventRecord + FBXCTestDaemonsProxy.synthesizeEventWithRecord:error: is already used internally by XCTest for low-level event synthesis; this PR simply exposes it as a HTTP endpoint with the same W3C JSON envelope that clients already know.
Compatibility
- Only additive — no existing routes are modified or removed.
- The
.withoutSessionregistration means clients that already carry a session ID will continue to work identically (the session-bound route is still present). - Tested on iPhone 15 (iPhone16,1), iOS 26.6.1, WDA 16.15.3. Both endpoints return
{"value": null, "sessionId": null}on success and a standard WDA error envelope on failure.
Testing
# sessionless Home button press
curl -X POST http://127.0.0.1:8100/wda/pressButton \
-H 'Content-Type: application/json' \
-d '{"name":"home"}'
# → {"value":null,"sessionId":null}
# absolute swipe (scroll feed) without a session
curl -X POST http://127.0.0.1:8100/wda/absolute-actions \
-H 'Content-Type: application/json' \
-d '{"actions":[{"type":"pointer","parameters":{"pointerType":"touch"},"actions":[
{"type":"pointerMove","x":195,"y":590},
{"type":"pointerDown","duration":0},
{"type":"pointerMove","x":195,"y":200,"duration":300},
{"type":"pointerUp","duration":0}]}]}'
# → {"value":null,"sessionId":null}9820f11 to
a64b983
Compare
| // Unlike the session-bound /actions handler, this does not resolve the active | ||
| // XCUIApplication, so it works without a live session and does not depend on | ||
| // the application's accessibility state. | ||
| + (id<FBResponsePayload>)handleAbsoluteTouchActions:(FBRouteRequest *)request |
There was a problem hiding this comment.
Please do the implementation in an extension class, so it could be properly unit- and integration tested.
| // the application's accessibility state. | ||
| + (id<FBResponsePayload>)handleAbsoluteTouchActions:(FBRouteRequest *)request | ||
| { | ||
| NSArray *sources = request.arguments[@"actions"]; |
There was a problem hiding this comment.
this implementation must have a proper input validator. As far as I can understand only a subset of W3C actions is supported (where origin points to absolute coordinates, also no key actions). This must be explicitly verified and errors should be thrown instead of ignoring silently
| } | ||
|
|
||
| NSError *error; | ||
| if (![FBXCTestDaemonsProxy synthesizeEventWithRecord:record error:&error]) { |
There was a problem hiding this comment.
by executing executing events like this there might be a potential race because of transition animations. The existing actions work around it by waiting for cool off.
| return FBResponseWithOK(); | ||
| } | ||
|
|
||
| // Parse a single W3C pointer source from the actions array and replay it as |
There was a problem hiding this comment.
this comment is not very useful. Consider isolating the actual implementation in separate documented classes instead
mykola-mokhnach
left a comment
There was a problem hiding this comment.
- The handler is a second, simplified W3C parser. Instead of a new copy, could the existing
FBW3CActionsSynthesizerget an "absolute coordinates only, skipactiveApplication" mode? That would address the slow application lookup without a second parser, and fixes such as display correction would apply to both endpoints. This is a different point from the extension-class comment.
| [[XCSynthesizedEventRecord alloc] initWithName:@"WDAAbsoluteTouch" | ||
| interfaceOrientation:0]; | ||
|
|
||
| for (NSDictionary *source in sources) { |
There was a problem hiding this comment.
The record is created with interfaceOrientation:0 (Unknown), and the coordinates are not rotated. The existing W3C synthesizer passes application.interfaceOrientation. In landscape, pointerMove x=100,y=200 is built with orientation 0, so the tap lands in the wrong place or synthesizeEvent fails. Tapping a known screen point is the main use of this endpoint, so it shouldn't be portrait-only.
| cursor += durationSec; | ||
| [path moveToPoint:pt atOffset:cursor]; | ||
| } | ||
| } else if ([type isEqualToString:@"pointerDown"]) { |
There was a problem hiding this comment.
initForTouchAtPoint: already emits a touch-down, and pointerDown then calls pressDownAtOffset: again. The existing synthesizer skips that call for the first move/down pair. For the standard tap pointerMove, pointerDown, pause, pointerUp, this produces two down events and one lift. XCTest may reject that sequence or send a duplicate down.
| NSNumber *durationMs = item[@"duration"]; | ||
| NSTimeInterval durationSec = durationMs ? [durationMs doubleValue] / 1000.0 : 0.0; | ||
|
|
||
| if ([type isEqualToString:@"pointerMove"]) { |
There was a problem hiding this comment.
The timeline handling has gaps beyond input validation. The first move's duration is dropped. A pointerDown after pointerUp (a double-tap in one source) reuses the same XCPointerEventPath, where the existing synthesizer starts a new path. A negative duration moves cursor backwards, so moveToPoint:atOffset: gets an earlier offset than the previous event.
| #import "FBRouteRequest.h" | ||
| #import "FBSession.h" | ||
| #import "XCUIApplication+FBTouchAction.h" | ||
| // FBBaseActionsSynthesizer.h transitively imports XCPointerEventPath.h and |
There was a problem hiding this comment.
The new imports and handler use private XCTest types but aren't wrapped in #if !TARGET_OS_TV && !TARGET_OS_WATCH, unlike the other touch-action code (FBBaseActionsSynthesizer.m, XCUIApplication+FBTouchAction.m). Does this still build on tvOS and watchOS?
| [[FBRoute POST:@"/wda/getPasteboard"].withoutSession respondWithTarget:self action:@selector(handleGetPasteboard:)], | ||
| [[FBRoute GET:@"/wda/batteryInfo"] respondWithTarget:self action:@selector(handleGetBatteryInfo:)], | ||
| #endif | ||
| [[FBRoute POST:@"/wda/pressButton"].withoutSession respondWithTarget:self action:@selector(handlePressButtonCommand:)], |
There was a problem hiding this comment.
please move this change to a separate pr
|
@Huseynbala-Gurbanli This repo is not maintained anymore. Please move your PR to https://github.com/appium/appium-ios/tree/main/packages/webdriveragent |
Motivation
/wda/pressButtononly works with a live session, so a client can't press Home to recover after the session is lost without first creating a new one./session/:id/actionsresolvesrequest.session.activeApplicationbefore dispatching. For plain coordinate gestures (a tap or swipe at a known screen point) this adds an application lookup that isn't needed, and it can be slow when the app's UI is continuously updating (e.g. video feeds).Changes
FBCustomCommands.m: also registerPOST /wda/pressButtonwith.withoutSession(the existing session route is kept).FBTouchActionCommands.m: addPOST /wda/absolute-actions, handled by a newhandleAbsoluteTouchActions:and registered both with and without a session. It accepts the W3C actions JSON, convertspointer/touchsources (pointerMove,pointerDown,pointerUp,pause) into anXCPointerEventPathand replays them through a singleXCSynthesizedEventRecordviaFBXCTestDaemonsProxy. It does not resolve the active application, so it works without a live session. Non-pointer and non-touch sources are ignored. The existing/actionshandler is not modified.Compatibility
Additive only; no existing routes are modified or removed.
Testing
I'll add device results and timings for this exact build shortly. Happy to adjust naming or scope (e.g. gate behind a setting) if maintainers prefer a different shape.