FBCustomCommands.m 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  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. An additional grant
  7. * of patent rights can be found in the PATENTS file in the same directory.
  8. */
  9. #import "FBCustomCommands.h"
  10. #import <XCTest/XCUIDevice.h>
  11. #import <CoreLocation/CoreLocation.h>
  12. #import "FBConfiguration.h"
  13. #import "FBKeyboard.h"
  14. #import "FBNotificationsHelper.h"
  15. #import "FBMathUtils.h"
  16. #import "FBPasteboard.h"
  17. #import "FBResponsePayload.h"
  18. #import "FBRoute.h"
  19. #import "FBRouteRequest.h"
  20. #import "FBRunLoopSpinner.h"
  21. #import "FBScreen.h"
  22. #import "FBSession.h"
  23. #import "FBXCodeCompatibility.h"
  24. #import "XCUIApplication.h"
  25. #import "XCUIApplication+FBHelpers.h"
  26. #import "XCUIDevice+FBHelpers.h"
  27. #import "XCUIElement.h"
  28. #import "XCUIElement+FBIsVisible.h"
  29. #import "XCUIElementQuery.h"
  30. #import "FBUnattachedAppLauncher.h"
  31. @implementation FBCustomCommands
  32. + (NSArray *)routes
  33. {
  34. return
  35. @[
  36. [[FBRoute POST:@"/timeouts"] respondWithTarget:self action:@selector(handleTimeouts:)],
  37. [[FBRoute POST:@"/wda/homescreen"].withoutSession respondWithTarget:self action:@selector(handleHomescreenCommand:)],
  38. [[FBRoute POST:@"/wda/deactivateApp"] respondWithTarget:self action:@selector(handleDeactivateAppCommand:)],
  39. [[FBRoute POST:@"/wda/keyboard/dismiss"] respondWithTarget:self action:@selector(handleDismissKeyboardCommand:)],
  40. [[FBRoute POST:@"/wda/lock"].withoutSession respondWithTarget:self action:@selector(handleLock:)],
  41. [[FBRoute POST:@"/wda/lock"] respondWithTarget:self action:@selector(handleLock:)],
  42. [[FBRoute POST:@"/wda/unlock"].withoutSession respondWithTarget:self action:@selector(handleUnlock:)],
  43. [[FBRoute POST:@"/wda/unlock"] respondWithTarget:self action:@selector(handleUnlock:)],
  44. [[FBRoute GET:@"/wda/locked"].withoutSession respondWithTarget:self action:@selector(handleIsLocked:)],
  45. [[FBRoute GET:@"/wda/locked"] respondWithTarget:self action:@selector(handleIsLocked:)],
  46. [[FBRoute GET:@"/wda/screen"] respondWithTarget:self action:@selector(handleGetScreen:)],
  47. [[FBRoute GET:@"/wda/screen"].withoutSession respondWithTarget:self action:@selector(handleGetScreen:)],
  48. [[FBRoute GET:@"/wda/activeAppInfo"] respondWithTarget:self action:@selector(handleActiveAppInfo:)],
  49. [[FBRoute GET:@"/wda/activeAppInfo"].withoutSession respondWithTarget:self action:@selector(handleActiveAppInfo:)],
  50. #if !TARGET_OS_TV // tvOS does not provide relevant APIs
  51. [[FBRoute POST:@"/wda/setPasteboard"] respondWithTarget:self action:@selector(handleSetPasteboard:)],
  52. [[FBRoute POST:@"/wda/setPasteboard"].withoutSession respondWithTarget:self action:@selector(handleSetPasteboard:)],
  53. [[FBRoute POST:@"/wda/getPasteboard"] respondWithTarget:self action:@selector(handleGetPasteboard:)],
  54. [[FBRoute POST:@"/wda/getPasteboard"].withoutSession respondWithTarget:self action:@selector(handleGetPasteboard:)],
  55. [[FBRoute GET:@"/wda/batteryInfo"] respondWithTarget:self action:@selector(handleGetBatteryInfo:)],
  56. #endif
  57. [[FBRoute POST:@"/wda/pressButton"] respondWithTarget:self action:@selector(handlePressButtonCommand:)],
  58. [[FBRoute POST:@"/wda/performAccessibilityAudit"] respondWithTarget:self action:@selector(handlePerformAccessibilityAudit:)],
  59. [[FBRoute POST:@"/wda/performIoHidEvent"] respondWithTarget:self action:@selector(handlePeformIOHIDEvent:)],
  60. [[FBRoute POST:@"/wda/expectNotification"] respondWithTarget:self action:@selector(handleExpectNotification:)],
  61. [[FBRoute POST:@"/wda/siri/activate"] respondWithTarget:self action:@selector(handleActivateSiri:)],
  62. [[FBRoute POST:@"/wda/apps/launchUnattached"].withoutSession respondWithTarget:self action:@selector(handleLaunchUnattachedApp:)],
  63. [[FBRoute GET:@"/wda/device/info"] respondWithTarget:self action:@selector(handleGetDeviceInfo:)],
  64. [[FBRoute POST:@"/wda/resetAppAuth"] respondWithTarget:self action:@selector(handleResetAppAuth:)],
  65. [[FBRoute GET:@"/wda/device/info"].withoutSession respondWithTarget:self action:@selector(handleGetDeviceInfo:)],
  66. [[FBRoute POST:@"/wda/device/appearance"].withoutSession respondWithTarget:self action:@selector(handleSetDeviceAppearance:)],
  67. [[FBRoute GET:@"/wda/device/location"] respondWithTarget:self action:@selector(handleGetLocation:)],
  68. [[FBRoute GET:@"/wda/device/location"].withoutSession respondWithTarget:self action:@selector(handleGetLocation:)],
  69. #if !TARGET_OS_TV // tvOS does not provide relevant APIs
  70. #if __clang_major__ >= 15
  71. [[FBRoute POST:@"/wda/element/:uuid/keyboardInput"] respondWithTarget:self action:@selector(handleKeyboardInput:)],
  72. #endif
  73. [[FBRoute GET:@"/wda/simulatedLocation"] respondWithTarget:self action:@selector(handleGetSimulatedLocation:)],
  74. [[FBRoute GET:@"/wda/simulatedLocation"].withoutSession respondWithTarget:self action:@selector(handleGetSimulatedLocation:)],
  75. [[FBRoute POST:@"/wda/simulatedLocation"] respondWithTarget:self action:@selector(handleSetSimulatedLocation:)],
  76. [[FBRoute POST:@"/wda/simulatedLocation"].withoutSession respondWithTarget:self action:@selector(handleSetSimulatedLocation:)],
  77. [[FBRoute DELETE:@"/wda/simulatedLocation"] respondWithTarget:self action:@selector(handleClearSimulatedLocation:)],
  78. [[FBRoute DELETE:@"/wda/simulatedLocation"].withoutSession respondWithTarget:self action:@selector(handleClearSimulatedLocation:)],
  79. #endif
  80. [[FBRoute OPTIONS:@"/*"].withoutSession respondWithTarget:self action:@selector(handlePingCommand:)],
  81. ];
  82. }
  83. #pragma mark - Commands
  84. + (id<FBResponsePayload>)handleHomescreenCommand:(FBRouteRequest *)request
  85. {
  86. NSError *error;
  87. if (![[XCUIDevice sharedDevice] fb_goToHomescreenWithError:&error]) {
  88. return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
  89. traceback:nil]);
  90. }
  91. return FBResponseWithOK();
  92. }
  93. + (id<FBResponsePayload>)handleDeactivateAppCommand:(FBRouteRequest *)request
  94. {
  95. NSNumber *requestedDuration = request.arguments[@"duration"];
  96. NSTimeInterval duration = (requestedDuration ? requestedDuration.doubleValue : 3.);
  97. NSError *error;
  98. if (![request.session.activeApplication fb_deactivateWithDuration:duration error:&error]) {
  99. return FBResponseWithUnknownError(error);
  100. }
  101. return FBResponseWithOK();
  102. }
  103. + (id<FBResponsePayload>)handleTimeouts:(FBRouteRequest *)request
  104. {
  105. // This method is intentionally not supported.
  106. return FBResponseWithOK();
  107. }
  108. + (id<FBResponsePayload>)handleDismissKeyboardCommand:(FBRouteRequest *)request
  109. {
  110. NSError *error;
  111. BOOL isDismissed = [request.session.activeApplication fb_dismissKeyboardWithKeyNames:request.arguments[@"keyNames"]
  112. error:&error];
  113. return isDismissed
  114. ? FBResponseWithOK()
  115. : FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
  116. traceback:nil]);
  117. }
  118. + (id<FBResponsePayload>)handlePingCommand:(FBRouteRequest *)request
  119. {
  120. return FBResponseWithOK();
  121. }
  122. #pragma mark - Helpers
  123. + (id<FBResponsePayload>)handleGetScreen:(FBRouteRequest *)request
  124. {
  125. XCUIApplication *app = XCUIApplication.fb_systemApplication;
  126. XCUIElement *mainStatusBar = app.statusBars.allElementsBoundByIndex.firstObject;
  127. CGSize statusBarSize = (nil == mainStatusBar) ? CGSizeZero : mainStatusBar.frame.size;
  128. #if TARGET_OS_TV
  129. CGSize screenSize = app.frame.size;
  130. #else
  131. CGSize screenSize = FBAdjustDimensionsForApplication(app.wdFrame.size, app.interfaceOrientation);
  132. #endif
  133. return FBResponseWithObject(
  134. @{
  135. @"screenSize":@{@"width": @(screenSize.width),
  136. @"height": @(screenSize.height)
  137. },
  138. @"statusBarSize": @{@"width": @(statusBarSize.width),
  139. @"height": @(statusBarSize.height),
  140. },
  141. @"scale": @([FBScreen scale]),
  142. });
  143. }
  144. + (id<FBResponsePayload>)handleLock:(FBRouteRequest *)request
  145. {
  146. NSError *error;
  147. if (![[XCUIDevice sharedDevice] fb_lockScreen:&error]) {
  148. return FBResponseWithUnknownError(error);
  149. }
  150. return FBResponseWithOK();
  151. }
  152. + (id<FBResponsePayload>)handleIsLocked:(FBRouteRequest *)request
  153. {
  154. BOOL isLocked = [XCUIDevice sharedDevice].fb_isScreenLocked;
  155. return FBResponseWithObject(isLocked ? @YES : @NO);
  156. }
  157. + (id<FBResponsePayload>)handleUnlock:(FBRouteRequest *)request
  158. {
  159. NSError *error;
  160. if (![[XCUIDevice sharedDevice] fb_unlockScreen:&error]) {
  161. return FBResponseWithUnknownError(error);
  162. }
  163. return FBResponseWithOK();
  164. }
  165. + (id<FBResponsePayload>)handleActiveAppInfo:(FBRouteRequest *)request
  166. {
  167. XCUIApplication *app = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
  168. return FBResponseWithObject(@{
  169. @"pid": @(app.processID),
  170. @"bundleId": app.bundleID,
  171. @"name": app.identifier,
  172. @"processArguments": [self processArguments:app],
  173. });
  174. }
  175. /**
  176. * Returns current active app and its arguments of active session
  177. *
  178. * @return The dictionary of current active bundleId and its process/environment argumens
  179. *
  180. * @example
  181. *
  182. * [self currentActiveApplication]
  183. * //=> {
  184. * // "processArguments" : {
  185. * // "env" : {
  186. * // "HAPPY" : "testing"
  187. * // },
  188. * // "args" : [
  189. * // "happy",
  190. * // "tseting"
  191. * // ]
  192. * // }
  193. *
  194. * [self currentActiveApplication]
  195. * //=> {}
  196. */
  197. + (NSDictionary *)processArguments:(XCUIApplication *)app
  198. {
  199. // Can be nil if no active activation is defined by XCTest
  200. if (app == nil) {
  201. return @{};
  202. }
  203. return
  204. @{
  205. @"args": app.launchArguments,
  206. @"env": app.launchEnvironment
  207. };
  208. }
  209. #if !TARGET_OS_TV
  210. + (id<FBResponsePayload>)handleSetPasteboard:(FBRouteRequest *)request
  211. {
  212. NSString *contentType = request.arguments[@"contentType"] ?: @"plaintext";
  213. NSData *content = [[NSData alloc] initWithBase64EncodedString:(NSString *)request.arguments[@"content"]
  214. options:NSDataBase64DecodingIgnoreUnknownCharacters];
  215. if (nil == content) {
  216. return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Cannot decode the pasteboard content from base64" traceback:nil]);
  217. }
  218. NSError *error;
  219. if (![FBPasteboard setData:content forType:contentType error:&error]) {
  220. return FBResponseWithUnknownError(error);
  221. }
  222. return FBResponseWithOK();
  223. }
  224. + (id<FBResponsePayload>)handleGetPasteboard:(FBRouteRequest *)request
  225. {
  226. NSString *contentType = request.arguments[@"contentType"] ?: @"plaintext";
  227. NSError *error;
  228. id result = [FBPasteboard dataForType:contentType error:&error];
  229. if (nil == result) {
  230. return FBResponseWithUnknownError(error);
  231. }
  232. return FBResponseWithObject([result base64EncodedStringWithOptions:0]);
  233. }
  234. + (id<FBResponsePayload>)handleGetBatteryInfo:(FBRouteRequest *)request
  235. {
  236. if (![[UIDevice currentDevice] isBatteryMonitoringEnabled]) {
  237. [[UIDevice currentDevice] setBatteryMonitoringEnabled:YES];
  238. }
  239. return FBResponseWithObject(@{
  240. @"level": @([UIDevice currentDevice].batteryLevel),
  241. @"state": @([UIDevice currentDevice].batteryState)
  242. });
  243. }
  244. #endif
  245. + (id<FBResponsePayload>)handlePressButtonCommand:(FBRouteRequest *)request
  246. {
  247. NSError *error;
  248. if (![XCUIDevice.sharedDevice fb_pressButton:(id)request.arguments[@"name"]
  249. forDuration:(NSNumber *)request.arguments[@"duration"]
  250. error:&error]) {
  251. return FBResponseWithUnknownError(error);
  252. }
  253. return FBResponseWithOK();
  254. }
  255. + (id<FBResponsePayload>)handleActivateSiri:(FBRouteRequest *)request
  256. {
  257. NSError *error;
  258. if (![XCUIDevice.sharedDevice fb_activateSiriVoiceRecognitionWithText:(id)request.arguments[@"text"] error:&error]) {
  259. return FBResponseWithUnknownError(error);
  260. }
  261. return FBResponseWithOK();
  262. }
  263. + (id <FBResponsePayload>)handlePeformIOHIDEvent:(FBRouteRequest *)request
  264. {
  265. NSNumber *page = request.arguments[@"page"];
  266. NSNumber *usage = request.arguments[@"usage"];
  267. NSNumber *duration = request.arguments[@"duration"];
  268. NSError *error;
  269. if (![XCUIDevice.sharedDevice fb_performIOHIDEventWithPage:page.unsignedIntValue
  270. usage:usage.unsignedIntValue
  271. duration:duration.doubleValue
  272. error:&error]) {
  273. return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
  274. traceback:nil]);
  275. }
  276. return FBResponseWithOK();
  277. }
  278. + (id <FBResponsePayload>)handleLaunchUnattachedApp:(FBRouteRequest *)request
  279. {
  280. NSString *bundle = (NSString *)request.arguments[@"bundleId"];
  281. if ([FBUnattachedAppLauncher launchAppWithBundleId:bundle]) {
  282. return FBResponseWithOK();
  283. }
  284. return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:@"LSApplicationWorkspace failed to launch app" traceback:nil]);
  285. }
  286. + (id <FBResponsePayload>)handleResetAppAuth:(FBRouteRequest *)request
  287. {
  288. NSNumber *resource = request.arguments[@"resource"];
  289. if (nil == resource) {
  290. NSString *errMsg = @"The 'resource' argument must be set to a valid resource identifier (numeric value). See https://developer.apple.com/documentation/xctest/xcuiprotectedresource?language=objc";
  291. return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMsg traceback:nil]);
  292. }
  293. [request.session.activeApplication resetAuthorizationStatusForResource:(XCUIProtectedResource)resource.longLongValue];
  294. return FBResponseWithOK();
  295. }
  296. /**
  297. Returns device location data.
  298. It requires to configure location access permission by manual.
  299. The response of 'latitude', 'longitude' and 'altitude' are always zero (0) without authorization.
  300. 'authorizationStatus' indicates current authorization status. '3' is 'Always'.
  301. https://developer.apple.com/documentation/corelocation/clauthorizationstatus
  302. Settings -> Privacy -> Location Service -> WebDriverAgent-Runner -> Always
  303. The return value could be zero even if the permission is set to 'Always'
  304. since the location service needs some time to update the location data.
  305. */
  306. + (id<FBResponsePayload>)handleGetLocation:(FBRouteRequest *)request
  307. {
  308. #if TARGET_OS_TV
  309. return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:@"unsupported"
  310. traceback:nil]);
  311. #else
  312. CLLocationManager *locationManager = [[CLLocationManager alloc] init];
  313. [locationManager setDistanceFilter:kCLHeadingFilterNone];
  314. // Always return the best acurate location data
  315. [locationManager setDesiredAccuracy:kCLLocationAccuracyBest];
  316. [locationManager setPausesLocationUpdatesAutomatically:NO];
  317. [locationManager startUpdatingLocation];
  318. CLAuthorizationStatus authStatus;
  319. if ([locationManager respondsToSelector:@selector(authorizationStatus)]) {
  320. NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[[locationManager class]
  321. instanceMethodSignatureForSelector:@selector(authorizationStatus)]];
  322. [invocation setSelector:@selector(authorizationStatus)];
  323. [invocation setTarget:locationManager];
  324. [invocation invoke];
  325. [invocation getReturnValue:&authStatus];
  326. } else {
  327. authStatus = [CLLocationManager authorizationStatus];
  328. }
  329. return FBResponseWithObject(@{
  330. @"authorizationStatus": @(authStatus),
  331. @"latitude": @(locationManager.location.coordinate.latitude),
  332. @"longitude": @(locationManager.location.coordinate.longitude),
  333. @"altitude": @(locationManager.location.altitude),
  334. });
  335. #endif
  336. }
  337. + (id<FBResponsePayload>)handleExpectNotification:(FBRouteRequest *)request
  338. {
  339. NSString *name = request.arguments[@"name"];
  340. if (nil == name) {
  341. NSString *message = @"Notification name argument must be provided";
  342. return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message traceback:nil]);
  343. }
  344. NSNumber *timeout = request.arguments[@"timeout"] ?: @60;
  345. NSString *type = request.arguments[@"type"] ?: @"plain";
  346. XCTWaiterResult result;
  347. if ([type isEqualToString:@"plain"]) {
  348. result = [FBNotificationsHelper waitForNotificationWithName:name timeout:timeout.doubleValue];
  349. } else if ([type isEqualToString:@"darwin"]) {
  350. result = [FBNotificationsHelper waitForDarwinNotificationWithName:name timeout:timeout.doubleValue];
  351. } else {
  352. NSString *message = [NSString stringWithFormat:@"Notification type could only be 'plain' or 'darwin'. Got '%@' instead", type];
  353. return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message traceback:nil]);
  354. }
  355. if (result != XCTWaiterResultCompleted) {
  356. NSString *message = [NSString stringWithFormat:@"Did not receive any expected %@ notifications within %@s",
  357. name, timeout];
  358. return FBResponseWithStatus([FBCommandStatus timeoutErrorWithMessage:message traceback:nil]);
  359. }
  360. return FBResponseWithOK();
  361. }
  362. + (id<FBResponsePayload>)handleSetDeviceAppearance:(FBRouteRequest *)request
  363. {
  364. NSString *name = [request.arguments[@"name"] lowercaseString];
  365. if (nil == name || !([name isEqualToString:@"light"] || [name isEqualToString:@"dark"])) {
  366. NSString *message = @"The appearance name must be either 'light' or 'dark'";
  367. return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message traceback:nil]);
  368. }
  369. FBUIInterfaceAppearance appearance = [name isEqualToString:@"light"]
  370. ? FBUIInterfaceAppearanceLight
  371. : FBUIInterfaceAppearanceDark;
  372. NSError *error;
  373. if (![XCUIDevice.sharedDevice fb_setAppearance:appearance error:&error]) {
  374. return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
  375. traceback:nil]);
  376. }
  377. return FBResponseWithOK();
  378. }
  379. + (id<FBResponsePayload>)handleGetDeviceInfo:(FBRouteRequest *)request
  380. {
  381. // Returns locale like ja_EN and zh-Hant_US. The format depends on OS
  382. // Developers should use this locale by default
  383. // https://developer.apple.com/documentation/foundation/nslocale/1414388-autoupdatingcurrentlocale
  384. NSString *currentLocale = [[NSLocale autoupdatingCurrentLocale] localeIdentifier];
  385. NSMutableDictionary *deviceInfo = [NSMutableDictionary dictionaryWithDictionary:
  386. @{
  387. @"currentLocale": currentLocale,
  388. @"timeZone": self.timeZone,
  389. @"name": UIDevice.currentDevice.name,
  390. @"model": UIDevice.currentDevice.model,
  391. @"uuid": [UIDevice.currentDevice.identifierForVendor UUIDString] ?: @"unknown",
  392. // https://developer.apple.com/documentation/uikit/uiuserinterfaceidiom?language=objc
  393. @"userInterfaceIdiom": @(UIDevice.currentDevice.userInterfaceIdiom),
  394. @"userInterfaceStyle": self.userInterfaceStyle,
  395. #if TARGET_OS_SIMULATOR
  396. @"isSimulator": @(YES),
  397. #else
  398. @"isSimulator": @(NO),
  399. #endif
  400. }];
  401. // https://developer.apple.com/documentation/foundation/nsprocessinfothermalstate
  402. deviceInfo[@"thermalState"] = @(NSProcessInfo.processInfo.thermalState);
  403. return FBResponseWithObject(deviceInfo);
  404. }
  405. /**
  406. * @return Current user interface style as a string
  407. */
  408. + (NSString *)userInterfaceStyle
  409. {
  410. if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"15.0")) {
  411. // Only iOS 15+ simulators/devices return correct data while
  412. // the api itself works in iOS 13 and 14 that has style preference.
  413. NSNumber *appearance = [XCUIDevice.sharedDevice fb_getAppearance];
  414. if (appearance != nil) {
  415. return [self getAppearanceName:appearance];
  416. }
  417. }
  418. static id userInterfaceStyle = nil;
  419. static dispatch_once_t styleOnceToken;
  420. dispatch_once(&styleOnceToken, ^{
  421. if ([UITraitCollection respondsToSelector:NSSelectorFromString(@"currentTraitCollection")]) {
  422. id currentTraitCollection = [UITraitCollection performSelector:NSSelectorFromString(@"currentTraitCollection")];
  423. if (nil != currentTraitCollection) {
  424. userInterfaceStyle = [currentTraitCollection valueForKey:@"userInterfaceStyle"];
  425. }
  426. }
  427. });
  428. if (nil == userInterfaceStyle) {
  429. return @"unsupported";
  430. }
  431. return [self getAppearanceName:userInterfaceStyle];
  432. }
  433. + (NSString *)getAppearanceName:(NSNumber *)appearance
  434. {
  435. switch ([appearance longLongValue]) {
  436. case FBUIInterfaceAppearanceUnspecified:
  437. return @"automatic";
  438. case FBUIInterfaceAppearanceLight:
  439. return @"light";
  440. case FBUIInterfaceAppearanceDark:
  441. return @"dark";
  442. default:
  443. return @"unknown";
  444. }
  445. }
  446. /**
  447. * @return The string of TimeZone. Returns TZ timezone id by default. Returns TimeZone name by Apple if TZ timezone id is not available.
  448. */
  449. + (NSString *)timeZone
  450. {
  451. NSTimeZone *localTimeZone = [NSTimeZone localTimeZone];
  452. // Apple timezone name like "US/New_York"
  453. NSString *timeZoneAbb = [localTimeZone abbreviation];
  454. if (timeZoneAbb == nil) {
  455. return [localTimeZone name];
  456. }
  457. // Convert timezone name to ids like "America/New_York" as TZ database Time Zones format
  458. // https://developer.apple.com/documentation/foundation/nstimezone
  459. NSString *timeZoneId = [[NSTimeZone timeZoneWithAbbreviation:timeZoneAbb] name];
  460. if (timeZoneId != nil) {
  461. return timeZoneId;
  462. }
  463. return [localTimeZone name];
  464. }
  465. #if !TARGET_OS_TV // tvOS does not provide relevant APIs
  466. + (id<FBResponsePayload>)handleGetSimulatedLocation:(FBRouteRequest *)request
  467. {
  468. NSError *error;
  469. CLLocation *location = [XCUIDevice.sharedDevice fb_getSimulatedLocation:&error];
  470. if (nil != error) {
  471. return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
  472. traceback:nil]);
  473. }
  474. return FBResponseWithObject(@{
  475. @"latitude": location ? @(location.coordinate.latitude) : NSNull.null,
  476. @"longitude": location ? @(location.coordinate.longitude) : NSNull.null,
  477. @"altitude": location ? @(location.altitude) : NSNull.null,
  478. });
  479. }
  480. + (id<FBResponsePayload>)handleSetSimulatedLocation:(FBRouteRequest *)request
  481. {
  482. NSNumber *longitude = request.arguments[@"longitude"];
  483. NSNumber *latitude = request.arguments[@"latitude"];
  484. if (nil == longitude || nil == latitude) {
  485. return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Both latitude and longitude must be provided"
  486. traceback:nil]);
  487. }
  488. NSError *error;
  489. CLLocation *location = [[CLLocation alloc] initWithLatitude:latitude.doubleValue
  490. longitude:longitude.doubleValue];
  491. if (![XCUIDevice.sharedDevice fb_setSimulatedLocation:location error:&error]) {
  492. return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
  493. traceback:nil]);
  494. }
  495. return FBResponseWithOK();
  496. }
  497. + (id<FBResponsePayload>)handleClearSimulatedLocation:(FBRouteRequest *)request
  498. {
  499. NSError *error;
  500. if (![XCUIDevice.sharedDevice fb_clearSimulatedLocation:&error]) {
  501. return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
  502. traceback:nil]);
  503. }
  504. return FBResponseWithOK();
  505. }
  506. #if __clang_major__ >= 15
  507. + (id<FBResponsePayload>)handleKeyboardInput:(FBRouteRequest *)request
  508. {
  509. FBElementCache *elementCache = request.session.elementCache;
  510. BOOL hasElement = ![request.parameters[@"uuid"] isEqual:@"0"];
  511. XCUIElement *destination = hasElement
  512. ? [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]
  513. checkStaleness:YES]
  514. : request.session.activeApplication;
  515. id keys = request.arguments[@"keys"];
  516. if (![destination respondsToSelector:@selector(typeKey:modifierFlags:)]) {
  517. NSString *message = @"typeKey API is only supported since Xcode15 and iPadOS 17";
  518. return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:message
  519. traceback:nil]);
  520. }
  521. if (![keys isKindOfClass:NSArray.class]) {
  522. NSString *message = @"The 'keys' argument must be an array";
  523. return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message
  524. traceback:nil]);
  525. }
  526. for (id item in (NSArray *)keys) {
  527. if ([item isKindOfClass:NSString.class]) {
  528. NSString *keyValue = [FBKeyboard keyValueForName:item] ?: item;
  529. [destination typeKey:keyValue modifierFlags:XCUIKeyModifierNone];
  530. } else if ([item isKindOfClass:NSDictionary.class]) {
  531. id key = [(NSDictionary *)item objectForKey:@"key"];
  532. if (![key isKindOfClass:NSString.class]) {
  533. NSString *message = [NSString stringWithFormat:@"All dictionaries of 'keys' array must have the 'key' item of type string. Got '%@' instead in the item %@", key, item];
  534. return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message
  535. traceback:nil]);
  536. }
  537. id modifiers = [(NSDictionary *)item objectForKey:@"modifierFlags"];
  538. NSUInteger modifierFlags = XCUIKeyModifierNone;
  539. if ([modifiers isKindOfClass:NSNumber.class]) {
  540. modifierFlags = [(NSNumber *)modifiers unsignedIntValue];
  541. }
  542. NSString *keyValue = [FBKeyboard keyValueForName:item] ?: key;
  543. [destination typeKey:keyValue modifierFlags:modifierFlags];
  544. } else {
  545. NSString *message = @"All items of the 'keys' array must be either dictionaries or strings";
  546. return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message
  547. traceback:nil]);
  548. }
  549. }
  550. return FBResponseWithOK();
  551. }
  552. #endif
  553. #endif
  554. + (id<FBResponsePayload>)handlePerformAccessibilityAudit:(FBRouteRequest *)request
  555. {
  556. NSError *error;
  557. NSArray *requestedTypes = request.arguments[@"auditTypes"];
  558. NSMutableSet *typesSet = [NSMutableSet set];
  559. if (nil == requestedTypes || 0 == [requestedTypes count]) {
  560. [typesSet addObject:@"XCUIAccessibilityAuditTypeAll"];
  561. } else {
  562. [typesSet addObjectsFromArray:requestedTypes];
  563. }
  564. NSArray *result = [request.session.activeApplication fb_performAccessibilityAuditWithAuditTypesSet:typesSet.copy
  565. error:&error];
  566. if (nil == result) {
  567. return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
  568. traceback:nil]);
  569. }
  570. return FBResponseWithObject(result);
  571. }
  572. @end