/* * Copyright (C) 2015-2020 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 "UIScriptControllerIOS.h" #if PLATFORM(IOS_FAMILY) #import "HIDEventGenerator.h" #import "PlatformViewHelpers.h" #import "PlatformWebView.h" #import "StringFunctions.h" #import "TestController.h" #import "TestRunnerWKWebView.h" #import "UIKitSPI.h" #import "UIScriptContext.h" #import #import #import #import #import #import #import #import #import #import #import #import #import SOFT_LINK_FRAMEWORK(UIKit) SOFT_LINK_CLASS(UIKit, UIPhysicalKeyboardEvent) @interface UIPhysicalKeyboardEvent (UIPhysicalKeyboardEventHack) @property (nonatomic, assign) NSInteger _modifierFlags; @end namespace WTR { static BOOL returnYes() { return YES; } static BOOL returnNo() { return NO; } static NSDictionary *toNSDictionary(CGRect rect) { return @{ @"left": @(rect.origin.x), @"top": @(rect.origin.y), @"width": @(rect.size.width), @"height": @(rect.size.height) }; } static Vector parseModifierArray(JSContextRef context, JSValueRef arrayValue) { if (!arrayValue) return { }; // The value may either be a string with a single modifier or an array of modifiers. if (JSValueIsString(context, arrayValue)) return { toWTFString(context, arrayValue) }; if (!JSValueIsObject(context, arrayValue)) return { }; JSObjectRef array = const_cast(arrayValue); unsigned length = arrayLength(context, array); Vector modifiers; modifiers.reserveInitialCapacity(length); for (unsigned i = 0; i < length; ++i) modifiers.append(toWTFString(context, JSObjectGetPropertyAtIndex(context, array, i, nullptr))); return modifiers; } Ref UIScriptController::create(UIScriptContext& context) { return adoptRef(*new UIScriptControllerIOS(context)); } void UIScriptControllerIOS::waitForOutstandingCallbacks() { HIDEventGenerator *eventGenerator = HIDEventGenerator.sharedHIDEventGenerator; NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:1]; while (eventGenerator.hasOutstandingCallbacks) { [NSRunLoop.currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:timeoutDate]; if ([timeoutDate compare:NSDate.date] == NSOrderedAscending) [NSException raise:@"WebKitTestRunnerTestProblem" format:@"The previous test completed before all synthesized events had been handled. Perhaps you're calling notifyDone() too early?"]; } } void UIScriptControllerIOS::doAfterPresentationUpdate(JSValueRef callback) { unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent); [webView() _doAfterNextPresentationUpdate:makeBlockPtr([this, strongThis = makeRef(*this), callbackID] { if (!m_context) return; m_context->asyncTaskComplete(callbackID); }).get()]; } void UIScriptControllerIOS::doAfterNextStablePresentationUpdate(JSValueRef callback) { unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent); [webView() _doAfterNextStablePresentationUpdate:makeBlockPtr([this, strongThis = makeRef(*this), callbackID] { if (!m_context) return; m_context->asyncTaskComplete(callbackID); }).get()]; } void UIScriptControllerIOS::ensurePositionInformationIsUpToDateAt(long x, long y, JSValueRef callback) { unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent); [webView() _requestActivatedElementAtPosition:CGPointMake(x, y) completionBlock:makeBlockPtr([this, strongThis = makeRef(*this), callbackID] (_WKActivatedElementInfo *) { if (!m_context) return; m_context->asyncTaskComplete(callbackID); }).get()]; } void UIScriptControllerIOS::doAfterVisibleContentRectUpdate(JSValueRef callback) { unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent); [webView() _doAfterNextVisibleContentRectUpdate:makeBlockPtr([this, strongThis = makeRef(*this), callbackID] { if (!m_context) return; m_context->asyncTaskComplete(callbackID); }).get()]; } void UIScriptControllerIOS::zoomToScale(double scale, JSValueRef callback) { unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent); [webView() zoomToScale:scale animated:YES completionHandler:makeBlockPtr([this, strongThis = makeRef(*this), callbackID] { if (!m_context) return; m_context->asyncTaskComplete(callbackID); }).get()]; } void UIScriptControllerIOS::retrieveSpeakSelectionContent(JSValueRef callback) { unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent); [webView() accessibilityRetrieveSpeakSelectionContentWithCompletionHandler:makeBlockPtr([this, strongThis = makeRef(*this), callbackID] { if (!m_context) return; m_context->asyncTaskComplete(callbackID); }).get()]; } JSRetainPtr UIScriptControllerIOS::accessibilitySpeakSelectionContent() const { return adopt(JSStringCreateWithCFString((CFStringRef)webView().accessibilitySpeakSelectionContent)); } void UIScriptControllerIOS::simulateAccessibilitySettingsChangeNotification(JSValueRef callback) { unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent); auto* webView = this->webView(); NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center postNotificationName:UIAccessibilityInvertColorsStatusDidChangeNotification object:webView]; [webView _doAfterNextPresentationUpdate:makeBlockPtr([this, strongThis = makeRef(*this), callbackID] { if (!m_context) return; m_context->asyncTaskComplete(callbackID); }).get()]; } double UIScriptControllerIOS::zoomScale() const { return webView().scrollView.zoomScale; } static CGPoint globalToContentCoordinates(TestRunnerWKWebView *webView, long x, long y) { CGPoint point = CGPointMake(x, y); point = [webView _convertPointFromContentsToView:point]; point = [webView convertPoint:point toView:nil]; point = [webView.window convertPoint:point toWindow:nil]; return point; } void UIScriptControllerIOS::touchDownAtPoint(long x, long y, long touchCount, JSValueRef callback) { unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent); auto location = globalToContentCoordinates(webView(), x, y); [[HIDEventGenerator sharedHIDEventGenerator] touchDown:location touchCount:touchCount completionBlock:makeBlockPtr([this, strongThis = makeRef(*this), callbackID] { if (!m_context) return; m_context->asyncTaskComplete(callbackID); }).get()]; } void UIScriptControllerIOS::liftUpAtPoint(long x, long y, long touchCount, JSValueRef callback) { unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent); auto location = globalToContentCoordinates(webView(), x, y); [[HIDEventGenerator sharedHIDEventGenerator] liftUp:location touchCount:touchCount completionBlock:makeBlockPtr([this, strongThis = makeRef(*this), callbackID] { if (!m_context) return; m_context->asyncTaskComplete(callbackID); }).get()]; } void UIScriptControllerIOS::singleTapAtPoint(long x, long y, JSValueRef callback) { singleTapAtPointWithModifiers(x, y, nullptr, callback); } void UIScriptControllerIOS::activateAtPoint(long x, long y, JSValueRef callback) { singleTapAtPoint(x, y, callback); } void UIScriptControllerIOS::waitForModalTransitionToFinish() const { while ([webView().window.rootViewController isPerformingModalTransition]) [NSRunLoop.currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:NSDate.distantFuture]; } void UIScriptControllerIOS::waitForSingleTapToReset() const { auto allPendingSingleTapGesturesHaveBeenReset = [&]() -> bool { for (UIGestureRecognizer *gesture in [platformContentView() gestureRecognizers]) { if (!gesture.enabled || ![gesture isKindOfClass:UITapGestureRecognizer.class]) continue; UITapGestureRecognizer *tapGesture = (UITapGestureRecognizer *)gesture; if (tapGesture.numberOfTapsRequired != 1 || tapGesture.numberOfTouches != 1 || tapGesture.state == UIGestureRecognizerStatePossible) continue; return false; } return true; }; auto startTime = MonotonicTime::now(); while (!allPendingSingleTapGesturesHaveBeenReset()) { [NSRunLoop.currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:NSDate.distantFuture]; if (MonotonicTime::now() - startTime > 1_s) break; } } void UIScriptControllerIOS::twoFingerSingleTapAtPoint(long x, long y, JSValueRef callback) { unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent); auto location = globalToContentCoordinates(webView(), x, y); [[HIDEventGenerator sharedHIDEventGenerator] twoFingerTap:location completionBlock:makeBlockPtr([this, strongThis = makeRef(*this), callbackID] { if (!m_context) return; m_context->asyncTaskComplete(callbackID); }).get()]; } void UIScriptControllerIOS::singleTapAtPointWithModifiers(long x, long y, JSValueRef modifierArray, JSValueRef callback) { unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent); singleTapAtPointWithModifiers(WebCore::FloatPoint(x, y), parseModifierArray(m_context->jsContext(), modifierArray), makeBlockPtr([this, protectedThis = makeRefPtr(*this), callbackID] { if (!m_context) return; m_context->asyncTaskComplete(callbackID); })); } void UIScriptControllerIOS::singleTapAtPointWithModifiers(WebCore::FloatPoint location, Vector&& modifierFlags, BlockPtr&& block) { // Animations on the scroll view could be in progress to reveal a form control which may interfere with hit testing (see wkb.ug/205458). [webView().scrollView _removeAllAnimations:NO]; // Necessary for popovers on iPad (used for elements such as