appium-webdriveragent 16.3.0 → 16.5.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 (59) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/PrivateHeaders/XCTest/XCUIDevice.h +0 -1
  3. package/Scripts/build-webdriveragent.mjs +9 -9
  4. package/Scripts/build.sh +8 -1
  5. package/WebDriverAgent.xcodeproj/project.pbxproj +125 -107
  6. package/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationTests_watchOS.xcscheme +4 -4
  7. package/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h +34 -0
  8. package/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m +72 -0
  9. package/WebDriverAgentLib/Commands/FBCustomCommands.m +36 -0
  10. package/WebDriverAgentLib/FBAlert.m +8 -6
  11. package/WebDriverAgentLib/Info.plist +2 -2
  12. package/WebDriverAgentTests/IntegrationTests_watchOS/IntegrationTests_watchOS-Bridging-Header.h +21 -0
  13. package/WebDriverAgentTests/IntegrationTests_watchOS/WDAAlertIntegrationTests.swift +11 -9
  14. package/WebDriverAgentTests/IntegrationTests_watchOS/WDAAppLifecycleIntegrationTests.swift +18 -38
  15. package/WebDriverAgentTests/IntegrationTests_watchOS/WDAClickIntegrationTests.swift +11 -17
  16. package/WebDriverAgentTests/IntegrationTests_watchOS/WDADeviceIntegrationTests.swift +74 -32
  17. package/WebDriverAgentTests/IntegrationTests_watchOS/WDAElementAttributeIntegrationTests.swift +23 -41
  18. package/WebDriverAgentTests/IntegrationTests_watchOS/WDAFindIntegrationTests.swift +70 -95
  19. package/WebDriverAgentTests/IntegrationTests_watchOS/WDAScreenshotAndSourceIntegrationTests.swift +24 -22
  20. package/WebDriverAgentTests/IntegrationTests_watchOS/WDATypingIntegrationTests.swift +44 -28
  21. package/WebDriverAgentTests/IntegrationTests_watchOS/WDAWatchInProcessTestCase.swift +51 -0
  22. package/build/lib/check-dependencies.d.ts.map +1 -1
  23. package/build/lib/check-dependencies.js +21 -7
  24. package/build/lib/check-dependencies.js.map +1 -1
  25. package/build/lib/constants.d.ts +1 -2
  26. package/build/lib/constants.d.ts.map +1 -1
  27. package/build/lib/constants.js +1 -2
  28. package/build/lib/constants.js.map +1 -1
  29. package/build/lib/types.d.ts +2 -2
  30. package/build/lib/types.d.ts.map +1 -1
  31. package/build/lib/utils/index.d.ts +1 -1
  32. package/build/lib/utils/index.d.ts.map +1 -1
  33. package/build/lib/utils/index.js +1 -1
  34. package/build/lib/utils/index.js.map +1 -1
  35. package/build/lib/utils/platform.d.ts +12 -0
  36. package/build/lib/utils/platform.d.ts.map +1 -1
  37. package/build/lib/utils/platform.js +23 -1
  38. package/build/lib/utils/platform.js.map +1 -1
  39. package/build/lib/utils/xctestrun.js +27 -3
  40. package/build/lib/utils/xctestrun.js.map +1 -1
  41. package/build/lib/webdriveragent.d.ts.map +1 -1
  42. package/build/lib/webdriveragent.js +4 -1
  43. package/build/lib/webdriveragent.js.map +1 -1
  44. package/build/lib/xcodebuild.d.ts.map +1 -1
  45. package/build/lib/xcodebuild.js +19 -9
  46. package/build/lib/xcodebuild.js.map +1 -1
  47. package/lib/check-dependencies.ts +23 -7
  48. package/lib/constants.ts +1 -2
  49. package/lib/types.ts +4 -2
  50. package/lib/utils/index.ts +1 -1
  51. package/lib/utils/platform.ts +25 -1
  52. package/lib/utils/xctestrun.ts +29 -3
  53. package/lib/webdriveragent.ts +5 -1
  54. package/lib/xcodebuild.ts +27 -12
  55. package/package.json +3 -2
  56. package/WebDriverAgentTests/IntegrationTests_watchOS/WDASessionIntegrationTests.swift +0 -72
  57. package/WebDriverAgentTests/IntegrationTests_watchOS/WDAUnknownCommandIntegrationTests.swift +0 -24
  58. package/WebDriverAgentTests/IntegrationTests_watchOS/WDAWatchHTTPClient.swift +0 -95
  59. package/WebDriverAgentTests/IntegrationTests_watchOS/WDAWatchIntegrationTestCase.swift +0 -61
