WKWebView Find-in-page API.
[WebKit-https.git] / Tools / TestWebKitAPI / cocoa / TestWKWebView.mm
index f751e56..603cb73 100644 (file)
 #import "config.h"
 #import "TestWKWebView.h"
 
-#if WK_API_ENABLED
-
+#import "ClassMethodSwizzler.h"
 #import "TestNavigationDelegate.h"
 #import "Utilities.h"
 
+#import <WebKit/WKWebViewConfigurationPrivate.h>
 #import <WebKit/WebKitPrivate.h>
+#import <WebKit/_WKActivatedElementInfo.h>
+#import <WebKit/_WKProcessPoolConfiguration.h>
 #import <objc/runtime.h>
 #import <wtf/RetainPtr.h>
 
 #if PLATFORM(MAC)
 #import <AppKit/AppKit.h>
 #import <Carbon/Carbon.h>
-#import <wtf/mac/AppKitCompatibilityDeclarations.h>
 #endif
 
-#if PLATFORM(IOS)
-#import <WebCore/SoftLinking.h>
+#if PLATFORM(IOS_FAMILY)
+#import "UIKitSPI.h"
+#import <MobileCoreServices/MobileCoreServices.h>
+#import <wtf/SoftLinking.h>
 SOFT_LINK_FRAMEWORK(UIKit)
 SOFT_LINK_CLASS(UIKit, UIWindow)
+
+@implementation WKWebView (WKWebViewTestingQuirks)
+
+// TestWebKitAPI is currently not a UIApplication so we are unable to track if it is in
+// the background or not (https://bugs.webkit.org/show_bug.cgi?id=175204). This can
+// cause our processes to get suspended on iOS. We work around this by having
+// WKWebView._isBackground always return NO in the context of API tests.
+- (BOOL)_isBackground
+{
+    return NO;
+}
+@end
 #endif
 
