Title: [285390] trunk
Revision
285390
Author
[email protected]
Date
2021-11-07 08:07:18 -0800 (Sun, 07 Nov 2021)

Log Message

Implement UIScriptController.sendEventStream() on macOS for wheel events
https://bugs.webkit.org/show_bug.cgi?id=232794

Reviewed by Tim Horton.

eventSender.mouseScrollByWithWheelAndMomentumPhases() is problematic for timing-sensitive
scrolling behaviors because the timestamps used are not under script control. It's also
a rather unwieldy API.

To make wheel event testing easier, implement enough of UIScriptController.sendEventStream()
on macOS that it can generate a series of wheel events with predicable timestamps
(currently hardcoded to be 16.6ms apart).

Tools:

* WebKitTestRunner/EventSenderProxy.h:
* WebKitTestRunner/mac/EventSenderProxy.mm:
(WTR::cgScrollPhaseFromPhase):
(WTR::cgGesturePhaseFromString):
(WTR::EventSenderProxy::sendWheelEvent):
* WebKitTestRunner/mac/UIScriptControllerMac.h:
* WebKitTestRunner/mac/UIScriptControllerMac.mm:
(WTR::eventPhaseFromString):
(WTR::UIScriptControllerMac::sendEventStream):

LayoutTests:

* fast/scrolling/mac/momentum-event-sequence-expected.txt: Added.
* fast/scrolling/mac/momentum-event-sequence.html: Added.
* resources/ui-helper.js:
(window.UIHelper.async mouseWheelSequence.await.new.Promise):
(window.UIHelper.async mouseWheelSequence):

Modified Paths

Added Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (285389 => 285390)


--- trunk/LayoutTests/ChangeLog	2021-11-07 04:07:44 UTC (rev 285389)
+++ trunk/LayoutTests/ChangeLog	2021-11-07 16:07:18 UTC (rev 285390)
@@ -1,3 +1,24 @@
+2021-11-07  Simon Fraser  <[email protected]>
+
+        Implement UIScriptController.sendEventStream() on macOS for wheel events
+        https://bugs.webkit.org/show_bug.cgi?id=232794
+
+        Reviewed by Tim Horton.
+
+        eventSender.mouseScrollByWithWheelAndMomentumPhases() is problematic for timing-sensitive
+        scrolling behaviors because the timestamps used are not under script control. It's also
+        a rather unwieldy API.
+        
+        To make wheel event testing easier, implement enough of UIScriptController.sendEventStream()
+        on macOS that it can generate a series of wheel events with predicable timestamps
+        (currently hardcoded to be 16.6ms apart).
+
+        * fast/scrolling/mac/momentum-event-sequence-expected.txt: Added.
+        * fast/scrolling/mac/momentum-event-sequence.html: Added.
+        * resources/ui-helper.js:
+        (window.UIHelper.async mouseWheelSequence.await.new.Promise):
+        (window.UIHelper.async mouseWheelSequence):
+
 2021-11-06  Tyler Wilcock  <[email protected]>
 
         AX: WebKit1 PluginViewBase objects with an associated widget()->platformWidget() should be considered attachments

Added: trunk/LayoutTests/fast/scrolling/mac/momentum-event-sequence-expected.txt (0 => 285390)


--- trunk/LayoutTests/fast/scrolling/mac/momentum-event-sequence-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/fast/scrolling/mac/momentum-event-sequence-expected.txt	2021-11-07 16:07:18 UTC (rev 285390)
@@ -0,0 +1,5 @@
+PASS window.scrollY is 46
+PASS successfullyParsed is true
+
+TEST COMPLETE
+

Added: trunk/LayoutTests/fast/scrolling/mac/momentum-event-sequence.html (0 => 285390)


