[iOS WK2] Make it possible for a test to describe a user gesture as a stream of event...
authorsimon.fraser@apple.com <simon.fraser@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 5 Oct 2016 00:23:51 +0000 (00:23 +0000)
committersimon.fraser@apple.com <simon.fraser@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 5 Oct 2016 00:23:51 +0000 (00:23 +0000)
https://bugs.webkit.org/show_bug.cgi?id=162934

Reviewed by Dean Jackson.

Tools:

With this change, a test can describe a user gesture in an "event stream", which is
some JSON describing an array of events with their underlying touches. The added
test describes a single tap.

The implementation fires up an NSThread, and sleeps the thread between events to dispatch
them at close to real time.

In future, HIDEventGenerator could use this internally for all of the "compound" interactions.

* DumpRenderTree/ios/UIScriptControllerIOS.mm:
(WTR::UIScriptController::sendEventStream):
* TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl:
* TestRunnerShared/UIScriptContext/UIScriptController.cpp:
(WTR::UIScriptController::sendEventStream):
* TestRunnerShared/UIScriptContext/UIScriptController.h:
* WebKitTestRunner/ios/HIDEventGenerator.h:
* WebKitTestRunner/ios/HIDEventGenerator.mm:
(transducerTypeFromString):
(phaseFromString):
(-[HIDEventGenerator eventMaskFromEventInfo:]):
(-[HIDEventGenerator touchFromEventInfo:]):
(-[HIDEventGenerator _createIOHIDEventWithInfo:]):
(-[HIDEventGenerator dispatchEventWithInfo:]):
(-[HIDEventGenerator eventDispatchThreadEntry:]):
(-[HIDEventGenerator sendEventStream:completionBlock:]):
* WebKitTestRunner/ios/UIScriptControllerIOS.mm:
(WTR::UIScriptController::sendEventStream):

LayoutTests:

* fast/events/ios/event-stream-single-tap-expected.txt: Added.
* fast/events/ios/event-stream-single-tap.html: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@206798 268f45cc-cd09-0410-ab3c-d52691b4dbfc

12 files changed:
LayoutTests/ChangeLog
LayoutTests/fast/events/ios/event-stream-single-tap-expected.txt [new file with mode: 0644]
LayoutTests/fast/events/ios/event-stream-single-tap.html [new file with mode: 0644]
Tools/ChangeLog
Tools/DumpRenderTree/ios/UIScriptControllerIOS.mm
Tools/TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl
Tools/TestRunnerShared/UIScriptContext/UIScriptController.cpp
Tools/TestRunnerShared/UIScriptContext/UIScriptController.h
Tools/WebKitTestRunner/ios/HIDEventGenerator.h
Tools/WebKitTestRunner/ios/HIDEventGenerator.mm
Tools/WebKitTestRunner/ios/IOKitSPI.h
Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm

index f857bd4..78afe15 100644 (file)
@@ -1,3 +1,13 @@
+2016-10-04  Simon Fraser  <simon.fraser@apple.com>
+
+        [iOS WK2] Make it possible for a test to describe a user gesture as a stream of events in JSON format
+        https://bugs.webkit.org/show_bug.cgi?id=162934
+
+        Reviewed by Dean Jackson.
+
+        * fast/events/ios/event-stream-single-tap-expected.txt: Added.
+        * fast/events/ios/event-stream-single-tap.html: Added.
+
 2016-10-04  Chris Dumez  <cdumez@apple.com>
 
         Add support for KeyboardEvent.isComposing attribute
