[Mac] Add support for testing swipes
https://bugs.webkit.org/show_bug.cgi?id=148700

Reviewed by Beth Dakin.

* WebKitTestRunner/EventSenderProxy.h:
* WebKitTestRunner/InjectedBundle/Bindings/EventSendingController.idl:
* WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl:
* WebKitTestRunner/InjectedBundle/EventSendingController.cpp:
(WTR::cgEventPhaseFromString):
(WTR::cgEventMomentumPhaseFromString):
(WTR::EventSendingController::mouseScrollByWithWheelAndMomentumPhases):
(WTR::EventSendingController::swipeGestureWithWheelAndMomentumPhases):
* WebKitTestRunner/InjectedBundle/EventSendingController.h:
* WebKitTestRunner/InjectedBundle/InjectedBundle.cpp:
(WTR::InjectedBundle::didReceiveMessageToPage):
* WebKitTestRunner/InjectedBundle/TestRunner.cpp:
(WTR::TestRunner::installDidBeginSwipeCallback):
(WTR::TestRunner::installWillEndSwipeCallback):
(WTR::TestRunner::installDidEndSwipeCallback):
(WTR::TestRunner::installDidRemoveSwipeSnapshotCallback):
(WTR::TestRunner::callDidBeginSwipeCallback):
(WTR::TestRunner::callWillEndSwipeCallback):
(WTR::TestRunner::callDidEndSwipeCallback):
(WTR::TestRunner::callDidRemoveSwipeSnapshotCallback):
* WebKitTestRunner/InjectedBundle/TestRunner.h:
* WebKitTestRunner/TestController.cpp:
(WTR::TestController::createOtherPage):
(WTR::TestController::createWebViewWithOptions):
(WTR::TestController::didReceiveMessageFromInjectedBundle):
(WTR::TestController::didBeginNavigationGesture):
(WTR::TestController::willEndNavigationGesture):
(WTR::TestController::didEndNavigationGesture):
(WTR::TestController::didRemoveNavigationGestureSnapshot):
* WebKitTestRunner/TestController.h:
* WebKitTestRunner/TestInvocation.cpp:
(WTR::TestInvocation::didBeginSwipe):
(WTR::TestInvocation::willEndSwipe):
(WTR::TestInvocation::didEndSwipe):
(WTR::TestInvocation::didRemoveSwipeSnapshot):
* WebKitTestRunner/TestInvocation.h:
Add callbacks when navigation gestures didBegin/willEnd/didEnd, and
when the snapshot is removed.

Add swipeGestureWithWheelAndMomentumPhases, just like the equivalent
mouseScrollBy function.

* WebKitTestRunner/mac/EventSenderProxy.mm:
(-[EventSenderSyntheticEvent initPressureEventAtLocation:globalLocation:stage:pressure:phase:time:eventNumber:]):
(-[EventSenderSyntheticEvent type]):
(-[EventSenderSyntheticEvent subtype]):
(-[EventSenderSyntheticEvent locationInWindow]):
(-[EventSenderSyntheticEvent location]):
(-[EventSenderSyntheticEvent momentumPhase]):
(-[EventSenderSyntheticEvent _isTouchesEnded]):
(-[EventSenderSyntheticEvent _cgsEventRecord]):
Rename EventSenderPressureEvent to EventSenderSyntheticEvent and add some
more adjustable values.

(WTR::EventSenderProxy::mouseForceDown):
(WTR::EventSenderProxy::mouseForceUp):
(WTR::EventSenderProxy::mouseForceChanged):
Adopt EventSenderSyntheticEvent.

(WTR::nsEventPhaseFromCGEventPhase):
(WTR::EventSenderProxy::swipeGestureWithWheelAndMomentumPhases):
Make use of EventSenderSyntheticEvent to synthesize swipe gesture events.

* WebKitTestRunner/mac/PlatformWebViewMac.mm:
(WTR::PlatformWebView::PlatformWebView):
Enable swipe.

* swipe/basic-cached-back-swipe-expected.txt: Added.
* swipe/basic-cached-back-swipe.html: Added.
* swipe/resources/swipe-test.js: Added.
(eventQueue.enqueueScrollEvent):
(eventQueue.enqueueSwipeEvent):
(eventQueue.hasPendingEvents):
(eventQueue._processEventQueue):
(eventQueue._processEventQueueSoon):
(shouldBe):
(log):
(dumpLog):
(initializeLog):
(startMeasuringDuration):
(measuredDurationShouldBeLessThan):
Add a test for the simplest case, a back swipe after a normal navigation
with the page cache enabled.

* TestExpectations:
* platform/mac-wk2/TestExpectations:
Disable these tests everywhere except Mac WebKit2.

* UIProcess/API/mac/WKView.mm:
(takeWindowSnapshot):
(-[WKView _takeViewSnapshot]):
Fall back to the non-hardware snapshotting path if the hardware path fails,
which usually happens if the view is fully off-screen (as in the case
of WebKitTestRunner).


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@189287 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index 3f2e1c2..0c62a9e 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,3 +1,31 @@
+2015-09-03  Tim Horton  <timothy_horton@apple.com>
+
+        [Mac] Add support for testing swipes
+        https://bugs.webkit.org/show_bug.cgi?id=148700
+
+        Reviewed by Beth Dakin.
+
+        * swipe/basic-cached-back-swipe-expected.txt: Added.
+        * swipe/basic-cached-back-swipe.html: Added.
+        * swipe/resources/swipe-test.js: Added.
+        (eventQueue.enqueueScrollEvent):
+        (eventQueue.enqueueSwipeEvent):
+        (eventQueue.hasPendingEvents):
+        (eventQueue._processEventQueue):
+        (eventQueue._processEventQueueSoon):
+        (shouldBe):
+        (log):
+        (dumpLog):
+        (initializeLog):
+        (startMeasuringDuration):
+        (measuredDurationShouldBeLessThan):
+        Add a test for the simplest case, a back swipe after a normal navigation
+        with the page cache enabled.
+
+        * TestExpectations:
+        * platform/mac-wk2/TestExpectations:
+        Disable these tests everywhere except Mac WebKit2.
+
 2015-09-03  Alexey Proskuryakov  <ap@apple.com>
 
         Test Russian ".рф" domain support
diff --git a/LayoutTests/TestExpectations b/LayoutTests/TestExpectations
index 47d04e8..63f5786 100644
--- a/LayoutTests/TestExpectations
+++ b/LayoutTests/TestExpectations
@@ -13,6 +13,7 @@
 editing/mac [ Skip ]
 editing/pasteboard/gtk [ Skip ]
 tiled-drawing [ Skip ]
+swipe [ Skip ]
 
 fast/forms/attributed-strings.html [ Skip ]
 fast/scrolling/latching [ Skip ]
diff --git a/LayoutTests/platform/mac-mavericks/TestExpectations b/LayoutTests/platform/mac-mavericks/TestExpectations
index f0e3ce1..3d89fb1 100644
--- a/LayoutTests/platform/mac-mavericks/TestExpectations
+++ b/LayoutTests/platform/mac-mavericks/TestExpectations
@@ -22,3 +22,6 @@
 
 # Colorspaces on CA OpenGL layers not available in Mavericks and Yosemite
 fast/canvas/webgl/match-page-color-space.html [ Skip ]
+
+# Swipe tests don't work on Mavericks (needs investigation)
+swipe [ Skip ]
diff --git a/LayoutTests/platform/mac-wk2/TestExpectations b/LayoutTests/platform/mac-wk2/TestExpectations
index 011a920..02019d4 100644
--- a/LayoutTests/platform/mac-wk2/TestExpectations
+++ b/LayoutTests/platform/mac-wk2/TestExpectations
@@ -6,6 +6,7 @@
 #//////////////////////////////////////////////////////////////////////////////////////////
 
 tiled-drawing [ Pass ]
+swipe [ Pass ]
 
 #//////////////////////////////////////////////////////////////////////////////////////////
 # End platform-specific directories.
