Web Replay: capture and replay wheel events and scroll commands
https://bugs.webkit.org/show_bug.cgi?id=129402

.:

Reviewed by Timothy Hatcher.

* ManualTests/inspector/replay-wheel-events.html: Added.

Source/WebCore:

Reviewed by Timothy Hatcher and Simon Fraser.

To capture and replay scrolling, the scrolling coordinator will force synchronous
scrolling during capture and replay. If the page is capturing or replaying,
ForceOnMainThread will be added to the coordinator's SynchronousScrollingReasons.

A callback was added to signal that replay session state have changed,
and thus the synchronous scrolling reasons should be recomputed.

Automated replay reftests for scrolling are not included, because they will be
too flaky until more nondeterminism is handled. Specifically, resource loading,
initial focus/active state, and parsing are known blocking issues.

Test: ManualTests/inspector/replay-wheel-events.html

* page/scrolling/ScrollingCoordinator.cpp: Add a new callback for replay state
session changes. Add ForceOnMainThread if the page's active input cursor is
capturing or replaying.

(WebCore::ScrollingCoordinator::synchronousScrollingReasons):
(WebCore::ScrollingCoordinator::replaySessionStateDidChange): Added.
* page/scrolling/ScrollingCoordinator.h:

* platform/PlatformWheelEvent.h:
* platform/ScrollTypes.h: Add explicit enum storage types so these enums can
be forward-declared. This is necessary to generate enum encode/decode implementations.

* replay/ReplayController.cpp:
(WebCore::ReplayController::setForceDeterministicSettings): If async scrolling
support is available, tell the scrolling tree to behave deterministically.

* replay/ReplayInputDispatchMethods.cpp:
(WebCore::HandleWheelEvent::dispatch): Added.
(WebCore::LogicalScrollPage::dispatch): Added.
(WebCore::ScrollPage::dispatch): Added.
* replay/SerializationMethods.cpp: Introduce more specific macros for values
deserialized to arbitraray lvalues, scalars, RefPtr, and unique_ptr. Fix existing
uses of decode macros.

(JSC::EncodingTraits<NondeterministicInputBase>::encodeValue): Fix macro name.
(JSC::EncodingTraits<KeypressCommand>::encodeValue): Fix macro name.
(JSC::EncodingTraits<PlatformKeyboardEvent>::encodeValue): Fix macro name.
(JSC::EncodingTraits<PlatformMouseEvent>::encodeValue): Fix macro name.
(JSC::PlatformWheelEventCocoa::PlatformWheelEventCocoa): Added. Encapsulate
and initialize data members specific to PLATFORM(COCOA).

(JSC::EncodingTraits<PlatformWheelEvent>::encodeValue): Added.
(JSC::EncodingTraits<PlatformWheelEvent>::decodeValue): Added.
* replay/SerializationMethods.h:
* replay/UserInputBridge.cpp: Fill in bridge methods to capture inputs.
(WebCore::UserInputBridge::handleWheelEvent):
(WebCore::UserInputBridge::scrollRecursively):
(WebCore::UserInputBridge::logicalScrollRecursively):
* replay/WebInputs.json: Add inputs and new enum types.


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@166811 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/ChangeLog b/ChangeLog
index 06157b6..d12ba14 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,12 @@
+2014-04-04  Brian J. Burg  <burg@cs.washington.edu>
+
+        Web Replay: capture and replay wheel events and scroll commands
+        https://bugs.webkit.org/show_bug.cgi?id=129402
+
+        Reviewed by Timothy Hatcher.
+
+        * ManualTests/inspector/replay-wheel-events.html: Added.
+
 2014-04-04  Raphael Kubo da Costa  <raphael.kubo.da.costa@intel.com>
 
         [GTK][CMake] Remove FindXt.cmake.
