/** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ #import "FBConfiguration.h" #import "AXSettings.h" #import "UIKeyboardImpl.h" #import "TIPreferencesController.h" #include #import #include "TargetConditionals.h" #import "FBXCodeCompatibility.h" #import "XCAXClient_iOS+FBSnapshotReqParams.h" #import "XCTestPrivateSymbols.h" #import "XCTestConfiguration.h" #import "XCUIApplication+FBUIInterruptions.h" static NSUInteger const DefaultStartingPort = 8567; static NSUInteger const DefaultMjpegServerPort = 9567; static NSUInteger const DefaultPortRange = 100; static char const *const controllerPrefBundlePath = "/System/Library/PrivateFrameworks/TextInput.framework/TextInput"; static NSString *const controllerClassName = @"TIPreferencesController"; static NSString *const FBKeyboardAutocorrectionKey = @"KeyboardAutocorrection"; static NSString *const FBKeyboardPredictionKey = @"KeyboardPrediction"; static NSString *const axSettingsClassName = @"AXSettings"; static BOOL FBShouldUseTestManagerForVisibilityDetection = NO; static BOOL FBShouldUseSingletonTestManager = YES; static BOOL FBShouldRespectSystemAlerts = NO; static CGFloat FBMjpegScalingFactor = 100.0; static BOOL FBMjpegShouldFixOrientation = NO; static NSUInteger FBMjpegServerScreenshotQuality = 25; static NSUInteger FBMjpegServerFramerate = 10; // Session-specific settings static BOOL FBShouldTerminateApp; static NSNumber* FBMaxTypingFrequency; static NSUInteger FBScreenshotQuality; static BOOL FBShouldUseFirstMatch; static BOOL FBShouldBoundElementsByIndex; static BOOL FBIncludeNonModalElements; static NSString *FBAcceptAlertButtonSelector; static NSString *FBDismissAlertButtonSelector; static NSString *FBAutoClickAlertSelector; static NSTimeInterval FBWaitForIdleTimeout; static NSTimeInterval FBAnimationCoolOffTimeout; static BOOL FBShouldUseCompactResponses; static NSString *FBElementResponseAttributes; static BOOL FBUseClearTextShortcut; static BOOL FBLimitXpathContextScope = YES; #if !TARGET_OS_TV static UIInterfaceOrientation FBScreenshotOrientation; #endif static BOOL FBShouldIncludeHittableInPageSource = NO; static BOOL FBShouldIncludeNativeFrameInPageSource = NO; static BOOL FBShouldIncludeMinMaxValueInPageSource = NO; @implementation FBConfiguration + (NSUInteger)defaultTypingFrequency { NSInteger defaultFreq = [[NSUserDefaults standardUserDefaults] integerForKey:@"com.apple.xctest.iOSMaximumTypingFrequency"]; return defaultFreq > 0 ? defaultFreq : 60; } + (void)initialize { [FBConfiguration resetSessionSettings]; } #pragma mark Public + (void)disableRemoteQueryEvaluation { [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"XCTDisableRemoteQueryEvaluation"]; } + (void)disableApplicationUIInterruptionsHandling { [XCUIApplication fb_disableUIInterruptionsHandling]; } + (void)enableXcTestDebugLogs { ((XCTestConfiguration *)XCTestConfiguration.activeTestConfiguration).emitOSLogs = YES; [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"XCTEmitOSLogs"]; } + (void)disableAttributeKeyPathAnalysis { [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"XCTDisableAttributeKeyPathAnalysis"]; } + (void)disableScreenshots { [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"DisableScreenshots"]; } + (void)enableScreenshots { [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"DisableScreenshots"]; } + (void)disableScreenRecordings { [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"DisableDiagnosticScreenRecordings"]; } + (void)enableScreenRecordings { [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"DisableDiagnosticScreenRecordings"]; } + (NSRange)bindingPortRange { // 'WebDriverAgent --port 8080' can be passed via the arguments to the process if (self.bindingPortRangeFromArguments.location != NSNotFound) { return self.bindingPortRangeFromArguments; } // Existence of USE_PORT in the environment implies the port range is managed by the launching process. if (NSProcessInfo.processInfo.environment[@"USE_PORT"] && [NSProcessInfo.processInfo.environment[@"USE_PORT"] length] > 0) { return NSMakeRange([NSProcessInfo.processInfo.environment[@"USE_PORT"] integerValue] , 1); } return NSMakeRange(DefaultStartingPort, DefaultPortRange); } + (NSInteger)mjpegServerPort { if (self.mjpegServerPortFromArguments != NSNotFound) { return self.mjpegServerPortFromArguments; } if (NSProcessInfo.processInfo.environment[@"MJPEG_SERVER_PORT"] && [NSProcessInfo.processInfo.environment[@"MJPEG_SERVER_PORT"] length] > 0) { return [NSProcessInfo.processInfo.environment[@"MJPEG_SERVER_PORT"] integerValue]; } return DefaultMjpegServerPort; } + (CGFloat)mjpegScalingFactor { return FBMjpegScalingFactor; } + (void)setMjpegScalingFactor:(CGFloat)scalingFactor { FBMjpegScalingFactor = scalingFactor; } + (BOOL)mjpegShouldFixOrientation { return FBMjpegShouldFixOrientation; } + (void)setMjpegShouldFixOrientation:(BOOL)enabled { FBMjpegShouldFixOrientation = enabled; } + (BOOL)verboseLoggingEnabled { return [NSProcessInfo.processInfo.environment[@"VERBOSE_LOGGING"] boolValue]; } + (void)setShouldUseTestManagerForVisibilityDetection:(BOOL)value { FBShouldUseTestManagerForVisibilityDetection = value; } + (BOOL)shouldUseTestManagerForVisibilityDetection { return FBShouldUseTestManagerForVisibilityDetection; } + (void)setShouldUseCompactResponses:(BOOL)value { FBShouldUseCompactResponses = value; } + (BOOL)shouldUseCompactResponses { return FBShouldUseCompactResponses; } + (void)setShouldTerminateApp:(BOOL)value { FBShouldTerminateApp = value; } + (BOOL)shouldTerminateApp { return FBShouldTerminateApp; } + (void)setElementResponseAttributes:(NSString *)value { FBElementResponseAttributes = value; } + (NSString *)elementResponseAttributes { return FBElementResponseAttributes; } + (void)setMaxTypingFrequency:(NSUInteger)value { FBMaxTypingFrequency = @(value); } + (NSUInteger)maxTypingFrequency { if (nil == FBMaxTypingFrequency) { return [self defaultTypingFrequency]; } return FBMaxTypingFrequency.integerValue <= 0 ? [self defaultTypingFrequency] : FBMaxTypingFrequency.integerValue; } + (void)setShouldUseSingletonTestManager:(BOOL)value { FBShouldUseSingletonTestManager = value; } + (BOOL)shouldUseSingletonTestManager { return FBShouldUseSingletonTestManager; } + (NSUInteger)mjpegServerFramerate { return FBMjpegServerFramerate; } + (void)setMjpegServerFramerate:(NSUInteger)framerate { FBMjpegServerFramerate = framerate; } + (NSUInteger)mjpegServerScreenshotQuality { return FBMjpegServerScreenshotQuality; } + (void)setMjpegServerScreenshotQuality:(NSUInteger)quality { FBMjpegServerScreenshotQuality = quality; } + (NSUInteger)screenshotQuality { return FBScreenshotQuality; } + (void)setScreenshotQuality:(NSUInteger)quality { FBScreenshotQuality = quality; } + (NSTimeInterval)waitForIdleTimeout { return FBWaitForIdleTimeout; } + (void)setWaitForIdleTimeout:(NSTimeInterval)timeout { FBWaitForIdleTimeout = timeout; } + (NSTimeInterval)animationCoolOffTimeout { return FBAnimationCoolOffTimeout; } + (void)setAnimationCoolOffTimeout:(NSTimeInterval)timeout { FBAnimationCoolOffTimeout = timeout; } // Works for Simulator and Real devices + (void)configureDefaultKeyboardPreferences { void *handle = dlopen(controllerPrefBundlePath, RTLD_LAZY); Class controllerClass = NSClassFromString(controllerClassName); TIPreferencesController *controller = [controllerClass sharedPreferencesController]; // Auto-Correction in Keyboards // 'setAutocorrectionEnabled' Was in TextInput.framework/TIKeyboardState.h over iOS 10.3 if ([controller respondsToSelector:@selector(setAutocorrectionEnabled:)]) { // Under iOS 10.2 controller.autocorrectionEnabled = NO; } else if ([controller respondsToSelector:@selector(setValue:forPreferenceKey:)]) { // Over iOS 10.3 [controller setValue:@NO forPreferenceKey:FBKeyboardAutocorrectionKey]; } // Predictive in Keyboards if ([controller respondsToSelector:@selector(setPredictionEnabled:)]) { controller.predictionEnabled = NO; } else if ([controller respondsToSelector:@selector(setValue:forPreferenceKey:)]) { [controller setValue:@NO forPreferenceKey:FBKeyboardPredictionKey]; } // To dismiss keyboard tutorial on iOS 11+ (iPad) if ([controller respondsToSelector:@selector(setValue:forPreferenceKey:)]) { [controller setValue:@YES forPreferenceKey:@"DidShowGestureKeyboardIntroduction"]; if (isSDKVersionGreaterThanOrEqualTo(@"13.0")) { [controller setValue:@YES forPreferenceKey:@"DidShowContinuousPathIntroduction"]; } [controller synchronizePreferences]; } dlclose(handle); } + (void)forceSimulatorSoftwareKeyboardPresence { #if TARGET_OS_SIMULATOR // Force toggle software keyboard on. // This can avoid 'Keyboard is not present' error which can happen // when send_keys are called by client [[UIKeyboardImpl sharedInstance] setAutomaticMinimizationEnabled:NO]; if ([(NSObject *)[UIKeyboardImpl sharedInstance] respondsToSelector:@selector(setSoftwareKeyboardShownByTouch:)]) { // Xcode 13 no longer has this method [[UIKeyboardImpl sharedInstance] setSoftwareKeyboardShownByTouch:YES]; } #endif } + (FBConfigurationKeyboardPreference)keyboardAutocorrection { return [self keyboardsPreference:FBKeyboardAutocorrectionKey]; } + (void)setKeyboardAutocorrection:(BOOL)isEnabled { [self configureKeyboardsPreference:isEnabled forPreferenceKey:FBKeyboardAutocorrectionKey]; } + (FBConfigurationKeyboardPreference)keyboardPrediction { return [self keyboardsPreference:FBKeyboardPredictionKey]; } + (void)setKeyboardPrediction:(BOOL)isEnabled { [self configureKeyboardsPreference:isEnabled forPreferenceKey:FBKeyboardPredictionKey]; } + (void)setSnapshotMaxDepth:(int)maxDepth { FBSetCustomParameterForElementSnapshot(FBSnapshotMaxDepthKey, @(maxDepth)); } + (int)snapshotMaxDepth { return [FBGetCustomParameterForElementSnapshot(FBSnapshotMaxDepthKey) intValue]; } + (void)setShouldRespectSystemAlerts:(BOOL)value { FBShouldRespectSystemAlerts = value; } + (BOOL)shouldRespectSystemAlerts { return FBShouldRespectSystemAlerts; } + (void)setUseFirstMatch:(BOOL)enabled { FBShouldUseFirstMatch = enabled; } + (BOOL)useFirstMatch { return FBShouldUseFirstMatch; } + (void)setBoundElementsByIndex:(BOOL)enabled { FBShouldBoundElementsByIndex = enabled; } + (BOOL)boundElementsByIndex { return FBShouldBoundElementsByIndex; } + (void)setIncludeNonModalElements:(BOOL)isEnabled { FBIncludeNonModalElements = isEnabled; } + (BOOL)includeNonModalElements { return FBIncludeNonModalElements; } + (void)setAcceptAlertButtonSelector:(NSString *)classChainSelector { FBAcceptAlertButtonSelector = classChainSelector; } + (NSString *)acceptAlertButtonSelector { return FBAcceptAlertButtonSelector; } + (void)setDismissAlertButtonSelector:(NSString *)classChainSelector { FBDismissAlertButtonSelector = classChainSelector; } + (NSString *)dismissAlertButtonSelector { return FBDismissAlertButtonSelector; } + (void)setAutoClickAlertSelector:(NSString *)classChainSelector { FBAutoClickAlertSelector = classChainSelector; } + (NSString *)autoClickAlertSelector { return FBAutoClickAlertSelector; } + (void)setUseClearTextShortcut:(BOOL)enabled { FBUseClearTextShortcut = enabled; } + (BOOL)useClearTextShortcut { return FBUseClearTextShortcut; } + (BOOL)limitXpathContextScope { return FBLimitXpathContextScope; } + (void)setLimitXpathContextScope:(BOOL)enabled { FBLimitXpathContextScope = enabled; } #if !TARGET_OS_TV + (BOOL)setScreenshotOrientation:(NSString *)orientation error:(NSError **)error { // Only UIInterfaceOrientationUnknown is over iOS 8. Others are over iOS 2. // https://developer.apple.com/documentation/uikit/uiinterfaceorientation/uiinterfaceorientationunknown if ([orientation.lowercaseString isEqualToString:@"portrait"]) { FBScreenshotOrientation = UIInterfaceOrientationPortrait; } else if ([orientation.lowercaseString isEqualToString:@"portraitupsidedown"]) { FBScreenshotOrientation = UIInterfaceOrientationPortraitUpsideDown; } else if ([orientation.lowercaseString isEqualToString:@"landscaperight"]) { FBScreenshotOrientation = UIInterfaceOrientationLandscapeRight; } else if ([orientation.lowercaseString isEqualToString:@"landscapeleft"]) { FBScreenshotOrientation = UIInterfaceOrientationLandscapeLeft; } else if ([orientation.lowercaseString isEqualToString:@"auto"]) { FBScreenshotOrientation = UIInterfaceOrientationUnknown; } else { return [[FBErrorBuilder.builder withDescriptionFormat: @"The orientation value '%@' is not known. Only the following orientation values are supported: " \ "'auto', 'portrait', 'portraitUpsideDown', 'landscapeRight' and 'landscapeLeft'", orientation] buildError:error]; } return YES; } + (NSInteger)screenshotOrientation { return FBScreenshotOrientation; } + (NSString *)humanReadableScreenshotOrientation { switch (FBScreenshotOrientation) { case UIInterfaceOrientationPortrait: return @"portrait"; case UIInterfaceOrientationPortraitUpsideDown: return @"portraitUpsideDown"; case UIInterfaceOrientationLandscapeRight: return @"landscapeRight"; case UIInterfaceOrientationLandscapeLeft: return @"landscapeLeft"; case UIInterfaceOrientationUnknown: return @"auto"; } } #endif + (void)resetSessionSettings { FBShouldTerminateApp = YES; FBShouldUseCompactResponses = YES; FBElementResponseAttributes = @"type,label"; FBMaxTypingFrequency = @([self defaultTypingFrequency]); FBScreenshotQuality = 3; FBShouldUseFirstMatch = NO; FBShouldBoundElementsByIndex = NO; // This is diabled by default because enabling it prevents the accessbility snapshot to be taken // (it always errors with kxIllegalArgument error) FBIncludeNonModalElements = NO; FBAcceptAlertButtonSelector = @""; FBDismissAlertButtonSelector = @""; FBAutoClickAlertSelector = @""; FBWaitForIdleTimeout = 10.; FBAnimationCoolOffTimeout = 2.; // 50 should be enough for the majority of the cases. The performance is acceptable for values up to 100. FBSetCustomParameterForElementSnapshot(FBSnapshotMaxDepthKey, @50); FBUseClearTextShortcut = YES; FBLimitXpathContextScope = YES; #if !TARGET_OS_TV FBScreenshotOrientation = UIInterfaceOrientationUnknown; #endif } #pragma mark Private + (FBConfigurationKeyboardPreference)keyboardsPreference:(nonnull NSString *)key { Class controllerClass = NSClassFromString(controllerClassName); TIPreferencesController *controller = [controllerClass sharedPreferencesController]; if ([key isEqualToString:FBKeyboardAutocorrectionKey]) { if ([controller respondsToSelector:@selector(boolForPreferenceKey:)]) { return [controller boolForPreferenceKey:FBKeyboardAutocorrectionKey] ? FBConfigurationKeyboardPreferenceEnabled : FBConfigurationKeyboardPreferenceDisabled; } else { [FBLogger log:@"Updating keyboard autocorrection preference is not supported"]; return FBConfigurationKeyboardPreferenceNotSupported; } } else if ([key isEqualToString:FBKeyboardPredictionKey]) { if ([controller respondsToSelector:@selector(boolForPreferenceKey:)]) { return [controller boolForPreferenceKey:FBKeyboardPredictionKey] ? FBConfigurationKeyboardPreferenceEnabled : FBConfigurationKeyboardPreferenceDisabled; } else { [FBLogger log:@"Updating keyboard prediction preference is not supported"]; return FBConfigurationKeyboardPreferenceNotSupported; } } @throw [[FBErrorBuilder.builder withDescriptionFormat:@"No available keyboardsPreferenceKey: '%@'", key] build]; } + (void)configureKeyboardsPreference:(BOOL)enable forPreferenceKey:(nonnull NSString *)key { void *handle = dlopen(controllerPrefBundlePath, RTLD_LAZY); Class controllerClass = NSClassFromString(controllerClassName); TIPreferencesController *controller = [controllerClass sharedPreferencesController]; if ([key isEqualToString:FBKeyboardAutocorrectionKey]) { // Auto-Correction in Keyboards if ([controller respondsToSelector:@selector(setAutocorrectionEnabled:)]) { controller.autocorrectionEnabled = enable; } else { [controller setValue:@(enable) forPreferenceKey:FBKeyboardAutocorrectionKey]; } } else if ([key isEqualToString:FBKeyboardPredictionKey]) { // Predictive in Keyboards if ([controller respondsToSelector:@selector(setPredictionEnabled:)]) { controller.predictionEnabled = enable; } else { [controller setValue:@(enable) forPreferenceKey:FBKeyboardPredictionKey]; } } [controller synchronizePreferences]; dlclose(handle); } + (NSString*)valueFromArguments: (NSArray *)arguments forKey: (NSString*)key { NSUInteger index = [arguments indexOfObject:key]; if (index == NSNotFound || index == arguments.count - 1) { return nil; } return arguments[index + 1]; } + (NSUInteger)mjpegServerPortFromArguments { NSString *portNumberString = [self valueFromArguments: NSProcessInfo.processInfo.arguments forKey: @"--mjpeg-server-port"]; NSUInteger port = (NSUInteger)[portNumberString integerValue]; if (port == 0) { return NSNotFound; } return port; } + (NSRange)bindingPortRangeFromArguments { NSString *portNumberString = [self valueFromArguments:NSProcessInfo.processInfo.arguments forKey: @"--port"]; NSUInteger port = (NSUInteger)[portNumberString integerValue]; if (port == 0) { return NSMakeRange(NSNotFound, 0); } return NSMakeRange(port, 1); } + (void)setReduceMotionEnabled:(BOOL)isEnabled { Class settingsClass = NSClassFromString(axSettingsClassName); AXSettings *settings = [settingsClass sharedInstance]; // Below does not work on real devices because of iOS security model // (lldb) po settings.reduceMotionEnabled = isEnabled // 2019-08-21 22:58:19.776165+0900 WebDriverAgentRunner-Runner[322:13361] [User Defaults] Couldn't write value for key ReduceMotionEnabled in CFPrefsPlistSource<0x28111a700> (Domain: com.apple.Accessibility, User: kCFPreferencesCurrentUser, ByHost: No, Container: (null), Contents Need Refresh: No): setting preferences outside an application's container requires user-preference-write or file-write-data sandbox access if ([settings respondsToSelector:@selector(setReduceMotionEnabled:)]) { [settings setReduceMotionEnabled:isEnabled]; } } + (BOOL)reduceMotionEnabled { Class settingsClass = NSClassFromString(axSettingsClassName); AXSettings *settings = [settingsClass sharedInstance]; if ([settings respondsToSelector:@selector(reduceMotionEnabled)]) { return settings.reduceMotionEnabled; } return NO; } + (void)setIncludeHittableInPageSource:(BOOL)enabled { FBShouldIncludeHittableInPageSource = enabled; } + (BOOL)includeHittableInPageSource { return FBShouldIncludeHittableInPageSource; } + (void)setIncludeNativeFrameInPageSource:(BOOL)enabled { FBShouldIncludeNativeFrameInPageSource = enabled; } + (BOOL)includeNativeFrameInPageSource { return FBShouldIncludeNativeFrameInPageSource; } + (void)setIncludeMinMaxValueInPageSource:(BOOL)enabled { FBShouldIncludeMinMaxValueInPageSource = enabled; } + (BOOL)includeMinMaxValueInPageSource { return FBShouldIncludeMinMaxValueInPageSource; } @end