FBCustomCommands.m 26 KB

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