@@ -14,7 +14,7 @@
14
14
  buildForAnalyzing = "NO">
15
15
  <BuildableReference
16
16
  BuildableIdentifier = "primary"
17
- BlueprintIdentifier = "4632809915F58C9095914E85"
17
+ BlueprintIdentifier = "5D9539D7055B5E6EED56D100"
18
18
  BuildableName = "IntegrationTests_watchOS.xctest"
19
19
  BlueprintName = "IntegrationTests_watchOS"
20
20
  ReferencedContainer = "container:WebDriverAgent.xcodeproj">
@@ -32,7 +32,7 @@
32
32
  skipped = "NO">
33
33
  <BuildableReference
34
34
  BuildableIdentifier = "primary"
35
- BlueprintIdentifier = "4632809915F58C9095914E85"
35
+ BlueprintIdentifier = "5D9539D7055B5E6EED56D100"
36
36
  BuildableName = "IntegrationTests_watchOS.xctest"
37
37
  BlueprintName = "IntegrationTests_watchOS"
38
38
  ReferencedContainer = "container:WebDriverAgent.xcodeproj">
@@ -53,7 +53,7 @@
53
53
  <MacroExpansion>
54
54
  <BuildableReference
55
55
  BuildableIdentifier = "primary"
56
- BlueprintIdentifier = "4632809915F58C9095914E85"
56
+ BlueprintIdentifier = "5D9539D7055B5E6EED56D100"
57
57
  BuildableName = "IntegrationTests_watchOS.xctest"
58
58
  BlueprintName = "IntegrationTests_watchOS"
59
59
  ReferencedContainer = "container:WebDriverAgent.xcodeproj">
@@ -69,7 +69,7 @@
69
69
  <MacroExpansion>
70
70
  <BuildableReference
71
71
  BuildableIdentifier = "primary"
72
- BlueprintIdentifier = "4632809915F58C9095914E85"
72
+ BlueprintIdentifier = "5D9539D7055B5E6EED56D100"
73
73
  BuildableName = "IntegrationTests_watchOS.xctest"
74
74
  BlueprintName = "IntegrationTests_watchOS"
75
75
  ReferencedContainer = "container:WebDriverAgent.xcodeproj">
@@ -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;
@@ -236,17 +236,18 @@
236
236
  }
237
237
 
238
238
  XCUIElement *acceptButton = nil;