diff --git a/ManualTests/inspector/replay-wheel-events.html b/ManualTests/inspector/replay-wheel-events.html
new file mode 100644
index 0000000..638df0c
--- /dev/null
+++ b/ManualTests/inspector/replay-wheel-events.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+        "http://www.w3.org/TR/html4/strict.dtd">
+<html lang="en">
+<head>
+<script src="./resources/crypto-md5.js"></script>
+<script type="text/javascript" language="javascript" charset="utf-8">
+
+    document.onmousewheel = handleEvent;
+    
+    window.dumpedEvents = [];
+    
+    function handleEvent(event) {
+        var properties = ["type", "eventPhase", "bubbles", "cancelable", "screenX", "screenY", "clientX", "clientY", "ctrlKey", "shiftKey", "altKey", "metaKey", "button", "deltaX", "deltaY", "deltaZ", "deltaMode", "wheelDeltaX", "wheelDeltaY"];
+        obj = {};
+        for (var key of properties)
+            obj[key] = event[key];
+    
+        dumpedEvents.push(obj);
+
+        var block = createBlock(hex_md5(JSON.stringify(obj)));
+        var blocksContainer = document.getElementById("blocks");
+        blocksContainer.appendChild(block);
+        
+        var hashLabel = document.getElementById("hash");
+        hash.textContent = hex_md5(JSON.stringify(dumpedEvents));
+    }
+    
+    function createBlock(hash) {
+        var color = "#" + hash.substr(0,6);
+        var block = document.createElement("span");
+        block.style.backgroundColor = color;
+        return block;
+    }
+    
+    function stateHash() {
+        return hex_md5(JSON.stringify(dumpedEvents));
+    }
+    
+</script>
+
+<style type="text/css">
+body {
+    max-width: 800px;
+}
+#blocks {
+    display: -webkit-flex;
+    width: 600px;
+    -webkit-flex-flow: row wrap;
+}
+    
+#blocks > span {
+    width: 10px;
+    height: 10px;
+    border-radius: 5px;
+    text-align: center;
+}
+</style>
+</head>
+<body>
+<p>This page is a manual test for capture and replay of scroll-related DOM events.</p>
+<p>Below, a block is created for each mousewheel event, where the color is derived from a hash of the event data. At the bottom is a cumulative hash of all event data.</p>
+<hr/>
+<textarea rows="3">
+
+This is a scrollable textarea.
+
+.
+
+..
+
+...
+
+....
+
+.....
+
+</textarea>
+<p>
+To test the replay functionality, open the Web Inspector, start capturing, and then scroll each of the iframe, overflow:scroll element, and the main page's flowed text. After some time, stop capturing and then replay.</p>
+<p>The replayed execution should produce the same sequence of blocks, and page contents should scroll the same distance and speed. More importantly, the cumulative hash value should be the same at the end of capturing and at the end of any subsequent replays.</p>
+</p>
+<hr/>
+<div id="hash"></div>
+<div id="blocks"></div>
+</body>
+</html>
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index 9200a04..d3bcb39 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,63 @@
+2014-04-04  Brian J. Burg  <burg@cs.washington.edu>
+
+        Web Replay: capture and replay wheel events and scroll commands
+        https://bugs.webkit.org/show_bug.cgi?id=129402
+
+        Reviewed by Timothy Hatcher and Simon Fraser.
+
+        To capture and replay scrolling, the scrolling coordinator will force synchronous 
+        scrolling during capture and replay. If the page is capturing or replaying,
+        ForceOnMainThread will be added to the coordinator's SynchronousScrollingReasons.
+
+        A callback was added to signal that replay session state have changed,
+        and thus the synchronous scrolling reasons should be recomputed.
+
+        Automated replay reftests for scrolling are not included, because they will be
+        too flaky until more nondeterminism is handled. Specifically, resource loading,
+        initial focus/active state, and parsing are known blocking issues.
+
+        Test: ManualTests/inspector/replay-wheel-events.html
+
+        * page/scrolling/ScrollingCoordinator.cpp: Add a new callback for replay state
+        session changes. Add ForceOnMainThread if the page's active input cursor is
+        capturing or replaying.
+
+        (WebCore::ScrollingCoordinator::synchronousScrollingReasons):
+        (WebCore::ScrollingCoordinator::replaySessionStateDidChange): Added.
+        * page/scrolling/ScrollingCoordinator.h:
+
+        * platform/PlatformWheelEvent.h:
+        * platform/ScrollTypes.h: Add explicit enum storage types so these enums can
+        be forward-declared. This is necessary to generate enum encode/decode implementations.
+
+        * replay/ReplayController.cpp:
+        (WebCore::ReplayController::setForceDeterministicSettings): If async scrolling
+        support is available, tell the scrolling tree to behave deterministically.
+
+        * replay/ReplayInputDispatchMethods.cpp:
+        (WebCore::HandleWheelEvent::dispatch): Added.
+        (WebCore::LogicalScrollPage::dispatch): Added.
+        (WebCore::ScrollPage::dispatch): Added.
+        * replay/SerializationMethods.cpp: Introduce more specific macros for values
+        deserialized to arbitraray lvalues, scalars, RefPtr, and unique_ptr. Fix existing
+        uses of decode macros.
+
+        (JSC::EncodingTraits<NondeterministicInputBase>::encodeValue): Fix macro name.
+        (JSC::EncodingTraits<KeypressCommand>::encodeValue): Fix macro name.
+        (JSC::EncodingTraits<PlatformKeyboardEvent>::encodeValue): Fix macro name.
+        (JSC::EncodingTraits<PlatformMouseEvent>::encodeValue): Fix macro name.
+        (JSC::PlatformWheelEventCocoa::PlatformWheelEventCocoa): Added. Encapsulate
+        and initialize data members specific to PLATFORM(COCOA).
+
+        (JSC::EncodingTraits<PlatformWheelEvent>::encodeValue): Added.
+        (JSC::EncodingTraits<PlatformWheelEvent>::decodeValue): Added.
+        * replay/SerializationMethods.h:
+        * replay/UserInputBridge.cpp: Fill in bridge methods to capture inputs.
+        (WebCore::UserInputBridge::handleWheelEvent):
+        (WebCore::UserInputBridge::scrollRecursively):
+        (WebCore::UserInputBridge::logicalScrollRecursively):
+        * replay/WebInputs.json: Add inputs and new enum types.
+
 2014-04-04  Yusuke Suzuki  <utatane.tea@gmail.com>
 
         Upgrade to SelectorFailsAllSiblings when Child selector is failed.
diff --git a/Source/WebCore/page/scrolling/ScrollingCoordinator.cpp b/Source/WebCore/page/scrolling/ScrollingCoordinator.cpp
index 8dad160..7701bc6 100644
--- a/Source/WebCore/page/scrolling/ScrollingCoordinator.cpp
+++ b/Source/WebCore/page/scrolling/ScrollingCoordinator.cpp
@@ -46,6 +46,11 @@
 #include "ScrollingCoordinatorCoordinatedGraphics.h"
 #endif
 