diff --git a/LayoutTests/swipe/basic-cached-back-swipe-expected.txt b/LayoutTests/swipe/basic-cached-back-swipe-expected.txt
new file mode 100644
index 0000000..e3d3e00
--- /dev/null
+++ b/LayoutTests/swipe/basic-cached-back-swipe-expected.txt
@@ -0,0 +1,11 @@
+swipe event (delta 0 0, phase 'maybegin')
+scroll event (delta 1 0, phase 'began')
+scroll event (delta 1 0, phase 'changed')
+didBeginSwipe
+swipe event (delta 1 0, phase 'changed')
+swipe event (delta 256 0, phase 'changed')
+swipe event (delta 0 0, phase 'ended')
+willEndSwipe
+didEndSwipe
+didRemoveSwipeSnapshot
+
diff --git a/LayoutTests/swipe/basic-cached-back-swipe.html b/LayoutTests/swipe/basic-cached-back-swipe.html
new file mode 100644
index 0000000..aecbab0
--- /dev/null
+++ b/LayoutTests/swipe/basic-cached-back-swipe.html
@@ -0,0 +1,98 @@
+<head>
+<style>
+html {
+    font-size: 32pt;
+}
+</style>
+<script src="resources/swipe-test.js"></script>
+<script>
+function startSwipeGesture()
+{
+    eventSender.mouseMoveTo(100, 100);
+
+    eventQueue.enqueueSwipeEvent(0, 0, 'maybegin');
+    eventQueue.enqueueScrollEvent(1, 0, 'began');
+    eventQueue.enqueueScrollEvent(1, 0, 'changed');
+}
+
+function completeSwipeGesture()
+{
+    eventQueue.enqueueSwipeEvent(1, 0, 'changed');
+    eventQueue.enqueueSwipeEvent(256, 0, 'changed');
+    eventQueue.enqueueSwipeEvent(0, 0, 'ended');
+}
+
+function didBeginSwipeCallback()
+{
+    log("didBeginSwipe");
+
+    shouldBe(false, eventQueue.hasPendingEvents(), "Event queue should be empty. Both scroll events should be required to start the swipe because of the swipe-start hysteresis.");
+
+    completeSwipeGesture();
+}
+
+function willEndSwipeCallback()
+{
+    log("willEndSwipe");
+
+    shouldBe(false, isFirstPage(), "The swipe should not yet have navigated away from the second page.");
+}
+
+function didEndSwipeCallback()
+{
+    log("didEndSwipe");
+
+    shouldBe(0, eventQueue.hasPendingEvents(), "Event queue should be empty. The swipe isn't complete until we see the end of the gesture.");
+    startMeasuringDuration("snapshotRemoval");
+}
+
+function didRemoveSwipeSnapshotCallback()
+{
+    log("didRemoveSwipeSnapshot");
+    
+    shouldBe(true, isFirstPage(), "The swipe should have navigated back to the first page.");
+    measuredDurationShouldBeLessThan("snapshotRemoval", 1000, "Because we're using the page cache, it shouldn't be long between the gesture completing and the snapshot being removed.")
+
+    dumpLog();
+    testRunner.notifyDone();
+}
+
+function isFirstPage()
+{
+    return window.location.href.indexOf("second") == -1;
+}
+
+window.onload = function () {
+    if (!window.eventSender || !window.testRunner) {
+        document.body.innerHTML = "This test must be run in WebKitTestRunner.";
+        return;
+    }
+
+    document.body.innerHTML = isFirstPage() ? "first" : "second";
+
+    if (isFirstPage()) {
+        initializeLog();
+
+        testRunner.setNavigationGesturesEnabled(true);
+
+        testRunner.installDidBeginSwipeCallback(didBeginSwipeCallback);
+        testRunner.installWillEndSwipeCallback(willEndSwipeCallback);
+        testRunner.installDidEndSwipeCallback(didEndSwipeCallback);
+        testRunner.installDidRemoveSwipeSnapshotCallback(didRemoveSwipeSnapshotCallback);
+
+        testRunner.overridePreference("WebKitUsesPageCachePreferenceKey", 1);
+        testRunner.dumpAsText();
+        testRunner.waitUntilDone();
+
+        setTimeout(function () { 
+            window.location.href = window.location.href + "?second";
+        }, 0);
+        return;
+    }
+
+    startSwipeGesture();
+};
+</script>
+</head>
+<body>
+</body>
\ No newline at end of file
diff --git a/LayoutTests/swipe/resources/swipe-test.js b/LayoutTests/swipe/resources/swipe-test.js
new file mode 100644
index 0000000..e03a121
--- /dev/null
+++ b/LayoutTests/swipe/resources/swipe-test.js
@@ -0,0 +1,70 @@
+var eventQueue = {
+    enqueueScrollEvent: function (x, y, phase) {
+        this._queue.push(function () {
+            log("scroll event (delta " + x + " " + y + ", phase '" + phase + "')");
+            window.eventSender.mouseScrollByWithWheelAndMomentumPhases(x, y, phase, 'none');
+        });
+        this._processEventQueueSoon();
+    },
+
+    enqueueSwipeEvent: function (x, y, phase) {
+        this._queue.push(function () {
+            log("swipe event (delta " + x + " " + y + ", phase '" + phase + "')");
+            window.eventSender.swipeGestureWithWheelAndMomentumPhases(x, y, phase, 'none');
+        });
+        this._processEventQueueSoon();
+    },
+
+    hasPendingEvents: function () {
+        return this._queue.length != 0;
+    },
+
+    _queue: [],
+
+    _processEventQueue: function () {
+        if (!this._queue.length)
+            return;
+
+        var item = this._queue.shift();
+        item();
+        this._processEventQueueSoon();
+    },
+
+    _processEventQueueSoon: function () {
+        clearTimeout(this._processingTimeout);
+        this._processingTimeout = setTimeout(this._processEventQueue.bind(this), 0);
+    }
+}
+
+function shouldBe(expected, actual, message)
+{
+    if (expected != actual)
+        log("Failure. " + message + " (expected: " + expected + ", actual: " + actual + ")");
+}
+
+function log(s)
+{
+    window.localStorage["swipeLogging"] += s + "<br/>";
+}
+
+function dumpLog()
+{
+    window.document.body.innerHTML = window.localStorage["swipeLogging"];
+}
+
+function initializeLog()
+{
+    window.localStorage["swipeLogging"] = "";
+}
+
+function startMeasuringDuration(key)
+{
+    window.localStorage[key + "swipeStartTime"] = Date.now();
+}
+
+function measuredDurationShouldBeLessThan(key, timeInMS, message)
+{
+    var duration = Date.now() - window.localStorage[key + "swipeStartTime"];
+    if (duration >= timeInMS)
+        log("Failure. " + message + " (expected: " + timeInMS + ", actual: " + duration + ")");
+}
\ No newline at end of file
diff --git a/Source/WebKit2/ChangeLog b/Source/WebKit2/ChangeLog
index af2b7bb..d54edaa 100644
--- a/Source/WebKit2/ChangeLog
+++ b/Source/WebKit2/ChangeLog
@@ -1,3 +1,17 @@
+2015-09-03  Tim Horton  <timothy_horton@apple.com>
+
+        [Mac] Add support for testing swipes
+        https://bugs.webkit.org/show_bug.cgi?id=148700
+
+        Reviewed by Beth Dakin.
+
+        * UIProcess/API/mac/WKView.mm:
+        (takeWindowSnapshot):
+        (-[WKView _takeViewSnapshot]):
+        Fall back to the non-hardware snapshotting path if the hardware path fails,
+        which usually happens if the view is fully off-screen (as in the case
+        of WebKitTestRunner).
+
 2015-09-02  Tim Horton  <timothy_horton@apple.com>
 
         Add a modern API way to know that the navigation gesture snapshot was removed, for WebKitTestRunner
diff --git a/Source/WebKit2/UIProcess/API/mac/WKView.mm b/Source/WebKit2/UIProcess/API/mac/WKView.mm
index 68ec374..fec52b3 100644
--- a/Source/WebKit2/UIProcess/API/mac/WKView.mm
+++ b/Source/WebKit2/UIProcess/API/mac/WKView.mm
@@ -3343,6 +3343,24 @@
     return _data->_rootLayer.get();
 }
 
