WKWebView Find-in-page API.
[WebKit-https.git] / Tools / TestWebKitAPI / cocoa / TestWKWebView.mm
index 818a02d..603cb73 100644 (file)
@@ -26,8 +26,7 @@
 #import "config.h"
 #import "TestWKWebView.h"
 
-#if WK_API_ENABLED
-
+#import "ClassMethodSwizzler.h"
 #import "TestNavigationDelegate.h"
 #import "Utilities.h"
 
 #if PLATFORM(MAC)
 #import <AppKit/AppKit.h>
 #import <Carbon/Carbon.h>
-#import <wtf/mac/AppKitCompatibilityDeclarations.h>
 #endif
 
-#if PLATFORM(IOS)
-#import <UIKit/UIKit.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 (WKWebViewTestingQuicks)
+@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
@@ -63,6 +62,129 @@ SOFT_LINK_CLASS(UIKit, UIWindow)
 @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;
 }
@@ -103,24 +225,18 @@ 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 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:clickCount pressure:simulatePressure];
     if (!simulatePressure) {
@@ -128,7 +244,6 @@ NSEventMask __simulated_forceClickAssociatedEventsMask(id self, SEL _cmd)
         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);
@@ -139,7 +254,6 @@ 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 clickCount:(NSUInteger)clickCount
@@ -174,9 +288,24 @@ 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 {
     RetainPtr<TestWKWebViewHostWindow> _hostWindow;
     RetainPtr<TestMessageHandler> _testHandler;
+#if PLATFORM(IOS_FAMILY)
+    std::unique_ptr<ClassMethodSwizzler> _sharedCalloutBarSwizzler;
+    InputSessionChangeCount _inputSessionChangeCount;
+#endif
 }
 
 - (instancetype)initWithFrame:(CGRect)frame
@@ -190,6 +319,15 @@ NSEventMask __simulated_forceClickAssociatedEventsMask(id self, SEL _cmd)
     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];
@@ -199,6 +337,12 @@ NSEventMask __simulated_forceClickAssociatedEventsMask(id self, SEL _cmd)
     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;
 }
 
@@ -224,6 +368,15 @@ NSEventMask __simulated_forceClickAssociatedEventsMask(id self, SEL _cmd)
 #endif
 }
 
+- (void)addToTestWindow
+{
+#if PLATFORM(MAC)
+    [[_hostWindow contentView] addSubview:self];
+#else
+    [_hostWindow addSubview:self];
+#endif
+}
+
 - (void)clearMessageHandlers:(NSArray *)messageNames
 {
     for (NSString *messageName in messageNames)
@@ -240,45 +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)synchronouslyLoadHTMLString:(NSString *)html
-{
-    NSURL *testResourceURL = [[[NSBundle mainBundle] bundleURL] URLByAppendingPathComponent:@"TestWebKitAPI.resources"];
-    [self loadHTMLString:html baseURL:testResourceURL];
-    [self _test_waitForDidFinishNavigation];
-}
-
-- (void)synchronouslyLoadTestPageNamed:(NSString *)pageName
-{
-    [self loadTestPageNamed:pageName];
-    [self _test_waitForDidFinishNavigation];
-}
-
-- (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();
-}
-
-- (NSString *)stringByEvaluatingJavaScript:(NSString *)script
-{
-    return [NSString stringWithFormat:@"%@", [self objectByEvaluatingJavaScript:script]];
-}
-
 - (void)waitForMessage:(NSString *)message
 {
     __block bool isDoneWaiting = false;
@@ -289,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];
 
@@ -311,23 +426,100 @@ NSEventMask __simulated_forceClickAssociatedEventsMask(id self, SEL _cmd)
     TestWebKitAPI::Util::run(&done);
 }
 
-- (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]];
+- (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)
 
-- (UIView <UITextInput> *)textInputContentView
+- (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 <UITextInput> *)[self valueForKey:@"_currentContentView"];
+    return (UIView <UITextInputPrivate, UITextInputMultiDocument> *)[self valueForKey:@"_currentContentView"];
 }
 
 - (RetainPtr<NSArray>)selectionRectsAfterPresentationUpdate
@@ -345,6 +537,22 @@ 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;
@@ -358,32 +566,82 @@ NSEventMask __simulated_forceClickAssociatedEventsMask(id self, SEL _cmd)
     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 clickCount:1];
+    [_hostWindow _mouseDownAtPoint:pointInWindow simulatePressure:simulatePressure clickCount:1];
 }
 
-- (void)mouseUpAtPoint:(NSPoint)point
+- (void)mouseUpAtPoint:(NSPoint)pointInWindow
 {
-    [_hostWindow _mouseUpAtPoint:point clickCount:1];
+    [_hostWindow _mouseUpAtPoint:pointInWindow clickCount:1];
 }
 
-- (void)mouseMoveToPoint:(NSPoint)point withFlags:(NSEventModifierFlags)flags
+- (void)mouseMoveToPoint:(NSPoint)pointInWindow withFlags:(NSEventModifierFlags)flags
 {
-    [self mouseMoved:[NSEvent mouseEventWithType:NSEventTypeMouseMoved location:point modifierFlags:flags timestamp:GetCurrentEventTime() windowNumber:[_hostWindow windowNumber] context:[NSGraphicsContext currentContext] eventNumber:++gEventNumber clickCount:0 pressure:0]];
+    [self mouseMoved:[self _mouseEventWithType:NSEventTypeMouseMoved atLocation:pointInWindow flags:flags timestamp:GetCurrentEventTime() clickCount:0]];
 }
 
-- (void)sendClicksAtPoint:(NSPoint)point numberOfClicks:(NSUInteger)numberOfClicks
+- (void)sendClicksAtPoint:(NSPoint)pointInWindow numberOfClicks:(NSUInteger)numberOfClicks
 {
     for (NSUInteger clickCount = 1; clickCount <= numberOfClicks; ++clickCount) {
-        [_hostWindow _mouseDownAtPoint:point simulatePressure:NO clickCount:clickCount];
-        [_hostWindow _mouseUpAtPoint:point clickCount: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];
     }
 }
 
@@ -391,7 +649,50 @@ NSEventMask __simulated_forceClickAssociatedEventsMask(id self, SEL _cmd)
 {
     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]];
+}
+
+@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)