+#if ENABLE(WEB_REPLAY)
+#include "ReplayController.h"
+#include <replay/InputCursor.h>
+#endif
+
 namespace WebCore {
 
 #if !PLATFORM(COCOA)
@@ -299,6 +304,11 @@
 
     if (m_forceSynchronousScrollLayerPositionUpdates)
         synchronousScrollingReasons |= ForcedOnMainThread;
+#if ENABLE(WEB_REPLAY)
+    InputCursor& cursor = m_page->replayController().activeInputCursor();
+    if (cursor.isCapturing() || cursor.isReplaying())
+        synchronousScrollingReasons |= ForcedOnMainThread;
+#endif
     if (frameView->hasSlowRepaintObjects())
         synchronousScrollingReasons |= HasSlowRepaintObjects;
     if (!supportsFixedPositionLayers() && frameView->hasViewportConstrainedObjects())
@@ -325,6 +335,13 @@
     updateSynchronousScrollingReasons();
 }
 
+#if ENABLE(WEB_REPLAY)
+void ScrollingCoordinator::replaySessionStateDidChange()
+{
+    updateSynchronousScrollingReasons();
+}
+#endif
+
 ScrollingNodeID ScrollingCoordinator::uniqueScrollLayerID()
 {
     static ScrollingNodeID uniqueScrollLayerID = 1;
diff --git a/Source/WebCore/page/scrolling/ScrollingCoordinator.h b/Source/WebCore/page/scrolling/ScrollingCoordinator.h
index 9dcb8fb..789c514 100644
--- a/Source/WebCore/page/scrolling/ScrollingCoordinator.h
+++ b/Source/WebCore/page/scrolling/ScrollingCoordinator.h
@@ -140,6 +140,11 @@
     void handleWheelEventPhase(PlatformWheelEventPhase);
 #endif
 
+#if ENABLE(WEB_REPLAY)
+    // Called when the page transitions between executing normally and deterministically.
+    void replaySessionStateDidChange();
+#endif
+
     // Force all scroll layer position updates to happen on the main thread.
     void setForceSynchronousScrollLayerPositionUpdates(bool);
 
diff --git a/Source/WebCore/platform/PlatformWheelEvent.h b/Source/WebCore/platform/PlatformWheelEvent.h
index aa308f8..62fdb66 100644
--- a/Source/WebCore/platform/PlatformWheelEvent.h
+++ b/Source/WebCore/platform/PlatformWheelEvent.h
@@ -48,13 +48,13 @@
     // and synthesized in other cases where platforms generate line-by-line scrolling events.
     // The ScrollByPageWheelEvent indicates that the wheel event should scroll an entire page.  In this case WebCore's built in paging behavior is used to page
     // up and down (you get the same behavior as if the user was clicking in a scrollbar track to page up or page down).
-    enum PlatformWheelEventGranularity {
+    enum PlatformWheelEventGranularity : uint64_t {
         ScrollByPageWheelEvent,
         ScrollByPixelWheelEvent,
     };
 
 #if PLATFORM(COCOA)
-    enum PlatformWheelEventPhase {
+    enum PlatformWheelEventPhase : uint64_t {
         PlatformWheelEventPhaseNone        = 0,
         PlatformWheelEventPhaseBegan       = 1 << 0,
         PlatformWheelEventPhaseStationary  = 1 << 1,
diff --git a/Source/WebCore/platform/ScrollTypes.h b/Source/WebCore/platform/ScrollTypes.h
index c98d778..5fd35c9 100644
--- a/Source/WebCore/platform/ScrollTypes.h
+++ b/Source/WebCore/platform/ScrollTypes.h
@@ -30,14 +30,14 @@
 
 namespace WebCore {
 
-    enum ScrollDirection {
+    enum ScrollDirection : uint64_t {
         ScrollUp,
         ScrollDown,
         ScrollLeft,
         ScrollRight
     };
 
-    enum ScrollLogicalDirection {
+    enum ScrollLogicalDirection : uint64_t {
         ScrollBlockDirectionBackward,
         ScrollBlockDirectionForward,
         ScrollInlineDirectionBackward,
@@ -103,7 +103,7 @@
         return ScrollUp;
     }
 
-    enum ScrollGranularity {
+    enum ScrollGranularity : uint64_t {
         ScrollByLine,
         ScrollByPage,
         ScrollByDocument,
diff --git a/Source/WebCore/replay/ReplayController.cpp b/Source/WebCore/replay/ReplayController.cpp
index edfc9eb..c907ba2 100644
--- a/Source/WebCore/replay/ReplayController.cpp
+++ b/Source/WebCore/replay/ReplayController.cpp
@@ -50,6 +50,10 @@
 #include <replay/EmptyInputCursor.h>
 #include <wtf/text/CString.h>
 
+#if ENABLE(ASYNC_SCROLLING)
+#include "ScrollingCoordinator.h"
+#endif
+
 namespace WebCore {
 
 ReplayController::ReplayController(Page& page)
@@ -66,17 +70,20 @@
 {
 }
 
-void ReplayController::setForceDeterministicSettings(bool shouldForce)
+void ReplayController::setForceDeterministicSettings(bool shouldForceDeterministicBehavior)
 {
-    ASSERT(shouldForce ^ (m_sessionState == SessionState::Inactive));
+    ASSERT(shouldForceDeterministicBehavior ^ (m_sessionState == SessionState::Inactive));
 
-    if (shouldForce) {
+    if (shouldForceDeterministicBehavior) {
         m_savedSettings.usesPageCache = m_page.settings().usesPageCache();
 
         m_page.settings().setUsesPageCache(false);
     } else {
         m_page.settings().setUsesPageCache(m_savedSettings.usesPageCache);
     }
+
+    if (ScrollingCoordinator* scrollingCoordinator = m_page.scrollingCoordinator())
+        scrollingCoordinator->replaySessionStateDidChange();
 }
 
 void ReplayController::setSessionState(SessionState state)
diff --git a/Source/WebCore/replay/ReplayInputDispatchMethods.cpp b/Source/WebCore/replay/ReplayInputDispatchMethods.cpp
index 1045e65..fba67dc 100644
--- a/Source/WebCore/replay/ReplayInputDispatchMethods.cpp
+++ b/Source/WebCore/replay/ReplayInputDispatchMethods.cpp
@@ -77,6 +77,21 @@
     controller.page().userInputBridge().handleMouseReleaseEvent(platformEvent(), InputSource::Synthetic);
 }
 
+void HandleWheelEvent::dispatch(ReplayController& controller)
+{
+    controller.page().userInputBridge().handleWheelEvent(platformEvent(), InputSource::Synthetic);
+}
+
+void LogicalScrollPage::dispatch(ReplayController& controller)
+{
+    controller.page().userInputBridge().logicalScrollRecursively(direction(), granularity(), InputSource::Synthetic);
+}
+
+void ScrollPage::dispatch(ReplayController& controller)
+{
+    controller.page().userInputBridge().scrollRecursively(direction(), granularity(), InputSource::Synthetic);
+}
+
 } // namespace WebCore
 
 #endif // ENABLE(WEB_REPLAY)
diff --git a/Source/WebCore/replay/SerializationMethods.cpp b/Source/WebCore/replay/SerializationMethods.cpp
index 5cb9ef1..72645b3 100644
--- a/Source/WebCore/replay/SerializationMethods.cpp
+++ b/Source/WebCore/replay/SerializationMethods.cpp
@@ -33,6 +33,7 @@
 #include "AllReplayInputs.h"
 #include "PlatformKeyboardEvent.h"
 #include "PlatformMouseEvent.h"
+#include "PlatformWheelEvent.h"
 #include "ReplayInputTypes.h"
 #include "SecurityOrigin.h"
 #include "URL.h"
@@ -44,31 +45,55 @@
 using WebCore::PlatformEvent;
 using WebCore::PlatformKeyboardEvent;
 using WebCore::PlatformMouseEvent;
+using WebCore::PlatformWheelEvent;
+using WebCore::PlatformWheelEventGranularity;
 using WebCore::SecurityOrigin;
 using WebCore::URL;
 using WebCore::inputTypes;
 
+#if PLATFORM(COCOA)
+using WebCore::PlatformWheelEventPhase;
+#endif
+
 #define IMPORT_FROM_WEBCORE_NAMESPACE(name) \
 using WebCore::name; \
 
 WEB_REPLAY_INPUT_NAMES_FOR_EACH(IMPORT_FROM_WEBCORE_NAMESPACE)
 #undef IMPORT_FROM_WEBCORE_NAMESPACE
 
-#define ENCODE_SCALAR_TYPE_WITH_KEY(_encodedValue, _type, _key, _value) \
+#define ENCODE_TYPE_WITH_KEY(_encodedValue, _type, _key, _value) \
     _encodedValue.put<_type>(ASCIILiteral(#_key), _value)
 
+#define ENCODE_OPTIONAL_TYPE_WITH_KEY(_encodedValue, _type, _key, _value, condition) \
+    if (condition) \
+        ENCODE_TYPE_WITH_KEY(_encodedValue, _type, _key, _value)
+
+#define DECODE_TYPE_WITH_KEY_TO_LVALUE(_encodedValue, _type, _key, _lvalue) \
+    if (!_encodedValue.get<_type>(ASCIILiteral(#_key), _lvalue)) \
+        return false
+
+#define DECODE_OPTIONAL_TYPE_WITH_KEY_TO_LVALUE(_encodedValue, _type, _key, _lvalue) \
+    bool _key ## WasDecoded = _encodedValue.get<_type>(ASCIILiteral(#_key), _lvalue)
+
+#define DECODE_REFCOUNTED_TYPE_WITH_KEY(_encodedValue, _type, _key) \
+    RefPtr<_type> _key; \
+    DECODE_TYPE_WITH_KEY_TO_LVALUE(_encodedValue, _type, _key, _key)
+
+#define DECODE_UNIQUE_TYPE_WITH_KEY(_encodedValue, _type, _key) \
+    std::unique_ptr<_type> _key; \
+    DECODE_TYPE_WITH_KEY_TO_LVALUE(_encodedValue, _type, _key, _key)
+
 #define DECODE_SCALAR_TYPE_WITH_KEY(_encodedValue, _type, _key) \
     _type _key; \
-    if (!_encodedValue.get<_type>(ASCIILiteral(#_key), _key)) \
-        return false
-
-#define ENCODE_OPTIONAL_SCALAR_TYPE_WITH_KEY(_encodedValue, _type, _key, _value, condition) \
-    if (condition) \
-        _encodedValue.put<_type>(ASCIILiteral(#_key), _value)
+    DECODE_TYPE_WITH_KEY_TO_LVALUE(_encodedValue, _type, _key, _key)
 
 #define DECODE_OPTIONAL_SCALAR_TYPE_WITH_KEY(_encodedValue, _type, _key) \
     _type _key; \
-    bool _key ## WasDecoded = _encodedValue.get<_type>(ASCIILiteral(#_key), _key)
+    DECODE_OPTIONAL_TYPE_WITH_KEY_TO_LVALUE(_encodedValue, _type, _key, _key)
+
+#define DECODE_OPTIONAL_REFCOUNTED_TYPE_WITH_KEY(_encodedValue, _type, _key) \
+    RefPtr<_type> _key; \
+    DECODE_OPTIONAL_TYPE_WITH_KEY_TO_LVALUE(_encodedValue, _type, _key, _key)
 
 namespace JSC {
 
@@ -77,7 +102,7 @@
     EncodedValue encodedValue = EncodedValue::createObject();
     const AtomicString& type = input.type();
 
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, String, type, type.string());
+    ENCODE_TYPE_WITH_KEY(encodedValue, String, type, type.string());
 
 #define ENCODE_IF_TYPE_TAG_MATCHES(name) \
     if (type == inputTypes().name) { \
@@ -134,8 +159,8 @@
 {
     EncodedValue encodedValue = EncodedValue::createObject();
 
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, String, commandName, command.commandName);
-    ENCODE_OPTIONAL_SCALAR_TYPE_WITH_KEY(encodedValue, String, text, command.text, !command.text.isEmpty());
+    ENCODE_TYPE_WITH_KEY(encodedValue, String, commandName, command.commandName);
+    ENCODE_OPTIONAL_TYPE_WITH_KEY(encodedValue, String, text, command.text, !command.text.isEmpty());
 
     return encodedValue;
 }
@@ -164,21 +189,21 @@
 {
     EncodedValue encodedValue = EncodedValue::createObject();
 
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, double, timestamp, input.timestamp());
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, PlatformEvent::Type, type, input.type());
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, PlatformEvent::Modifiers, modifiers, static_cast<PlatformEvent::Modifiers>(input.modifiers()));
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, String, text, input.text());
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, String, unmodifiedText, input.unmodifiedText());
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, String, keyIdentifier, input.keyIdentifier());
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, int, windowsVirtualKeyCode, input.windowsVirtualKeyCode());
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, int, nativeVirtualKeyCode, input.nativeVirtualKeyCode());
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, int, macCharCode, input.macCharCode());
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, bool, autoRepeat, input.isAutoRepeat());
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, bool, keypad, input.isKeypad());
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, bool, systemKey, input.isSystemKey());
+    ENCODE_TYPE_WITH_KEY(encodedValue, double, timestamp, input.timestamp());
+    ENCODE_TYPE_WITH_KEY(encodedValue, PlatformEvent::Type, type, input.type());
+    ENCODE_TYPE_WITH_KEY(encodedValue, PlatformEvent::Modifiers, modifiers, static_cast<PlatformEvent::Modifiers>(input.modifiers()));
+    ENCODE_TYPE_WITH_KEY(encodedValue, String, text, input.text());
+    ENCODE_TYPE_WITH_KEY(encodedValue, String, unmodifiedText, input.unmodifiedText());
+    ENCODE_TYPE_WITH_KEY(encodedValue, String, keyIdentifier, input.keyIdentifier());
+    ENCODE_TYPE_WITH_KEY(encodedValue, int, windowsVirtualKeyCode, input.windowsVirtualKeyCode());
+    ENCODE_TYPE_WITH_KEY(encodedValue, int, nativeVirtualKeyCode, input.nativeVirtualKeyCode());
+    ENCODE_TYPE_WITH_KEY(encodedValue, int, macCharCode, input.macCharCode());
+    ENCODE_TYPE_WITH_KEY(encodedValue, bool, autoRepeat, input.isAutoRepeat());
+    ENCODE_TYPE_WITH_KEY(encodedValue, bool, keypad, input.isKeypad());
+    ENCODE_TYPE_WITH_KEY(encodedValue, bool, systemKey, input.isSystemKey());
 #if USE(APPKIT)
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, bool, handledByInputMethod, input.handledByInputMethod());
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, Vector<KeypressCommand>, commands, input.commands());
+    ENCODE_TYPE_WITH_KEY(encodedValue, bool, handledByInputMethod, input.handledByInputMethod());
+    ENCODE_TYPE_WITH_KEY(encodedValue, Vector<KeypressCommand>, commands, input.commands());
 #endif
     return encodedValue;
 }
@@ -215,18 +240,18 @@
 {
     EncodedValue encodedValue = EncodedValue::createObject();
 
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, int, positionX, input.position().x());
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, int, positionY, input.position().y());
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, int, globalPositionX, input.globalPosition().x());
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, int, globalPositionY, input.globalPosition().y());
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, MouseButton, button, input.button());
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, PlatformEvent::Type, type, input.type());
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, int, clickCount, input.clickCount());
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, bool, shiftKey, input.shiftKey());
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, bool, ctrlKey, input.ctrlKey());
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, bool, altKey, input.altKey());
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, bool, metaKey, input.metaKey());
-    ENCODE_SCALAR_TYPE_WITH_KEY(encodedValue, int, timestamp, input.timestamp());
+    ENCODE_TYPE_WITH_KEY(encodedValue, int, positionX, input.position().x());
+    ENCODE_TYPE_WITH_KEY(encodedValue, int, positionY, input.position().y());
+    ENCODE_TYPE_WITH_KEY(encodedValue, int, globalPositionX, input.globalPosition().x());
+    ENCODE_TYPE_WITH_KEY(encodedValue, int, globalPositionY, input.globalPosition().y());
+    ENCODE_TYPE_WITH_KEY(encodedValue, MouseButton, button, input.button());
+    ENCODE_TYPE_WITH_KEY(encodedValue, PlatformEvent::Type, type, input.type());
+    ENCODE_TYPE_WITH_KEY(encodedValue, int, clickCount, input.clickCount());
+    ENCODE_TYPE_WITH_KEY(encodedValue, bool, shiftKey, input.shiftKey());
+    ENCODE_TYPE_WITH_KEY(encodedValue, bool, ctrlKey, input.ctrlKey());
+    ENCODE_TYPE_WITH_KEY(encodedValue, bool, altKey, input.altKey());
+    ENCODE_TYPE_WITH_KEY(encodedValue, bool, metaKey, input.metaKey());
+    ENCODE_TYPE_WITH_KEY(encodedValue, int, timestamp, input.timestamp());
 
     return encodedValue;
 }