+static RetainPtr<CGImageRef> takeWindowSnapshot(CGSWindowID windowID, bool captureAtNominalResolution)
+{
+    CGSWindowCaptureOptions options = kCGSCaptureIgnoreGlobalClipShape;
+    if (captureAtNominalResolution)
+        options |= kCGSWindowCaptureNominalResolution;
+    RetainPtr<CFArrayRef> windowSnapshotImages = adoptCF(CGSHWCaptureWindowList(CGSMainConnectionID(), &windowID, 1, options));
+
+    if (windowSnapshotImages && CFArrayGetCount(windowSnapshotImages.get()))
+        return (CGImageRef)CFArrayGetValueAtIndex(windowSnapshotImages.get(), 0);
+
+    // Fall back to the non-hardware capture path if we didn't get a snapshot
+    // (which usually happens if the window is fully off-screen).
+    CGWindowImageOption imageOptions = kCGWindowImageBoundsIgnoreFraming | kCGWindowImageShouldBeOpaque;
+    if (captureAtNominalResolution)
+        imageOptions |= kCGWindowImageNominalResolution;
+    return adoptCF(CGWindowListCreateImage(CGRectNull, kCGWindowListOptionIncludingWindow, windowID, imageOptions));
+}
+
 - (PassRefPtr<ViewSnapshot>)_takeViewSnapshot
 {
     NSWindow *window = self.window;
@@ -3353,20 +3371,18 @@
 
     CGSWindowCaptureOptions options = kCGSCaptureIgnoreGlobalClipShape;
     RetainPtr<CFArrayRef> windowSnapshotImages = adoptCF(CGSHWCaptureWindowList(CGSMainConnectionID(), &windowID, 1, options));
-    if (!windowSnapshotImages || !CFArrayGetCount(windowSnapshotImages.get()))
-        return nullptr;
 
-    RetainPtr<CGImageRef> windowSnapshotImage = (CGImageRef)CFArrayGetValueAtIndex(windowSnapshotImages.get(), 0);
+    RetainPtr<CGImageRef> windowSnapshotImage = takeWindowSnapshot(windowID, false);
+    if (!windowSnapshotImage)
+        return nullptr;
 
     // Work around <rdar://problem/17084993>; re-request the snapshot at kCGWindowImageNominalResolution if it was captured at the wrong scale.
     CGFloat desiredSnapshotWidth = window.frame.size.width * window.screen.backingScaleFactor;
-    if (CGImageGetWidth(windowSnapshotImage.get()) != desiredSnapshotWidth) {
-        options |= kCGSWindowCaptureNominalResolution;
-        windowSnapshotImages = adoptCF(CGSHWCaptureWindowList(CGSMainConnectionID(), &windowID, 1, options));
-        if (!windowSnapshotImages || !CFArrayGetCount(windowSnapshotImages.get()))
-            return nullptr;
-        windowSnapshotImage = (CGImageRef)CFArrayGetValueAtIndex(windowSnapshotImages.get(), 0);
-    }
+    if (CGImageGetWidth(windowSnapshotImage.get()) != desiredSnapshotWidth)
+        windowSnapshotImage = takeWindowSnapshot(windowID, true);
+
+    if (!windowSnapshotImage)
+        return nullptr;
 
     [self _ensureGestureController];
 
diff --git a/Tools/ChangeLog b/Tools/ChangeLog
index c44c603..9bd1798 100644
--- a/Tools/ChangeLog
+++ b/Tools/ChangeLog
@@ -1,3 +1,77 @@
+2015-09-03  Tim Horton  <timothy_horton@apple.com>
+
+        [Mac] Add support for testing swipes
+        https://bugs.webkit.org/show_bug.cgi?id=148700
+
+        Reviewed by Beth Dakin.
+
+        * WebKitTestRunner/EventSenderProxy.h:
+        * WebKitTestRunner/InjectedBundle/Bindings/EventSendingController.idl:
+        * WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl:
+        * WebKitTestRunner/InjectedBundle/EventSendingController.cpp:
+        (WTR::cgEventPhaseFromString):
+        (WTR::cgEventMomentumPhaseFromString):
+        (WTR::EventSendingController::mouseScrollByWithWheelAndMomentumPhases):
+        (WTR::EventSendingController::swipeGestureWithWheelAndMomentumPhases):
+        * WebKitTestRunner/InjectedBundle/EventSendingController.h:
+        * WebKitTestRunner/InjectedBundle/InjectedBundle.cpp:
+        (WTR::InjectedBundle::didReceiveMessageToPage):
+        * WebKitTestRunner/InjectedBundle/TestRunner.cpp:
+        (WTR::TestRunner::installDidBeginSwipeCallback):
+        (WTR::TestRunner::installWillEndSwipeCallback):
+        (WTR::TestRunner::installDidEndSwipeCallback):
+        (WTR::TestRunner::installDidRemoveSwipeSnapshotCallback):
+        (WTR::TestRunner::callDidBeginSwipeCallback):
+        (WTR::TestRunner::callWillEndSwipeCallback):
+        (WTR::TestRunner::callDidEndSwipeCallback):
+        (WTR::TestRunner::callDidRemoveSwipeSnapshotCallback):
+        * WebKitTestRunner/InjectedBundle/TestRunner.h:
+        * WebKitTestRunner/TestController.cpp:
+        (WTR::TestController::createOtherPage):
+        (WTR::TestController::createWebViewWithOptions):
+        (WTR::TestController::didReceiveMessageFromInjectedBundle):
+        (WTR::TestController::didBeginNavigationGesture):
+        (WTR::TestController::willEndNavigationGesture):
+        (WTR::TestController::didEndNavigationGesture):
+        (WTR::TestController::didRemoveNavigationGestureSnapshot):
+        * WebKitTestRunner/TestController.h:
+        * WebKitTestRunner/TestInvocation.cpp:
+        (WTR::TestInvocation::didBeginSwipe):
+        (WTR::TestInvocation::willEndSwipe):
+        (WTR::TestInvocation::didEndSwipe):
+        (WTR::TestInvocation::didRemoveSwipeSnapshot):
+        * WebKitTestRunner/TestInvocation.h:
+        Add callbacks when navigation gestures didBegin/willEnd/didEnd, and
+        when the snapshot is removed.
+
+        Add swipeGestureWithWheelAndMomentumPhases, just like the equivalent
+        mouseScrollBy function.
+
+        * WebKitTestRunner/mac/EventSenderProxy.mm:
+        (-[EventSenderSyntheticEvent initPressureEventAtLocation:globalLocation:stage:pressure:phase:time:eventNumber:]):
+        (-[EventSenderSyntheticEvent type]):
+        (-[EventSenderSyntheticEvent subtype]):
+        (-[EventSenderSyntheticEvent locationInWindow]):
+        (-[EventSenderSyntheticEvent location]):
+        (-[EventSenderSyntheticEvent momentumPhase]):
+        (-[EventSenderSyntheticEvent _isTouchesEnded]):
+        (-[EventSenderSyntheticEvent _cgsEventRecord]):
+        Rename EventSenderPressureEvent to EventSenderSyntheticEvent and add some
+        more adjustable values.
+
+        (WTR::EventSenderProxy::mouseForceDown):
+        (WTR::EventSenderProxy::mouseForceUp):
+        (WTR::EventSenderProxy::mouseForceChanged):
+        Adopt EventSenderSyntheticEvent.
+
+        (WTR::nsEventPhaseFromCGEventPhase):
+        (WTR::EventSenderProxy::swipeGestureWithWheelAndMomentumPhases):
+        Make use of EventSenderSyntheticEvent to synthesize swipe gesture events.
+
+        * WebKitTestRunner/mac/PlatformWebViewMac.mm:
+        (WTR::PlatformWebView::PlatformWebView):
+        Enable swipe.
+
 2015-09-03  Myles C. Maxfield  <mmaxfield@apple.com>
 
         [WK2] Allow tagging tests with metadata which needs to be known at web process creation time
diff --git a/Tools/WebKitTestRunner/EventSenderProxy.h b/Tools/WebKitTestRunner/EventSenderProxy.h
index 23110ab..e1ea670 100644
--- a/Tools/WebKitTestRunner/EventSenderProxy.h
+++ b/Tools/WebKitTestRunner/EventSenderProxy.h
@@ -64,6 +64,7 @@
     void mouseMoveTo(double x, double y);
     void mouseScrollBy(int x, int y);
     void mouseScrollByWithWheelAndMomentumPhases(int x, int y, int phase, int momentum);
+    void swipeGestureWithWheelAndMomentumPhases(int x, int y, int phase, int momentum);
     void continuousMouseScrollBy(int x, int y, bool paged);
 
     void leapForward(int milliseconds);
diff --git a/Tools/WebKitTestRunner/InjectedBundle/Bindings/EventSendingController.idl b/Tools/WebKitTestRunner/InjectedBundle/Bindings/EventSendingController.idl
index 47f4b97..5cbbcdb 100644
--- a/Tools/WebKitTestRunner/InjectedBundle/Bindings/EventSendingController.idl
+++ b/Tools/WebKitTestRunner/InjectedBundle/Bindings/EventSendingController.idl
@@ -32,6 +32,7 @@
     void mouseForceChanged(double force);
     void mouseScrollBy(long x, long y);
     void mouseScrollByWithWheelAndMomentumPhases(long x, long y, DOMString phase, DOMString momentum);
+    void swipeGestureWithWheelAndMomentumPhases(long x, long y, DOMString phase, DOMString momentum);
     void continuousMouseScrollBy(long x, long y, optional boolean paged);
     object contextClick();
     void scheduleAsynchronousClick();
diff --git a/Tools/WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl b/Tools/WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl
index 5fd4242..75a97d7 100644
--- a/Tools/WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl
+++ b/Tools/WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl
@@ -76,6 +76,7 @@
     void setAsynchronousSpellCheckingEnabled(boolean value);
     void setPrinting();
     void setShouldDecideNavigationPolicyAfterDelay(boolean value);
+    void setNavigationGesturesEnabled(boolean value);
 
     // Special DOM functions.
     void clearBackForwardList();
@@ -204,5 +205,11 @@
     // Hooks to the JSC compiler.
     object numberOfDFGCompiles(object function);
     object neverInlineFunction(object function);
+
+    // Swipe gestures
+    void installDidBeginSwipeCallback(object callback);
+    void installWillEndSwipeCallback(object callback);
+    void installDidEndSwipeCallback(object callback);
+    void installDidRemoveSwipeSnapshotCallback(object callback);
 };
 