+@implementation WKWebView (TestWebKitAPI)
+
+- (void)loadTestPageNamed:(NSString *)pageName
+{
+    NSURLRequest *request = [NSURLRequest requestWithURL:[[NSBundle mainBundle] URLForResource:pageName withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"]];
+    [self loadRequest:request];
+}
+
+- (void)synchronouslyGoBack
+{
+    [self goBack];
+    [self _test_waitForDidFinishNavigation];
+}
+
+- (void)synchronouslyGoForward
+{
+    [self goForward];
+    [self _test_waitForDidFinishNavigation];
+}
+
+- (void)synchronouslyLoadRequest:(NSURLRequest *)request
+{
+    [self loadRequest:request];
+    [self _test_waitForDidFinishNavigation];
+}
+
+- (void)synchronouslyLoadHTMLString:(NSString *)html baseURL:(NSURL *)url
+{
+    [self loadHTMLString:html baseURL:url];
+    [self _test_waitForDidFinishNavigation];
+}
+
+- (void)synchronouslyLoadHTMLString:(NSString *)html
+{
+    [self synchronouslyLoadHTMLString:html baseURL:[[[NSBundle mainBundle] bundleURL] URLByAppendingPathComponent:@"TestWebKitAPI.resources"]];
+}
+
+- (void)synchronouslyLoadTestPageNamed:(NSString *)pageName
+{
+    [self loadTestPageNamed:pageName];
+    [self _test_waitForDidFinishNavigation];
+}
+
+- (BOOL)_synchronouslyExecuteEditCommand:(NSString *)command argument:(NSString *)argument
+{
+    __block bool done = false;
+    __block bool success;
+    [self _executeEditCommand:command argument:argument completion:^(BOOL completionSuccess) {
+        done = true;
+        success = completionSuccess;
+    }];
+    TestWebKitAPI::Util::run(&done);
+    return success;
+}
+
+- (NSArray<NSString *> *)tagsInBody
+{
+    return [self objectByEvaluatingJavaScript:@"Array.from(document.body.getElementsByTagName('*')).map(e => e.tagName)"];
+}
+
+- (void)expectElementTagsInOrder:(NSArray<NSString *> *)tagNames
+{
+    auto remainingTags = adoptNS([tagNames mutableCopy]);
+    NSArray<NSString *> *tagsInBody = self.tagsInBody;
+    for (NSString *tag in tagsInBody.reverseObjectEnumerator) {
+        if ([tag isEqualToString:[remainingTags lastObject]])
+            [remainingTags removeLastObject];
+        if (![remainingTags count])
+            break;
+    }
+    EXPECT_EQ([remainingTags count], 0U);
+    if ([remainingTags count])
+        NSLog(@"Expected to find ordered tags: %@ in: %@", tagNames, tagsInBody);
+}
+
+- (void)expectElementCount:(NSInteger)count querySelector:(NSString *)querySelector
+{
+    NSString *script = [NSString stringWithFormat:@"document.querySelectorAll('%@').length", querySelector];
+    EXPECT_EQ(count, [self stringByEvaluatingJavaScript:script].integerValue);
+}
+
+- (void)expectElementTag:(NSString *)tagName toComeBefore:(NSString *)otherTagName
+{
+    [self expectElementTagsInOrder:@[tagName, otherTagName]];
+}
+
+- (id)objectByEvaluatingJavaScript:(NSString *)script
+{
+    bool isWaitingForJavaScript = false;
+    RetainPtr<id> evalResult;
+    [self _evaluateJavaScriptWithoutUserGesture:script completionHandler:[&] (id result, NSError *error) {
+        evalResult = result;
+        isWaitingForJavaScript = true;
+        EXPECT_TRUE(!error);
+        if (error)
+            NSLog(@"Encountered error: %@ while evaluating script: %@", error, script);
+    }];
+    TestWebKitAPI::Util::run(&isWaitingForJavaScript);
+    return evalResult.autorelease();
+}
+
+- (id)objectByEvaluatingJavaScriptWithUserGesture:(NSString *)script
+{
+    bool isWaitingForJavaScript = false;
+    RetainPtr<id> evalResult;
+    [self evaluateJavaScript:script completionHandler:[&] (id result, NSError *error) {
+        evalResult = result;
+        isWaitingForJavaScript = true;
+        EXPECT_TRUE(!error);
+        if (error)
+            NSLog(@"Encountered error: %@ while evaluating script: %@", error, script);
+    }];
+    TestWebKitAPI::Util::run(&isWaitingForJavaScript);
+    return evalResult.autorelease();
+}
+
+- (NSString *)stringByEvaluatingJavaScript:(NSString *)script
+{
+    return [NSString stringWithFormat:@"%@", [self objectByEvaluatingJavaScript:script]];
+}
+
+@end
+
 @implementation TestMessageHandler {
     NSMutableDictionary<NSString *, dispatch_block_t> *_messageHandlers;
 }
@@ -87,32 +225,25 @@ SOFT_LINK_CLASS(UIKit, UIWindow)
 #if PLATFORM(MAC)
 static int gEventNumber = 1;
 
-#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 101003
 NSEventMask __simulated_forceClickAssociatedEventsMask(id self, SEL _cmd)
 {
     return NSEventMaskPressure | NSEventMaskLeftMouseDown | NSEventMaskLeftMouseUp | NSEventMaskLeftMouseDragged;
 }
-#endif
 
-- (void)_mouseDownAtPoint:(NSPoint)point simulatePressure:(BOOL)simulatePressure
+- (void)_mouseDownAtPoint:(NSPoint)point simulatePressure:(BOOL)simulatePressure clickCount:(NSUInteger)clickCount
 {
     NSEventType mouseEventType = NSEventTypeLeftMouseDown;
 
     NSEventMask modifierFlags = 0;
-#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 101003
     if (simulatePressure)
         modifierFlags |= NSEventMaskPressure;
-#else
-    simulatePressure = NO;
-#endif
 
-    NSEvent *event = [NSEvent mouseEventWithType:mouseEventType location:point modifierFlags:modifierFlags timestamp:GetCurrentEventTime() windowNumber:self.windowNumber context:[NSGraphicsContext currentContext] eventNumber:++gEventNumber clickCount:1 pressure:simulatePressure];
+    NSEvent *event = [NSEvent mouseEventWithType:mouseEventType location:point modifierFlags:modifierFlags timestamp:GetCurrentEventTime() windowNumber:self.windowNumber context:[NSGraphicsContext currentContext] eventNumber:++gEventNumber clickCount:clickCount pressure:simulatePressure];
     if (!simulatePressure) {
         [self sendEvent:event];
         return;
     }
 
-#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 101003
     IMP simulatedAssociatedEventsMaskImpl = (IMP)__simulated_forceClickAssociatedEventsMask;
     Method associatedEventsMaskMethod = class_getInstanceMethod([NSEvent class], @selector(associatedEventsMask));
     IMP originalAssociatedEventsMaskImpl = method_setImplementation(associatedEventsMaskMethod, simulatedAssociatedEventsMaskImpl);
@@ -123,12 +254,11 @@ NSEventMask __simulated_forceClickAssociatedEventsMask(id self, SEL _cmd)
         // to prevent subsequent event sending tests from being affected.
         method_setImplementation(associatedEventsMaskMethod, originalAssociatedEventsMaskImpl);
     }
-#endif
 }
 
-- (void)_mouseUpAtPoint:(NSPoint)point
+- (void)_mouseUpAtPoint:(NSPoint)point clickCount:(NSUInteger)clickCount
 {
-    [self sendEvent:[NSEvent mouseEventWithType:NSEventTypeLeftMouseUp location:point modifierFlags:0 timestamp:GetCurrentEventTime() windowNumber:self.windowNumber context:[NSGraphicsContext currentContext] eventNumber:++gEventNumber clickCount:1 pressure:0]];
+    [self sendEvent:[NSEvent mouseEventWithType:NSEventTypeLeftMouseUp location:point modifierFlags:0 timestamp:GetCurrentEventTime() windowNumber:self.windowNumber context:[NSGraphicsContext currentContext] eventNumber:++gEventNumber clickCount:clickCount pressure:0]];
 }
 #endif // PLATFORM(MAC)
 
@@ -158,36 +288,91 @@ NSEventMask __simulated_forceClickAssociatedEventsMask(id self, SEL _cmd)
 
 @end
 
+#if PLATFORM(IOS_FAMILY)
+
+using InputSessionChangeCount = NSUInteger;
+static InputSessionChangeCount nextInputSessionChangeCount()
+{
+    static InputSessionChangeCount gInputSessionChangeCount = 0;
+    return ++gInputSessionChangeCount;
+}
+
+#endif
+
 @implementation TestWKWebView {
-    TestWKWebViewHostWindow *_hostWindow;
+    RetainPtr<TestWKWebViewHostWindow> _hostWindow;
     RetainPtr<TestMessageHandler> _testHandler;
+#if PLATFORM(IOS_FAMILY)
+    std::unique_ptr<ClassMethodSwizzler> _sharedCalloutBarSwizzler;
+    InputSessionChangeCount _inputSessionChangeCount;
+#endif
 }
 
 - (instancetype)initWithFrame:(CGRect)frame
 {
-    WKWebViewConfiguration *defaultConfiguration = [[WKWebViewConfiguration alloc] init];
+    WKWebViewConfiguration *defaultConfiguration = [[[WKWebViewConfiguration alloc] init] autorelease];
     return [self initWithFrame:frame configuration:defaultConfiguration];
 }
 
 - (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration
 {
-    if (self = [super initWithFrame:frame configuration:configuration])
+    return [self initWithFrame:frame configuration:configuration addToWindow:YES];
+}
+
+#if PLATFORM(IOS_FAMILY)
+
+static UICalloutBar *suppressUICalloutBar()
+{
+    return nil;
+}
+
+#endif
+
+- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration addToWindow:(BOOL)addToWindow
+{
+    self = [super initWithFrame:frame configuration:configuration];
+    if (!self)
+        return nil;
+
+    if (addToWindow)
         [self _setUpTestWindow:frame];
 
+#if PLATFORM(IOS_FAMILY)
+    // FIXME: Remove this workaround once <https://webkit.org/b/175204> is fixed.
+    _sharedCalloutBarSwizzler = makeUnique<ClassMethodSwizzler>([UICalloutBar class], @selector(sharedCalloutBar), reinterpret_cast<IMP>(suppressUICalloutBar));
+    _inputSessionChangeCount = 0;
+#endif
+
     return self;
 }
 
+- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration processPoolConfiguration:(_WKProcessPoolConfiguration *)processPoolConfiguration
+{
+    [configuration setProcessPool:[[[WKProcessPool alloc] _initWithConfiguration:processPoolConfiguration] autorelease]];
+    return [self initWithFrame:frame configuration:configuration];
+}
+
 - (void)_setUpTestWindow:(NSRect)frame
 {
 #if PLATFORM(MAC)
-    _hostWindow = [[TestWKWebViewHostWindow alloc] initWithContentRect:frame styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:NO];
+    _hostWindow = adoptNS([[TestWKWebViewHostWindow alloc] initWithContentRect:frame styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:NO]);
     [_hostWindow setFrameOrigin:NSMakePoint(0, 0)];
     [_hostWindow setIsVisible:YES];
+    [_hostWindow contentView].wantsLayer = YES;
     [[_hostWindow contentView] addSubview:self];
     [_hostWindow makeKeyAndOrderFront:self];
 #else
-    _hostWindow = [[TestWKWebViewHostWindow alloc] initWithFrame:frame];
-    _hostWindow.hidden = NO;
+    _hostWindow = adoptNS([[TestWKWebViewHostWindow alloc] initWithFrame:frame]);
+    [_hostWindow setHidden:NO];
+    [_hostWindow addSubview:self];
+#endif
+}
+
+- (void)addToTestWindow
+{
+#if PLATFORM(MAC)
+    [[_hostWindow contentView] addSubview:self];
+#else
     [_hostWindow addSubview:self];
 #endif
 }
@@ -208,33 +393,6 @@ NSEventMask __simulated_forceClickAssociatedEventsMask(id self, SEL _cmd)
     [_testHandler addMessage:message withHandler:action];
 }
 
-- (void)loadTestPageNamed:(NSString *)pageName
-{
-    NSURLRequest *request = [NSURLRequest requestWithURL:[[NSBundle mainBundle] URLForResource:pageName withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"]];
-    [self loadRequest:request];
-}
-
-- (void)synchronouslyLoadTestPageNamed:(NSString *)pageName
-{
-    [self loadTestPageNamed:pageName];
-    [self _test_waitForDidFinishNavigation];
-}
-
-- (NSString *)stringByEvaluatingJavaScript:(NSString *)script
-{
-    __block bool isWaitingForJavaScript = false;
-    __block NSString *evalResult = nil;
-    [self evaluateJavaScript:script completionHandler:^(id result, NSError *error)
-    {
-        evalResult = [[NSString alloc] initWithFormat:@"%@", result];
-        isWaitingForJavaScript = true;
-        EXPECT_TRUE(!error);
-    }];
-
-    TestWebKitAPI::Util::run(&isWaitingForJavaScript);
-    return [evalResult autorelease];
-}
-
 - (void)waitForMessage:(NSString *)message
 {
     __block bool isDoneWaiting = false;
@@ -245,7 +403,8 @@ NSEventMask __simulated_forceClickAssociatedEventsMask(id self, SEL _cmd)
     TestWebKitAPI::Util::run(&isDoneWaiting);
 }
 
-- (void)performAfterLoading:(dispatch_block_t)actions {
+- (void)performAfterLoading:(dispatch_block_t)actions
+{
     TestMessageHandler *handler = [[TestMessageHandler alloc] init];
     [handler addMessage:@"loaded" withHandler:actions];
 
@@ -257,12 +416,112 @@ NSEventMask __simulated_forceClickAssociatedEventsMask(id self, SEL _cmd)
     [contentController addScriptMessageHandler:handler name:@"onloadHandler"];
 }
 
+- (void)waitForNextPresentationUpdate
+{
+    __block bool done = false;
+    [self _doAfterNextPresentationUpdate:^() {
+        done = true;
+    }];
+
+    TestWebKitAPI::Util::run(&done);
+}
+
+- (void)forceDarkMode
+{
+#if HAVE(OS_DARK_MODE_SUPPORT)
+#if USE(APPKIT)
+    [self setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]];
+#else
+    [self setOverrideUserInterfaceStyle:UIUserInterfaceStyleDark];
+#endif
+#endif
+}
+
+- (NSString *)stylePropertyAtSelectionStart:(NSString *)propertyName
+{
+    NSString *script = [NSString stringWithFormat:@"getComputedStyle(getSelection().getRangeAt(0).startContainer.parentElement)['%@']", propertyName];
+    return [self stringByEvaluatingJavaScript:script];
+}
+
+- (NSString *)stylePropertyAtSelectionEnd:(NSString *)propertyName
+{
+    NSString *script = [NSString stringWithFormat:@"getComputedStyle(getSelection().getRangeAt(0).endContainer.parentElement)['%@']", propertyName];
+    return [self stringByEvaluatingJavaScript:script];
+}
+
+- (void)collapseToStart
+{
+    [self evaluateJavaScript:@"getSelection().collapseToStart()" completionHandler:nil];
+}
+
+- (void)collapseToEnd
+{
+    [self evaluateJavaScript:@"getSelection().collapseToEnd()" completionHandler:nil];
+}
+
+- (BOOL)selectionRangeHasStartOffset:(int)start endOffset:(int)end
+{
+    __block bool isDone = false;
+    __block bool matches = true;
+    [self evaluateJavaScript:@"window.getSelection().getRangeAt(0).startOffset" completionHandler:^(id result, NSError *error) {
+        if ([(NSNumber *)result intValue] != start)
+            matches = false;
+    }];
+    [self evaluateJavaScript:@"window.getSelection().getRangeAt(0).endOffset" completionHandler:^(id result, NSError *error) {
+        if ([(NSNumber *)result intValue] != end)
+            matches = false;
+        isDone = true;
+    }];
+    TestWebKitAPI::Util::run(&isDone);
+
+    return matches;
+}
+
+#if PLATFORM(IOS_FAMILY)
+
+- (void)didStartFormControlInteraction
+{
+    _inputSessionChangeCount = nextInputSessionChangeCount();
+}
+
+- (void)didEndFormControlInteraction
+{
+    _inputSessionChangeCount = 0;
+}
+
+#endif // PLATFORM(IOS_FAMILY)
+
 @end
 
-#if PLATFORM(IOS)
+#if PLATFORM(IOS_FAMILY)
 
 @implementation TestWKWebView (IOSOnly)
 
+- (void)evaluateJavaScriptAndWaitForInputSessionToChange:(NSString *)script
+{
+    auto initialChangeCount = _inputSessionChangeCount;
+    BOOL hasEmittedWarning = NO;
+    NSTimeInterval secondsToWaitUntilWarning = 2;
+    NSTimeInterval startTime = [NSDate timeIntervalSinceReferenceDate];
+
+    [self objectByEvaluatingJavaScript:script];
+    while ([[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantPast]]) {
+        if (_inputSessionChangeCount != initialChangeCount)
+            break;
+
+        if (hasEmittedWarning || startTime + secondsToWaitUntilWarning >= [NSDate timeIntervalSinceReferenceDate])
+            continue;
+
+        NSLog(@"Warning: expecting input session change count to differ from %lu", static_cast<unsigned long>(initialChangeCount));
+        hasEmittedWarning = YES;
+    }
+}
+
+- (UIView <UITextInputPrivate, UITextInputMultiDocument> *)textInputContentView
+{
+    return (UIView <UITextInputPrivate, UITextInputMultiDocument> *)[self valueForKey:@"_currentContentView"];
+}
+
 - (RetainPtr<NSArray>)selectionRectsAfterPresentationUpdate
 {
     RetainPtr<TestWKWebView> retainedSelf = self;
@@ -278,30 +537,162 @@ NSEventMask __simulated_forceClickAssociatedEventsMask(id self, SEL _cmd)
     return selectionRects;
 }
 
+- (CGRect)caretViewRectInContentCoordinates
+{
+    UIView *selectionView = [self.textInputContentView valueForKeyPath:@"interactionAssistant.selectionView"];
+    CGRect caretFrame = [[selectionView valueForKeyPath:@"caretView.frame"] CGRectValue];
+    return [selectionView convertRect:caretFrame toView:self.textInputContentView];
+}
+
+- (NSArray<NSValue *> *)selectionViewRectsInContentCoordinates
+{
+    NSMutableArray *selectionRects = [NSMutableArray array];
+    NSArray<UITextSelectionRect *> *rects = [self.textInputContentView valueForKeyPath:@"interactionAssistant.selectionView.rangeView.rects"];
+    for (UITextSelectionRect *rect in rects)
+        [selectionRects addObject:[NSValue valueWithCGRect:rect.rect]];
+    return selectionRects;
+}
+
+- (_WKActivatedElementInfo *)activatedElementAtPosition:(CGPoint)position
+{
+    __block RetainPtr<_WKActivatedElementInfo> info;
+    __block bool finished = false;
+    [self _requestActivatedElementAtPosition:position completionBlock:^(_WKActivatedElementInfo *elementInfo) {
+        info = elementInfo;
+        finished = true;
+    }];
+
+    TestWebKitAPI::Util::run(&finished);
+    return info.autorelease();
+}
+
+static WKContentView *recursiveFindWKContentView(UIView *view)
+{
+    if ([view isKindOfClass:NSClassFromString(@"WKContentView")])
+        return (WKContentView *)view;
+
+    for (UIView *subview in view.subviews) {
+        WKContentView *contentView = recursiveFindWKContentView(subview);
+        if (contentView)
+            return contentView;
+    }
+
+    return nil;
+}
+
+- (WKContentView *)wkContentView
+{
+    return recursiveFindWKContentView(self);
+}
+
 @end
 
 #endif
 
 #if PLATFORM(MAC)
 @implementation TestWKWebView (MacOnly)
-- (void)mouseDownAtPoint:(NSPoint)point simulatePressure:(BOOL)simulatePressure
+- (void)mouseDownAtPoint:(NSPoint)pointInWindow simulatePressure:(BOOL)simulatePressure
 {
-    [_hostWindow _mouseDownAtPoint:point simulatePressure:simulatePressure];
+    [_hostWindow _mouseDownAtPoint:pointInWindow simulatePressure:simulatePressure clickCount:1];
 }
 
-- (void)mouseUpAtPoint:(NSPoint)point
+- (void)mouseUpAtPoint:(NSPoint)pointInWindow
 {
-    [_hostWindow _mouseUpAtPoint:point];
+    [_hostWindow _mouseUpAtPoint:pointInWindow clickCount:1];
 }
 
-- (void)typeCharacter:(char)character {
+- (void)mouseMoveToPoint:(NSPoint)pointInWindow withFlags:(NSEventModifierFlags)flags
+{
+    [self mouseMoved:[self _mouseEventWithType:NSEventTypeMouseMoved atLocation:pointInWindow flags:flags timestamp:GetCurrentEventTime() clickCount:0]];
+}
+
+- (void)sendClicksAtPoint:(NSPoint)pointInWindow numberOfClicks:(NSUInteger)numberOfClicks
+{
+    for (NSUInteger clickCount = 1; clickCount <= numberOfClicks; ++clickCount) {
+        [_hostWindow _mouseDownAtPoint:pointInWindow simulatePressure:NO clickCount:clickCount];
+        [_hostWindow _mouseUpAtPoint:pointInWindow clickCount:clickCount];
+    }
+}
+
+- (void)sendClickAtPoint:(NSPoint)pointInWindow
+{
+    [self sendClicksAtPoint:pointInWindow numberOfClicks:1];
+}
+
+- (void)mouseEnterAtPoint:(NSPoint)pointInWindow
+{
+    [self mouseEntered:[self _mouseEventWithType:NSEventTypeMouseEntered atLocation:pointInWindow]];
+}
+
+- (void)mouseDragToPoint:(NSPoint)pointInWindow
+{
+    [self mouseDragged:[self _mouseEventWithType:NSEventTypeLeftMouseDragged atLocation:pointInWindow]];
+}
+
+- (NSEvent *)_mouseEventWithType:(NSEventType)type atLocation:(NSPoint)pointInWindow
+{
+    return [self _mouseEventWithType:type atLocation:pointInWindow flags:0 timestamp:GetCurrentEventTime() clickCount:0];
+}
+
+- (NSEvent *)_mouseEventWithType:(NSEventType)type atLocation:(NSPoint)locationInWindow flags:(NSEventModifierFlags)flags timestamp:(NSTimeInterval)timestamp clickCount:(NSUInteger)clickCount
+{
+    switch (type) {
+    case NSEventTypeMouseEntered:
+    case NSEventTypeMouseExited:
+        return [NSEvent enterExitEventWithType:type location:locationInWindow modifierFlags:flags timestamp:timestamp windowNumber:[_hostWindow windowNumber] context:[NSGraphicsContext currentContext] eventNumber:++gEventNumber trackingNumber:1 userData:nil];
+    default:
+        return [NSEvent mouseEventWithType:type location:locationInWindow modifierFlags:flags timestamp:timestamp windowNumber:[_hostWindow windowNumber] context:[NSGraphicsContext currentContext] eventNumber:++gEventNumber clickCount:clickCount pressure:0];
+    }
+}
+
+- (NSWindow *)hostWindow
+{
+    return _hostWindow.get();
+}
+
+- (void)typeCharacter:(char)character
+{
     NSString *characterAsString = [NSString stringWithFormat:@"%c" , character];
     NSEventType keyDownEventType = NSEventTypeKeyDown;
     NSEventType keyUpEventType = NSEventTypeKeyUp;
-    [self keyDown:[NSEvent keyEventWithType:keyDownEventType location:NSZeroPoint modifierFlags:0 timestamp:GetCurrentEventTime() windowNumber:_hostWindow.windowNumber context:nil characters:characterAsString charactersIgnoringModifiers:characterAsString isARepeat:NO keyCode:character]];
-    [self keyUp:[NSEvent keyEventWithType:keyUpEventType location:NSZeroPoint modifierFlags:0 timestamp:GetCurrentEventTime() windowNumber:_hostWindow.windowNumber context:nil characters:characterAsString charactersIgnoringModifiers:characterAsString isARepeat:NO keyCode:character]];
+    [self keyDown:[NSEvent keyEventWithType:keyDownEventType location:NSZeroPoint modifierFlags:0 timestamp:GetCurrentEventTime() windowNumber:[_hostWindow windowNumber] context:nil characters:characterAsString charactersIgnoringModifiers:characterAsString isARepeat:NO keyCode:character]];
+    [self keyUp:[NSEvent keyEventWithType:keyUpEventType location:NSZeroPoint modifierFlags:0 timestamp:GetCurrentEventTime() windowNumber:[_hostWindow windowNumber] context:nil characters:characterAsString charactersIgnoringModifiers:characterAsString isARepeat:NO keyCode:character]];
 }
+
+@end
+#endif // PLATFORM(MAC)
+
+#if PLATFORM(IOS_FAMILY)
+@implementation UIView (WKTestingUIViewUtilities)
+
+- (UIView *)wkFirstSubviewWithClass:(Class)targetClass
+{
+    for (UIView *view in self.subviews) {
+        if ([view isKindOfClass:targetClass])
+            return view;
+    
+        UIView *foundSubview = [view wkFirstSubviewWithClass:targetClass];
+        if (foundSubview)
+            return foundSubview;
+    }
+    
+    return nil;
+}
+
+- (UIView *)wkFirstSubviewWithBoundsSize:(CGSize)size
+{
+    for (UIView *view in self.subviews) {
+        if (CGSizeEqualToSize([view bounds].size, size))
+            return view;
+    
+        UIView *foundSubview = [view wkFirstSubviewWithBoundsSize:size];
+        if (foundSubview)
+            return foundSubview;
+    }
+    
+    return nil;
+}
+
 @end
-#endif
 
-#endif // WK_API_ENABLED
+#endif // PLATFORM(IOS_FAMILY)