239
- if (FBConfiguration.sharedInstance.acceptAlertButtonSelector.length) {
239
+ NSString *acceptAlertButtonSelector = FBConfiguration.sharedInstance.acceptAlertButtonSelector ?: @"";
240
+ if (acceptAlertButtonSelector.length) {
240
241
  NSString *errorReason = nil;
241
242
  @try {
242
- acceptButton = [[alertElement fb_descendantsMatchingClassChain:FBConfiguration.sharedInstance.acceptAlertButtonSelector
243
+ acceptButton = [[alertElement fb_descendantsMatchingClassChain:acceptAlertButtonSelector
243
244
  shouldReturnAfterFirstMatch:YES] firstObject];
244
245
  } @catch (NSException *ex) {
245
246
  errorReason = ex.reason;
246
247
  }
247
248
  if (nil == acceptButton) {
248
249
  [FBLogger logFmt:@"Cannot find any match for Accept alert button using the class chain selector '%@'",
249
- FBConfiguration.sharedInstance.acceptAlertButtonSelector];
250
+ acceptAlertButtonSelector];
250
251
  if (nil != errorReason) {
251
252
  [FBLogger logFmt:@"Original error: %@", errorReason];
252
253
  }
@@ -293,17 +294,18 @@
293
294
  }
294
295
 
295
296
  XCUIElement *dismissButton = nil;
296
- if (FBConfiguration.sharedInstance.dismissAlertButtonSelector.length) {
297
+ NSString *dismissAlertButtonSelector = FBConfiguration.sharedInstance.dismissAlertButtonSelector ?: @"";
298
+ if (dismissAlertButtonSelector.length) {
297
299
  NSString *errorReason = nil;
298
300
  @try {
299
- dismissButton = [[alertElement fb_descendantsMatchingClassChain:FBConfiguration.sharedInstance.dismissAlertButtonSelector
301
+ dismissButton = [[alertElement fb_descendantsMatchingClassChain:dismissAlertButtonSelector
300
302
  shouldReturnAfterFirstMatch:YES] firstObject];
301
303
  } @catch (NSException *ex) {
302
304
  errorReason = ex.reason;
303
305
  }
304
306
  if (nil == dismissButton) {
305
307
  [FBLogger logFmt:@"Cannot find any match for Dismiss alert button using the class chain selector '%@'",
306
- FBConfiguration.sharedInstance.dismissAlertButtonSelector];
308
+ dismissAlertButtonSelector];
307
309
  if (nil != errorReason) {
308
310
  [FBLogger logFmt:@"Original error: %@", errorReason];
309
311
  }
@@ -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.5.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.5.0</string>
23
23
  <key>NSPrincipalClass</key>
24
24
  <string/>
25
25
  </dict>
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Copyright (c) 2015-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ // Exposes the WebDriverAgentLib_watchOS categories these tests call directly, in-process,
10
+ // instead of going through the HTTP server.
11
+
12
+ #import "XCUIElement+FBFind.h"
13
+ #import "XCUIElement+FBClassChain.h"
14
+ #import "XCUIElement+FBIsVisible.h"
15
+ #import "XCUIElement+FBUtilities.h"
16
+ #import "XCUIElement+FBWebDriverAttributes.h"
17
+ #import "XCUIElement+FBTyping.h"
18
+ #import "XCUIApplication+FBHelpers.h"
19
+ #import "XCUIDevice+FBHelpers.h"
20
+ #import "FBAlert.h"
21
+ #import "FBElement.h"
@@ -8,16 +8,18 @@
8
8
 
9
9
  import XCTest
10
10
 
11
- /// Covers FBAlertViewCommands' error path - IntegrationApp_watchOS never presents a real alert,
12
- /// so this only exercises the "no alert open" error, not the success path.
13
- final class WDAAlertIntegrationTests: WDAWatchIntegrationTestCase {
14
- func testAlertTextWithNoAlertPresentReturnsAnError() throws {
15
- let response = try client.get("/session/\(sessionId!)/alert/text")
16
- XCTAssertNotEqual(response.statusCode, 200)
11
+ /// Covers FBAlert's no-alert-present behavior directly, in-process. IntegrationApp_watchOS never
12
+ /// presents a real alert, so this only exercises the "no alert open" path (`text`/`buttonLabels`
13
+ /// both just return nil - no HTTP-error-style throw at this layer), not the success path.
14
+ final class WDAAlertIntegrationTests: WDAWatchInProcessTestCase {
15
+ // All reads, no taps/typing - reuse the same running app across the whole class.
16
+ override class var relaunchForEachTest: Bool { false }
17
+
18
+ func testAlertTextWithNoAlertPresentReturnsNil() {
19
+ XCTAssertNil(FBAlert(application: app).text())
17
20
  }
18
21
 
19
- func testAlertButtonsWithNoAlertPresentReturnsAnError() throws {
20
- let response = try client.get("/session/\(sessionId!)/wda/alert/buttons")
21
- XCTAssertNotEqual(response.statusCode, 200)
22
+ func testAlertButtonsWithNoAlertPresentReturnsNil() {
23
+ XCTAssertNil(FBAlert(application: app).buttonLabels())
22
24
  }
23
25
  }
@@ -8,50 +8,30 @@
8
8
 
9
9
  import XCTest
10
10
 
11
- /// Covers the app lifecycle routes (launch/activate/terminate/state/list). Unlike
12
- /// /wda/homescreen or /wda/lock, these only ever target IntegrationApp_watchOS, so they're
13
- /// safe to run repeatedly without disrupting shared simulator state.
14
- final class WDAAppLifecycleIntegrationTests: WDAWatchIntegrationTestCase {
15
- // XCUIApplicationState raw values: NotRunning = 1, RunningForeground = 4.
16
- private static let notRunningState = 1
17
- private static let runningForegroundState = 4
18
-
19
- func testAppsListIncludesTheAppUnderTest() throws {
20
- let response = try client.get("/session/\(sessionId!)/wda/apps/list")
21
- XCTAssertEqual(response.statusCode, 200)
22
- guard let apps = response.value as? [[String: Any]] else {
23
- return XCTFail("Expected an array of active apps: \(String(describing: response.json))")
24
- }
25
- XCTAssertTrue(apps.contains { $0["bundleId"] as? String == Self.integrationAppBundleId })
11
+ /// Covers the app lifecycle routes (launch/activate/terminate/state/list). FBSession's
12
+ /// implementations of these are themselves thin wrappers over plain XCTest XCUIApplication
13
+ /// APIs (see FBSession.m), so this calls `.launch()`/`.activate()`/`.terminate()`/`.state`
14
+ /// directly instead of going through the HTTP server; only the apps-list route has a real WDA
15
+ /// category behind it (`+fb_activeAppsInfo`).
16
+ final class WDAAppLifecycleIntegrationTests: WDAWatchInProcessTestCase {
17
+ func testAppsListIncludesTheAppUnderTest() {
18
+ let apps = XCUIApplication.fb_activeAppsInfo()
19
+ XCTAssertTrue(apps.contains { $0["bundleId"] as? String == WDAWatchInProcessTestCase.integrationAppBundleId })
26
20
  }
27
21
 
28
- func testTerminateThenRelaunchLifecycle() throws {
29
- let bundleId = Self.integrationAppBundleId
30
-
31
- let terminateResponse = try client.post("/session/\(sessionId!)/wda/apps/terminate", body: ["bundleId": bundleId])
32
- XCTAssertEqual(terminateResponse.statusCode, 200)
33
- XCTAssertEqual(terminateResponse.value as? Bool, true)
34
-
35
- let stateAfterTerminate = try client.post("/session/\(sessionId!)/wda/apps/state", body: ["bundleId": bundleId])
36
- XCTAssertEqual(stateAfterTerminate.value as? Int, Self.notRunningState)
22
+ func testTerminateThenRelaunchLifecycle() {
23
+ app.terminate()
24
+ XCTAssertEqual(app.state, .notRunning)
37
25
 
38
- let launchResponse = try client.post("/session/\(sessionId!)/wda/apps/launch", body: ["bundleId": bundleId])
39
- XCTAssertEqual(launchResponse.statusCode, 200)
40
- Thread.sleep(forTimeInterval: 1)
41
-
42
- let stateAfterLaunch = try client.post("/session/\(sessionId!)/wda/apps/state", body: ["bundleId": bundleId])
43
- XCTAssertEqual(stateAfterLaunch.value as? Int, Self.runningForegroundState)
26
+ app.launch()
27
+ XCTAssertEqual(app.state, .runningForeground)
44
28
 
45
29
  // The app relaunched fresh, so its accessibility tree should be back too.
46
- XCTAssertNoThrow(try findElement(byAccessibilityId: "tapMeButton"))
30
+ XCTAssertTrue(app.buttons["tapMeButton"].waitForExistence(timeout: 15))
47
31
  }
48
32
 
49
- func testActivateBringsTheAppToTheForeground() throws {
50
- let bundleId = Self.integrationAppBundleId
51
- let activateResponse = try client.post("/session/\(sessionId!)/wda/apps/activate", body: ["bundleId": bundleId])
52
- XCTAssertEqual(activateResponse.statusCode, 200)
53
-
54
- let stateResponse = try client.post("/session/\(sessionId!)/wda/apps/state", body: ["bundleId": bundleId])
55
- XCTAssertEqual(stateResponse.value as? Int, Self.runningForegroundState)
33
+ func testActivateBringsTheAppToTheForeground() {
34
+ app.activate()
35
+ XCTAssertEqual(app.state, .runningForeground)
56
36
  }
57
37
  }
@@ -8,24 +8,18 @@
8
8
 
9
9
  import XCTest
10
10
 
11
- /// Covers /element/:uuid/click - the only tap-like interaction registered on watchOS
12
- /// (gesture synthesis routes aren't).
13
- final class WDAClickIntegrationTests: WDAWatchIntegrationTestCase {
14
- func testClickingButtonUpdatesTheResultLabel() throws {
15
- let labelId = try findElement(byAccessibilityId: "resultLabel")
16
- XCTAssertEqual(try attributeValue(labelId, "label"), "Idle")
11
+ /// Covers /element/:uuid/click, which on watchOS (like iOS) is just standard XCTest `-tap`
12
+ /// (see FBElementCommands.handleClick:) - so this just calls `.tap()` directly, the same way
13
+ /// the iOS/tvOS integration tests do. The "click a nonexistent element UUID" case isn't a `tap`
14
+ /// behavior at all (it's FBElementCache's UUID-lookup failure), so it's covered by the HTTP
15
+ /// end-to-end tests instead, where there's an actual UUID/route layer to miss against.
16
+ final class WDAClickIntegrationTests: WDAWatchInProcessTestCase {
17
+ func testClickingButtonUpdatesTheResultLabel() {
18
+ let label = app.staticTexts["resultLabel"]
19
+ XCTAssertEqual(label.wdValue ?? label.wdLabel, "Idle")
17
20
 
18
- let buttonId = try findElement(byAccessibilityId: "tapMeButton")
19
- let clickResponse = try client.post("/session/\(sessionId!)/element/\(buttonId)/click")
20
- XCTAssertEqual(clickResponse.statusCode, 200)
21
+ app.buttons["tapMeButton"].tap()
21
22
 
22
- // Re-find: element UUIDs aren't stable across snapshots, and the label just changed.
23
- let updatedLabelId = try findElement(byAccessibilityId: "resultLabel")
24
- XCTAssertEqual(try attributeValue(updatedLabelId, "label"), "Tapped")
25
- }
26
-
27
- func testClickingAnElementThatDoesNotExistReturnsAnError() throws {
28
- let response = try client.post("/session/\(sessionId!)/element/00000000-0000-0000-0000-000000000000/click")
29
- XCTAssertNotEqual(response.statusCode, 200)
23
+ XCTAssertEqual(label.wdValue ?? label.wdLabel, "Tapped")
30
24
  }
31
25
  }
@@ -6,45 +6,87 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
 
9
+ import CoreLocation
10
+ import WatchKit
9
11
  import XCTest
10
12
 
11
- /// Covers device-info and location routes. These only read device state or set a simulated
12
- /// GPS fix, so unlike /wda/lock or /wda/voiceOver/enable, they won't disrupt later tests.
13
- final class WDADeviceIntegrationTests: WDAWatchIntegrationTestCase {
14
- func testDeviceInfo() throws {
15
- let response = try client.get("/session/\(sessionId!)/wda/device/info")
16
- XCTAssertEqual(response.statusCode, 200)
17
- XCTAssertFalse((response.valueDict?["name"] as? String ?? "").isEmpty)
18
- XCTAssertFalse((response.valueDict?["model"] as? String ?? "").isEmpty)
13
+ /// Covers device-info, active-app-info, simulated location, digital crown rotation, and hand
14
+ /// gesture, calling the same underlying APIs FBCustomCommands/XCUIDevice+FBHelpers use directly
15
+ /// in-process. The real (non-simulated) GPS location route and the two pure argument-validation
16
+ /// cases (missing delta/gesture name, rejected before any category method is even called) aren't
17
+ /// WDA category behavior, so they're not covered here.
18
+ final class WDADeviceIntegrationTests: WDAWatchInProcessTestCase {
19
+ func testDeviceInfo() {
20
+ XCTAssertFalse(WKInterfaceDevice.current().name.isEmpty)
21
+ XCTAssertFalse(WKInterfaceDevice.current().model.isEmpty)
19
22
  }
20
23
 
21
- func testActiveAppInfo() throws {
22
- let response = try client.get("/session/\(sessionId!)/wda/activeAppInfo")
23
- XCTAssertEqual(response.statusCode, 200)
24
- XCTAssertEqual(response.valueDict?["bundleId"] as? String, Self.integrationAppBundleId)
25
- XCTAssertGreaterThan(response.valueDict?["pid"] as? Int ?? 0, 0)
24
+ func testActiveAppInfo() {
25
+ let apps = XCUIApplication.fb_activeAppsInfo()
26
+ let ourApp = apps.first { $0["bundleId"] as? String == WDAWatchInProcessTestCase.integrationAppBundleId }
27
+ XCTAssertNotNil(ourApp)
28
+ XCTAssertGreaterThan(ourApp?["pid"] as? Int ?? 0, 0)
26
29
  }
27
30
 
28
- func testDeviceLocation() throws {
29
- let response = try client.get("/session/\(sessionId!)/wda/device/location")
30
- XCTAssertEqual(response.statusCode, 200)
31
- XCTAssertNotNil(response.valueDict?["latitude"])
32
- XCTAssertNotNil(response.valueDict?["authorizationStatus"])
31
+ func testSimulatedLocationRoundTrip() throws {
32
+ let location = CLLocation(latitude: 37.3230, longitude: -122.0322)
33
+ try XCUIDevice.shared.fb_setSimulatedLocation(location)
34
+
35
+ let fetched = try XCUIDevice.shared.fb_getSimulatedLocation()
36
+ XCTAssertEqual(fetched.coordinate.latitude, 37.3230, accuracy: 0.001)
37
+ XCTAssertEqual(fetched.coordinate.longitude, -122.0322, accuracy: 0.001)
38
+
39
+ try XCUIDevice.shared.fb_clearSimulatedLocation()
33
40
  }
34
41
 
35
- func testSimulatedLocationRoundTrip() throws {
36
- let setResponse = try client.post("/session/\(sessionId!)/wda/simulatedLocation", body: [
37
- "latitude": 37.3230,
38
- "longitude": -122.0322,
39
- ])
40
- XCTAssertEqual(setResponse.statusCode, 200)
41
-
42
- let getResponse = try client.get("/session/\(sessionId!)/wda/simulatedLocation")
43
- XCTAssertEqual(getResponse.statusCode, 200)
44
- XCTAssertEqual(getResponse.valueDict?["latitude"] as? Double ?? 0, 37.3230, accuracy: 0.001)
45
- XCTAssertEqual(getResponse.valueDict?["longitude"] as? Double ?? 0, -122.0322, accuracy: 0.001)
46
-
47
- let clearResponse = try client.delete("/session/\(sessionId!)/wda/simulatedLocation")
48
- XCTAssertEqual(clearResponse.statusCode, 200)
42
+ /// rotateDigitalCrownByDelta:/performHandGesture: were only added to the SDK in Xcode 16.3 - WDA calls
43
+ /// them dynamically (NSInvocation) so it keeps building on older Xcode too, but they only actually work
44
+ /// when the toolchain that built this very test bundle (same build as the runner) is new enough. Checked
45
+ /// the same way WDA itself does, via responds(to:), rather than hardcoding an Xcode/OS version number here.
46
+ private var supportsDigitalCrownAndHandGesture: Bool {
47
+ XCUIDevice.shared.responds(to: NSSelectorFromString("rotateDigitalCrownByDelta:"))
48
+ }
49
+
50
+ private func assertSucceeds(_ expectedToSucceed: Bool, file: StaticString = #filePath, line: UInt = #line, _ body: () throws -> Void) {
51
+ do {
52
+ try body()
53
+ XCTAssertTrue(expectedToSucceed, "expected this to throw, but it didn't", file: file, line: line)
54
+ } catch {
55
+ XCTAssertFalse(expectedToSucceed, "expected this to succeed, but it threw \(error)", file: file, line: line)
56
+ }
57
+ }
58
+
59
+ func testRotateDigitalCrown() {
60
+ assertSucceeds(supportsDigitalCrownAndHandGesture) {
61
+ try XCUIDevice.shared.fb_rotateDigitalCrown(0.2, velocity: 1.0)
62
+ }
63
+ }
64
+
65
+ func testPerformHandGestureDoubleTap() {
66
+ assertSucceeds(supportsDigitalCrownAndHandGesture) {
67
+ try XCUIDevice.shared.fb_performHandGesture("doubleTap")
68
+ }
69
+ }
70
+
71
+ func testPerformHandGestureFlick() {
72
+ // flick additionally needs watchOS 26+ (still internally versioned 12.0 pre-rename) on top of the
73
+ // Xcode 16.3+ toolchain floor - below that OS version it isn't even advertised as a supported name,
74
+ // so the server rejects it the same way it would reject any other unknown gesture name.
75
+ let shouldSucceed: Bool
76
+ if #available(watchOS 12.0, *) {
77
+ shouldSucceed = supportsDigitalCrownAndHandGesture
78
+ } else {
79
+ shouldSucceed = false
80
+ }
81
+ assertSucceeds(shouldSucceed) {
82
+ try XCUIDevice.shared.fb_performHandGesture("flick")
83
+ }
84
+ }
85
+
86
+ func testPerformHandGestureUnsupportedName() {
87
+ // Not a real gesture name in any environment, so always rejected regardless of toolchain/OS version.
88
+ assertSucceeds(false) {
89
+ try XCUIDevice.shared.fb_performHandGesture("clench")
90
+ }
49
91
  }
50
92
  }
@@ -8,50 +8,32 @@
8
8
 
9
9
  import XCTest
10
10
 
11
- /// Covers FBElementCommands' read-only element/window attribute routes.
12
- final class WDAElementAttributeIntegrationTests: WDAWatchIntegrationTestCase {
13
- func testWindowSizeAndRect() throws {
14
- let sizeResponse = try client.get("/session/\(sessionId!)/window/size")
15
- XCTAssertEqual(sizeResponse.statusCode, 200)
16
- XCTAssertGreaterThan(sizeResponse.valueDict?["width"] as? Double ?? 0, 0)
17
- XCTAssertGreaterThan(sizeResponse.valueDict?["height"] as? Double ?? 0, 0)
18
-
19
- let rectResponse = try client.get("/session/\(sessionId!)/window/rect")
20
- XCTAssertEqual(rectResponse.statusCode, 200)
21
- XCTAssertGreaterThan(rectResponse.valueDict?["width"] as? Double ?? 0, 0)
11
+ /// Covers the FBElement attribute properties (wdEnabled/wdVisible/wdRect/wdType/wdLabel/
12
+ /// wdSelected/wdAccessible/fb_valueForWDAttributeName:) directly, in-process - the same
13
+ /// properties FBElementCommands' attribute routes read from.
14
+ final class WDAElementAttributeIntegrationTests: WDAWatchInProcessTestCase {
15
+ // All reads, no taps/typing - reuse the same running app across the whole class.
16
+ override class var relaunchForEachTest: Bool { false }
17
+
18
+ func testWindowSizeAndRect() {
19
+ XCTAssertGreaterThan(app.wdFrame.width, 0)
20
+ XCTAssertGreaterThan(app.wdFrame.height, 0)
22
21
  }
23
22
 
24
- func testButtonAttributes() throws {
25
- let buttonId = try findElement(byAccessibilityId: "tapMeButton")
26
-
27
- let enabledResponse = try client.get("/session/\(sessionId!)/element/\(buttonId)/enabled")
28
- XCTAssertEqual(enabledResponse.value as? Bool, true)
29
-
30
- let displayedResponse = try client.get("/session/\(sessionId!)/element/\(buttonId)/displayed")
31
- XCTAssertEqual(displayedResponse.value as? Bool, true)
32
-
33
- let rectResponse = try client.get("/session/\(sessionId!)/element/\(buttonId)/rect")
34
- XCTAssertEqual(rectResponse.statusCode, 200)
35
- XCTAssertGreaterThan(rectResponse.valueDict?["width"] as? Double ?? 0, 0)
36
-
37
- let nameResponse = try client.get("/session/\(sessionId!)/element/\(buttonId)/name")
38
- XCTAssertEqual(nameResponse.valueString, "XCUIElementTypeButton")
39
-
40
- let labelResponse = try client.get("/session/\(sessionId!)/element/\(buttonId)/attribute/label")
41
- XCTAssertEqual(labelResponse.valueString, "Tap Me")
42
-
43
- let accessibleResponse = try client.get("/session/\(sessionId!)/wda/element/\(buttonId)/accessible")
44
- XCTAssertEqual(accessibleResponse.statusCode, 200)
23
+ func testButtonAttributes() {
24
+ let button = app.buttons["tapMeButton"]
25
+ XCTAssertTrue(button.isWDEnabled)
26
+ XCTAssertTrue(button.isWDVisible)
27
+ XCTAssertGreaterThan(button.wdRect["width"] as? Double ?? 0, 0)
28
+ XCTAssertEqual(button.wdType, "XCUIElementTypeButton")
29
+ XCTAssertEqual(button.wdLabel, "Tap Me")
30
+ XCTAssertEqual(button.fb_value(forWDAttributeName: "label") as? String, "Tap Me")
31
+ XCTAssertTrue(button.isWDAccessible)
45
32
  }
46
33
 
47
- func testResultLabelTextAndSelectedState() throws {
48
- let labelId = try findElement(byAccessibilityId: "resultLabel")
49
-
50
- let textResponse = try client.get("/session/\(sessionId!)/element/\(labelId)/text")
51
- XCTAssertEqual(textResponse.valueString, "Idle")
52
-
53
- let selectedResponse = try client.get("/session/\(sessionId!)/element/\(labelId)/selected")
54
- XCTAssertEqual(selectedResponse.statusCode, 200)
55
- XCTAssertEqual(selectedResponse.value as? Bool, false)
34
+ func testResultLabelTextAndSelectedState() {
35
+ let label = app.staticTexts["resultLabel"]
36
+ XCTAssertEqual(label.wdValue ?? label.wdLabel, "Idle")
37
+ XCTAssertFalse(label.isWDSelected)
56
38
  }
57
39
  }