diff --git a/Tools/WebKitTestRunner/InjectedBundle/EventSendingController.cpp b/Tools/WebKitTestRunner/InjectedBundle/EventSendingController.cpp
index b007dcf..349163a 100644
--- a/Tools/WebKitTestRunner/InjectedBundle/EventSendingController.cpp
+++ b/Tools/WebKitTestRunner/InjectedBundle/EventSendingController.cpp
@@ -391,6 +391,40 @@
     WKBundlePagePostMessage(InjectedBundle::singleton().page()->page(), EventSenderMessageName.get(), EventSenderMessageBody.get());
 }
 
+static uint64_t cgEventPhaseFromString(JSStringRef phaseStr)
+{
+    if (JSStringIsEqualToUTF8CString(phaseStr, "none"))
+        return 0;
+    if (JSStringIsEqualToUTF8CString(phaseStr, "began"))
+        return 1; // kCGScrollPhaseBegan
+    if (JSStringIsEqualToUTF8CString(phaseStr, "changed"))
+        return 2; // kCGScrollPhaseChanged
+    if (JSStringIsEqualToUTF8CString(phaseStr, "ended"))
+        return 4; // kCGScrollPhaseEnded
+    if (JSStringIsEqualToUTF8CString(phaseStr, "cancelled"))
+        return 8; // kCGScrollPhaseCancelled
+    if (JSStringIsEqualToUTF8CString(phaseStr, "maybegin"))
+        return 128; // kCGScrollPhaseMayBegin
+
+    ASSERT_NOT_REACHED();
+    return 0;
+}
+
+static uint64_t cgEventMomentumPhaseFromString(JSStringRef phaseStr)
+{
+    if (JSStringIsEqualToUTF8CString(phaseStr, "none"))
+        return 0; // kCGMomentumScrollPhaseNone
+    if (JSStringIsEqualToUTF8CString(phaseStr, "begin"))
+        return 1; // kCGMomentumScrollPhaseBegin
+    if (JSStringIsEqualToUTF8CString(phaseStr, "continue"))
+        return 2; // kCGMomentumScrollPhaseContinue
+    if (JSStringIsEqualToUTF8CString(phaseStr, "end"))
+        return 3; // kCGMomentumScrollPhaseEnd
+
+    ASSERT_NOT_REACHED();
+    return 0;
+}
+
 void EventSendingController::mouseScrollByWithWheelAndMomentumPhases(int x, int y, JSStringRef phaseStr, JSStringRef momentumStr)
 {
     WKRetainPtr<WKStringRef> EventSenderMessageName(AdoptWK, WKStringCreateWithUTF8CString("EventSender"));
@@ -408,33 +442,44 @@
     WKRetainPtr<WKDoubleRef> yRef(AdoptWK, WKDoubleCreate(y));
     WKDictionarySetItem(EventSenderMessageBody.get(), yKey.get(), yRef.get());
 
-    uint64_t phase = 0;
-    if (JSStringIsEqualToUTF8CString(phaseStr, "none"))
-        phase = 0;
-    else if (JSStringIsEqualToUTF8CString(phaseStr, "began"))
-        phase = 1; // kCGScrollPhaseBegan
-    else if (JSStringIsEqualToUTF8CString(phaseStr, "changed"))
-        phase = 2; // kCGScrollPhaseChanged
-    else if (JSStringIsEqualToUTF8CString(phaseStr, "ended"))
-        phase = 4; // kCGScrollPhaseEnded
-    else if (JSStringIsEqualToUTF8CString(phaseStr, "cancelled"))
-        phase = 8; // kCGScrollPhaseCancelled
-    else if (JSStringIsEqualToUTF8CString(phaseStr, "maybegin"))
-        phase = 128; // kCGScrollPhaseMayBegin
+    uint64_t phase = cgEventPhaseFromString(phaseStr);
+    uint64_t momentum = cgEventMomentumPhaseFromString(momentumStr);
 
     WKRetainPtr<WKStringRef> phaseKey(AdoptWK, WKStringCreateWithUTF8CString("Phase"));
     WKRetainPtr<WKUInt64Ref> phaseRef(AdoptWK, WKUInt64Create(phase));
     WKDictionarySetItem(EventSenderMessageBody.get(), phaseKey.get(), phaseRef.get());
 
-    uint64_t momentum = 0;
-    if (JSStringIsEqualToUTF8CString(momentumStr, "none"))
-        momentum = 0; // kCGMomentumScrollPhaseNone
-    else if (JSStringIsEqualToUTF8CString(momentumStr, "begin"))
-        momentum = 1; // kCGMomentumScrollPhaseBegin
-    else if (JSStringIsEqualToUTF8CString(momentumStr, "continue"))
-        momentum = 2; // kCGMomentumScrollPhaseContinue
-    else if (JSStringIsEqualToUTF8CString(momentumStr, "end"))
-        momentum = 3; // kCGMomentumScrollPhaseEnd
+    WKRetainPtr<WKStringRef> momentumKey(AdoptWK, WKStringCreateWithUTF8CString("Momentum"));
+    WKRetainPtr<WKUInt64Ref> momentumRef(AdoptWK, WKUInt64Create(momentum));
+    WKDictionarySetItem(EventSenderMessageBody.get(), momentumKey.get(), momentumRef.get());
+
+    WKBundlePageForceRepaint(InjectedBundle::singleton().page()->page()); // Triggers a scrolling tree commit.
+    WKBundlePagePostMessage(InjectedBundle::singleton().page()->page(), EventSenderMessageName.get(), EventSenderMessageBody.get());
+}
+
+void EventSendingController::swipeGestureWithWheelAndMomentumPhases(int x, int y, JSStringRef phaseStr, JSStringRef momentumStr)
+{
+    WKRetainPtr<WKStringRef> EventSenderMessageName(AdoptWK, WKStringCreateWithUTF8CString("EventSender"));
+    WKRetainPtr<WKMutableDictionaryRef> EventSenderMessageBody(AdoptWK, WKMutableDictionaryCreate());
+
+    WKRetainPtr<WKStringRef> subMessageKey(AdoptWK, WKStringCreateWithUTF8CString("SubMessage"));
+    WKRetainPtr<WKStringRef> subMessageName(AdoptWK, WKStringCreateWithUTF8CString("SwipeGestureWithWheelAndMomentumPhases"));
+    WKDictionarySetItem(EventSenderMessageBody.get(), subMessageKey.get(), subMessageName.get());
+
+    WKRetainPtr<WKStringRef> xKey(AdoptWK, WKStringCreateWithUTF8CString("X"));
+    WKRetainPtr<WKDoubleRef> xRef(AdoptWK, WKDoubleCreate(x));
+    WKDictionarySetItem(EventSenderMessageBody.get(), xKey.get(), xRef.get());
+
+    WKRetainPtr<WKStringRef> yKey(AdoptWK, WKStringCreateWithUTF8CString("Y"));
+    WKRetainPtr<WKDoubleRef> yRef(AdoptWK, WKDoubleCreate(y));
+    WKDictionarySetItem(EventSenderMessageBody.get(), yKey.get(), yRef.get());
+
+    uint64_t phase = cgEventPhaseFromString(phaseStr);
+    uint64_t momentum = cgEventMomentumPhaseFromString(momentumStr);
+
+    WKRetainPtr<WKStringRef> phaseKey(AdoptWK, WKStringCreateWithUTF8CString("Phase"));
+    WKRetainPtr<WKUInt64Ref> phaseRef(AdoptWK, WKUInt64Create(phase));
+    WKDictionarySetItem(EventSenderMessageBody.get(), phaseKey.get(), phaseRef.get());
 
     WKRetainPtr<WKStringRef> momentumKey(AdoptWK, WKStringCreateWithUTF8CString("Momentum"));
     WKRetainPtr<WKUInt64Ref> momentumRef(AdoptWK, WKUInt64Create(momentum));
diff --git a/Tools/WebKitTestRunner/InjectedBundle/EventSendingController.h b/Tools/WebKitTestRunner/InjectedBundle/EventSendingController.h
index 4e8100c..5755f19 100644
--- a/Tools/WebKitTestRunner/InjectedBundle/EventSendingController.h
+++ b/Tools/WebKitTestRunner/InjectedBundle/EventSendingController.h
@@ -51,6 +51,7 @@
     void mouseForceChanged(double force);
     void mouseScrollBy(int x, int y);
     void mouseScrollByWithWheelAndMomentumPhases(int x, int y, JSStringRef phase, JSStringRef momentum);
+    void swipeGestureWithWheelAndMomentumPhases(int x, int y, JSStringRef phase, JSStringRef momentum);
     void continuousMouseScrollBy(int x, int y, bool paged);
     JSValueRef contextClick();
     void leapForward(int milliseconds);
diff --git a/Tools/WebKitTestRunner/InjectedBundle/InjectedBundle.cpp b/Tools/WebKitTestRunner/InjectedBundle/InjectedBundle.cpp
index 0571a35..5be6bf9 100644
--- a/Tools/WebKitTestRunner/InjectedBundle/InjectedBundle.cpp
+++ b/Tools/WebKitTestRunner/InjectedBundle/InjectedBundle.cpp
@@ -215,6 +215,26 @@
         return;
     }
 