@@ -253,6 +278,102 @@
     return true;
 }
 
+#if PLATFORM(COCOA)
+struct PlatformWheelEventCocoaArguments {
+    bool directionInvertedFromDevice;
+    bool hasPreciseScrollingDeltas;
+    PlatformWheelEventPhase phase;
+    PlatformWheelEventPhase momentumPhase;
+    int scrollCount;
+    float unacceleratedScrollingDeltaX;
+    float unacceleratedScrollingDeltaY;
+};
+
+class PlatformWheelEventCocoa : public PlatformWheelEvent {
+public:
+    PlatformWheelEventCocoa(PlatformWheelEvent& event, PlatformWheelEventCocoaArguments& arguments)
+        : PlatformWheelEvent(event)
+    {
+        m_directionInvertedFromDevice = arguments.directionInvertedFromDevice;
+        m_hasPreciseScrollingDeltas = arguments.hasPreciseScrollingDeltas;
+        m_phase = arguments.phase;
+        m_momentumPhase = arguments.momentumPhase;
+        m_scrollCount = arguments.scrollCount;
+        m_unacceleratedScrollingDeltaX = arguments.unacceleratedScrollingDeltaX;
+        m_unacceleratedScrollingDeltaY = arguments.unacceleratedScrollingDeltaY;
+    }
+};
+#endif // PLATFORM(COCOA)
+
+EncodedValue EncodingTraits<PlatformWheelEvent>::encodeValue(const PlatformWheelEvent& input)
+{
+    EncodedValue encodedData = EncodedValue::createObject();
+
+    ENCODE_TYPE_WITH_KEY(encodedData, int, positionX, input.position().x());
+    ENCODE_TYPE_WITH_KEY(encodedData, int, positionY, input.position().y());
+    ENCODE_TYPE_WITH_KEY(encodedData, int, globalPositionX, input.globalPosition().x());
+    ENCODE_TYPE_WITH_KEY(encodedData, int, globalPositionY, input.globalPosition().y());
+    ENCODE_TYPE_WITH_KEY(encodedData, bool, shiftKey, input.shiftKey());
+    ENCODE_TYPE_WITH_KEY(encodedData, bool, ctrlKey, input.ctrlKey());
+    ENCODE_TYPE_WITH_KEY(encodedData, bool, altKey, input.altKey());
+    ENCODE_TYPE_WITH_KEY(encodedData, bool, metaKey, input.metaKey());
+    ENCODE_TYPE_WITH_KEY(encodedData, float, deltaX, input.deltaX());
+    ENCODE_TYPE_WITH_KEY(encodedData, float, deltaY, input.deltaY());
+    ENCODE_TYPE_WITH_KEY(encodedData, float, wheelTicksX, input.wheelTicksX());
+    ENCODE_TYPE_WITH_KEY(encodedData, float, wheelTicksY, input.wheelTicksY());
+    ENCODE_TYPE_WITH_KEY(encodedData, PlatformWheelEventGranularity, granularity, static_cast<PlatformWheelEventGranularity>(input.granularity()));
+
+#if PLATFORM(COCOA)
+    ENCODE_TYPE_WITH_KEY(encodedData, bool, directionInvertedFromDevice, input.directionInvertedFromDevice());
+    ENCODE_TYPE_WITH_KEY(encodedData, bool, hasPreciseScrollingDeltas, input.hasPreciseScrollingDeltas());
+    ENCODE_TYPE_WITH_KEY(encodedData, PlatformWheelEventPhase, phase, static_cast<PlatformWheelEventPhase>(input.phase()));
+    ENCODE_TYPE_WITH_KEY(encodedData, PlatformWheelEventPhase, momentumPhase, static_cast<PlatformWheelEventPhase>(input.momentumPhase()));
+    ENCODE_TYPE_WITH_KEY(encodedData, int, scrollCount, input.scrollCount());
+    ENCODE_TYPE_WITH_KEY(encodedData, float, unacceleratedScrollingDeltaX, input.unacceleratedScrollingDeltaX());
+    ENCODE_TYPE_WITH_KEY(encodedData, float, unacceleratedScrollingDeltaY, input.unacceleratedScrollingDeltaY());
+#endif
+
+    return encodedData;
+}
+
+bool EncodingTraits<PlatformWheelEvent>::decodeValue(EncodedValue& encodedData, std::unique_ptr<PlatformWheelEvent>& input)
+{
+    DECODE_SCALAR_TYPE_WITH_KEY(encodedData, int, positionX);
+    DECODE_SCALAR_TYPE_WITH_KEY(encodedData, int, positionY);
+    DECODE_SCALAR_TYPE_WITH_KEY(encodedData, int, globalPositionX);
+    DECODE_SCALAR_TYPE_WITH_KEY(encodedData, int, globalPositionY);
+    DECODE_SCALAR_TYPE_WITH_KEY(encodedData, bool, shiftKey);
+    DECODE_SCALAR_TYPE_WITH_KEY(encodedData, bool, ctrlKey);
+    DECODE_SCALAR_TYPE_WITH_KEY(encodedData, bool, altKey);
+    DECODE_SCALAR_TYPE_WITH_KEY(encodedData, bool, metaKey);
+    DECODE_SCALAR_TYPE_WITH_KEY(encodedData, float, deltaX);
+    DECODE_SCALAR_TYPE_WITH_KEY(encodedData, float, deltaY);
+    DECODE_SCALAR_TYPE_WITH_KEY(encodedData, float, wheelTicksX);
+    DECODE_SCALAR_TYPE_WITH_KEY(encodedData, float, wheelTicksY);
+    DECODE_SCALAR_TYPE_WITH_KEY(encodedData, PlatformWheelEventGranularity, granularity);
+
+#if PLATFORM(COCOA)
+    PlatformWheelEventCocoaArguments arguments;
+    DECODE_TYPE_WITH_KEY_TO_LVALUE(encodedData, bool, directionInvertedFromDevice, arguments.directionInvertedFromDevice);
+    DECODE_TYPE_WITH_KEY_TO_LVALUE(encodedData, bool, hasPreciseScrollingDeltas, arguments.hasPreciseScrollingDeltas);
+    DECODE_TYPE_WITH_KEY_TO_LVALUE(encodedData, PlatformWheelEventPhase, phase, arguments.phase);
+    DECODE_TYPE_WITH_KEY_TO_LVALUE(encodedData, PlatformWheelEventPhase, momentumPhase, arguments.momentumPhase);
+    DECODE_TYPE_WITH_KEY_TO_LVALUE(encodedData, int, scrollCount, arguments.scrollCount);
+    DECODE_TYPE_WITH_KEY_TO_LVALUE(encodedData, float, unacceleratedScrollingDeltaX, arguments.unacceleratedScrollingDeltaX);
+    DECODE_TYPE_WITH_KEY_TO_LVALUE(encodedData, float, unacceleratedScrollingDeltaY, arguments.unacceleratedScrollingDeltaY);
+#endif
+
+    PlatformWheelEvent event(IntPoint(positionX, positionY), IntPoint(globalPositionX, globalPositionY),
+        deltaX, deltaY, wheelTicksX, wheelTicksY, granularity, shiftKey, ctrlKey, altKey, metaKey);
+
+#if PLATFORM(COCOA)
+    input = std::make_unique<PlatformWheelEventCocoa>(event, arguments);
+#else
+    input = std::make_unique<PlatformWheelEvent>(event);
+#endif
+    return true;
+}
+
 EncodedValue EncodingTraits<SecurityOrigin>::encodeValue(RefPtr<SecurityOrigin> input)
 {
     return EncodedValue::createString(input->toString());
diff --git a/Source/WebCore/replay/SerializationMethods.h b/Source/WebCore/replay/SerializationMethods.h
index 7afa232..37895e5 100644
--- a/Source/WebCore/replay/SerializationMethods.h
+++ b/Source/WebCore/replay/SerializationMethods.h
@@ -40,6 +40,7 @@
 class Page;
 class PlatformKeyboardEvent;
 class PlatformMouseEvent;
+class PlatformWheelEvent;
 class SecurityOrigin;
 class URL;
 
@@ -81,6 +82,13 @@
     static bool decodeValue(EncodedValue&, std::unique_ptr<WebCore::PlatformMouseEvent>& value);
 };
 
