appium-webdriveragent 16.3.0 → 16.4.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [16.4.0](https://github.com/appium/WebDriverAgent/compare/v16.3.0...v16.4.0) (2026-08-19)
2
+
3
+ ### Features
4
+
5
+ * Add Digital Crown rotation and hand gesture support for watchOS ([#1215](https://github.com/appium/WebDriverAgent/issues/1215)) ([5e021f8](https://github.com/appium/WebDriverAgent/commit/5e021f8d12791d126e9524763d9455ceaf04bb2f))
6
+
1
7
  ## [16.3.0](https://github.com/appium/WebDriverAgent/compare/v16.2.2...v16.3.0) (2026-08-18)
2
8
 
3
9
  ### Features
@@ -89,7 +89,6 @@
89
89
  - (void)_silentPressButton:(long long)button;
90
90
  - (void)holdHomeButtonForDuration:(double)duration;
91
91
  - (void)pressLockButton;
92
- - (void)rotateDigitalCrown:(double)crown velocity:(double)velocity;
93
92
  - (void)ensureSystemAppIsLoaded;
94
93
  - (void)attachLocalizableStringsData;
95
94
  - (_Bool)startHIDEventRecordingWithError:(id *)error;
@@ -166,6 +166,40 @@ typedef NS_ENUM(NSUInteger, FBUIInterfaceAppearance) {
166
166
  */
167
167
  - (nullable NSNumber *)fb_getAppearance;
168
168
 
169
+ #if TARGET_OS_WATCH
170
+ /**
171
+ Rotates the Digital Crown on Apple Watch. See
172
+ https://developer.apple.com/documentation/xcuiautomation/xcuidevice/rotatedigitalcrown(delta:velocity:)
173
+ and https://developer.apple.com/documentation/xcuiautomation/xcuidevice/rotatedigitalcrown(delta:)
174
+ Invoked dynamically (this selector was only added to the SDK in Xcode 16.3), so this compiles against
175
+ older Xcode versions too - it just fails at runtime with an error if the OS/SDK combination in use
176
+ doesn't actually implement it.
177
+
178
+ @param delta The number of full crown rotations, e.g. 1.0 is one complete turn.
179
+ The sign gives the direction: positive rotates up/clockwise, negative rotates down/counterclockwise.
180
+ @param velocity The rotation speed, in rotations per second, e.g. 1.0 completes one full
181
+ rotation per second. This is the raw value behind the public XCUIGestureVelocity
182
+ API, which only exposes it via named presets (.slow/.default/.fast).
183
+ Pass nil to use XCTest's own default velocity instead of specifying one explicitly.
184
+ @param error If there is an error, upon return contains an NSError object that describes the problem.
185
+ @return YES if the operation succeeds, otherwise NO.
186
+ */
187
+ - (BOOL)fb_rotateDigitalCrown:(double)delta velocity:(nullable NSNumber *)velocity error:(NSError **)error;
188
+
189
+ /**
190
+ Performs a hand gesture on Apple Watch (e.g. Double Tap, Wrist Flick). See
191
+ https://developer.apple.com/documentation/xcuiautomation/xcuidevice/perform(handgesture:)
192
+ Invoked dynamically (this selector was only added to the SDK in Xcode 16.3), so this compiles against
193
+ older Xcode versions too - it just fails at runtime with an error if the OS/SDK combination in use
194
+ doesn't actually implement it.
195
+
196
+ @param gestureName One of the supported gesture names: doubleTap (watchOS 10+), flick (watchOS 26+).
197
+ @param error If there is an error, upon return contains an NSError object that describes the problem.
198
+ @return YES if the operation succeeds, otherwise NO.
199
+ */
200
+ - (BOOL)fb_performHandGesture:(NSString *)gestureName error:(NSError **)error;
201
+ #endif // TARGET_OS_WATCH
202
+
169
203
  #if !TARGET_OS_TV
170
204
  /**
171
205
  Allows to set a simulated geolocation coordinates.
@@ -86,6 +86,27 @@ NSDictionary<NSString *, NSNumber *> *fb_availableButtonNames(void) {
86
86
  }
87
87
  #endif
88
88
 
89
+ #if TARGET_OS_WATCH
90
+ // Raw values from XCUIDeviceHandGesture (XCUIAutomation/XCUIDeviceHandGesture.h): doubleTap = 1, flick = 2.
91
+ // Referenced by raw integer rather than the enum constant, and invoked via NSInvocation in
92
+ // fb_performHandGesture:error: below, since that enum/selector was only added to the SDK in Xcode 16.3
93
+ // (flick specifically needs watchOS 12.0/26 - see the @available check below). This way the code compiles
94
+ // against any Xcode version; unsupported gestures/selectors are only rejected at runtime.
95
+ NSDictionary<NSString *, NSNumber *> *fb_availableHandGestureNames(void) {
96
+ static dispatch_once_t onceToken;
97
+ static NSDictionary *result;
98
+ dispatch_once(&onceToken, ^{
99
+ NSMutableDictionary *gestures = [NSMutableDictionary dictionary];
100
+ gestures[@"doubletap"] = @(1);
101
+ if (@available(watchOS 12.0, *)) {
102
+ gestures[@"flick"] = @(2);
103
+ }
104
+ result = [gestures copy];
105
+ });
106
+ return result;
107
+ }
108
+ #endif // TARGET_OS_WATCH
109
+
89
110
  @implementation XCUIDevice (FBHelpers)
90
111
 
91
112
  static bool fb_isLocked;
@@ -386,6 +407,57 @@ static bool fb_isLocked;
386
407
  : nil;
387
408
  }
388
409
 
410
+ #if TARGET_OS_WATCH
411
+ - (BOOL)fb_rotateDigitalCrown:(double)delta velocity:(nullable NSNumber *)velocity error:(NSError **)error
412
+ {
413
+ SEL selector = nil == velocity
414
+ ? NSSelectorFromString(@"rotateDigitalCrownByDelta:")
415
+ : NSSelectorFromString(@"rotateDigitalCrownByDelta:withVelocity:");
416
+ if (nil == selector || ![self respondsToSelector:selector]) {
417
+ return [[[FBErrorBuilder builder]
418
+ withDescriptionFormat:@"Digital Crown rotation is not supported by the current Xcode SDK/OS combination"]
419
+ buildError:error];
420
+ }
421
+ NSMethodSignature *signature = [self methodSignatureForSelector:selector];
422
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
423
+ [invocation setSelector:selector];
424
+ [invocation setTarget:self];
425
+ [invocation setArgument:&delta atIndex:2];
426
+ if (velocity) {
427
+ double velocityValue = velocity.doubleValue;
428
+ [invocation setArgument:&velocityValue atIndex:3];
429
+ }
430
+ [invocation invoke];
431
+ return YES;
432
+ }
433
+
434
+ - (BOOL)fb_performHandGesture:(NSString *)gestureName error:(NSError **)error
435
+ {
436
+ NSDictionary<NSString *, NSNumber *> *availableGestures = fb_availableHandGestureNames();
437
+ NSNumber *gestureValue = availableGestures[gestureName.lowercaseString];
438
+ if (!gestureValue) {
439
+ NSArray *sortedKeys = [availableGestures.allKeys sortedArrayUsingSelector:@selector(compare:)];
440
+ return [[[FBErrorBuilder builder]
441
+ withDescriptionFormat:@"The hand gesture '%@' is not supported. The device under test only supports the following hand gestures: %@", gestureName, sortedKeys]
442
+ buildError:error];
443
+ }
444
+ SEL selector = NSSelectorFromString(@"performHandGesture:");
445
+ if (nil == selector || ![self respondsToSelector:selector]) {
446
+ return [[[FBErrorBuilder builder]
447
+ withDescriptionFormat:@"Hand gesture automation is not supported by the current Xcode SDK/OS combination"]
448
+ buildError:error];
449
+ }
450
+ NSMethodSignature *signature = [self methodSignatureForSelector:selector];
451
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
452
+ [invocation setSelector:selector];
453
+ [invocation setTarget:self];
454
+ NSInteger gestureRawValue = gestureValue.integerValue;
455
+ [invocation setArgument:&gestureRawValue atIndex:2];
456
+ [invocation invoke];
457
+ return YES;
458
+ }
459
+ #endif // TARGET_OS_WATCH
460
+
389
461
  #if !TARGET_OS_TV
390
462
  - (BOOL)fb_setSimulatedLocation:(CLLocation *)location error:(NSError **)error
391
463
  {
@@ -63,6 +63,10 @@
63
63
  [[FBRoute GET:@"/wda/batteryInfo"] respondWithTarget:self action:@selector(handleGetBatteryInfo:)],
64
64
  #endif
65
65
  [[FBRoute POST:@"/wda/pressButton"] respondWithTarget:self action:@selector(handlePressButtonCommand:)],
66
+ #if TARGET_OS_WATCH
67
+ [[FBRoute POST:@"/wda/rotateDigitalCrown"] respondWithTarget:self action:@selector(handleRotateDigitalCrownCommand:)],
68
+ [[FBRoute POST:@"/wda/performHandGesture"] respondWithTarget:self action:@selector(handlePerformHandGestureCommand:)],
69
+ #endif
66
70
  [[FBRoute POST:@"/wda/performAccessibilityAudit"] respondWithTarget:self action:@selector(handlePerformAccessibilityAudit:)],
67
71
  [[FBRoute POST:@"/wda/performIoHidEvent"] respondWithTarget:self action:@selector(handlePeformIOHIDEvent:)],
68
72
  [[FBRoute POST:@"/wda/expectNotification"] respondWithTarget:self action:@selector(handleExpectNotification:)],
@@ -295,6 +299,38 @@
295
299
  return FBResponseWithOK();
296
300
  }
297
301
 
302
+ #if TARGET_OS_WATCH
303
+ + (id<FBResponsePayload>)handleRotateDigitalCrownCommand:(FBRouteRequest *)request
304
+ {
305
+ NSNumber *delta = request.arguments[@"delta"];
306
+ if (nil == delta) {
307
+ return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"'delta' argument is mandatory"
308
+ traceback:nil]);
309
+ }
310
+ NSError *error;
311
+ if (![XCUIDevice.sharedDevice fb_rotateDigitalCrown:delta.doubleValue
312
+ velocity:request.arguments[@"velocity"]
313
+ error:&error]) {
314
+ return FBResponseWithUnknownError(error);
315
+ }
316
+ return FBResponseWithOK();
317
+ }
318
+
319
+ + (id<FBResponsePayload>)handlePerformHandGestureCommand:(FBRouteRequest *)request
320
+ {
321
+ NSString *gestureName = request.arguments[@"name"];
322
+ if (nil == gestureName) {
323
+ return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"'name' argument is mandatory"
324
+ traceback:nil]);
325
+ }
326
+ NSError *error;
327
+ if (![XCUIDevice.sharedDevice fb_performHandGesture:gestureName error:&error]) {
328
+ return FBResponseWithUnknownError(error);
329
+ }
330
+ return FBResponseWithOK();
331
+ }
332
+ #endif
333
+
298
334
  + (id<FBResponsePayload>)handleActivateSiri:(FBRouteRequest *)request
299
335
  {
300
336
  NSError *error;
@@ -15,11 +15,11 @@
15
15
  <key>CFBundlePackageType</key>
16
16
  <string>FMWK</string>
17
17
  <key>CFBundleShortVersionString</key>
18
- <string>16.3.0</string>
18
+ <string>16.4.0</string>
19
19
  <key>CFBundleSignature</key>
20
20
  <string>????</string>
21
21
  <key>CFBundleVersion</key>
22
- <string>16.3.0</string>
22
+ <string>16.4.0</string>
23
23
  <key>NSPrincipalClass</key>
24
24
  <string/>
25
25
  </dict>
@@ -47,4 +47,67 @@ final class WDADeviceIntegrationTests: WDAWatchIntegrationTestCase {
47
47
  let clearResponse = try client.delete("/session/\(sessionId!)/wda/simulatedLocation")
48
48
  XCTAssertEqual(clearResponse.statusCode, 200)
49
49
  }
50
+
51
+ /// rotateDigitalCrownByDelta:/performHandGesture: were only added to the SDK in Xcode 16.3 - WDA calls
52
+ /// them dynamically (NSInvocation) so it keeps building on older Xcode too, but they only actually work
53
+ /// when the toolchain that built this very test bundle (same build as the runner) is new enough. Checked
54
+ /// the same way WDA itself does, via responds(to:), rather than hardcoding an Xcode/OS version number here.
55
+ private var supportsDigitalCrownAndHandGesture: Bool {
56
+ XCUIDevice.shared.responds(to: Selector(("rotateDigitalCrownByDelta:")))
57
+ }
58
+
59
+ func testRotateDigitalCrown() throws {
60
+ let response = try client.post("/session/\(sessionId!)/wda/rotateDigitalCrown", body: [
61
+ "delta": 0.2,
62
+ "velocity": 1.0,
63
+ ])
64
+ XCTAssertEqual(response.statusCode, supportsDigitalCrownAndHandGesture ? 200 : 500)
65
+ }
66
+
67
+ func testRotateDigitalCrownDefaultVelocity() throws {
68
+ let response = try client.post("/session/\(sessionId!)/wda/rotateDigitalCrown", body: [
69
+ "delta": -0.2,
70
+ ])
71
+ XCTAssertEqual(response.statusCode, supportsDigitalCrownAndHandGesture ? 200 : 500)
72
+ }
73
+
74
+ func testRotateDigitalCrownMissingDelta() throws {
75
+ // Argument validation happens before the dynamic dispatch check, so this is always a 400.
76
+ let response = try client.post("/session/\(sessionId!)/wda/rotateDigitalCrown")
77
+ XCTAssertEqual(response.statusCode, 400)
78
+ }
79
+
80
+ func testPerformHandGestureDoubleTap() throws {
81
+ let response = try client.post("/session/\(sessionId!)/wda/performHandGesture", body: [
82
+ "name": "doubleTap",
83
+ ])
84
+ XCTAssertEqual(response.statusCode, supportsDigitalCrownAndHandGesture ? 200 : 500)
85
+ }
86
+
87
+ func testPerformHandGestureFlick() throws {
88
+ let response = try client.post("/session/\(sessionId!)/wda/performHandGesture", body: [
89
+ "name": "flick",
90
+ ])
91
+ // flick additionally needs watchOS 26+ (still internally versioned 12.0 pre-rename) on top of the
92
+ // Xcode 16.3+ toolchain floor - below that OS version it isn't even advertised as a supported name,
93
+ // so the server rejects it the same way it would reject any other unknown gesture name.
94
+ if #available(watchOS 12.0, *), supportsDigitalCrownAndHandGesture {
95
+ XCTAssertEqual(response.statusCode, 200)
96
+ } else {
97
+ XCTAssertEqual(response.statusCode, 500)
98
+ }
99
+ }
100
+
101
+ func testPerformHandGestureUnsupportedName() throws {
102
+ // Not a real gesture name in any environment, so always rejected regardless of toolchain/OS version.
103
+ let response = try client.post("/session/\(sessionId!)/wda/performHandGesture", body: [
104
+ "name": "clench",
105
+ ])
106
+ XCTAssertEqual(response.statusCode, 500)
107
+ }
108
+
109
+ func testPerformHandGestureMissingName() throws {
110
+ let response = try client.post("/session/\(sessionId!)/wda/performHandGesture")
111
+ XCTAssertEqual(response.statusCode, 400)
112
+ }
50
113
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-webdriveragent",
3
- "version": "16.3.0",
3
+ "version": "16.4.0",
4
4
  "description": "Package bundling WebDriverAgent",
5
5
  "keywords": [
6
6
  "Appium",