+    if (WKStringIsEqualToUTF8CString(messageName, "CallDidBeginSwipeCallback")) {
+        m_testRunner->callDidBeginSwipeCallback();
+        return;
+    }
+
+    if (WKStringIsEqualToUTF8CString(messageName, "CallWillEndSwipeCallback")) {
+        m_testRunner->callWillEndSwipeCallback();
+        return;
+    }
+
+    if (WKStringIsEqualToUTF8CString(messageName, "CallDidEndSwipeCallback")) {
+        m_testRunner->callDidEndSwipeCallback();
+        return;
+    }
+
+    if (WKStringIsEqualToUTF8CString(messageName, "CallDidRemoveSwipeSnapshotCallback")) {
+        m_testRunner->callDidRemoveSwipeSnapshotCallback();
+        return;
+    }
+
     if (WKStringIsEqualToUTF8CString(messageName, "WorkQueueProcessedCallback")) {
         if (!topLoadingFrame() && !m_testRunner->waitToDump())
             InjectedBundle::page()->dump();
diff --git a/Tools/WebKitTestRunner/InjectedBundle/TestRunner.cpp b/Tools/WebKitTestRunner/InjectedBundle/TestRunner.cpp
index 3b616d5..cd4791b 100644
--- a/Tools/WebKitTestRunner/InjectedBundle/TestRunner.cpp
+++ b/Tools/WebKitTestRunner/InjectedBundle/TestRunner.cpp
@@ -509,7 +509,11 @@
     AddChromeInputFieldCallbackID = 1,
     RemoveChromeInputFieldCallbackID,
     FocusWebViewCallbackID,
-    SetBackingScaleFactorCallbackID
+    SetBackingScaleFactorCallbackID,
+    DidBeginSwipeCallbackID,
+    WillEndSwipeCallbackID,
+    DidEndSwipeCallbackID,
+    DidRemoveSwipeSnapshotCallbackID
 };
 
 static void cacheTestRunnerCallback(unsigned index, JSValueRef callback)
@@ -860,4 +864,51 @@
     WKBundlePagePostMessage(InjectedBundle::singleton().page()->page(), messageName.get(), messageBody.get());
 }
 
+void TestRunner::setNavigationGesturesEnabled(bool value)
+{
+    WKRetainPtr<WKStringRef> messageName(AdoptWK, WKStringCreateWithUTF8CString("SetNavigationGesturesEnabled"));
+    WKRetainPtr<WKBooleanRef> messageBody(AdoptWK, WKBooleanCreate(value));
+    WKBundlePagePostMessage(InjectedBundle::singleton().page()->page(), messageName.get(), messageBody.get());
+}
+
+void TestRunner::installDidBeginSwipeCallback(JSValueRef callback)
+{
+    cacheTestRunnerCallback(DidBeginSwipeCallbackID, callback);
+}
+
+void TestRunner::installWillEndSwipeCallback(JSValueRef callback)
+{
+    cacheTestRunnerCallback(WillEndSwipeCallbackID, callback);
+}
+
+void TestRunner::installDidEndSwipeCallback(JSValueRef callback)
+{
+    cacheTestRunnerCallback(DidEndSwipeCallbackID, callback);
+}
+
+void TestRunner::installDidRemoveSwipeSnapshotCallback(JSValueRef callback)
+{
+    cacheTestRunnerCallback(DidRemoveSwipeSnapshotCallbackID, callback);
+}
+
+void TestRunner::callDidBeginSwipeCallback()
+{
+    callTestRunnerCallback(DidBeginSwipeCallbackID);
+}
+
+void TestRunner::callWillEndSwipeCallback()
+{
+    callTestRunnerCallback(WillEndSwipeCallbackID);
+}
+
+void TestRunner::callDidEndSwipeCallback()
+{
+    callTestRunnerCallback(DidEndSwipeCallbackID);
+}
+
+void TestRunner::callDidRemoveSwipeSnapshotCallback()
+{
+    callTestRunnerCallback(DidRemoveSwipeSnapshotCallbackID);
+}
+
 } // namespace WTR
diff --git a/Tools/WebKitTestRunner/InjectedBundle/TestRunner.h b/Tools/WebKitTestRunner/InjectedBundle/TestRunner.h
index 4d2f25c..dd39596 100644
--- a/Tools/WebKitTestRunner/InjectedBundle/TestRunner.h
+++ b/Tools/WebKitTestRunner/InjectedBundle/TestRunner.h
@@ -285,6 +285,16 @@
 
     bool shouldDecideNavigationPolicyAfterDelay() const { return m_shouldDecideNavigationPolicyAfterDelay; }
     void setShouldDecideNavigationPolicyAfterDelay(bool);
+    void setNavigationGesturesEnabled(bool);
+
+    void installDidBeginSwipeCallback(JSValueRef);
+    void installWillEndSwipeCallback(JSValueRef);
+    void installDidEndSwipeCallback(JSValueRef);
+    void installDidRemoveSwipeSnapshotCallback(JSValueRef);
+    void callDidBeginSwipeCallback();
+    void callWillEndSwipeCallback();
+    void callDidEndSwipeCallback();
+    void callDidRemoveSwipeSnapshotCallback();
 
 private:
     TestRunner();
diff --git a/Tools/WebKitTestRunner/InjectedBundle/ios/EventSenderProxyIOS.mm b/Tools/WebKitTestRunner/InjectedBundle/ios/EventSenderProxyIOS.mm
index cab3f18..ac54a1c 100644
--- a/Tools/WebKitTestRunner/InjectedBundle/ios/EventSenderProxyIOS.mm
+++ b/Tools/WebKitTestRunner/InjectedBundle/ios/EventSenderProxyIOS.mm
@@ -103,6 +103,11 @@
     // Write me.
 }
 
+void EventSenderProxy::swipeGestureWithWheelAndMomentumPhases(int, int, int, int)
+{
+    // Write me.
+}
+
 void EventSenderProxy::continuousMouseScrollBy(int x, int y, bool paged)
 {
     // Write me.
diff --git a/Tools/WebKitTestRunner/PlatformWebView.h b/Tools/WebKitTestRunner/PlatformWebView.h
index 011e4cd..9688c4c 100644
--- a/Tools/WebKitTestRunner/PlatformWebView.h
+++ b/Tools/WebKitTestRunner/PlatformWebView.h
@@ -86,6 +86,7 @@
     const ViewOptions& options() const { return m_options; }
 
     void changeWindowScaleIfNeeded(float newScale);
+    void setNavigationGesturesEnabled(bool);
 
 #if PLATFORM(GTK)
     void dismissAllPopupMenus();
diff --git a/Tools/WebKitTestRunner/TestController.cpp b/Tools/WebKitTestRunner/TestController.cpp
index dc375ec..886056a 100644
--- a/Tools/WebKitTestRunner/TestController.cpp
+++ b/Tools/WebKitTestRunner/TestController.cpp
@@ -294,10 +294,10 @@
         didReceiveAuthenticationChallenge,
         processDidCrash,
         copyWebCryptoMasterKey,
-        0, // didBeginNavigationGesture
-        0, // willEndNavigationGesture
-        0, // didEndNavigationGesture
-        0, // didRemoveNavigationGestureSnapshot
+        didBeginNavigationGesture,
+        willEndNavigationGesture,
+        didEndNavigationGesture,
+        didRemoveNavigationGestureSnapshot
     };
     WKPageSetPageNavigationClient(newPage, &pageNavigationClient.base);
 
@@ -539,10 +539,10 @@
         didReceiveAuthenticationChallenge,
         processDidCrash,
         copyWebCryptoMasterKey,
-        0, // didBeginNavigationGesture
-        0, // willEndNavigationGesture
-        0, // didEndNavigationGesture
-        0, // didRemoveNavigationGestureSnapshot
+        didBeginNavigationGesture,
+        willEndNavigationGesture,
+        didEndNavigationGesture,
+        didRemoveNavigationGestureSnapshot
     };
     WKPageSetPageNavigationClient(m_mainWebView->page(), &pageNavigationClient.base);
 
@@ -746,6 +746,8 @@
 
     m_shouldDecideNavigationPolicyAfterDelay = false;
 
+    setNavigationGesturesEnabled(false);
+
     WKPageLoadURL(m_mainWebView->page(), blankURL());
     runUntil(m_doneResetting, shortTimeout);
     return m_doneResetting;
@@ -1137,6 +1139,23 @@
             return;
         }
 
