blob: 657d9a016d31feb5b8b61accccf7610c74600200 [file] [log] [blame] [edit]
/*
* 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 "CocoaColorSerialization.h"
#import "HIDEventGenerator.h"
#import "PlatformViewHelpers.h"
#import "PlatformWebView.h"
#import "StringFunctions.h"
#import "TestController.h"
#import "TestRunnerWKWebView.h"
#import "UIKitSPIForTesting.h"
#import "UIScriptContext.h"
#import <JavaScriptCore/JavaScriptCore.h>
#import <JavaScriptCore/OpaqueJSString.h>
#import <UIKit/UIKit.h>
#import <WebCore/FloatPoint.h>
#import <WebCore/FloatRect.h>
#import <WebKit/WKWebViewPrivate.h>
#import <WebKit/WKWebViewPrivateForTesting.h>
#import <WebKit/WebKit.h>
#import <pal/spi/ios/BrowserEngineKitSPI.h>
#import <pal/spi/ios/GraphicsServicesSPI.h>
#import <wtf/BlockPtr.h>
#import <wtf/MonotonicTime.h>
#import <wtf/SoftLinking.h>
#import <wtf/Vector.h>
#import <wtf/cocoa/TypeCastsCocoa.h>
#import <wtf/darwin/DispatchExtras.h>
SOFT_LINK_FRAMEWORK(UIKit)
SOFT_LINK_CLASS(UIKit, UIPhysicalKeyboardEvent)
@interface UIPhysicalKeyboardEvent (UIPhysicalKeyboardEventHack)
@property (nonatomic, assign, setter=_setModifierFlags:) NSInteger _modifierFlags;
@end
@interface UIScrollView (WebKitTestRunner)
- (CGRect)_wtr_visibleBoundsInCoordinateSpace:(id<UICoordinateSpace>)coordinateSpace;
@end
@implementation UIScrollView (WebKitTestRunner)
- (CGRect)_wtr_visibleBoundsInCoordinateSpace:(id<UICoordinateSpace>)coordinateSpace
{
auto scrollOffset = self.contentOffset;
auto scrollSize = self.bounds.size;
auto visibleRect = CGRectMake(scrollOffset.x, scrollOffset.y, scrollSize.width, scrollSize.height);
return [self convertRect:visibleRect toCoordinateSpace:coordinateSpace];
}
@end
@interface UIView (WebKitTestRunner)
- (UIView *)_wtr_frontmostViewAtPoint:(CGPoint)point;
@end
@implementation UIView (WebKitTestRunner)
- (UIView *)_wtr_frontmostViewAtPoint:(CGPoint)point
{
if (self.hidden || !self.alpha)
return nil;
for (UIView *subview in self.subviews.reverseObjectEnumerator) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
if (RetainPtr frontmostView = [subview _wtr_frontmostViewAtPoint:convertedPoint])
return frontmostView.unsafeGet();
}
if (![self.layer.presentationLayer containsPoint:point])
return nil;
if ([self.layer.name isEqualToString:@"Page TiledBacking containment"])
return nil;
return self;
}
@end
namespace WTR {
#if HAVE(UI_TEXT_SELECTION_DISPLAY_INTERACTION)
static bool isHiddenOrHasHiddenAncestor(UIView *view)
{
for (auto currentAncestor = view; currentAncestor; currentAncestor = currentAncestor.superview) {
if (currentAncestor.hidden)
return true;
}
return false;
}
#endif // HAVE(UI_TEXT_SELECTION_DISPLAY_INTERACTION)
static NSDictionary *toNSDictionary(CGRect rect)
{
return @{
@"left": @(rect.origin.x),
@"top": @(rect.origin.y),
@"width": @(rect.size.width),
@"height": @(rect.size.height)
};
}
static NSDictionary *toNSDictionary(UIEdgeInsets insets)
{
return @{
@"top" : @(insets.top),
@"left" : @(insets.left),
@"bottom" : @(insets.bottom),
@"right" : @(insets.right)
};
}
static RetainPtr<NSDictionary> toNSDictionary(CGPathRef path)
{
auto pathElementTypeToString = [](CGPathElementType type) {
switch (type) {
case kCGPathElementMoveToPoint:
return @"MoveToPoint";
case kCGPathElementAddLineToPoint:
return @"AddLineToPoint";
case kCGPathElementAddQuadCurveToPoint:
return @"AddQuadCurveToPoint";
case kCGPathElementAddCurveToPoint:
return @"AddCurveToPoint";
case kCGPathElementCloseSubpath:
return @"CloseSubpath";
default:
return @"Unknown";
}
};
auto attributes = adoptNS([[NSMutableDictionary alloc] init]);
CGPathApplyWithBlock(path, ^(const CGPathElement *element) {
if (!element)
return;
NSString *typeString = pathElementTypeToString(element->type);
[attributes setObject:@{
@"x": @(element->points->x),
@"y": @(element->points->y),
} forKey:typeString];
});
return attributes;
}
static Vector<String> 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<JSObjectRef>(arrayValue);
unsigned length = arrayLength(context, array);
Vector<String> modifiers;
modifiers.reserveInitialCapacity(length);
for (unsigned i = 0; i < length; ++i)
modifiers.append(toWTFString(context, JSObjectGetPropertyAtIndex(context, array, i, nullptr)));
return modifiers;
}
static Class internalClassNamed(NSString *className)
{
auto result = NSClassFromString(className);
if (!result)
NSLog(@"Warning: an internal class named '%@' does not exist.", className);
return result;
}
Ref<UIScriptController> 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::doAfterNextStablePresentationUpdate(JSValueRef callback)
{
unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
[webView() _doAfterNextStablePresentationUpdate:makeBlockPtr([this, protectedThis = Ref { *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, protectedThis = Ref { *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, protectedThis = Ref { *this }, callbackID] {
if (!m_context)
return;
m_context->asyncTaskComplete(callbackID);
}).get()];
}
void UIScriptControllerIOS::doAfterNextVisibleContentRectAndStablePresentationUpdate(JSValueRef callback)
{
unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
[webView() _doAfterNextVisibleContentRectAndStablePresentationUpdate:makeBlockPtr([this, protectedThis = Ref { *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, protectedThis = Ref { *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, protectedThis = Ref { *this }, callbackID] {
if (!m_context)
return;
m_context->asyncTaskComplete(callbackID);
}).get()];
}
JSRetainPtr<JSStringRef> 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, protectedThis = Ref { *this }, callbackID] {
if (!m_context)
return;
m_context->asyncTaskComplete(callbackID);
}).get()];
}
double UIScriptControllerIOS::zoomScale() const
{
return webView().scrollView.zoomScale;
}
bool UIScriptControllerIOS::isZoomingOrScrolling() const
{
return webView().zoomingOrScrolling;
}
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, protectedThis = Ref { *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, protectedThis = Ref { *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, protectedThis = Ref { *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 = Ref { *this }, callbackID] {
if (!m_context)
return;
m_context->asyncTaskComplete(callbackID);
}));
}
void UIScriptControllerIOS::singleTapAtPointWithModifiers(WebCore::FloatPoint location, Vector<String>&& modifierFlags, BlockPtr<void()>&& 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 <select>) to finish dismissing (see wkb.ug/206759).
waitForModalTransitionToFinish();
waitForSingleTapToReset();
for (auto& modifierFlag : modifierFlags)
[[HIDEventGenerator sharedHIDEventGenerator] keyDown:modifierFlag.createNSString().get()];
[[HIDEventGenerator sharedHIDEventGenerator] tap:globalToContentCoordinates(webView(), location.x(), location.y()) completionBlock:[this, protectedThis = Ref { *this }, modifierFlags = WTF::move(modifierFlags), block = WTF::move(block)] () mutable {
if (!m_context)
return;
for (size_t i = modifierFlags.size(); i; ) {
--i;
[[HIDEventGenerator sharedHIDEventGenerator] keyUp:modifierFlags[i].createNSString().get()];
}
[[HIDEventGenerator sharedHIDEventGenerator] sendMarkerHIDEventWithCompletionBlock:block.get()];
}];
}
void UIScriptControllerIOS::doubleTapAtPoint(long x, long y, float delay, JSValueRef callback)
{
unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
[[HIDEventGenerator sharedHIDEventGenerator] doubleTap:globalToContentCoordinates(webView(), x, y) delay:delay completionBlock:makeBlockPtr([this, protectedThis = Ref { *this }, callbackID] {
if (!m_context)
return;
m_context->asyncTaskComplete(callbackID);
}).get()];
}
void UIScriptControllerIOS::stylusDownAtPoint(long x, long y, float azimuthAngle, float altitudeAngle, float pressure, JSValueRef callback)
{
unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
auto location = globalToContentCoordinates(webView(), x, y);
[[HIDEventGenerator sharedHIDEventGenerator] stylusDownAtPoint:location azimuthAngle:azimuthAngle altitudeAngle:altitudeAngle pressure:pressure completionBlock:makeBlockPtr([this, protectedThis = Ref { *this }, callbackID] {
if (!m_context)
return;
m_context->asyncTaskComplete(callbackID);
}).get()];
}
void UIScriptControllerIOS::stylusMoveToPoint(long x, long y, float azimuthAngle, float altitudeAngle, float pressure, JSValueRef callback)
{
unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
auto location = globalToContentCoordinates(webView(), x, y);
[[HIDEventGenerator sharedHIDEventGenerator] stylusMoveToPoint:location azimuthAngle:azimuthAngle altitudeAngle:altitudeAngle pressure:pressure completionBlock:makeBlockPtr([this, protectedThis = Ref { *this }, callbackID] {
if (!m_context)
return;
m_context->asyncTaskComplete(callbackID);
}).get()];
}
void UIScriptControllerIOS::stylusUpAtPoint(long x, long y, JSValueRef callback)
{
unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
auto location = globalToContentCoordinates(webView(), x, y);
[[HIDEventGenerator sharedHIDEventGenerator] stylusUpAtPoint:location completionBlock:makeBlockPtr([this, protectedThis = Ref { *this }, callbackID] {
if (!m_context)
return;
m_context->asyncTaskComplete(callbackID);
}).get()];
}
void UIScriptControllerIOS::stylusTapAtPoint(long x, long y, float azimuthAngle, float altitudeAngle, float pressure, JSValueRef callback)
{
stylusTapAtPointWithModifiers(x, y, azimuthAngle, altitudeAngle, pressure, nullptr, callback);
}
void UIScriptControllerIOS::stylusTapAtPointWithModifiers(long x, long y, float azimuthAngle, float altitudeAngle, float pressure, JSValueRef modifierArray, JSValueRef callback)
{
unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
waitForSingleTapToReset();
auto modifierFlags = parseModifierArray(m_context->jsContext(), modifierArray);
for (auto& modifierFlag : modifierFlags)
[[HIDEventGenerator sharedHIDEventGenerator] keyDown:modifierFlag.createNSString().get()];
auto location = globalToContentCoordinates(webView(), x, y);
[[HIDEventGenerator sharedHIDEventGenerator] stylusTapAtPoint:location azimuthAngle:azimuthAngle altitudeAngle:altitudeAngle pressure:pressure completionBlock:makeBlockPtr([this, protectedThis = Ref { *this }, callbackID, modifierFlags = WTF::move(modifierFlags)] {
if (!m_context)
return;
for (size_t i = modifierFlags.size(); i; ) {
--i;
[[HIDEventGenerator sharedHIDEventGenerator] keyUp:modifierFlags[i].createNSString().get()];
}
[[HIDEventGenerator sharedHIDEventGenerator] sendMarkerHIDEventWithCompletionBlock:makeBlockPtr([this, protectedThis = Ref { *this }, callbackID] {
if (!m_context)
return;
m_context->asyncTaskComplete(callbackID);
}).get()];
}).get()];
}
void convertCoordinates(TestRunnerWKWebView *webView, NSMutableDictionary *event)
{
if (event[HIDEventTouchesKey]) {
for (NSMutableDictionary *touch in event[HIDEventTouchesKey]) {
NSNumber *touchX = touch[HIDEventXKey] == [NSNull null] ? nil : touch[HIDEventXKey];
NSNumber *touchY = touch[HIDEventYKey] == [NSNull null] ? nil : touch[HIDEventYKey];
auto location = globalToContentCoordinates(webView, (long)[touchX doubleValue], (long)[touchY doubleValue]);
touch[HIDEventXKey] = @(location.x);
touch[HIDEventYKey] = @(location.y);
}
}
}
void UIScriptControllerIOS::sendEventStream(JSStringRef eventsJSON, JSValueRef callback)
{
unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
String jsonString = eventsJSON->string();
auto eventInfo = dynamic_objc_cast<NSDictionary>([NSJSONSerialization JSONObjectWithData:[jsonString.createNSString() dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingMutableContainers | NSJSONReadingMutableLeaves error:nil]);
auto *webView = this->webView();
for (NSMutableDictionary *event in eventInfo[TopLevelEventInfoKey]) {
if (![event[HIDEventCoordinateSpaceKey] isEqualToString:HIDEventCoordinateSpaceTypeContent])
continue;
if (event[HIDEventStartEventKey])
convertCoordinates(webView, event[HIDEventStartEventKey]);
if (event[HIDEventEndEventKey])
convertCoordinates(webView, event[HIDEventEndEventKey]);
if (event[HIDEventTouchesKey])
convertCoordinates(webView, event);
}
if (!eventInfo || ![eventInfo isKindOfClass:[NSDictionary class]]) {
WTFLogAlways("JSON is not convertible to a dictionary");
return;
}
auto completion = makeBlockPtr([this, protectedThis = Ref { *this }, callbackID] {
if (!m_context)
return;
m_context->asyncTaskComplete(callbackID);
});
[[HIDEventGenerator sharedHIDEventGenerator] sendEventStream:eventInfo completionBlock:completion.get()];
}
static NSDictionary *dictionaryForFingerEventWithContentPoint(CGPoint point, NSString* phase, Seconds timeOffset)
{
return @{
HIDEventCoordinateSpaceKey : HIDEventCoordinateSpaceTypeContent,
HIDEventTimeOffsetKey : @(timeOffset.seconds()),
HIDEventInputType : HIDEventInputTypeHand,
HIDEventTouchesKey : @[
@{
HIDEventTouchIDKey : @1,
HIDEventInputType : HIDEventInputTypeFinger,
HIDEventPhaseKey : phase,
HIDEventXKey : @(point.x),
HIDEventYKey : @(point.y),
},
],
};
}
void UIScriptControllerIOS::dragFromPointToPoint(long startX, long startY, long endX, long endY, double durationSeconds, JSValueRef callback)
{
unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
CGPoint startPoint = globalToContentCoordinates(webView(), startX, startY);
CGPoint endPoint = globalToContentCoordinates(webView(), endX, endY);
NSDictionary *touchDownInfo = dictionaryForFingerEventWithContentPoint(startPoint, HIDEventPhaseBegan, 0_s);
NSDictionary *interpolatedEvents = @{
HIDEventInterpolateKey : HIDEventInterpolationTypeLinear,
HIDEventCoordinateSpaceKey : HIDEventCoordinateSpaceTypeContent,
HIDEventTimestepKey : @(0.016),
HIDEventStartEventKey : dictionaryForFingerEventWithContentPoint(startPoint, HIDEventPhaseMoved, 0_s),
HIDEventEndEventKey : dictionaryForFingerEventWithContentPoint(endPoint, HIDEventPhaseMoved, Seconds(durationSeconds)),
};
NSDictionary *liftUpInfo = dictionaryForFingerEventWithContentPoint(endPoint, HIDEventPhaseEnded, Seconds(durationSeconds));
NSDictionary *eventStream = @{
TopLevelEventInfoKey : @[
touchDownInfo,
interpolatedEvents,
liftUpInfo,
],
};
auto completion = makeBlockPtr([this, protectedThis = Ref { *this }, callbackID] {
if (!m_context)
return;
m_context->asyncTaskComplete(callbackID);
});
[[HIDEventGenerator sharedHIDEventGenerator] sendEventStream:eventStream completionBlock:completion.get()];
}
void UIScriptControllerIOS::longPressAtPoint(long x, long y, JSValueRef callback)
{
unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
[[HIDEventGenerator sharedHIDEventGenerator] longPress:globalToContentCoordinates(webView(), x, y) completionBlock:makeBlockPtr([this, protectedThis = Ref { *this }, callbackID] {
if (!m_context)
return;
m_context->asyncTaskComplete(callbackID);
}).get()];
}
void UIScriptControllerIOS::enterText(JSStringRef text)
{
auto textAsCFString = adoptCF(JSStringCopyCFString(kCFAllocatorDefault, text));
[webView() _simulateTextEntered:(NSString *)textAsCFString.get()];
}
void UIScriptControllerIOS::typeCharacterUsingHardwareKeyboard(JSStringRef character, JSValueRef callback)
{
unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
// Assumes that the keyboard is already shown.
[[HIDEventGenerator sharedHIDEventGenerator] keyPress:toWTFString(character).createNSString().get() completionBlock:makeBlockPtr([protectedThis = Ref { *this }, callbackID] {
if (protectedThis->m_context)
protectedThis->m_context->asyncTaskComplete(callbackID);
}).get()];
}
enum class IsKeyDown : bool { No, Yes };
static UIPhysicalKeyboardEvent *createUIPhysicalKeyboardEvent(NSString *hidInputString, NSString *uiEventInputString, UIKeyModifierFlags modifierFlags, UIKeyboardInputFlags inputFlags, IsKeyDown isKeyDown)
{
auto* keyboardEvent = [getUIPhysicalKeyboardEventClassSingleton() _eventWithInput:uiEventInputString inputFlags:inputFlags];
[keyboardEvent _setModifierFlags:modifierFlags];
auto hidEvent = createHIDKeyEvent(hidInputString, keyboardEvent.timestamp, isKeyDown == IsKeyDown::Yes);
[keyboardEvent _setHIDEvent:hidEvent.get() keyboard:nullptr];
return keyboardEvent;
}
void UIScriptControllerIOS::rawKeyDown(JSStringRef key)
{
// Key can be either a single Unicode code point or the name of a special key (e.g. "downArrow").
// HIDEventGenerator knows how to map these special keys to the appropriate keycode.
[[HIDEventGenerator sharedHIDEventGenerator] keyDown:toWTFString(key).createNSString().get()];
[[HIDEventGenerator sharedHIDEventGenerator] sendMarkerHIDEventWithCompletionBlock:^{ /* Do nothing */ }];
}
void UIScriptControllerIOS::rawKeyUp(JSStringRef key)
{
// Key can be either a single Unicode code point or the name of a special key (e.g. "downArrow").
// HIDEventGenerator knows how to map these special keys to the appropriate keycode.
[[HIDEventGenerator sharedHIDEventGenerator] keyUp:toWTFString(key).createNSString().get()];
[[HIDEventGenerator sharedHIDEventGenerator] sendMarkerHIDEventWithCompletionBlock:^{ /* Do nothing */ }];
}
void UIScriptControllerIOS::keyDown(JSStringRef character, JSValueRef modifierArray)
{
// Character can be either a single Unicode code point or the name of a special key (e.g. "downArrow").
// HIDEventGenerator knows how to map these special keys to the appropriate keycode.
auto inputString = toWTFString(character);
auto modifierFlags = parseModifierArray(m_context->jsContext(), modifierArray);
for (auto& modifierFlag : modifierFlags)
[[HIDEventGenerator sharedHIDEventGenerator] keyDown:modifierFlag.createNSString().get()];
[[HIDEventGenerator sharedHIDEventGenerator] keyDown:inputString.createNSString().get()];
[[HIDEventGenerator sharedHIDEventGenerator] keyUp:inputString.createNSString().get()];
for (size_t i = modifierFlags.size(); i; ) {
--i;
[[HIDEventGenerator sharedHIDEventGenerator] keyUp:modifierFlags[i].createNSString().get()];
}
[[HIDEventGenerator sharedHIDEventGenerator] sendMarkerHIDEventWithCompletionBlock:^{ /* Do nothing */ }];
}
void UIScriptControllerIOS::dismissFormAccessoryView()
{
[webView() dismissFormAccessoryView];
}
JSObjectRef UIScriptControllerIOS::filePickerAcceptedTypeIdentifiers()
{
NSArray *acceptedTypeIdentifiers = [webView() _filePickerAcceptedTypeIdentifiers];
return JSValueToObject(m_context->jsContext(), [JSValue valueWithObject:acceptedTypeIdentifiers inContext:[JSContext contextWithJSGlobalContextRef:m_context->jsContext()]].JSValueRef, nullptr);
}
void UIScriptControllerIOS::dismissFilePicker(JSValueRef callback)
{
TestRunnerWKWebView *webView = this->webView();
[webView _dismissFilePicker];
// Round-trip with the WebProcess to make sure it has been notified of the dismissal.
unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
[webView evaluateJavaScript:@"" completionHandler:makeBlockPtr([this, protectedThis = Ref { *this }, callbackID] (id result, NSError *error) {
if (!m_context)
return;
m_context->asyncTaskComplete(callbackID);
}).get()];
}
JSRetainPtr<JSStringRef> UIScriptControllerIOS::selectFormPopoverTitle() const
{
return adopt(JSStringCreateWithCFString((CFStringRef)webView().selectFormPopoverTitle));
}
JSRetainPtr<JSStringRef> UIScriptControllerIOS::textContentType() const
{
return adopt(JSStringCreateWithCFString((CFStringRef)(webView().textContentTypeForTesting ?: @"")));
}
JSRetainPtr<JSStringRef> UIScriptControllerIOS::formInputLabel() const
{
return adopt(JSStringCreateWithCFString((CFStringRef)webView().formInputLabel));
}
void UIScriptControllerIOS::selectFormAccessoryPickerRow(long rowIndex)
{
[webView() selectFormAccessoryPickerRow:rowIndex];
}
bool UIScriptControllerIOS::selectFormAccessoryHasCheckedItemAtRow(long rowIndex) const
{
return [webView() selectFormAccessoryHasCheckedItemAtRow:rowIndex];
}
void UIScriptControllerIOS::setTimePickerValue(long hour, long minute)
{
[webView() setTimePickerValueToHour:hour minute:minute];
}
double UIScriptControllerIOS::timePickerValueHour() const
{
return [webView() timePickerValueHour];
}
double UIScriptControllerIOS::timePickerValueMinute() const
{
return [webView() timePickerValueMinute];
}
bool UIScriptControllerIOS::isPresentingModally() const
{
return !!webView().window.rootViewController.presentedViewController;
}
static CGPoint contentOffsetBoundedIfNecessary(UIScrollView *scrollView, long x, long y, ScrollToOptions* options)
{
auto contentOffset = CGPointMake(x, y);
bool constrain = !options || !options->unconstrained;
if (constrain) {
UIEdgeInsets contentInsets = scrollView.contentInset;
CGSize contentSize = scrollView.contentSize;
CGSize scrollViewSize = scrollView.bounds.size;
CGFloat maxHorizontalOffset = contentSize.width + contentInsets.right - scrollViewSize.width;
contentOffset.x = std::min(maxHorizontalOffset, contentOffset.x);
contentOffset.x = std::max(-contentInsets.left, contentOffset.x);
CGFloat maxVerticalOffset = contentSize.height + contentInsets.bottom - scrollViewSize.height;
contentOffset.y = std::min(maxVerticalOffset, contentOffset.y);
contentOffset.y = std::max(-contentInsets.top, contentOffset.y);
}
return contentOffset;
}
double UIScriptControllerIOS::contentOffsetX() const
{
return webView().scrollView.contentOffset.x;
}
double UIScriptControllerIOS::contentOffsetY() const
{
return webView().scrollView.contentOffset.y;
}
JSObjectRef UIScriptControllerIOS::adjustedContentInset() const
{
return JSValueToObject(m_context->jsContext(), [JSValue valueWithObject:toNSDictionary(webView().scrollView.adjustedContentInset) inContext:[JSContext contextWithJSGlobalContextRef:m_context->jsContext()]].JSValueRef, nullptr);
}
bool UIScriptControllerIOS::scrollUpdatesDisabled() const
{
return webView()._scrollingUpdatesDisabledForTesting;
}
void UIScriptControllerIOS::setScrollUpdatesDisabled(bool disabled)
{
webView()._scrollingUpdatesDisabledForTesting = disabled;
}
void UIScriptControllerIOS::scrollToOffset(long x, long y, ScrollToOptions* options)
{
TestRunnerWKWebView *webView = this->webView();
auto offset = contentOffsetBoundedIfNecessary(webView.scrollView, x, y, options);
[webView.scrollView setContentOffset:offset animated:YES];
}
void UIScriptControllerIOS::immediateScrollToOffset(long x, long y, ScrollToOptions* options)
{
TestRunnerWKWebView *webView = this->webView();
auto offset = contentOffsetBoundedIfNecessary(webView.scrollView, x, y, options);
[webView.scrollView setContentOffset:offset animated:NO];
}
static UIScrollView *enclosingScrollViewIncludingSelf(UIView *view)
{
do {
if ([view isKindOfClass:[UIScrollView class]])
return static_cast<UIScrollView *>(view);
} while ((view = [view superview]));
return nil;
}
void UIScriptControllerIOS::immediateScrollElementAtContentPointToOffset(long x, long y, long xScrollOffset, long yScrollOffset)
{
UIView *contentView = platformContentView();
UIView *hitView = [contentView hitTest:CGPointMake(x, y) withEvent:nil];
UIScrollView *enclosingScrollView = enclosingScrollViewIncludingSelf(hitView);
[enclosingScrollView setContentOffset:CGPointMake(xScrollOffset, yScrollOffset)];
}
void UIScriptControllerIOS::immediateZoomToScale(double scale)
{
[webView().scrollView setZoomScale:scale animated:NO];
}
void UIScriptControllerIOS::keyboardAccessoryBarNext()
{
[webView() keyboardAccessoryBarNext];
}
void UIScriptControllerIOS::keyboardAccessoryBarPrevious()
{
[webView() keyboardAccessoryBarPrevious];
}
bool UIScriptControllerIOS::isShowingKeyboard() const
{
return webView().showingKeyboard;
}
bool UIScriptControllerIOS::hasInputSession() const
{
return webView().isInteractingWithFormControl;
}
void UIScriptControllerIOS::selectWordForReplacement()
{
#if USE(BROWSERENGINEKIT)
if (auto asyncInput = asyncTextInput()) {
[asyncInput selectWordForReplacement];
return;
}
#endif // USE(BROWSERENGINEKIT)
auto contentView = static_cast<id<UIWKInteractionViewProtocol>>(platformContentView());
[contentView selectWordForReplacement];
}
void UIScriptControllerIOS::applyAutocorrection(JSStringRef newString, JSStringRef oldString, JSValueRef callback, bool underline)
{
unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
#if USE(BROWSERENGINEKIT)
if (auto asyncInput = asyncTextInput()) {
auto completionWrapper = makeBlockPtr([this, protectedThis = Ref { *this }, callbackID](NSArray<UITextSelectionRect *> *) {
dispatch_async(mainDispatchQueueSingleton(), makeBlockPtr([this, protectedThis = Ref { *this }, callbackID] {
if (m_context)
m_context->asyncTaskComplete(callbackID);
}).get());
});
auto options = underline ? BETextReplacementOptionsAddUnderline : BETextReplacementOptionsNone;
[asyncInput replaceText:toWTFString(oldString).createNSString().get() withText:toWTFString(newString).createNSString().get() options:options completionHandler:completionWrapper.get()];
return;
}
#endif // USE(BROWSERENGINEKIT)
auto contentView = static_cast<id<UIWKInteractionViewProtocol>>(platformContentView());
[contentView applyAutocorrection:toWTFString(newString).createNSString().get() toString:toWTFString(oldString).createNSString().get() shouldUnderline:underline withCompletionHandler:makeBlockPtr([this, protectedThis = Ref { *this }, callbackID](UIWKAutocorrectionRects *) {
dispatch_async(mainDispatchQueueSingleton(), makeBlockPtr([this, protectedThis = Ref { *this }, callbackID] {
// applyAutocorrection can call its completion handler synchronously,
// which makes UIScriptController unhappy (see bug 172884).
if (!m_context)
return;
m_context->asyncTaskComplete(callbackID);
}).get());
}).get()];
}
double UIScriptControllerIOS::minimumZoomScale() const
{
return webView().scrollView.minimumZoomScale;
}
double UIScriptControllerIOS::maximumZoomScale() const
{
return webView().scrollView.maximumZoomScale;
}
std::optional<bool> UIScriptControllerIOS::stableStateOverride() const
{
TestRunnerWKWebView *webView = this->webView();
if (webView._stableStateOverride)
return webView._stableStateOverride.boolValue;
return std::nullopt;
}
void UIScriptControllerIOS::setStableStateOverride(std::optional<bool> overrideValue)
{
TestRunnerWKWebView *webView = this->webView();
if (overrideValue)
webView._stableStateOverride = @(overrideValue.value());
else
webView._stableStateOverride = nil;
}
JSObjectRef UIScriptControllerIOS::contentVisibleRect() const
{
CGRect contentVisibleRect = webView()._contentVisibleRect;
WebCore::FloatRect rect(contentVisibleRect.origin.x, contentVisibleRect.origin.y, contentVisibleRect.size.width, contentVisibleRect.size.height);
return m_context->objectFromRect(rect);
}
CGRect UIScriptControllerIOS::selectionViewBoundsClippedToContentView(UIView *view, std::optional<CGRect>&& rectInView) const
{
auto contentView = platformContentView();
auto rect = [view convertRect:rectInView.value_or(view.bounds) toView:contentView];
rect = CGRectIntersection(contentView.bounds, rect);
for (RetainPtr parent = [view superview]; parent; parent = [parent superview]) {
RetainPtr scroller = dynamic_objc_cast<UIScrollView>(parent.get());
if (!scroller)
continue;
if (scroller == webView().scrollView)
break;
CGRect visibleRectInContentView = [scroller _wtr_visibleBoundsInCoordinateSpace:contentView];
rect = CGRectIntersection(visibleRectInContentView, rect);
}
#if USE(BROWSERENGINEKIT)
if (auto asyncInput = asyncTextInput()) {
auto selectionClipRect = asyncInput.selectionClipRect;
if (!CGRectIsNull(selectionClipRect))
rect = CGRectIntersection(selectionClipRect, rect);
return rect;
}
#endif
auto selectionClipRect = [(UIView <UITextInputInternal> *)contentView _selectionClipRect];
if (!CGRectIsNull(selectionClipRect))
rect = CGRectIntersection(selectionClipRect, rect);
return rect;
}
#if HAVE(UI_TEXT_SELECTION_DISPLAY_INTERACTION)
static void sanityCheckCustomHandlePath(UIView<UITextSelectionHandleView> *handle)
{
RetainPtr bezierPath = [handle customShape];
if (!bezierPath)
return;
if (auto customPathBounds = CGPathGetBoundingBox([bezierPath CGPath]); !CGRectIntersectsRect(handle.bounds, customPathBounds))
RELEASE_ASSERT_NOT_REACHED_WITH_MESSAGE("Selection handle path %@ does not intersect %@", bezierPath.get(), handle);
}
#endif // HAVE(UI_TEXT_SELECTION_DISPLAY_INTERACTION)
JSObjectRef UIScriptControllerIOS::selectionStartGrabberViewRect() const
{
RetainPtr<UIView> handleView;
#if HAVE(UI_TEXT_SELECTION_DISPLAY_INTERACTION)
if (RetainPtr view = [textSelectionDisplayInteraction().handleViews firstObject]; !isHiddenOrHasHiddenAncestor(view.get())) {
sanityCheckCustomHandlePath(view.get());
handleView = WTF::move(view);
}
#endif
auto frameInContentViewCoordinates = selectionViewBoundsClippedToContentView(handleView.get());
auto jsContext = m_context->jsContext();
return JSValueToObject(jsContext, [JSValue valueWithObject:toNSDictionary(frameInContentViewCoordinates) inContext:[JSContext contextWithJSGlobalContextRef:jsContext]].JSValueRef, nullptr);
}
JSObjectRef UIScriptControllerIOS::selectionEndGrabberViewRect() const
{
RetainPtr<UIView> handleView;
#if HAVE(UI_TEXT_SELECTION_DISPLAY_INTERACTION)
if (RetainPtr view = [textSelectionDisplayInteraction().handleViews lastObject]; !isHiddenOrHasHiddenAncestor(view.get())) {
sanityCheckCustomHandlePath(view.get());
handleView = WTF::move(view);
}
#endif
auto frameInContentViewCoordinates = selectionViewBoundsClippedToContentView(handleView.get());
auto jsContext = m_context->jsContext();
return JSValueToObject(jsContext, [JSValue valueWithObject:toNSDictionary(frameInContentViewCoordinates) inContext:[JSContext contextWithJSGlobalContextRef:jsContext]].JSValueRef, nullptr);
}
JSObjectRef UIScriptControllerIOS::selectionEndGrabberViewShapePathDescription() const
{
UIView *handleView = nil;
#if HAVE(UI_TEXT_SELECTION_DISPLAY_INTERACTION)
if (auto view = textSelectionDisplayInteraction().handleViews.lastObject; !isHiddenOrHasHiddenAncestor(view))
handleView = view;
#endif
return JSValueToObject(m_context->jsContext(), [JSValue valueWithObject:toNSDictionary((CGPathRef)[handleView valueForKeyPath:@"stemView.shapeLayer.path"]).get() inContext:[JSContext contextWithJSGlobalContextRef:m_context->jsContext()]].JSValueRef, nullptr);
}
JSObjectRef UIScriptControllerIOS::selectionCaretViewRect(id<UICoordinateSpace> coordinateSpace) const
{
UIView *contentView = platformContentView();
UIView *caretView = nil;
#if HAVE(UI_TEXT_SELECTION_DISPLAY_INTERACTION)
if (auto view = textSelectionDisplayInteraction().cursorView; !isHiddenOrHasHiddenAncestor(view))
caretView = view;
#endif
auto contentRect = selectionViewBoundsClippedToContentView(caretView);
if (coordinateSpace != contentView)
contentRect = [coordinateSpace convertRect:contentRect fromCoordinateSpace:contentView];
return JSValueToObject(m_context->jsContext(), [JSValue valueWithObject:toNSDictionary(contentRect) inContext:[JSContext contextWithJSGlobalContextRef:m_context->jsContext()]].JSValueRef, nullptr);
}
JSObjectRef UIScriptControllerIOS::selectionCaretViewRect() const
{
return selectionCaretViewRect(platformContentView());
}
JSObjectRef UIScriptControllerIOS::selectionCaretViewRectInGlobalCoordinates() const
{
return selectionCaretViewRect(webView());
}
JSObjectRef UIScriptControllerIOS::selectionRangeViewRects() const
{
UIView *contentView = platformContentView();
UIView *rangeView = nil;
auto rectsAsDictionaries = adoptNS([[NSMutableArray alloc] init]);
NSArray<UITextSelectionRect *> *textRectInfoArray = nil;
#if HAVE(UI_TEXT_SELECTION_DISPLAY_INTERACTION)
if (!textRectInfoArray) {
if (auto view = textSelectionDisplayInteraction().highlightView; !isHiddenOrHasHiddenAncestor(view)) {
rangeView = view;
textRectInfoArray = view.selectionRects;
}
}
#endif
for (UITextSelectionRect *textRectInfo in textRectInfoArray) {
auto rangeRectInContentViewCoordinates = selectionViewBoundsClippedToContentView(rangeView, textRectInfo.rect);
[rectsAsDictionaries addObject:toNSDictionary(CGRectIntersection(rangeRectInContentViewCoordinates, contentView.bounds))];
}
return JSValueToObject(m_context->jsContext(), [JSValue valueWithObject:rectsAsDictionaries.get() inContext:[JSContext contextWithJSGlobalContextRef:m_context->jsContext()]].JSValueRef, nullptr);
}
JSObjectRef UIScriptControllerIOS::inputViewBounds() const
{
return JSValueToObject(m_context->jsContext(), [JSValue valueWithObject:toNSDictionary(webView()._inputViewBoundsInWindow) inContext:[JSContext contextWithJSGlobalContextRef:m_context->jsContext()]].JSValueRef, nullptr);
}
JSRetainPtr<JSStringRef> UIScriptControllerIOS::scrollingTreeAsText() const
{
return adopt(JSStringCreateWithCFString((CFStringRef)[webView() _scrollingTreeAsText]));
}
JSRetainPtr<JSStringRef> UIScriptControllerIOS::uiViewTreeAsText() const
{
return adopt(JSStringCreateWithCFString((CFStringRef)[webView() _uiViewTreeAsText]));
}
JSRetainPtr<JSStringRef> UIScriptControllerIOS::uiViewTreeAsTextForViewWithLayerID(unsigned long long layerID) const
{
return adopt(JSStringCreateWithCFString((CFStringRef)[webView() _uiViewTreeAsTextForViewWithLayerID:layerID]));
}
bool UIScriptControllerIOS::mayContainEditableElementsInRect(unsigned x, unsigned y, unsigned width, unsigned height)
{
auto contentRect = CGRectMake(x, y, width, height);
return [webView() _mayContainEditableElementsInRect:[webView() convertRect:contentRect fromView:platformContentView()]];
}
void UIScriptControllerIOS::simulateRotation(DeviceOrientation* orientation, JSValueRef callback)
{
if (!orientation) {
ASSERT_NOT_REACHED();
return;
}
webView().usesSafariLikeRotation = NO;
simulateRotation(*orientation, callback);
}
void UIScriptControllerIOS::simulateRotationLikeSafari(DeviceOrientation* orientation, JSValueRef callback)
{
if (!orientation) {
ASSERT_NOT_REACHED();
return;
}
webView().usesSafariLikeRotation = YES;
simulateRotation(*orientation, callback);
}
#if HAVE(UI_WINDOW_SCENE_GEOMETRY_PREFERENCES)
static RetainPtr<UIWindowSceneGeometryPreferences> toWindowSceneGeometryPreferences(DeviceOrientation orientation)
{
UIInterfaceOrientationMask orientations = 0;
switch (orientation) {
case DeviceOrientation::Portrait:
orientations = UIInterfaceOrientationMaskPortrait;
break;
case DeviceOrientation::PortraitUpsideDown:
orientations = UIInterfaceOrientationMaskPortraitUpsideDown;
break;
case DeviceOrientation::LandscapeLeft:
orientations = UIInterfaceOrientationMaskLandscapeLeft;
break;
case DeviceOrientation::LandscapeRight:
orientations = UIInterfaceOrientationMaskLandscapeRight;
break;
}
ASSERT(orientations);
return adoptNS([[UIWindowSceneGeometryPreferencesIOS alloc] initWithInterfaceOrientations:orientations]);
}
#else
static UIDeviceOrientation toUIDeviceOrientation(DeviceOrientation orientation)
{
switch (orientation) {
case DeviceOrientation::Portrait:
return UIDeviceOrientationPortrait;
case DeviceOrientation::PortraitUpsideDown:
return UIDeviceOrientationPortraitUpsideDown;
case DeviceOrientation::LandscapeLeft:
return UIDeviceOrientationLandscapeLeft;
case DeviceOrientation::LandscapeRight:
return UIDeviceOrientationLandscapeRight;
}
ASSERT_NOT_REACHED();
return UIDeviceOrientationPortrait;
}
#endif // HAVE(UI_WINDOW_SCENE_GEOMETRY_PREFERENCES)
void UIScriptControllerIOS::simulateRotation(DeviceOrientation orientation, JSValueRef callback)
{
auto callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
webView().rotationDidEndCallback = makeBlockPtr([this, protectedThis = Ref { *this }, callbackID] {
if (m_context)
m_context->asyncTaskComplete(callbackID);
webView().rotationDidEndCallback = nil;
}).get();
#if HAVE(UI_WINDOW_SCENE_GEOMETRY_PREFERENCES)
[webView().window.windowScene requestGeometryUpdateWithPreferences:toWindowSceneGeometryPreferences(orientation).get() errorHandler:^(NSError *error) {
NSLog(@"Failed to simulate rotation with error: %@", error);
}];
#else
[[UIDevice currentDevice] setOrientation:toUIDeviceOrientation(orientation) animated:YES];
#endif
}
void UIScriptControllerIOS::setDidStartFormControlInteractionCallback(JSValueRef callback)
{
UIScriptController::setDidStartFormControlInteractionCallback(callback);
webView().didStartFormControlInteractionCallback = makeBlockPtr([this, protectedThis = Ref { *this }] {
if (!m_context)
return;
m_context->fireCallback(CallbackTypeDidStartFormControlInteraction);
}).get();
}
void UIScriptControllerIOS::setDidEndFormControlInteractionCallback(JSValueRef callback)
{
UIScriptController::setDidEndFormControlInteractionCallback(callback);
webView().didEndFormControlInteractionCallback = makeBlockPtr([this, protectedThis = Ref { *this }] {
if (!m_context)
return;
m_context->fireCallback(CallbackTypeDidEndFormControlInteraction);
}).get();
}
void UIScriptControllerIOS::setWillBeginZoomingCallback(JSValueRef callback)
{
UIScriptController::setWillBeginZoomingCallback(callback);
webView().willBeginZoomingCallback = makeBlockPtr([this, protectedThis = Ref { *this }] {
if (!m_context)
return;
m_context->fireCallback(CallbackTypeWillBeginZooming);
}).get();
}
void UIScriptControllerIOS::setDidEndZoomingCallback(JSValueRef callback)
{
UIScriptController::setDidEndZoomingCallback(callback);
webView().didEndZoomingCallback = makeBlockPtr([this, protectedThis = Ref { *this }] {
if (!m_context)
return;
m_context->fireCallback(CallbackTypeDidEndZooming);
}).get();
}
void UIScriptControllerIOS::setDidShowKeyboardCallback(JSValueRef callback)
{
UIScriptController::setDidShowKeyboardCallback(callback);
webView().didShowKeyboardCallback = makeBlockPtr([this, protectedThis = Ref { *this }] {
if (!m_context)
return;
m_context->fireCallback(CallbackTypeDidShowKeyboard);
}).get();
}
void UIScriptControllerIOS::setDidHideKeyboardCallback(JSValueRef callback)
{
UIScriptController::setDidHideKeyboardCallback(callback);
webView().didHideKeyboardCallback = makeBlockPtr([this, protectedThis = Ref { *this }] {
if (!m_context)
return;
m_context->fireCallback(CallbackTypeDidHideKeyboard);
}).get();
}
void UIScriptControllerIOS::setWillStartInputSessionCallback(JSValueRef callback)
{
UIScriptController::setWillStartInputSessionCallback(callback);
webView().willStartInputSessionCallback = makeBlockPtr([this, protectedThis = Ref { *this }] {
if (!m_context)
return;
m_context->fireCallback(CallbackTypeWillStartInputSession);
}).get();
}
void UIScriptControllerIOS::chooseMenuAction(JSStringRef jsAction, JSValueRef callback)
{
auto action = adoptCF(JSStringCopyCFString(kCFAllocatorDefault, jsAction));
auto rect = rectForMenuAction(action.get());
if (rect.isEmpty())
return;
unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
singleTapAtPointWithModifiers(rect.center(), { }, makeBlockPtr([this, protectedThis = Ref { *this }, callbackID] {
if (m_context)
m_context->asyncTaskComplete(callbackID);
}));
}
bool UIScriptControllerIOS::isShowingPopover() const
{
return webView().showingPopover;
}
bool UIScriptControllerIOS::isShowingFormValidationBubble() const
{
return webView().showingFormValidationBubble;
}
void UIScriptControllerIOS::setWillPresentPopoverCallback(JSValueRef callback)
{
UIScriptController::setWillPresentPopoverCallback(callback);
webView().willPresentPopoverCallback = makeBlockPtr([this, protectedThis = Ref { *this }] {
if (!m_context)
return;
m_context->fireCallback(CallbackTypeWillPresentPopover);
}).get();
}
void UIScriptControllerIOS::setDidDismissPopoverCallback(JSValueRef callback)
{
UIScriptController::setDidDismissPopoverCallback(callback);
webView().didDismissPopoverCallback = makeBlockPtr([this, protectedThis = Ref { *this }] {
if (!m_context)
return;
m_context->fireCallback(CallbackTypeDidDismissPopover);
}).get();
}
void UIScriptControllerIOS::setDidPresentViewControllerCallback(JSValueRef callback)
{
UIScriptController::setDidPresentViewControllerCallback(callback);
webView().didPresentViewControllerCallback = makeBlockPtr([this, protectedThis = Ref { *this }] {
if (!m_context)
return;
m_context->fireCallback(CallbackTypeDidPresentViewController);
}).get();
}
JSObjectRef UIScriptControllerIOS::rectForMenuAction(JSStringRef jsAction) const
{
auto action = adoptCF(JSStringCopyCFString(kCFAllocatorDefault, jsAction));
auto rect = rectForMenuAction(action.get());
if (rect.isEmpty())
return nullptr;
return m_context->objectFromRect(rect);
}
WebCore::FloatRect UIScriptControllerIOS::rectForMenuAction(CFStringRef action) const
{
UIView *viewForAction = nil;
auto searchForLabel = [&](UIWindow *window) -> UILabel * {
for (UILabel *label in findAllViewsInHierarchyOfType(window, UILabel.class)) {
if ([label.text isEqualToString:(__bridge NSString *)action])
return label;
}
return nil;
};
if (!viewForAction)
viewForAction = searchForLabel(webView().window) ?: searchForLabel(webView().textEffectsWindow);
if (!viewForAction)
return { };
CGRect rectInRootViewCoordinates = [viewForAction convertRect:viewForAction.bounds toView:platformContentView()];
return WebCore::FloatRect(rectInRootViewCoordinates.origin.x, rectInRootViewCoordinates.origin.y, rectInRootViewCoordinates.size.width, rectInRootViewCoordinates.size.height);
}
JSObjectRef UIScriptControllerIOS::menuRect() const
{
auto containerView = findAllViewsInHierarchyOfType(webView().textEffectsWindow, internalClassNamed(@"_UIEditMenuListView")).firstObject;
return containerView ? toObject([containerView convertRect:containerView.bounds toView:platformContentView()]) : nullptr;
}
JSObjectRef UIScriptControllerIOS::contextMenuPreviewRect() const
{
#if HAVE(LIQUID_GLASS)
RetainPtr platterName = @"_UIContentPlatterView";
#else
RetainPtr platterName = @"_UIMorphingPlatterView";
#endif
RetainPtr<UIView> container = findAllViewsInHierarchyOfType(webView().window, internalClassNamed(platterName.get())).firstObject;
if (!container)
return nullptr;
return toObject([container convertRect:container.get().bounds toView:nil]);
}
JSObjectRef UIScriptControllerIOS::contextMenuRect() const
{
auto *window = webView().window;
auto *contextMenuView = findAllViewsInHierarchyOfType(window, internalClassNamed(@"_UIContextMenuView")).firstObject;
if (!contextMenuView)
return nullptr;
return toObject([contextMenuView convertRect:contextMenuView.bounds toView:nil]);
}
JSObjectRef UIScriptControllerIOS::toObject(CGRect rect) const
{
return m_context->objectFromRect(WebCore::FloatRect(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height));
}
bool UIScriptControllerIOS::isDismissingMenu() const
{
return webView().dismissingMenu;
}
void UIScriptControllerIOS::setDidEndScrollingCallback(JSValueRef callback)
{
UIScriptController::setDidEndScrollingCallback(callback);
webView().didEndScrollingCallback = makeBlockPtr([this, protectedThis = Ref { *this }] {
if (!m_context)
return;
m_context->fireCallback(CallbackTypeDidEndScrolling);
}).get();
}
void UIScriptControllerIOS::clearAllCallbacks()
{
[webView() resetInteractionCallbacks];
}
void UIScriptControllerIOS::setSafeAreaInsets(double top, double right, double bottom, double left)
{
UIEdgeInsets insets = UIEdgeInsetsMake(top, left, bottom, right);
webView().overrideSafeAreaInsets = insets;
}
void UIScriptControllerIOS::beginInteractiveObscuredInsetsChange()
{
[webView() _beginInteractiveObscuredInsetsChange];
}
void UIScriptControllerIOS::endInteractiveObscuredInsetsChange()
{
[webView() _endInteractiveObscuredInsetsChange];
}
void UIScriptControllerIOS::beginBackSwipe(JSValueRef callback)
{
[webView() _beginBackSwipeForTesting];
}
void UIScriptControllerIOS::completeBackSwipe(JSValueRef callback)
{
[webView() _completeBackSwipeForTesting];
}
void UIScriptControllerIOS::activateDataListSuggestion(unsigned index, JSValueRef callback)
{
[webView() _selectDataListOption:index];
unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
dispatch_async(mainDispatchQueueSingleton(), makeBlockPtr([this, protectedThis = Ref { *this }, callbackID] {
if (!m_context)
return;
m_context->asyncTaskComplete(callbackID);
}).get());
}
bool UIScriptControllerIOS::isShowingDataListSuggestions() const
{
return [webView() _isShowingDataListSuggestions];
}
void UIScriptControllerIOS::setSelectedColorForColorPicker(double red, double green, double blue)
{
UIColor *color = [UIColor colorWithRed:red green:green blue:blue alpha:1.0f];
[webView() setSelectedColorForColorPicker:color];
}
void UIScriptControllerIOS::setKeyboardInputModeIdentifier(JSStringRef identifier)
{
TestController::singleton().setKeyboardInputModeIdentifier(toWTFString(identifier));
}
void UIScriptControllerIOS::setFocusStartsInputSessionPolicy(JSStringRef policyJS)
{
RetainPtr webView = this->webView();
auto policyString = toWTFString(policyJS);
if (policyString == "allow"_s)
webView.get().focusStartsInputSessionPolicy = _WKFocusStartsInputSessionPolicyAllow;
else if (policyString == "disallow"_s)
webView.get().focusStartsInputSessionPolicy = _WKFocusStartsInputSessionPolicyDisallow;
else if (policyString == "auto"_s)
webView.get().focusStartsInputSessionPolicy = _WKFocusStartsInputSessionPolicyAuto;
else
NSLog(@"setFocusStartsInputSessionPolicy received an invalid policy `%s`.", policyString.utf8().data());
}
// FIXME: Write this in terms of HIDEventGenerator once we know how to reset caps lock state
// on test completion to avoid it effecting subsequent tests.
void UIScriptControllerIOS::toggleCapsLock(JSValueRef callback)
{
m_capsLockOn = !m_capsLockOn;
auto uiKeyModifierFlag = m_capsLockOn ? UIKeyModifierAlphaShift : 0;
auto *keyboardEventDown = createUIPhysicalKeyboardEvent(@"capsLock", @"", uiKeyModifierFlag, kUIKeyboardInputModifierFlagsChanged, IsKeyDown::Yes);
auto *keyboardEventUp = createUIPhysicalKeyboardEvent(@"capsLock", @"", uiKeyModifierFlag, kUIKeyboardInputModifierFlagsChanged, IsKeyDown::No);
auto *pressInfo = [[UIApplication sharedApplication] _pressInfoForPhysicalKeyboardEvent:keyboardEventDown];
auto press = adoptNS([[UIPress alloc] init]);
[press _loadStateFromPressInfo:pressInfo];
auto *presses = [NSSet setWithObject:press.get()];
[platformContentView() pressesBegan:presses withEvent:keyboardEventDown];
[platformContentView() pressesEnded:presses withEvent:keyboardEventUp];
doAsyncTask(callback);
}
bool UIScriptControllerIOS::keyboardIsAutomaticallyShifted() const
{
return UIKeyboardImpl.activeInstance.isAutoShifted;
}
unsigned UIScriptControllerIOS::keyboardUpdateForChangedSelectionCount() const
{
return TestController::singleton().keyboardUpdateForChangedSelectionCount();
}
unsigned UIScriptControllerIOS::keyboardWillHideCount() const
{
return static_cast<unsigned>(webView().keyboardWillHideCount);
}
bool UIScriptControllerIOS::isAnimatingDragCancel() const
{
return webView()._animatingDragCancel;
}
JSRetainPtr<JSStringRef> UIScriptControllerIOS::selectionCaretBackgroundColor() const
{
UIColor *backgroundColor = nil;
#if HAVE(UI_TEXT_SELECTION_DISPLAY_INTERACTION)
if (auto view = textSelectionDisplayInteraction().cursorView; !isHiddenOrHasHiddenAncestor(view))
backgroundColor = view.tintColor;
#endif
if (!backgroundColor)
return nil;
auto serialization = WebCoreTestSupport::serializationForCSS(backgroundColor).createCFString();
return adopt(JSStringCreateWithCFString(serialization.get()));
}
JSObjectRef UIScriptControllerIOS::tapHighlightViewRect() const
{
return JSValueToObject(m_context->jsContext(), [JSValue valueWithObject:toNSDictionary(webView()._tapHighlightViewRect) inContext:[JSContext contextWithJSGlobalContextRef:m_context->jsContext()]].JSValueRef, nullptr);
}
JSObjectRef UIScriptControllerIOS::attachmentInfo(JSStringRef jsAttachmentIdentifier)
{
RetainPtr attachmentIdentifier = toWTFString(jsAttachmentIdentifier).createNSString();
_WKAttachment *attachment = [webView() _attachmentForIdentifier:attachmentIdentifier.get()];
_WKAttachmentInfo *attachmentInfo = attachment.info;
NSDictionary *attachmentInfoDictionary = @{
@"id": attachmentIdentifier.get(),
@"name": attachmentInfo.name,
@"contentType": attachmentInfo.contentType,
@"filePath": attachmentInfo.filePath,
@"size": @(attachmentInfo.data.length),
};
return JSValueToObject(m_context->jsContext(), [JSValue valueWithObject:attachmentInfoDictionary inContext:[JSContext contextWithJSGlobalContextRef:m_context->jsContext()]].JSValueRef, nullptr);
}
UIView *UIScriptControllerIOS::platformContentView() const
{
return webView().contentView;
}
JSObjectRef UIScriptControllerIOS::calendarType() const
{
UIView *contentView = webView().contentView;
NSString *calendarTypeString = [contentView valueForKeyPath:@"dateTimeInputControl.dateTimePickerCalendarType"];
auto jsContext = m_context->jsContext();
return JSValueToObject(jsContext, [JSValue valueWithObject:calendarTypeString inContext:[JSContext contextWithJSGlobalContextRef:jsContext]].JSValueRef, nullptr);
}
void UIScriptControllerIOS::setHardwareKeyboardAttached(bool attached)
{
GSEventSetHardwareKeyboardAttached(attached, 0);
TestController::singleton().setIsInHardwareKeyboardMode(attached);
}
void UIScriptControllerIOS::setAllowsViewportShrinkToFit(bool allows)
{
webView()._allowsViewportShrinkToFit = allows;
}
void UIScriptControllerIOS::setScrollViewKeyboardAvoidanceEnabled(bool enabled)
{
webView().scrollView.firstResponderKeyboardAvoidanceEnabled = enabled;
}
void UIScriptControllerIOS::doAfterDoubleTapDelay(JSValueRef callback)
{
unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
NSTimeInterval maximumIntervalBetweenSuccessiveTaps = 0;
for (UIGestureRecognizer *gesture in [platformContentView() gestureRecognizers]) {
if (![gesture isKindOfClass:[UITapGestureRecognizer class]])
continue;
UITapGestureRecognizer *tapGesture = (UITapGestureRecognizer *)gesture;
if (tapGesture.numberOfTapsRequired < 2)
continue;
if (tapGesture.maximumIntervalBetweenSuccessiveTaps > maximumIntervalBetweenSuccessiveTaps)
maximumIntervalBetweenSuccessiveTaps = tapGesture.maximumIntervalBetweenSuccessiveTaps;
}
if (maximumIntervalBetweenSuccessiveTaps) {
const NSTimeInterval additionalDelayBetweenSuccessiveTaps = 0.01;
maximumIntervalBetweenSuccessiveTaps += additionalDelayBetweenSuccessiveTaps;
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(maximumIntervalBetweenSuccessiveTaps * NSEC_PER_SEC)), mainDispatchQueueSingleton(), makeBlockPtr([this, protectedThis = Ref { *this }, callbackID] {
if (!m_context)
return;
m_context->asyncTaskComplete(callbackID);
}).get());
}
void UIScriptControllerIOS::copyText(JSStringRef text)
{
UIPasteboard.generalPasteboard.string = text->string().createNSString().get();
}
int64_t UIScriptControllerIOS::pasteboardChangeCount() const
{
return UIPasteboard.generalPasteboard.changeCount;
}
void UIScriptControllerIOS::installTapGestureOnWindow(JSValueRef callback)
{
m_context->registerCallback(callback, CallbackTypeWindowTapRecognized);
webView().windowTapRecognizedCallback = makeBlockPtr([this, strongThis = Ref { *this }] {
if (!m_context)
return;
m_context->fireCallback(CallbackTypeWindowTapRecognized);
}).get();
}
bool UIScriptControllerIOS::suppressSoftwareKeyboard() const
{
return webView()._suppressSoftwareKeyboard;
}
void UIScriptControllerIOS::setSuppressSoftwareKeyboard(bool suppressSoftwareKeyboard)
{
webView()._suppressSoftwareKeyboard = suppressSoftwareKeyboard;
}
void UIScriptControllerIOS::presentFindNavigator()
{
#if HAVE(UIFINDINTERACTION)
[webView().findInteraction presentFindNavigatorShowingReplace:NO];
#endif
}
void UIScriptControllerIOS::dismissFindNavigator()
{
#if HAVE(UIFINDINTERACTION)
[webView().findInteraction dismissFindNavigator];
#endif
}
bool UIScriptControllerIOS::isWebContentFirstResponder() const
{
return [webView() _contentViewIsFirstResponder];
}
void UIScriptControllerIOS::setInlinePrediction(JSStringRef text, unsigned startIndex)
{
RetainPtr plainText = text->string().substring(startIndex).createNSString();
auto attributedText = adoptNS([[NSAttributedString alloc] initWithString:plainText.get() attributes:@{
NSBackgroundColorAttributeName : UIColor.clearColor,
NSForegroundColorAttributeName : UIColor.systemGrayColor,
}]);
[UIKeyboardImpl.activeInstance setInlineCompletionAsMarkedText:attributedText.get() selectedRange:NSMakeRange(0, 0) inputString:plainText.get() searchString:@""];
}
void UIScriptControllerIOS::acceptInlinePrediction()
{
if (!UIKeyboardImpl.activeInstance.hasInlineCompletionAsMarkedText)
return;
[(id<UITextInput>)platformContentView() unmarkText];
auto emptyText = adoptNS([[NSAttributedString alloc] initWithString:@""]);
[UIKeyboardImpl.activeInstance setInlineCompletionAsMarkedText:emptyText.get() selectedRange:NSMakeRange(0, 0) inputString:@"" searchString:@""];
}
void UIScriptControllerIOS::becomeFirstResponder()
{
[webView() becomeFirstResponder];
}
void UIScriptControllerIOS::resignFirstResponder()
{
[webView() resignFirstResponder];
}
#if USE(BROWSERENGINEKIT)
id<BETextInput> UIScriptControllerIOS::asyncTextInput() const
{
static BOOL conformsToAsyncTextInput = class_conformsToProtocol(NSClassFromString(@"WKContentView"), @protocol(BETextInput));
if (!conformsToAsyncTextInput)
return nil;
return (id<BETextInput>)platformContentView();
}
#endif // USE(BROWSERENGINEKIT)
#if HAVE(UI_TEXT_SELECTION_DISPLAY_INTERACTION)
UITextSelectionDisplayInteraction *UIScriptControllerIOS::textSelectionDisplayInteraction() const
{
return dynamic_objc_cast<UITextSelectionDisplayInteraction>([platformContentView() valueForKeyPath:@"interactionAssistant._selectionViewManager"]);
}
#endif
JSRetainPtr<JSStringRef> UIScriptControllerIOS::scrollbarStateForScrollingNodeID(unsigned long long scrollingNodeID, unsigned long long processID, bool isVertical) const
{
return adopt(JSStringCreateWithCFString((CFStringRef) [webView() _scrollbarState:scrollingNodeID processID:processID isVertical:isVertical]));
}
JSRetainPtr<JSStringRef> UIScriptControllerIOS::frontmostViewAtPoint(int x, int y)
{
if (RetainPtr view = [platformContentView() _wtr_frontmostViewAtPoint:CGPointMake(x, y)])
return adopt(JSStringCreateWithUTF8CString(class_getName([view class])));
return nil;
}
bool UIScriptControllerIOS::didCallEnsurePositionInformationIsUpToDateSinceLastCheck() const
{
return webView().didCallEnsurePositionInformationIsUpToDateSinceLastCheck;
}
void UIScriptControllerIOS::clearEnsurePositionInformationIsUpToDateTracking()
{
[webView() clearEnsurePositionInformationIsUpToDateTracking];
}
}
#endif // PLATFORM(IOS_FAMILY)