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 +6 -0
- package/PrivateHeaders/XCTest/XCUIDevice.h +0 -1
- package/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h +34 -0
- package/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m +72 -0
- package/WebDriverAgentLib/Commands/FBCustomCommands.m +36 -0
- package/WebDriverAgentLib/Info.plist +2 -2
- package/WebDriverAgentTests/IntegrationTests_watchOS/WDADeviceIntegrationTests.swift +63 -0
- package/package.json +1 -1
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.
|
|
18
|
+
<string>16.4.0</string>
|
|
19
19
|
<key>CFBundleSignature</key>
|
|
20
20
|
<string>????</string>
|
|
21
21
|
<key>CFBundleVersion</key>
|
|
22
|
-
<string>16.
|
|
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
|
}
|