+        if (WKStringIsEqualToUTF8CString(subMessageName, "SwipeGestureWithWheelAndMomentumPhases")) {
+            WKRetainPtr<WKStringRef> xKey = adoptWK(WKStringCreateWithUTF8CString("X"));
+            double x = WKDoubleGetValue(static_cast<WKDoubleRef>(WKDictionaryGetItemForKey(messageBodyDictionary, xKey.get())));
+
+            WKRetainPtr<WKStringRef> yKey = adoptWK(WKStringCreateWithUTF8CString("Y"));
+            double y = WKDoubleGetValue(static_cast<WKDoubleRef>(WKDictionaryGetItemForKey(messageBodyDictionary, yKey.get())));
+
+            WKRetainPtr<WKStringRef> phaseKey = adoptWK(WKStringCreateWithUTF8CString("Phase"));
+            int phase = static_cast<int>(WKUInt64GetValue(static_cast<WKUInt64Ref>(WKDictionaryGetItemForKey(messageBodyDictionary, phaseKey.get()))));
+            WKRetainPtr<WKStringRef> momentumKey = adoptWK(WKStringCreateWithUTF8CString("Momentum"));
+            int momentum = static_cast<int>(WKUInt64GetValue(static_cast<WKUInt64Ref>(WKDictionaryGetItemForKey(messageBodyDictionary, momentumKey.get()))));
+
+            m_eventSenderProxy->swipeGestureWithWheelAndMomentumPhases(x, y, phase, momentum);
+
+            return;
+        }
+
         ASSERT_NOT_REACHED();
     }
 
@@ -1373,6 +1392,26 @@
     static_cast<TestController*>(const_cast<void*>(clientInfo))->processDidCrash();
 }
 
+void TestController::didBeginNavigationGesture(WKPageRef page, const void *clientInfo)
+{
+    static_cast<TestController*>(const_cast<void*>(clientInfo))->didBeginNavigationGesture(page);
+}
+
+void TestController::willEndNavigationGesture(WKPageRef page, WKBackForwardListItemRef backForwardListItem, const void *clientInfo)
+{
+    static_cast<TestController*>(const_cast<void*>(clientInfo))->willEndNavigationGesture(page, backForwardListItem);
+}
+
+void TestController::didEndNavigationGesture(WKPageRef page, WKBackForwardListItemRef backForwardListItem, const void *clientInfo)
+{
+    static_cast<TestController*>(const_cast<void*>(clientInfo))->didEndNavigationGesture(page, backForwardListItem);
+}
+
+void TestController::didRemoveNavigationGestureSnapshot(WKPageRef page, const void *clientInfo)
+{
+    static_cast<TestController*>(const_cast<void*>(clientInfo))->didRemoveNavigationGestureSnapshot(page);
+}
+
 WKPluginLoadPolicy TestController::decidePolicyForPluginLoad(WKPageRef page, WKPluginLoadPolicy currentPluginLoadPolicy, WKDictionaryRef pluginInformation, WKStringRef* unavailabilityDescription, const void* clientInfo)
 {
     return static_cast<TestController*>(const_cast<void*>(clientInfo))->decidePolicyForPluginLoad(page, currentPluginLoadPolicy, pluginInformation, unavailabilityDescription);
@@ -1470,6 +1509,26 @@
         exit(1);
 }
 
+void TestController::didBeginNavigationGesture(WKPageRef)
+{
+    m_currentInvocation->didBeginSwipe();
+}
+
+void TestController::willEndNavigationGesture(WKPageRef, WKBackForwardListItemRef)
+{
+    m_currentInvocation->willEndSwipe();
+}
+
+void TestController::didEndNavigationGesture(WKPageRef, WKBackForwardListItemRef)
+{
+    m_currentInvocation->didEndSwipe();
+}
+
+void TestController::didRemoveNavigationGestureSnapshot(WKPageRef)
+{
+    m_currentInvocation->didRemoveSwipeSnapshot();
+}
+
 void TestController::simulateWebNotificationClick(uint64_t notificationID)
 {
     m_webNotificationProvider.simulateWebNotificationClick(notificationID);
@@ -1688,6 +1747,11 @@
     m_currentInvocation->outputText(String::format("WebView updated the title for history URL \"%s\" to \"%s\".\n", toSTD(urlStringWK).c_str(), toSTD(title).c_str()));
 }
 
+void TestController::setNavigationGesturesEnabled(bool value)
+{
+    m_mainWebView->setNavigationGesturesEnabled(value);
+}
+
 #if !PLATFORM(COCOA)
 void TestController::platformWillRunTest(const TestInvocation&)
 {
diff --git a/Tools/WebKitTestRunner/TestController.h b/Tools/WebKitTestRunner/TestController.h
index 27147a4..22f89d8 100644
--- a/Tools/WebKitTestRunner/TestController.h
+++ b/Tools/WebKitTestRunner/TestController.h
@@ -126,6 +126,8 @@
 
     void setShouldDecideNavigationPolicyAfterDelay(bool value) { m_shouldDecideNavigationPolicyAfterDelay = value; }
 
+    void setNavigationGesturesEnabled(bool value);
+
 private:
     void initialize(int argc, const char* argv[]);
     void createWebViewWithOptions(const ViewOptions&);
@@ -142,6 +144,9 @@
     static PlatformWebView* platformCreateOtherPage(PlatformWebView* parentView, WKPageConfigurationRef, const ViewOptions&);
     void platformResetPreferencesToConsistentValues();
     void platformResetStateToConsistentValues();
+#if PLATFORM(COCOA)
+    void cocoaResetStateToConsistentValues();
+#endif
     void platformConfigureViewForTest(const TestInvocation&);
     void platformWillRunTest(const TestInvocation&);
     void platformRunUntil(bool& done, double timeout);
@@ -186,6 +191,15 @@
     static void processDidCrash(WKPageRef, const void* clientInfo);
     void processDidCrash();
 
+    static void didBeginNavigationGesture(WKPageRef, const void*);
+    static void willEndNavigationGesture(WKPageRef, WKBackForwardListItemRef, const void*);
+    static void didEndNavigationGesture(WKPageRef, WKBackForwardListItemRef, const void*);
+    static void didRemoveNavigationGestureSnapshot(WKPageRef, const void*);
+    void didBeginNavigationGesture(WKPageRef);
+    void willEndNavigationGesture(WKPageRef, WKBackForwardListItemRef);
+    void didEndNavigationGesture(WKPageRef, WKBackForwardListItemRef);
+    void didRemoveNavigationGestureSnapshot(WKPageRef);
+
     static WKPluginLoadPolicy decidePolicyForPluginLoad(WKPageRef, WKPluginLoadPolicy currentPluginLoadPolicy, WKDictionaryRef pluginInformation, WKStringRef* unavailabilityDescription, const void* clientInfo);
     WKPluginLoadPolicy decidePolicyForPluginLoad(WKPageRef, WKPluginLoadPolicy currentPluginLoadPolicy, WKDictionaryRef pluginInformation, WKStringRef* unavailabilityDescription);
     
diff --git a/Tools/WebKitTestRunner/TestInvocation.cpp b/Tools/WebKitTestRunner/TestInvocation.cpp
index 091911e..c4dee36 100644
--- a/Tools/WebKitTestRunner/TestInvocation.cpp
+++ b/Tools/WebKitTestRunner/TestInvocation.cpp
@@ -635,6 +635,13 @@
         return;
     }
 
+    if (WKStringIsEqualToUTF8CString(messageName, "SetNavigationGesturesEnabled")) {
+        ASSERT(WKGetTypeID(messageBody) == WKBooleanGetTypeID());
+        WKBooleanRef value = static_cast<WKBooleanRef>(messageBody);
+        TestController::singleton().setNavigationGesturesEnabled(WKBooleanGetValue(value));
+        return;
+    }
+
     ASSERT_NOT_REACHED();
 }
 
@@ -685,4 +692,28 @@
     m_textOutput.append(text);
 }
 
+void TestInvocation::didBeginSwipe()
+{
+    WKRetainPtr<WKStringRef> messageName = adoptWK(WKStringCreateWithUTF8CString("CallDidBeginSwipeCallback"));
+    WKPagePostMessageToInjectedBundle(TestController::singleton().mainWebView()->page(), messageName.get(), 0);
+}
+
+void TestInvocation::willEndSwipe()
+{
+    WKRetainPtr<WKStringRef> messageName = adoptWK(WKStringCreateWithUTF8CString("CallWillEndSwipeCallback"));
+    WKPagePostMessageToInjectedBundle(TestController::singleton().mainWebView()->page(), messageName.get(), 0);
+}
+
+void TestInvocation::didEndSwipe()
+{
+    WKRetainPtr<WKStringRef> messageName = adoptWK(WKStringCreateWithUTF8CString("CallDidEndSwipeCallback"));
+    WKPagePostMessageToInjectedBundle(TestController::singleton().mainWebView()->page(), messageName.get(), 0);
+}
+
+void TestInvocation::didRemoveSwipeSnapshot()
+{
+    WKRetainPtr<WKStringRef> messageName = adoptWK(WKStringCreateWithUTF8CString("CallDidRemoveSwipeSnapshotCallback"));
+    WKPagePostMessageToInjectedBundle(TestController::singleton().mainWebView()->page(), messageName.get(), 0);
+}
+
 } // namespace WTR
