/* * Copyright (C) 2014 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ #import "config.h" #import "TestController.h" #import "GeneratedTouchesDebugWindow.h" #import "HIDEventGenerator.h" #import "IOSLayoutTestCommunication.h" #import "PlatformWebView.h" #import "TestInvocation.h" #import "TestRunnerWKWebView.h" #import "TextInputSPI.h" #import "UIKitSPI.h" #import #import #import #import #import #import #import #import #import #import #import #import #import static BOOL overrideIsInHardwareKeyboardMode() { return NO; } static void overridePresentMenuOrPopoverOrViewController() { } #if !HAVE(NONDESTRUCTIVE_IMAGE_PASTE_SUPPORT_QUERY) static BOOL overrideKeyboardDelegateSupportsImagePaste(id, SEL) { return NO; } #endif namespace WTR { static bool isDoneWaitingForKeyboardToDismiss = true; static bool isDoneWaitingForMenuToDismiss = true; static void handleKeyboardWillHideNotification(CFNotificationCenterRef, void*, CFStringRef, const void*, CFDictionaryRef) { isDoneWaitingForKeyboardToDismiss = false; } static void handleKeyboardDidHideNotification(CFNotificationCenterRef, void*, CFStringRef, const void*, CFDictionaryRef) { isDoneWaitingForKeyboardToDismiss = true; } static void handleMenuWillHideNotification(CFNotificationCenterRef, void*, CFStringRef, const void*, CFDictionaryRef) { isDoneWaitingForMenuToDismiss = false; } static void handleMenuDidHideNotification(CFNotificationCenterRef, void*, CFStringRef, const void*, CFDictionaryRef) { isDoneWaitingForMenuToDismiss = true; } void TestController::notifyDone() { UIView *contentView = mainWebView()->platformView().contentView; UIView *selectionView = [contentView valueForKeyPath:@"interactionAssistant.selectionView"]; [selectionView _removeAllAnimations:YES]; } void TestController::platformInitialize() { setUpIOSLayoutTestCommunication(); cocoaPlatformInitialize(); [UIApplication sharedApplication].idleTimerDisabled = YES; [[UIScreen mainScreen] _setScale:2.0]; auto center = CFNotificationCenterGetLocalCenter(); CFNotificationCenterAddObserver(center, this, handleKeyboardWillHideNotification, (CFStringRef)UIKeyboardWillHideNotification, nullptr, CFNotificationSuspensionBehaviorDeliverImmediately); CFNotificationCenterAddObserver(center, this, handleKeyboardDidHideNotification, (CFStringRef)UIKeyboardDidHideNotification, nullptr, CFNotificationSuspensionBehaviorDeliverImmediately); CFNotificationCenterAddObserver(center, this, handleMenuWillHideNotification, (CFStringRef)UIMenuControllerWillHideMenuNotification, nullptr, CFNotificationSuspensionBehaviorDeliverImmediately); CFNotificationCenterAddObserver(center, this, handleMenuDidHideNotification, (CFStringRef)UIMenuControllerDidHideMenuNotification, nullptr, CFNotificationSuspensionBehaviorDeliverImmediately); } void TestController::platformDestroy() { tearDownIOSLayoutTestCommunication(); auto center = CFNotificationCenterGetLocalCenter(); CFNotificationCenterRemoveObserver(center, this, (CFStringRef)UIKeyboardWillHideNotification, nullptr); CFNotificationCenterRemoveObserver(center, this, (CFStringRef)UIKeyboardDidHideNotification, nullptr); CFNotificationCenterRemoveObserver(center, this, (CFStringRef)UIMenuControllerWillHideMenuNotification, nullptr); CFNotificationCenterRemoveObserver(center, this, (CFStringRef)UIMenuControllerDidHideMenuNotification, nullptr); } void TestController::initializeInjectedBundlePath() { NSString *nsBundlePath = [[NSBundle mainBundle].builtInPlugInsPath stringByAppendingPathComponent:@"WebKitTestRunnerInjectedBundle.bundle"]; m_injectedBundlePath.adopt(WKStringCreateWithCFString((CFStringRef)nsBundlePath)); } void TestController::initializeTestPluginDirectory() { m_testPluginDirectory.adopt(WKStringCreateWithCFString((CFStringRef)[[NSBundle mainBundle] bundlePath])); } void TestController::configureContentExtensionForTest(const TestInvocation&) { } static _WKDragInteractionPolicy dragInteractionPolicy(const TestOptions& options) { auto policy = options.dragInteractionPolicy(); if (policy == "always-enable") return _WKDragInteractionPolicyAlwaysEnable; if (policy == "always-disable") return _WKDragInteractionPolicyAlwaysDisable; return _WKDragInteractionPolicyDefault; } bool TestController::platformResetStateToConsistentValues(const TestOptions& options) { cocoaResetStateToConsistentValues(options); [UIKeyboardImpl.activeInstance setCorrectionLearningAllowed:NO]; [UIPasteboard generalPasteboard].items = @[ ]; [[UIApplication sharedApplication] _cancelAllTouches]; [[UIDevice currentDevice] setOrientation:UIDeviceOrientationPortrait animated:NO]; UIKeyboardPreferencesController *keyboardPreferences = UIKeyboardPreferencesController.sharedPreferencesController; auto globalPreferencesDomainName = CFSTR("com.apple.Preferences"); auto automaticMinimizationEnabledPreferenceKey = @"AutomaticMinimizationEnabled"; if (![keyboardPreferences boolForPreferenceKey:automaticMinimizationEnabledPreferenceKey]) { [keyboardPreferences setValue:@YES forPreferenceKey:automaticMinimizationEnabledPreferenceKey]; CFPreferencesSetAppValue((__bridge CFStringRef)automaticMinimizationEnabledPreferenceKey, kCFBooleanTrue, globalPreferencesDomainName); } // Ensures that changing selection does not cause the software keyboard to appear, // even when the hardware keyboard is attached. auto hardwareKeyboardLastSeenPreferenceKey = @"HardwareKeyboardLastSeen"; auto preferencesActions = keyboardPreferences.preferencesActions; if (![preferencesActions oneTimeActionCompleted:hardwareKeyboardLastSeenPreferenceKey]) [preferencesActions didTriggerOneTimeAction:hardwareKeyboardLastSeenPreferenceKey]; // Disables the dictation keyboard shortcut for testing. auto dictationKeyboardShortcutPreferenceKey = @"HWKeyboardDictationShortcut"; auto dictationKeyboardShortcutValueForTesting = @(-1); if (![dictationKeyboardShortcutValueForTesting isEqual:[keyboardPreferences valueForPreferenceKey:dictationKeyboardShortcutPreferenceKey]]) { [keyboardPreferences setValue:dictationKeyboardShortcutValueForTesting forPreferenceKey:dictationKeyboardShortcutPreferenceKey]; CFPreferencesSetAppValue((__bridge CFStringRef)dictationKeyboardShortcutPreferenceKey, (__bridge CFNumberRef)dictationKeyboardShortcutValueForTesting, globalPreferencesDomainName); } GSEventSetHardwareKeyboardAttached(true, 0); // Override the implementation of +[UIKeyboard isInHardwareKeyboardMode] to ensure that test runs are deterministic // regardless of whether a hardware keyboard is attached. We intentionally never restore the original implementation. // // FIXME: Investigate whether this can be removed. The swizzled return value is inconsistent with GSEventSetHardwareKeyboardAttached. method_setImplementation(class_getClassMethod([UIKeyboard class], @selector(isInHardwareKeyboardMode)), reinterpret_cast(overrideIsInHardwareKeyboardMode)); #if !HAVE(NONDESTRUCTIVE_IMAGE_PASTE_SUPPORT_QUERY) // FIXME: Remove this workaround once -[UIKeyboardImpl delegateSupportsImagePaste] no longer increments the general pasteboard's changeCount. if (!m_keyboardDelegateSupportsImagePasteSwizzler) m_keyboardDelegateSupportsImagePasteSwizzler = makeUnique(UIKeyboardImpl.class, @selector(delegateSupportsImagePaste), reinterpret_cast(overrideKeyboardDelegateSupportsImagePaste)); #endif if (m_overriddenKeyboardInputMode) { m_overriddenKeyboardInputMode = nil; m_inputModeSwizzlers.clear(); [UIKeyboardImpl.sharedInstance prepareKeyboardInputModeFromPreferences:nil]; } m_presentPopoverSwizzlers.clear(); if (!options.shouldPresentPopovers()) { #if USE(UICONTEXTMENU) m_presentPopoverSwizzlers.append(makeUnique([UIContextMenuInteraction class], @selector(_presentMenuAtLocation:), reinterpret_cast(overridePresentMenuOrPopoverOrViewController))); #endif m_presentPopoverSwizzlers.append(makeUnique([UIPopoverController class], @selector(presentPopoverFromRect:inView:permittedArrowDirections:animated:), reinterpret_cast(overridePresentMenuOrPopoverOrViewController))); m_presentPopoverSwizzlers.append(makeUnique([UIViewController class], @selector(presentViewController:animated:completion:), reinterpret_cast(overridePresentMenuOrPopoverOrViewController))); } BOOL shouldRestoreFirstResponder = NO; if (PlatformWebView* platformWebView = mainWebView()) { TestRunnerWKWebView *webView = platformWebView->platformView(); webView._suppressSoftwareKeyboard = NO; webView._stableStateOverride = nil; webView._scrollingUpdatesDisabledForTesting = NO; webView.usesSafariLikeRotation = NO; webView.overrideSafeAreaInsets = UIEdgeInsetsZero; [webView _clearOverrideLayoutParameters]; [webView _clearInterfaceOrientationOverride]; [webView resetCustomMenuAction]; [webView setAllowedMenuActions:nil]; webView._dragInteractionPolicy = dragInteractionPolicy(options); UIScrollView *scrollView = webView.scrollView; [scrollView _removeAllAnimations:YES]; [scrollView setZoomScale:1 animated:NO]; auto currentContentInset = scrollView.contentInset; auto contentInsetTop = options.contentInsetTop(); if (currentContentInset.top != contentInsetTop) { currentContentInset.top = contentInsetTop; scrollView.contentInset = currentContentInset; scrollView.contentOffset = CGPointMake(-currentContentInset.left, -currentContentInset.top); } if (webView.interactingWithFormControl) shouldRestoreFirstResponder = [webView resignFirstResponder]; [webView immediatelyDismissContextMenuIfNeeded]; } UIMenuController.sharedMenuController.menuVisible = NO; runUntil(isDoneWaitingForKeyboardToDismiss, m_currentInvocation->shortTimeout()); runUntil(isDoneWaitingForMenuToDismiss, m_currentInvocation->shortTimeout()); if (PlatformWebView* platformWebView = mainWebView()) { TestRunnerWKWebView *webView = platformWebView->platformView(); UIViewController *webViewController = [[webView window] rootViewController]; MonotonicTime waitEndTime = MonotonicTime::now() + m_currentInvocation->shortTimeout(); bool hasPresentedViewController = !![webViewController presentedViewController]; while (hasPresentedViewController && MonotonicTime::now() < waitEndTime) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantPast]]; hasPresentedViewController = !![webViewController presentedViewController]; } if (hasPresentedViewController) { // As a last resort, just dismiss the remaining presented view controller ourselves. __block BOOL isDoneDismissingViewController = NO; [webViewController dismissViewControllerAnimated:NO completion:^{ isDoneDismissingViewController = YES; }]; runUntil(isDoneDismissingViewController, m_currentInvocation->shortTimeout()); hasPresentedViewController = !![webViewController presentedViewController]; } if (hasPresentedViewController) { TestInvocation::dumpWebProcessUnresponsiveness("TestController::platformResetStateToConsistentValues - Failed to remove presented view controller\n"); return false; } } if (shouldRestoreFirstResponder) [mainWebView()->platformView() becomeFirstResponder]; return true; } void TestController::platformConfigureViewForTest(const TestInvocation& test) { [[GeneratedTouchesDebugWindow sharedGeneratedTouchesDebugWindow] setShouldShowTouches:test.options().shouldShowTouches()]; TestRunnerWKWebView *webView = mainWebView()->platformView(); if (!test.options().useFlexibleViewport()) return; UIWindowScene *scene = webView.window.windowScene; CGRect sceneBounds = [UIScreen mainScreen].bounds; if (scene.sizeRestrictions) { // For platforms that support resizeable scenes, resize to match iPad 5th Generation, // the default iPad device used for layout testing. // We add the status bar in here because it is subtracted back out in viewRectForWindowRect. static constexpr CGSize defaultTestingiPadViewSize = { 768, 1004 }; sceneBounds = CGRectMake(0, 0, defaultTestingiPadViewSize.width, defaultTestingiPadViewSize.height + CGRectGetHeight(UIApplication.sharedApplication.statusBarFrame)); scene.sizeRestrictions.minimumSize = sceneBounds.size; scene.sizeRestrictions.maximumSize = sceneBounds.size; } CGSize oldSize = webView.bounds.size; mainWebView()->resizeTo(sceneBounds.size.width, sceneBounds.size.height, PlatformWebView::WebViewSizingMode::HeightRespectsStatusBar); CGSize newSize = webView.bounds.size; if (!CGSizeEqualToSize(oldSize, newSize)) { __block bool doneResizing = false; [webView _doAfterNextVisibleContentRectUpdate: ^{ doneResizing = true; }]; platformRunUntil(doneResizing, noTimeout); } // We also pass data to InjectedBundle::beginTesting() to have it call // WKBundlePageSetUseTestingViewportConfiguration(false). } TestFeatures TestController::platformSpecificFeatureDefaultsForTest(const TestCommand&) const { return { }; } void TestController::platformInitializeContext() { } void TestController::runModal(PlatformWebView* view) { UIWindow *window = [view->platformView() window]; if (!window) return; // FIXME: how to perform on iOS? // [[UIApplication sharedApplication] runModalForWindow:window]; } void TestController::abortModal() { } const char* TestController::platformLibraryPathForTesting() { static NeverDestroyed> platformLibraryPath; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ platformLibraryPath.get() = [@"~/Library/Application Support/WebKitTestRunner" stringByExpandingTildeInPath]; }); return [platformLibraryPath.get() UTF8String]; } void TestController::setHidden(bool) { // FIXME: implement for iOS } static UIKeyboardInputMode *swizzleCurrentInputMode() { return TestController::singleton().overriddenKeyboardInputMode(); } static NSArray *swizzleActiveInputModes() { return @[ TestController::singleton().overriddenKeyboardInputMode() ]; } void TestController::setKeyboardInputModeIdentifier(const String& identifier) { m_inputModeSwizzlers.clear(); m_overriddenKeyboardInputMode = [UIKeyboardInputMode keyboardInputModeWithIdentifier:identifier]; if (!m_overriddenKeyboardInputMode) { ASSERT_NOT_REACHED(); return; } auto controllerClass = UIKeyboardInputModeController.class; m_inputModeSwizzlers.reserveCapacity(3); m_inputModeSwizzlers.uncheckedAppend(makeUnique(controllerClass, @selector(currentInputMode), reinterpret_cast(swizzleCurrentInputMode))); m_inputModeSwizzlers.uncheckedAppend(makeUnique(controllerClass, @selector(currentInputModeInPreference), reinterpret_cast(swizzleCurrentInputMode))); m_inputModeSwizzlers.uncheckedAppend(makeUnique(controllerClass, @selector(activeInputModes), reinterpret_cast(swizzleActiveInputModes))); [UIKeyboardImpl.sharedInstance prepareKeyboardInputModeFromPreferences:nil]; } } // namespace WTR