diff --git a/LayoutTests/fast/events/ios/event-stream-single-tap-expected.txt b/LayoutTests/fast/events/ios/event-stream-single-tap-expected.txt
new file mode 100644 (file)
index 0000000..d13c167
--- /dev/null
@@ -0,0 +1 @@
+PASS: received click.
diff --git a/LayoutTests/fast/events/ios/event-stream-single-tap.html b/LayoutTests/fast/events/ios/event-stream-single-tap.html
new file mode 100644 (file)
index 0000000..5c018f1
--- /dev/null
@@ -0,0 +1,86 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ useFlexibleViewport=true ] -->
+
+<html>
+<meta name="viewport" content="initial-scale=1.0, width=device-width">
+<head>
+    <style>
+        #target {
+            height: 100px;
+            width: 100px;
+            background-color: gray;
+        }
+    </style>
+    <script>
+
+        if (window.testRunner) {
+            testRunner.dumpAsText();
+            testRunner.waitUntilDone();
+        }
+
+        function getUIScript(x, y)
+        {
+            return `
+            (function() {
+                var eventStream = {
+                    events : [
+                        {
+                            inputType : "hand",
+                            timeOffset : 0,
+                            touches : [
+                                {
+                                    inputType : "finger",
+                                    phase : "began",
+                                    id : 1,
+                                    x : ${x},
+                                    y : ${y}
+                                }
+                            ]
+                        },
+                        {
+                            inputType : "hand",
+                            timeOffset : 0.0005,
+                            touches : [
+                                {
+                                    inputType : "finger",
+                                    phase : "ended",
+                                    id : 1,
+                                    x : ${x},
+                                    y : ${y}
+                                }
+                            ]
+                        },
+                    ]
+                };
+
+                uiController.sendEventStream(JSON.stringify(eventStream), function() {
+                    uiController.uiScriptComplete();
+                });
+            })();`
+        }
+        
+        function handleClick()
+        {
+            document.getElementById("result").textContent = "PASS: received click.";
+            if (window.testRunner)
+                testRunner.notifyDone();
+        }
+
+        function runTest()
+        {
+            var target = document.getElementById("target");
+            target.addEventListener("click", handleClick, false);
+
+            if (!testRunner.runUIScript)
+                return;
+
+            testRunner.runUIScript(getUIScript(60, 60), function() {});
+        }
+        
+        window.addEventListener("load", runTest, false);
+    </script>
+</head>
+<body>
+    <div id="target"></div>
+    <div id="result">FAIL: did not receive click event.</div>
+</body>
+</html>
index 74ade01..ae29ce5 100644 (file)
@@ -1,3 +1,38 @@
+2016-10-04  Simon Fraser  <simon.fraser@apple.com>
+
+        [iOS WK2] Make it possible for a test to describe a user gesture as a stream of events in JSON format
+        https://bugs.webkit.org/show_bug.cgi?id=162934
+
+        Reviewed by Dean Jackson.
+
+        With this change, a test can describe a user gesture in an "event stream", which is
+        some JSON describing an array of events with their underlying touches. The added
+        test describes a single tap.
+        
+        The implementation fires up an NSThread, and sleeps the thread between events to dispatch
+        them at close to real time.
+        
+        In future, HIDEventGenerator could use this internally for all of the "compound" interactions.
+
+        * DumpRenderTree/ios/UIScriptControllerIOS.mm:
+        (WTR::UIScriptController::sendEventStream):
+        * TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl:
+        * TestRunnerShared/UIScriptContext/UIScriptController.cpp:
+        (WTR::UIScriptController::sendEventStream):
+        * TestRunnerShared/UIScriptContext/UIScriptController.h:
+        * WebKitTestRunner/ios/HIDEventGenerator.h:
+        * WebKitTestRunner/ios/HIDEventGenerator.mm:
+        (transducerTypeFromString):
+        (phaseFromString):
+        (-[HIDEventGenerator eventMaskFromEventInfo:]):
+        (-[HIDEventGenerator touchFromEventInfo:]):
+        (-[HIDEventGenerator _createIOHIDEventWithInfo:]):
+        (-[HIDEventGenerator dispatchEventWithInfo:]):
+        (-[HIDEventGenerator eventDispatchThreadEntry:]):
+        (-[HIDEventGenerator sendEventStream:completionBlock:]):
+        * WebKitTestRunner/ios/UIScriptControllerIOS.mm:
+        (WTR::UIScriptController::sendEventStream):
+
 2016-10-04  Megan Gardner  <megan_gardner@apple.com>
 
         Add Megan to contributor's list