--- trunk/LayoutTests/fast/scrolling/mac/momentum-event-sequence.html	                        (rev 0)
+++ trunk/LayoutTests/fast/scrolling/mac/momentum-event-sequence.html	2021-11-07 16:07:18 UTC (rev 285390)
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <style>
+        body {
+            height: 2000px;
+            width: 200%;
+        }
+    </style>
+    <script src=""
+    <script src=""
+    <script>
+        var jsTestIsAsync = true;
+        
+        async function testEventSequence()
+        {
+            const wheelEventSquence = {
+                "events" : [
+                    {
+                        "type" : "wheel",
+                        "viewX" : 100,
+                        "viewY" : 100,
+                        "deltaX" : 0,
+                        "deltaY" : -10, // Note that this delta is currently ignored.
+                        "phase" : "began"
+                    },
+                    {
+                        "type" : "wheel",
+                        "deltaX" : 0,
+                        "deltaY" : -10,
+                        "phase" : "changed"
+                    },
+                    {
+                        "type" : "wheel",
+                        "deltaX" : 0,
+                        "deltaY" : -8,
+                        "phase" : "changed"
+                    },
+                    {
+                        "type" : "wheel",
+                        "deltaX" : 0,
+                        "deltaY" : 0,
+                        "phase" : "ended"
+                    },
+                    {
+                        "type" : "wheel",
+                        "deltaX" : 0,
+                        "deltaY" : -10,
+                        "momentumPhase" : "began"
+                    },
+                    {
+                        "type" : "wheel",
+                        "deltaX" : 0,
+                        "deltaY" : -12,
+                        "momentumPhase" : "changed"
+                    },
+                    {
+                        "type" : "wheel",
+                        "deltaX" : 0,
+                        "deltaY" : -6,
+                        "momentumPhase" : "changed"
+                    },
+                    {
+                        "type" : "wheel",
+                        "momentumPhase" : "ended"
+                    }
+                ]
+            };
+
+            await UIHelper.mouseWheelSequence(wheelEventSquence);
+            shouldBe('window.scrollY', '46');
+        }
+
+        async function scrollTest()
+        {
+            await testEventSequence();
+            finishJSTest();
+        }
+
+        window.addEventListener('load', () => {
+            setTimeout(scrollTest, 0);
+        }, false);
+    </script>
+</head>
+<body>
+    <script src=""
+</body>
+</html>

Modified: trunk/LayoutTests/resources/ui-helper.js (285389 => 285390)


--- trunk/LayoutTests/resources/ui-helper.js	2021-11-07 04:07:44 UTC (rev 285389)
+++ trunk/LayoutTests/resources/ui-helper.js	2021-11-07 16:07:18 UTC (rev 285390)
@@ -108,6 +108,28 @@
         await UIHelper.animationFrame();
     }
 