+template<> struct EncodingTraits<WebCore::PlatformWheelEvent> {
+    typedef WebCore::PlatformWheelEvent DecodedType;
+
+    static EncodedValue encodeValue(const WebCore::PlatformWheelEvent& value);
+    static bool decodeValue(EncodedValue&, std::unique_ptr<WebCore::PlatformWheelEvent>& value);
+};
+
 template<> struct EncodingTraits<WebCore::URL> {
     typedef WebCore::URL DecodedType;
 
diff --git a/Source/WebCore/replay/UserInputBridge.cpp b/Source/WebCore/replay/UserInputBridge.cpp
index 383c08c..fc8457a 100644
--- a/Source/WebCore/replay/UserInputBridge.cpp
+++ b/Source/WebCore/replay/UserInputBridge.cpp
@@ -160,8 +160,19 @@
     return m_page.focusController().focusedOrMainFrame().eventHandler().handleAccessKey(keyEvent);
 }
 
-bool UserInputBridge::handleWheelEvent(const PlatformWheelEvent& wheelEvent, InputSource)
+bool UserInputBridge::handleWheelEvent(const PlatformWheelEvent& wheelEvent, InputSource inputSource)
 {
+#if ENABLE(WEB_REPLAY)
+    EARLY_RETURN_IF_SHOULD_IGNORE_INPUT;
+
+    if (activeCursor().isCapturing()) {
+        std::unique_ptr<PlatformWheelEvent> ownedEvent = std::make_unique<PlatformWheelEvent>(wheelEvent);
+        activeCursor().appendInput<HandleWheelEvent>(std::move(ownedEvent));
+    }
+#else
+    UNUSED_PARAM(inputSource);
+#endif
+
     return m_page.mainFrame().eventHandler().handleWheelEvent(wheelEvent);
 }
 