diff --git a/Tools/WebKitTestRunner/TestInvocation.h b/Tools/WebKitTestRunner/TestInvocation.h
index ff760e7..5981ada 100644
--- a/Tools/WebKitTestRunner/TestInvocation.h
+++ b/Tools/WebKitTestRunner/TestInvocation.h
@@ -54,6 +54,12 @@
     void dumpWebProcessUnresponsiveness();
     static void dumpWebProcessUnresponsiveness(const char* errorMessage);
     void outputText(const WTF::String&);
+
+    void didBeginSwipe();
+    void willEndSwipe();
+    void didEndSwipe();
+    void didRemoveSwipeSnapshot();
+
 private:
     void dumpResults();
     static void dump(const char* textToStdout, const char* textToStderr = 0, bool seenError = false);
diff --git a/Tools/WebKitTestRunner/cocoa/TestControllerCocoa.mm b/Tools/WebKitTestRunner/cocoa/TestControllerCocoa.mm
index d99834c..e653b89 100644
--- a/Tools/WebKitTestRunner/cocoa/TestControllerCocoa.mm
+++ b/Tools/WebKitTestRunner/cocoa/TestControllerCocoa.mm
@@ -108,7 +108,7 @@
         [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:endDate];
 }
 
-void TestController::platformResetStateToConsistentValues()
+void TestController::cocoaResetStateToConsistentValues()
 {
 #if WK_API_ENABLED
     __block bool doneRemoving = false;
diff --git a/Tools/WebKitTestRunner/efl/EventSenderProxyEfl.cpp b/Tools/WebKitTestRunner/efl/EventSenderProxyEfl.cpp
index 7a68c7f..38ae04f 100644
--- a/Tools/WebKitTestRunner/efl/EventSenderProxyEfl.cpp
+++ b/Tools/WebKitTestRunner/efl/EventSenderProxyEfl.cpp
@@ -399,6 +399,11 @@
     mouseScrollBy(x, y);
 }
 
+void EventSenderProxy::swipeGestureWithWheelAndMomentumPhases(int, int, int, int)
+{
+    notImplemented();
+}
+
 void EventSenderProxy::leapForward(int milliseconds)
 {
     if (m_eventQueue.isEmpty())
diff --git a/Tools/WebKitTestRunner/efl/PlatformWebViewEfl.cpp b/Tools/WebKitTestRunner/efl/PlatformWebViewEfl.cpp
index 81166b2..d136aa5 100644
--- a/Tools/WebKitTestRunner/efl/PlatformWebViewEfl.cpp
+++ b/Tools/WebKitTestRunner/efl/PlatformWebViewEfl.cpp
@@ -156,5 +156,9 @@
 {
 }
 
+void PlatformWebView::setNavigationGesturesEnabled(bool)
+{
+}
+
 } // namespace WTR
 
diff --git a/Tools/WebKitTestRunner/gtk/EventSenderProxyGtk.cpp b/Tools/WebKitTestRunner/gtk/EventSenderProxyGtk.cpp
index b8f5cbe..af360aa 100644
--- a/Tools/WebKitTestRunner/gtk/EventSenderProxyGtk.cpp
+++ b/Tools/WebKitTestRunner/gtk/EventSenderProxyGtk.cpp
@@ -451,6 +451,11 @@
     mouseScrollBy(x, y);
 }
 
+void EventSenderProxy::swipeGestureWithWheelAndMomentumPhases(int, int, int, int)
+{
+    notImplemented();
+}
+
 void EventSenderProxy::leapForward(int milliseconds)
 {
     if (m_eventQueue.isEmpty())
diff --git a/Tools/WebKitTestRunner/gtk/PlatformWebViewGtk.cpp b/Tools/WebKitTestRunner/gtk/PlatformWebViewGtk.cpp
index a2e6265..ad9b027 100644
--- a/Tools/WebKitTestRunner/gtk/PlatformWebViewGtk.cpp
+++ b/Tools/WebKitTestRunner/gtk/PlatformWebViewGtk.cpp
@@ -158,5 +158,9 @@
     }, nullptr);
 }
 
+void PlatformWebView::setNavigationGesturesEnabled(bool)
+{
+}
+
 } // namespace WTR
 
diff --git a/Tools/WebKitTestRunner/ios/PlatformWebViewIOS.mm b/Tools/WebKitTestRunner/ios/PlatformWebViewIOS.mm
index 32d3dec..2e9c511 100644
--- a/Tools/WebKitTestRunner/ios/PlatformWebViewIOS.mm
+++ b/Tools/WebKitTestRunner/ios/PlatformWebViewIOS.mm
@@ -207,4 +207,8 @@
     return true;
 }
 
+void PlatformWebView::setNavigationGesturesEnabled(bool enabled)
+{
+}
+
 } // namespace WTR
diff --git a/Tools/WebKitTestRunner/ios/TestControllerIOS.mm b/Tools/WebKitTestRunner/ios/TestControllerIOS.mm
index 6453d42..167161e 100644
--- a/Tools/WebKitTestRunner/ios/TestControllerIOS.mm
+++ b/Tools/WebKitTestRunner/ios/TestControllerIOS.mm
@@ -86,6 +86,11 @@
     WKPreferencesSetMinimumZoomFontSize(preferences, 0);
 }
 