+    static async mouseWheelSequence(eventStream)
+    {
+        if (!this.isWebKit2()) {
+            console.log('UIHelper.mouseWheelSequence() does not work in DumpRenderTree')
+            return Promise.resolve();
+        }
+
+        eventSender.monitorWheelEvents();
+        const eventStreamAsString = JSON.stringify(eventStream);
+        await new Promise(resolve => {
+            testRunner.runUIScript(`
+                (function() {
+                    uiController.sendEventStream(\`${eventStreamAsString}\`, () => {
+                        uiController.uiScriptComplete();
+                    });
+                })();
+            `, resolve);
+        });
+
+        await UIHelper.waitForScrollCompletion();
+    }
+
     static async waitForScrollCompletion()
     {
         return new Promise(resolve => {

Modified: trunk/Tools/ChangeLog (285389 => 285390)


--- trunk/Tools/ChangeLog	2021-11-07 04:07:44 UTC (rev 285389)
+++ trunk/Tools/ChangeLog	2021-11-07 16:07:18 UTC (rev 285390)
@@ -1,3 +1,28 @@
+2021-11-07  Simon Fraser  <[email protected]>
+
+        Implement UIScriptController.sendEventStream() on macOS for wheel events
+        https://bugs.webkit.org/show_bug.cgi?id=232794
+
+        Reviewed by Tim Horton.
+
+        eventSender.mouseScrollByWithWheelAndMomentumPhases() is problematic for timing-sensitive
+        scrolling behaviors because the timestamps used are not under script control. It's also
+        a rather unwieldy API.
+        
+        To make wheel event testing easier, implement enough of UIScriptController.sendEventStream()
+        on macOS that it can generate a series of wheel events with predicable timestamps
+        (currently hardcoded to be 16.6ms apart).
+
+        * WebKitTestRunner/EventSenderProxy.h:
+        * WebKitTestRunner/mac/EventSenderProxy.mm:
+        (WTR::cgScrollPhaseFromPhase):
+        (WTR::cgGesturePhaseFromString):
+        (WTR::EventSenderProxy::sendWheelEvent):
+        * WebKitTestRunner/mac/UIScriptControllerMac.h:
+        * WebKitTestRunner/mac/UIScriptControllerMac.mm:
+        (WTR::eventPhaseFromString):
+        (WTR::UIScriptControllerMac::sendEventStream):
+
 2021-11-06  David Kilzer  <[email protected]>
 
         Remove empty directories from from svn.webkit.org repository as of r285357

Modified: trunk/Tools/WebKitTestRunner/EventSenderProxy.h (285389 => 285390)


--- trunk/Tools/WebKitTestRunner/EventSenderProxy.h	2021-11-07 04:07:44 UTC (rev 285389)
+++ trunk/Tools/WebKitTestRunner/EventSenderProxy.h	2021-11-07 16:07:18 UTC (rev 285390)
@@ -60,6 +60,8 @@
     void mouseForceClick();
     void startAndCancelMouseForceClick();
     void mouseMoveTo(double x, double y, WKStringRef pointerType = nullptr);
+    
+    // Legacy wheel events.
     void mouseScrollBy(int x, int y);
     void mouseScrollByWithWheelAndMomentumPhases(int x, int y, int phase, int momentum);
 #if PLATFORM(GTK)
@@ -67,6 +69,21 @@
 #endif
     void continuousMouseScrollBy(int x, int y, bool paged);
 
+#if PLATFORM(MAC)
+    enum class WheelEventPhase : uint8_t {
+        None,
+        Began,
+        Changed,
+        Ended,
+        Cancelled,
+        MayBegin,
+    };
+    
+    using EventTimestamp = uint64_t; // mach_absolute_time units.
+
+    void sendWheelEvent(EventTimestamp, double globalX, double globalY, double deltaX, double deltaY, WheelEventPhase, WheelEventPhase momentumPhase);
+#endif
+
     void leapForward(int milliseconds);
 
     void keyDown(WKStringRef key, WKEventModifiers, unsigned location);

Modified: trunk/Tools/WebKitTestRunner/mac/EventSenderProxy.mm (285389 => 285390)


--- trunk/Tools/WebKitTestRunner/mac/EventSenderProxy.mm	2021-11-07 04:07:44 UTC (rev 285389)
+++ trunk/Tools/WebKitTestRunner/mac/EventSenderProxy.mm	2021-11-07 16:07:18 UTC (rev 285390)
@@ -753,6 +753,76 @@
     }
 }
 
+
+static CGScrollPhase cgScrollPhaseFromPhase(EventSenderProxy::WheelEventPhase phase)
+{
+    switch (phase) {
+    case EventSenderProxy::WheelEventPhase::None:
+        return static_cast<CGScrollPhase>(0);
+    case EventSenderProxy::WheelEventPhase::Began:
+        return kCGScrollPhaseBegan;
+    case EventSenderProxy::WheelEventPhase::Changed:
+        return kCGScrollPhaseChanged;
+    case EventSenderProxy::WheelEventPhase::Ended:
+        return kCGScrollPhaseEnded;
+    case EventSenderProxy::WheelEventPhase::Cancelled:
+        return kCGScrollPhaseCancelled;
+    case EventSenderProxy::WheelEventPhase::MayBegin:
+        return kCGScrollPhaseMayBegin;
+    }
+    ASSERT_NOT_REACHED();
+    return static_cast<CGScrollPhase>(0);
+}
+
+static CGGesturePhase cgGesturePhaseFromString(EventSenderProxy::WheelEventPhase phase)
+{
+    switch (phase) {
+    case EventSenderProxy::WheelEventPhase::None:
+        return kCGGesturePhaseNone;
+    case EventSenderProxy::WheelEventPhase::Began:
+        return kCGGesturePhaseBegan;
+    case EventSenderProxy::WheelEventPhase::Changed:
+        return kCGGesturePhaseChanged;
+    case EventSenderProxy::WheelEventPhase::Ended:
+        return kCGGesturePhaseEnded;
+    case EventSenderProxy::WheelEventPhase::Cancelled:
+        return kCGGesturePhaseCancelled;
+    case EventSenderProxy::WheelEventPhase::MayBegin:
+        return kCGGesturePhaseMayBegin;
+    }
+
+    ASSERT_NOT_REACHED();
+    return kCGGesturePhaseNone;
+}
+
+void EventSenderProxy::sendWheelEvent(EventTimestamp timestamp, double windowX, double windowY, double deltaX, double deltaY, WheelEventPhase phase, WheelEventPhase momentumPhase)
+{
+    constexpr uint32_t wheelCount = 2;
+    auto cgScrollEvent = adoptCF(CGEventCreateScrollWheelEvent2(nullptr, kCGScrollEventUnitPixel, wheelCount, deltaY, deltaX, 0));
+    CGEventSetTimestamp(cgScrollEvent.get(), timestamp);
+
+    // Set the CGEvent location in flipped coords relative to the first screen, which
+    // compensates for the behavior of +[NSEvent eventWithCGEvent:] when the event has
+    // no associated window. See <rdar://problem/17180591>.
+    CGPoint flippedWindowMousePosition = CGPointMake(windowX, [[[NSScreen screens] objectAtIndex:0] frame].size.height - windowY);
+    CGEventSetLocation(cgScrollEvent.get(), flippedWindowMousePosition);
+
+    CGEventSetIntegerValueField(cgScrollEvent.get(), kCGScrollWheelEventIsContinuous, 1);
+    CGEventSetIntegerValueField(cgScrollEvent.get(), kCGScrollWheelEventScrollPhase, cgScrollPhaseFromPhase(phase));
+    CGEventSetIntegerValueField(cgScrollEvent.get(), kCGScrollWheelEventMomentumPhase, cgGesturePhaseFromString(momentumPhase));
+
+    NSEvent* event = [NSEvent eventWithCGEvent:cgScrollEvent.get()];
+    // Our event should have the correct settings:
+    if (NSView *targetView = [m_testController->mainWebView()->platformView() hitTest:[event locationInWindow]]) {
+        [NSApp _setCurrentEvent:event];
+        [targetView scrollWheel:event];
+        [NSApp _setCurrentEvent:nil];
+    } else {
+        NSPoint windowLocation = [event locationInWindow];
+        WTFLogAlways("EventSenderProxy::sendWheelEvent failed to find the target view at %f,%f\n", windowLocation.x, windowLocation.y);
+    }
+}
+
 #if ENABLE(MAC_GESTURE_EVENTS)
 
 void EventSenderProxy::scaleGestureStart(double scale)

Modified: trunk/Tools/WebKitTestRunner/mac/UIScriptControllerMac.h (285389 => 285390)


--- trunk/Tools/WebKitTestRunner/mac/UIScriptControllerMac.h	2021-11-07 04:07:44 UTC (rev 285389)
+++ trunk/Tools/WebKitTestRunner/mac/UIScriptControllerMac.h	2021-11-07 16:07:18 UTC (rev 285390)
@@ -63,6 +63,8 @@
     void chooseMenuAction(JSStringRef, JSValueRef) override;
 
     void activateAtPoint(long x, long y, JSValueRef callback) override;
+    
+    void sendEventStream(JSStringRef, JSValueRef) override;
 
     NSTableView *dataListSuggestionsTableView() const;
 };