@@ -175,13 +186,31 @@
     m_page.focusController().setFocused(focused);
 }
 
-bool UserInputBridge::scrollRecursively(ScrollDirection direction, ScrollGranularity granularity, InputSource)
+bool UserInputBridge::scrollRecursively(ScrollDirection direction, ScrollGranularity granularity, InputSource inputSource)
 {
+#if ENABLE(WEB_REPLAY)
+    EARLY_RETURN_IF_SHOULD_IGNORE_INPUT;
+
+    if (activeCursor().isCapturing())
+        activeCursor().appendInput<ScrollPage>(direction, granularity);
+#else
+    UNUSED_PARAM(inputSource);
+#endif
+
     return m_page.focusController().focusedOrMainFrame().eventHandler().scrollRecursively(direction, granularity, nullptr);
 }
 
-bool UserInputBridge::logicalScrollRecursively(ScrollLogicalDirection direction, ScrollGranularity granularity, InputSource)
+bool UserInputBridge::logicalScrollRecursively(ScrollLogicalDirection direction, ScrollGranularity granularity, InputSource inputSource)
 {
+#if ENABLE(WEB_REPLAY)
+    EARLY_RETURN_IF_SHOULD_IGNORE_INPUT;
+
+    if (activeCursor().isCapturing())
+        activeCursor().appendInput<LogicalScrollPage>(direction, granularity);
+#else
+    UNUSED_PARAM(inputSource);
+#endif
+
     return m_page.focusController().focusedOrMainFrame().eventHandler().logicalScrollRecursively(direction, granularity, nullptr);
 }
 