+void TestController::platformResetStateToConsistentValues()
+{
+    cocoaResetStateToConsistentValues();
+}
+
 void TestController::platformConfigureViewForTest(const TestInvocation& test)
 {
     if (shouldMakeViewportFlexible(test)) {
diff --git a/Tools/WebKitTestRunner/mac/EventSenderProxy.mm b/Tools/WebKitTestRunner/mac/EventSenderProxy.mm
index 6b1b77b..379baec 100644
--- a/Tools/WebKitTestRunner/mac/EventSenderProxy.mm
+++ b/Tools/WebKitTestRunner/mac/EventSenderProxy.mm
@@ -40,25 +40,42 @@
 - (void)_setCurrentEvent:(NSEvent *)event;
 @end
 
-#if defined(__LP64__) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 101003
-@interface EventSenderPressureEvent : NSEvent {
+#if defined(__LP64__)
+struct WKTRCGSEventRecord {
+    char offset1[150];
+    uint8_t phase;
+    char offset2[13];
+    float deltaX;
+    float deltaY;
+    char offset3[76];
+} __attribute__((packed));
+#endif
+
+@interface EventSenderSyntheticEvent : NSEvent {
 @public
     NSPoint _eventSender_locationInWindow;
     NSPoint _eventSender_location;
     NSInteger _eventSender_stage;
     float _eventSender_pressure;
     NSEventPhase _eventSender_phase;
+    NSEventPhase _eventSender_momentumPhase;
     NSTimeInterval _eventSender_timestamp;
     NSInteger _eventSender_eventNumber;
+    short _eventSender_subtype;
+    NSEventType _eventSender_type;
+
+#if defined(__LP64__)
+    WKTRCGSEventRecord _eventSender_cgsEventRecord;
+#endif
 }
 
-- (id)initAtLocation:(NSPoint)location globalLocation:(NSPoint)globalLocation stage:(NSInteger)stage pressure:(float)pressure phase:(NSEventPhase)phase time:(NSTimeInterval)time eventNumber:(NSInteger)eventNumber;
+- (id)initPressureEventAtLocation:(NSPoint)location globalLocation:(NSPoint)globalLocation stage:(NSInteger)stage pressure:(float)pressure phase:(NSEventPhase)phase time:(NSTimeInterval)time eventNumber:(NSInteger)eventNumber;
 - (NSTimeInterval)timestamp;
 @end
 
-@implementation EventSenderPressureEvent
+@implementation EventSenderSyntheticEvent
 
-- (id)initAtLocation:(NSPoint)location globalLocation:(NSPoint)globalLocation stage:(NSInteger)stage pressure:(float)pressure phase:(NSEventPhase)phase time:(NSTimeInterval)time eventNumber:(NSInteger)eventNumber
+- (id)initPressureEventAtLocation:(NSPoint)location globalLocation:(NSPoint)globalLocation stage:(NSInteger)stage pressure:(float)pressure phase:(NSEventPhase)phase time:(NSTimeInterval)time eventNumber:(NSInteger)eventNumber
 {
     self = [super init];
 
@@ -72,6 +89,9 @@
     _eventSender_phase = phase;
     _eventSender_timestamp = time;
     _eventSender_eventNumber = eventNumber;
+#if defined(__LP64__) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101003
+    _eventSender_type = NSEventTypePressure;
+#endif
 
     return self;
 }
@@ -83,17 +103,29 @@
 
 - (NSEventType)type
 {
-    return NSEventTypePressure;
+    return _eventSender_type;
 }
 
+#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 10100
+- (NSEventSubtype)subtype
+{
+    return (NSEventSubtype)_eventSender_subtype;
+}
+#else
+- (short)subtype
+{
+    return _eventSender_subtype;
+}
+#endif
+
 - (NSPoint)locationInWindow
 {
-    return self->_eventSender_location;
+    return _eventSender_location;
 }
 
 - (NSPoint)location
 {
-    return self->_eventSender_locationInWindow;
+    return _eventSender_locationInWindow;
 }
 
 - (NSInteger)stage
@@ -111,13 +143,29 @@
     return _eventSender_phase;
 }
 
+- (NSEventPhase)momentumPhase
+{
+    return _eventSender_momentumPhase;
+}
+
 - (NSInteger)eventNumber
 {
     return _eventSender_eventNumber;
 }
 
+- (BOOL)_isTouchesEnded
+{
+    return false;
+}
+
+#if defined(__LP64__)
+- (WKTRCGSEventRecord)_cgsEventRecord
+{
+    return _eventSender_cgsEventRecord;
+}
+#endif
+
 @end
-#endif // defined(__LP64__) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 101003
 
 namespace WTR {
 
@@ -290,17 +338,17 @@
     m_clickPosition = m_position;
 }
 
-#if defined(__LP64__) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 101003
+#if defined(__LP64__) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101003
 void EventSenderProxy::mouseForceDown()
 {
-    EventSenderPressureEvent *firstEvent = [[EventSenderPressureEvent alloc] initAtLocation:NSMakePoint(m_position.x, m_position.y)
+    EventSenderSyntheticEvent *firstEvent = [[EventSenderSyntheticEvent alloc] initPressureEventAtLocation:NSMakePoint(m_position.x, m_position.y)
         globalLocation:([m_testController->mainWebView()->platformWindow() convertRectToScreen:NSMakeRect(m_position.x, m_position.y, 1, 1)].origin)
         stage:1
         pressure:0.9
         phase:NSEventPhaseChanged
         time:absoluteTimeForEventTime(currentEventTime())
         eventNumber:++eventNumber];
-    EventSenderPressureEvent *secondEvent = [[EventSenderPressureEvent alloc] initAtLocation:NSMakePoint(m_position.x, m_position.y)
+    EventSenderSyntheticEvent *secondEvent = [[EventSenderSyntheticEvent alloc] initPressureEventAtLocation:NSMakePoint(m_position.x, m_position.y)
         globalLocation:([m_testController->mainWebView()->platformWindow() convertRectToScreen:NSMakeRect(m_position.x, m_position.y, 1, 1)].origin)
         stage:2
         pressure:0.1
@@ -332,14 +380,14 @@
 
 void EventSenderProxy::mouseForceUp()
 {
-    EventSenderPressureEvent *firstEvent = [[EventSenderPressureEvent alloc] initAtLocation:NSMakePoint(m_position.x, m_position.y)
+    EventSenderSyntheticEvent *firstEvent = [[EventSenderSyntheticEvent alloc] initPressureEventAtLocation:NSMakePoint(m_position.x, m_position.y)
         globalLocation:([m_testController->mainWebView()->platformWindow() convertRectToScreen:NSMakeRect(m_position.x, m_position.y, 1, 1)].origin)
         stage:2
         pressure:0.1
         phase:NSEventPhaseChanged
         time:absoluteTimeForEventTime(currentEventTime())
         eventNumber:++eventNumber];
-    EventSenderPressureEvent *secondEvent = [[EventSenderPressureEvent alloc] initAtLocation:NSMakePoint(m_position.x, m_position.y)
+    EventSenderSyntheticEvent *secondEvent = [[EventSenderSyntheticEvent alloc] initPressureEventAtLocation:NSMakePoint(m_position.x, m_position.y)
         globalLocation:([m_testController->mainWebView()->platformWindow() convertRectToScreen:NSMakeRect(m_position.x, m_position.y, 1, 1)].origin)
         stage:1
         pressure:0.9
@@ -372,7 +420,7 @@
 
 void EventSenderProxy::mouseForceChanged(float force)
 {
-    EventSenderPressureEvent *event = [[EventSenderPressureEvent alloc] initAtLocation:NSMakePoint(m_position.x, m_position.y)
+    EventSenderSyntheticEvent *event = [[EventSenderSyntheticEvent alloc] initPressureEventAtLocation:NSMakePoint(m_position.x, m_position.y)
         globalLocation:([m_testController->mainWebView()->platformWindow() convertRectToScreen:NSMakeRect(m_position.x, m_position.y, 1, 1)].origin)
         stage:force < 1 ? 1 : 2
         pressure:force
@@ -407,7 +455,7 @@
 void EventSenderProxy::mouseForceChanged(float)
 {
 }
-#endif // defined(__LP64__) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 101003
+#endif // defined(__LP64__) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101003
 
 void EventSenderProxy::mouseMoveTo(double x, double y)
 {
@@ -705,4 +753,51 @@
     }
 }
 
+static NSEventPhase nsEventPhaseFromCGEventPhase(int phase)
+{
+    switch (phase) {
+    case 0: // kCGSGesturePhaseNone
+        return NSEventPhaseNone;
+    case 1: // kCGSGesturePhaseBegan
+        return NSEventPhaseBegan;
+    case 2: // kCGSGesturePhaseChanged
+        return NSEventPhaseChanged;
+    case 4: // kCGSGesturePhaseEnded
+        return NSEventPhaseEnded;
+    case 8: // kCGSGesturePhaseCancelled
+        return NSEventPhaseCancelled;
+    case 128: // kCGSGesturePhaseMayBegin
+        return NSEventPhaseMayBegin;
+    }
+
+    ASSERT_NOT_REACHED();
+    return NSEventPhaseNone;
+}
+
+void EventSenderProxy::swipeGestureWithWheelAndMomentumPhases(int x, int y, int phase, int momentum)
+{
+    EventSenderSyntheticEvent *event = [[EventSenderSyntheticEvent alloc] init];
+
+    // "mayBegin" a swipe is actually a scroll wheel event.
+    event->_eventSender_type = (phase == 128) ? NSScrollWheel : NSEventTypeGesture;
+    event->_eventSender_subtype = 6; // kIOHIDEventTypeScroll
+    event->_eventSender_locationInWindow = NSMakePoint(m_position.x, m_position.y);
+    event->_eventSender_location = ([m_testController->mainWebView()->platformWindow() convertRectToScreen:NSMakeRect(m_position.x, m_position.y, 1, 1)].origin);
+    event->_eventSender_phase = nsEventPhaseFromCGEventPhase(phase);
+    event->_eventSender_momentumPhase = nsEventPhaseFromCGEventPhase(momentum);
+    event->_eventSender_timestamp = absoluteTimeForEventTime(currentEventTime());
+    event->_eventSender_eventNumber = ++eventNumber;
+
+#if defined(__LP64__)
+    event->_eventSender_cgsEventRecord.phase = phase;
+    event->_eventSender_cgsEventRecord.deltaX = (float)x;
+    event->_eventSender_cgsEventRecord.deltaY = (float)y;
+#else
+    NSLog(@"Synthetic swipe gestures are not implemented for 32-bit WebKitTestRunner.");
+#endif
+
+    NSLog(@"sending swipe event %d %d phase %d", x, y, phase);
+    [NSApp sendEvent:event];
+}
+
 } // namespace WTR
diff --git a/Tools/WebKitTestRunner/mac/PlatformWebViewMac.mm b/Tools/WebKitTestRunner/mac/PlatformWebViewMac.mm
index 8368391..934ccbf 100644
--- a/Tools/WebKitTestRunner/mac/PlatformWebViewMac.mm
+++ b/Tools/WebKitTestRunner/mac/PlatformWebViewMac.mm
@@ -269,4 +269,9 @@
     setWindowFrame(wkFrame);
 }
 
+void PlatformWebView::setNavigationGesturesEnabled(bool enabled)
+{
+    [platformView() setAllowsBackForwardNavigationGestures:enabled];
+}
+
 } // namespace WTR
diff --git a/Tools/WebKitTestRunner/mac/TestControllerMac.mm b/Tools/WebKitTestRunner/mac/TestControllerMac.mm
index 71aaa6e..8380a9f 100644
--- a/Tools/WebKitTestRunner/mac/TestControllerMac.mm
+++ b/Tools/WebKitTestRunner/mac/TestControllerMac.mm
@@ -86,6 +86,15 @@
 {
 }
 
+void TestController::platformResetStateToConsistentValues()
+{
+    cocoaResetStateToConsistentValues();
+
+    while ([NSApp nextEventMatchingMask:NSEventMaskGesture | NSScrollWheelMask untilDate:nil inMode:NSDefaultRunLoopMode dequeue:YES]) {
+        // Clear out (and ignore) any pending gesture and scroll wheel events.
+    }
+}
+
 void TestController::updatePlatformSpecificViewOptionsForTest(ViewOptions& viewOptions, const TestInvocation& test) const
 {
     viewOptions.useThreadedScrolling = shouldUseThreadedScrolling(test);