index 0e64594..0c5728e 100644 (file)
@@ -108,6 +108,10 @@ void UIScriptController::stylusTapAtPoint(long x, long y, float azimuthAngle, fl
 {
 }
 
+void UIScriptController::sendEventStream(JSStringRef eventsJSON, JSValueRef callback)
+{
+}
+
 void UIScriptController::typeCharacterUsingHardwareKeyboard(JSStringRef character, JSValueRef callback)
 {
 }
index f7f096e..5e6c26e 100644 (file)
@@ -49,6 +49,40 @@ interface UIScriptController {
     void keyDownUsingHardwareKeyboard(DOMString character, object callback);
     void keyUpUsingHardwareKeyboard(DOMString character, object callback);
 
+    // eventsJSON describes a series of user events in JSON form. For the keys, see HIDEventGenerator.mm.
+    // For example, this JSON describes a touch down followed by a touch up (i.e. a single tap).
+    //  {
+    //      "events" : [
+    //          {
+    //              "inputType" : "hand",
+    //              "timeOffset" : 0,
+    //              "touches" : [
+    //                  {
+    //                      "inputType" : "finger",
+    //                      "phase" : "began",
+    //                      "id" : 1,
+    //                      "x" : 100,
+    //                      "y" : 120
+    //                  }
+    //              ]
+    //          },
+    //          {
+    //              "inputType" : "hand",
+    //              "timeOffset" : 0.002, // seconds relative to the first event
+    //              "touches" : [
+    //                  {
+    //                      "inputType" : "finger",
+    //                      "phase" : "ended",
+    //                      "id" : 1,
+    //                      "x" : 100,
+    //                      "y" : 120
+    //                  }
+    //              ]
+    //          },
+    //      ]
+    //  }
+    void sendEventStream(DOMString eventsJSON, object callback);
+
     // Equivalent of pressing the Done button in the form accessory bar.
     void dismissFormAccessoryView();
 
index c1b8578..5c4cba5 100644 (file)
@@ -180,6 +180,10 @@ void UIScriptController::stylusTapAtPoint(long x, long y, float azimuthAngle, fl
 {
 }
 
+void UIScriptController::sendEventStream(JSStringRef eventsJSON, JSValueRef callback)
+{
+}
+
 void UIScriptController::typeCharacterUsingHardwareKeyboard(JSStringRef, JSValueRef)
 {
 }
index 6e222cc..085a7ca 100644 (file)
@@ -63,7 +63,9 @@ public:
     void stylusTapAtPoint(long x, long y, float azimuthAngle, float altitudeAngle, float pressure, JSValueRef callback);
 
     void longPressAtPoint(long x, long y, JSValueRef callback);
-    
+
+    void sendEventStream(JSStringRef eventsJSON, JSValueRef callback);
+
     void typeCharacterUsingHardwareKeyboard(JSStringRef character, JSValueRef callback);
     void keyDownUsingHardwareKeyboard(JSStringRef character, JSValueRef callback);
     void keyUpUsingHardwareKeyboard(JSStringRef character, JSValueRef callback);
index e584b69..747c867 100644 (file)
 
 #import <CoreGraphics/CGGeometry.h>
 
+// Keys for sendEventStream:completionBlock:.
+extern NSString* const TopLevelEventInfoKey;
+extern NSString* const HIDEventInputType;
+extern NSString* const HIDEventTimeOffsetKey;
+extern NSString* const HIDEventPhaseKey;
+extern NSString* const HIDEventTouchIDKey;
+extern NSString* const HIDEventPressureKey;
+extern NSString* const HIDEventXKey;
+extern NSString* const HIDEventYKey;
+extern NSString* const HIDEventTwistKey;
+extern NSString* const HIDEventMajorRadiusKey;
+extern NSString* const HIDEventMinorRadiusKey;
+extern NSString* const HIDEventTouchesKey;
+
+// Values for HIDEventInputType.
+extern NSString* const HIDEventInputTypeHand;
+extern NSString* const HIDEventInputTypeFinger;
+extern NSString* const HIDEventInputTypeStylus;
+
+// Values for HIDEventPhaseKey.
+extern NSString* const HIDEventPhaseBegan;
+extern NSString* const HIDEventPhaseMoved;
+extern NSString* const HIDEventPhaseEnded;
+extern NSString* const HIDEventPhaseCanceled;
+
+
 @interface HIDEventGenerator : NSObject
 
 + (HIDEventGenerator *)sharedHIDEventGenerator;
@@ -56,6 +82,9 @@
 - (void)pinchCloseWithStartPoint:(CGPoint)startLocation endPoint:(CGPoint)endLocation duration:(double)seconds completionBlock:(void (^)(void))completionBlock;
 - (void)pinchOpenWithStartPoint:(CGPoint)startLocation endPoint:(CGPoint)endLocation duration:(double)seconds completionBlock:(void (^)(void))completionBlock;
 
+// Event stream
+- (void)sendEventStream:(NSDictionary *)eventInfo completionBlock:(void (^)(void))completionBlock;
+
 - (void)markerEventReceived:(IOHIDEventRef)event;
 
 // Keyboard
index 0a85827..c277fb2 100644 (file)
 SOFT_LINK_PRIVATE_FRAMEWORK(BackBoardServices)
 SOFT_LINK(BackBoardServices, BKSHIDEventSetDigitizerInfo, void, (IOHIDEventRef digitizerEvent, uint32_t contextID, uint8_t systemGestureisPossible, uint8_t isSystemGestureStateChangeEvent, CFStringRef displayUUID, CFTimeInterval initialTouchTimestamp, float maxForce), (digitizerEvent, contextID, systemGestureisPossible, isSystemGestureStateChangeEvent, displayUUID, initialTouchTimestamp, maxForce));
 
+NSString* const TopLevelEventInfoKey = @"events";
+NSString* const HIDEventInputType = @"inputType";
+NSString* const HIDEventTimeOffsetKey = @"timeOffset";
+NSString* const HIDEventTouchesKey = @"touches";
+NSString* const HIDEventPhaseKey = @"phase";
+NSString* const HIDEventTouchIDKey = @"id";
+NSString* const HIDEventPressureKey = @"pressure";
+NSString* const HIDEventXKey = @"x";
+NSString* const HIDEventYKey = @"y";
+NSString* const HIDEventTwistKey = @"twist";
+NSString* const HIDEventMajorRadiusKey = @"majorRadius";
+NSString* const HIDEventMinorRadiusKey = @"minorRadius";
+
+NSString* const HIDEventInputTypeHand = @"hand";
+NSString* const HIDEventInputTypeFinger = @"finger";
+NSString* const HIDEventInputTypeStylus = @"stylus";
+
+NSString* const HIDEventPhaseBegan = @"began";
+NSString* const HIDEventPhaseMoved = @"moved";
+NSString* const HIDEventPhaseEnded = @"ended";
+NSString* const HIDEventPhaseCanceled = @"canceled";
+
 static const NSTimeInterval fingerLiftDelay = 0.05;
 static const NSTimeInterval multiTapInterval = 0.15;
 static const NSTimeInterval fingerMoveInterval = 0.016;
@@ -146,6 +168,130 @@ static void delayBetweenMove(int eventIndex, double elapsed)
     [self _sendHIDEvent:eventRef.get()];
 }
 
+static IOHIDDigitizerTransducerType transducerTypeFromString(NSString * transducerTypeString)
+{
+    if ([transducerTypeString isEqualToString:HIDEventInputTypeHand])
+        return kIOHIDDigitizerTransducerTypeHand;
+
+    if ([transducerTypeString isEqualToString:HIDEventInputTypeFinger])
+        return kIOHIDDigitizerTransducerTypeFinger;
+
+    if ([transducerTypeString isEqualToString:HIDEventInputTypeStylus])
+        return kIOHIDDigitizerTransducerTypeStylus;
+    
+    ASSERT_NOT_REACHED();
+    return 0;
+}
+
+static UITouchPhase phaseFromString(NSString *string)
+{
+    if ([string isEqualToString:HIDEventPhaseBegan])
+        return UITouchPhaseBegan;
+
+    if ([string isEqualToString:HIDEventPhaseMoved])
+        return UITouchPhaseMoved;
+
+    if ([string isEqualToString:HIDEventPhaseEnded])
+        return UITouchPhaseEnded;
+
+    if ([string isEqualToString:HIDEventPhaseCanceled])
+        return UITouchPhaseCancelled;
+
+    return UITouchPhaseStationary;
+}
+
+- (IOHIDDigitizerEventMask)eventMaskFromEventInfo:(NSDictionary *)info
+{
+    NSArray *childEvents = info[HIDEventTouchesKey];
+    for (NSDictionary *touchInfo in childEvents) {
+        UITouchPhase phase = phaseFromString(touchInfo[HIDEventPhaseKey]);
+        // If there are any new or ended events, mask includes touch.
+        if (phase == UITouchPhaseBegan || phase == UITouchPhaseEnded || phase == UITouchPhaseCancelled)
+            return kIOHIDDigitizerEventTouch;
+    }
+    
+    return 0;
+}
+
+// Returns 1 for all events where the fingers are on the glass (everything but enced and canceled).
+- (CFIndex)touchFromEventInfo:(NSDictionary *)info
+{
+    NSArray *childEvents = info[HIDEventTouchesKey];
+    for (NSDictionary *touchInfo in childEvents) {
+        UITouchPhase phase = phaseFromString(touchInfo[HIDEventPhaseKey]);
+        if (phase == UITouchPhaseBegan || phase == UITouchPhaseMoved || phase == UITouchPhaseStationary)
+            return 1;
+    }
+    
+    return 0;
+}
+
+// FIXME: callers of _createIOHIDEventType could switch to this.
+- (IOHIDEventRef)_createIOHIDEventWithInfo:(NSDictionary *)info
+{
+    uint64_t machTime = mach_absolute_time();
+
+    IOHIDDigitizerEventMask eventMask = [self eventMaskFromEventInfo:info];
+
+    CFIndex range = 0;
+    // touch is 1 if a finger is down.
+    CFIndex touch = [self touchFromEventInfo:info];
+
+    IOHIDEventRef eventRef = IOHIDEventCreateDigitizerEvent(kCFAllocatorDefault, machTime,
+        transducerTypeFromString(info[HIDEventInputType]),  // transducerType
+        0,                                                  // index
+        0,                                                  // identifier
+        eventMask,                                          // event mask
+        0,                                                  // button event
+        0,                                                  // x
+        0,                                                  // y
+        0,                                                  // z
+        0,                                                  // presure
+        0,                                                  // twist
+        range,                                              // range
+        touch,                                              // touch
+        kIOHIDEventOptionNone);
+
+    IOHIDEventSetIntegerValue(eventRef, kIOHIDEventFieldDigitizerIsDisplayIntegrated, 1);
+
+    NSArray *childEvents = info[HIDEventTouchesKey];
+    for (NSDictionary *touchInfo in childEvents) {
+
+        IOHIDDigitizerEventMask childEventMask = 0;
+
+        UITouchPhase phase = phaseFromString(touchInfo[HIDEventPhaseKey]);
+        if (phase != UITouchPhaseCancelled && phase != UITouchPhaseBegan && phase != UITouchPhaseEnded)
+            childEventMask |= kIOHIDDigitizerEventPosition;
+
+        if (phase == UITouchPhaseBegan || phase == UITouchPhaseEnded || phase == UITouchPhaseCancelled)
+            childEventMask |= (kIOHIDDigitizerEventTouch | kIOHIDDigitizerEventRange);
+
+        if (phase == UITouchPhaseCancelled)
+            childEventMask |= kIOHIDDigitizerEventCancel;
+
+        IOHIDEventRef subEvent = IOHIDEventCreateDigitizerFingerEvent(kCFAllocatorDefault, machTime,
+            [touchInfo[HIDEventTouchIDKey] intValue],               // index
+            2,                                                      // identifier (which finger we think it is). FIXME: this should come from the data.
+            childEventMask,
+            [touchInfo[HIDEventXKey] floatValue],
+            [touchInfo[HIDEventYKey] floatValue],
+            0, // z
+            [touchInfo[HIDEventPressureKey] floatValue],
+            [touchInfo[HIDEventTwistKey] floatValue],
+            touch,                                                  // range
+            touch,                                                  // touch
+            kIOHIDEventOptionNone);
+
+        IOHIDEventSetFloatValue(subEvent, kIOHIDEventFieldDigitizerMajorRadius, [touchInfo[HIDEventMajorRadiusKey] floatValue]);
+        IOHIDEventSetFloatValue(subEvent, kIOHIDEventFieldDigitizerMinorRadius, [touchInfo[HIDEventMinorRadiusKey] floatValue]);
+
+        IOHIDEventAppendEvent(eventRef, subEvent, 0);
+        CFRelease(subEvent);
+    }
+
+    return eventRef;
+}
+
 - (IOHIDEventRef)_createIOHIDEventType:(HandEventType)eventType
 {
     BOOL isTouching = (eventType == HandEventTouched || eventType == HandEventMoved || eventType == HandEventChordChanged || eventType == StylusEventTouched || eventType == StylusEventMoved);
@@ -766,4 +912,62 @@ static inline uint32_t hidUsageCodeForCharacter(NSString *key)
     [self _sendMarkerHIDEventWithCompletionBlock:completionBlock];
 }
 
+- (void)dispatchEventWithInfo:(NSDictionary *)eventInfo
+{
+    ASSERT([NSThread isMainThread]);
+
+    RetainPtr<IOHIDEventRef> eventRef = adoptCF([self _createIOHIDEventWithInfo:eventInfo]);
+    [self _sendHIDEvent:eventRef.get()];
+}
+
+- (void)eventDispatchThreadEntry:(NSDictionary *)threadData
+{
+    NSDictionary *eventStream = threadData[@"eventInfo"];
+    void (^completionBlock)() = threadData[@"completionBlock"];
+
+    NSArray *events = eventStream[TopLevelEventInfoKey];
+    if (!events.count) {
+        NSLog(@"No events found in event stream");
+        return;
+    }
+
+    CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
+    
+    for (NSDictionary *eventInfo in events) {
+        NSTimeInterval eventRelativeTime = [eventInfo[HIDEventTimeOffsetKey] doubleValue];
+        CFAbsoluteTime targetTime = startTime + eventRelativeTime;
+        
+        CFTimeInterval waitTime = targetTime - CFAbsoluteTimeGetCurrent();
+        if (waitTime > 0)
+            [NSThread sleepForTimeInterval:waitTime];
+
+        dispatch_async(dispatch_get_main_queue(), ^ {
+            [self dispatchEventWithInfo:eventInfo];
+        });
+    }
+
+    dispatch_async(dispatch_get_main_queue(), ^ {
+        [self _sendMarkerHIDEventWithCompletionBlock:completionBlock];
+    });
+}
+
+- (void)sendEventStream:(NSDictionary *)eventInfo completionBlock:(void (^)(void))completionBlock
+{
+    if (!eventInfo) {
+        NSLog(@"eventInfo is nil");
+        if (completionBlock)
+            completionBlock();
+        return;
+    }
+    
+    NSDictionary* threadData = @{
+        @"eventInfo": [eventInfo copy],
+        @"completionBlock": [[completionBlock copy] autorelease]
+    };
+    
+    NSThread *eventDispatchThread = [[[NSThread alloc] initWithTarget:self selector:@selector(eventDispatchThreadEntry:) object:threadData] autorelease];
+    eventDispatchThread.qualityOfService = NSQualityOfServiceUserInteractive;
+    [eventDispatchThread start];
+}
+
 @end
index 236938f..787cc9f 100644 (file)
@@ -117,6 +117,8 @@ enum {
 };
 
 enum {
+    kIOHIDDigitizerTransducerTypeStylus  = 0,
+    kIOHIDDigitizerTransducerTypeFinger = 2,
     kIOHIDDigitizerTransducerTypeHand = 3
 };
 typedef uint32_t IOHIDDigitizerTransducerType;
index c5db0e4..f6214da 100644 (file)
@@ -35,6 +35,7 @@
 #import "TestRunnerWKWebView.h"
 #import "UIScriptContext.h"
 #import <JavaScriptCore/JavaScriptCore.h>
+#import <JavaScriptCore/OpaqueJSString.h>
 #import <UIKit/UIKit.h>
 #import <WebCore/FloatRect.h>
 #import <WebKit/WKWebViewPrivate.h>
@@ -175,6 +176,24 @@ void UIScriptController::stylusTapAtPoint(long x, long y, float azimuthAngle, fl
     }];
 }
 
+void UIScriptController::sendEventStream(JSStringRef eventsJSON, JSValueRef callback)
+{
+    unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
+
+    String jsonString = eventsJSON->string();
+    auto eventInfo = dynamic_objc_cast<NSDictionary>([NSJSONSerialization JSONObjectWithData:[(NSString *)jsonString dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil]);
+    if (!eventInfo || ![eventInfo isKindOfClass:[NSDictionary class]]) {
+        WTFLogAlways("JSON is not convertible to a dictionary");
+        return;
+    }
+    
+    [[HIDEventGenerator sharedHIDEventGenerator] sendEventStream:eventInfo completionBlock:^{
+        if (!m_context)
+            return;
+        m_context->asyncTaskComplete(callbackID);
+    }];
+}
+
 void UIScriptController::dragFromPointToPoint(long startX, long startY, long endX, long endY, double durationSeconds, JSValueRef callback)
 {
     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);