diff --git a/Source/WebCore/replay/WebInputs.json b/Source/WebCore/replay/WebInputs.json
index e24fd9b..3e6aefb 100644
--- a/Source/WebCore/replay/WebInputs.json
+++ b/Source/WebCore/replay/WebInputs.json
@@ -63,6 +63,60 @@
                 "header": "platform/PlatformMouseEvent.h"
             },
             {
+                "name": "PlatformWheelEvent", "mode": "OWNED",
+                "header": "platform/PlatformWheelEvent.h"
+            },
+            {
+                "name": "PlatformWheelEventGranularity", "mode": "SCALAR", "storage": "uint64_t",
+                "flags": ["ENUM"],
+                "values": ["ScrollByPageWheelEvent", "ScrollByPixelWheelEvent"],
+                "header": "platform/PlatformWheelEvent.h"
+            },
+            {
+                "name": "PlatformWheelEventPhase", "mode": "SCALAR", "storage": "uint64_t",
+                "flags": ["ENUM"],
+                "guard": "PLATFORM(COCOA)",
+                "values": [
+                    "PlatformWheelEventPhaseNone",
+                    "PlatformWheelEventPhaseBegan",
+                    "PlatformWheelEventPhaseStationary",
+                    "PlatformWheelEventPhaseChanged",
+                    "PlatformWheelEventPhaseEnded",
+                    "PlatformWheelEventPhaseCancelled",
+                    "PlatformWheelEventPhaseMayBegin"
+                ],
+                "header": "platform/PlatformWheelEvent.h"
+            },
+            {
+                "name": "ScrollDirection", "mode": "SCALAR", "storage": "uint64_t",
+                "flags": ["ENUM"],
+                "values": ["ScrollUp", "ScrollDown", "ScrollLeft", "ScrollRight"],
+                "header": "platform/ScrollTypes.h"
+            },
+            {
+                "name": "ScrollGranularity", "mode": "SCALAR", "storage": "uint64_t",
+                "flags": ["ENUM"],
+                "values": [
+                    "ScrollByLine",
+                    "ScrollByPage",
+                    "ScrollByDocument",
+                    "ScrollByPixel",
+                    "ScrollByPrecisePixel"
+                ],
+                "header": "platform/ScrollTypes.h"
+            },
+            {
+                "name": "ScrollLogicalDirection", "mode": "SCALAR", "storage": "uint64_t",
+                "flags": ["ENUM"],
+                "values": [
+                    "ScrollBlockDirectionBackward",
+                    "ScrollBlockDirectionForward",
+                    "ScrollInlineDirectionBackward",
+                    "ScrollInlineDirectionForward"
+                ],
+                "header": "platform/ScrollTypes.h"
+            },
+            {
                 "name": "SecurityOrigin", "mode": "SHARED",
                 "header": "page/SecurityOrigin.h"
             },
@@ -146,6 +200,14 @@
             ]
         },
         {
+            "name": "HandleWheelEvent",
+            "description": "The embedder signalled a mouse wheel event.",
+            "queue": "EVENT_LOOP",
+            "members": [
+                { "name": "platformEvent", "type": "PlatformWheelEvent" }
+            ]
+        },
+        {
             "name": "InitialNavigation",
             "description": "Initiate the initial main frame navigation.",
             "queue": "EVENT_LOOP",
@@ -155,6 +217,24 @@
                 { "name": "url", "type": "URL" },
                 { "name": "referrer", "type": "String" }
             ]
+        },
+        {
+            "name": "LogicalScrollPage",
+            "description": "The embedder signalled a logical scroll event.",
+            "queue": "EVENT_LOOP",
+            "members": [
+                { "name": "direction", "type": "ScrollLogicalDirection" },
+                { "name": "granularity", "type": "ScrollGranularity" }
+            ]
+        },
+        {
+            "name": "ScrollPage",
+            "description": "The embedder signalled a scroll event.",
+            "queue": "EVENT_LOOP",
+            "members": [
+                { "name": "direction", "type": "ScrollDirection" },
+                { "name": "granularity", "type": "ScrollGranularity" }
+            ]
         }
     ]
 }