Modified: trunk/Tools/WebKitTestRunner/mac/UIScriptControllerMac.mm (285389 => 285390)


--- trunk/Tools/WebKitTestRunner/mac/UIScriptControllerMac.mm	2021-11-07 04:07:44 UTC (rev 285389)
+++ trunk/Tools/WebKitTestRunner/mac/UIScriptControllerMac.mm	2021-11-07 16:07:18 UTC (rev 285390)
@@ -44,6 +44,7 @@
 #import <_javascript_Core/OpaqueJSString.h>
 #import <WebKit/WKWebViewPrivate.h>
 #import <WebKit/WKWebViewPrivateForTesting.h>
+#import <mach/mach_time.h>
 #import <wtf/BlockPtr.h>
 #import <wtf/WorkQueue.h>
 
@@ -307,4 +308,113 @@
     [[LayoutTestSpellChecker checker] setResultsFromJSValue:results inContext:m_context->jsContext()];
 }
 
+static NSString* const TopLevelEventInfoKey = @"events";
+static NSString* const EventTypeKey = @"type";
+static NSString* const ViewRelativeXPositionKey = @"viewX";
+static NSString* const ViewRelativeYPositionKey = @"viewY";
+static NSString* const DeltaXKey = @"deltaX";
+static NSString* const DeltaYKey = @"deltaY";
+static NSString* const PhaseKey = @"phase";
+static NSString* const MomentumPhaseKey = @"momentumPhase";
+
+static EventSenderProxy::WheelEventPhase eventPhaseFromString(NSString *phaseStr)
+{
+    if ([phaseStr isEqualToString:@"began"])
+        return EventSenderProxy::WheelEventPhase::Began;
+    if ([phaseStr isEqualToString:@"changed"])
+        return EventSenderProxy::WheelEventPhase::Changed;
+    if ([phaseStr isEqualToString:@"ended"])
+        return EventSenderProxy::WheelEventPhase::Ended;
+    if ([phaseStr isEqualToString:@"cancelled"])
+        return EventSenderProxy::WheelEventPhase::Cancelled;
+    if ([phaseStr isEqualToString:@"maybegin"])
+        return EventSenderProxy::WheelEventPhase::MayBegin;
+
+    ASSERT_NOT_REACHED();
+    return EventSenderProxy::WheelEventPhase::None;
+}
+
+void UIScriptControllerMac::sendEventStream(JSStringRef eventsJSON, JSValueRef callback)
+{
+    auto* eventSender = TestController::singleton().eventSenderProxy();
+    if (!eventSender) {
+        ASSERT_NOT_REACHED();
+        return;
+    }
+
+    unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
+
+    auto jsonString = eventsJSON->string();
+    auto eventInfo = dynamic_objc_cast<NSDictionary>([NSJSONSerialization JSONObjectWithData:[(NSString *)jsonString dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingMutableContainers | NSJSONReadingMutableLeaves error:nil]);
+    if (!eventInfo || ![eventInfo isKindOfClass:[NSDictionary class]]) {
+        WTFLogAlways("JSON is not convertible to a dictionary");
+        return;
+    }
+
+    auto *webView = this->webView();
+
+    double currentViewRelativeX = 0;
+    double currentViewRelativeY = 0;
+
+    constexpr uint64_t nanosecondsPerSecond = 1e9;
+    constexpr uint64_t nanosecondsEventInterval = nanosecondsPerSecond / 60;
+
+    auto currentTime = mach_absolute_time();
+
+    for (NSMutableDictionary *event in eventInfo[TopLevelEventInfoKey]) {
+
+        id eventType = event[EventTypeKey];
+        if (!event[EventTypeKey]) {
+            WTFLogAlways("Missing event type");
+            break;
+        }
+        
+        if ([eventType isEqualToString:@"wheel"]) {
+            auto phase = EventSenderProxy::WheelEventPhase::None;
+            auto momentumPhase = EventSenderProxy::WheelEventPhase::None;
+
+            if (!event[PhaseKey] && !event[MomentumPhaseKey]) {
+                WTFLogAlways("Event must specify phase or momentumPhase");
+                break;
+            }
+
+            if (id phaseString = event[PhaseKey])
+                phase = eventPhaseFromString(phaseString);
+
+            if (id phaseString = event[MomentumPhaseKey])
+                momentumPhase = eventPhaseFromString(phaseString);
+
+            ASSERT_IMPLIES(phase == EventSenderProxy::WheelEventPhase::None, momentumPhase != EventSenderProxy::WheelEventPhase::None);
+            ASSERT_IMPLIES(momentumPhase == EventSenderProxy::WheelEventPhase::None, phase != EventSenderProxy::WheelEventPhase::None);
+
+            if (event[ViewRelativeXPositionKey])
+                currentViewRelativeX = [event[ViewRelativeXPositionKey] floatValue];
+
+            if (event[ViewRelativeYPositionKey])
+                currentViewRelativeY = [event[ViewRelativeYPositionKey] floatValue];
+
+            double deltaX = 0;
+            double deltaY = 0;
+
+            if (event[DeltaXKey])
+                deltaX = [event[DeltaXKey] floatValue];
+
+            if (event[DeltaYKey])
+                deltaY = [event[DeltaYKey] floatValue];
+
+            auto windowPoint = [webView convertPoint:CGPointMake(currentViewRelativeX, currentViewRelativeY) toView:nil];
+            eventSender->sendWheelEvent(currentTime, windowPoint.x, windowPoint.y, deltaX, deltaY, phase, momentumPhase);
+        }
+
+        currentTime += nanosecondsEventInterval;
+    }
+
+    WorkQueue::main().dispatch([this, strongThis = Ref { *this }, callbackID] {
+        if (!m_context)
+            return;
+        m_context->asyncTaskComplete(callbackID);
+    });
+}
+
+
 } // namespace WTR
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to