[Pointer Events] event.isPrimary doesn't always represent the oldest active touch
authorgraouts@webkit.org <graouts@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 17 Nov 2018 09:29:52 +0000 (09:29 +0000)
committergraouts@webkit.org <graouts@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 17 Nov 2018 09:29:52 +0000 (09:29 +0000)
https://bugs.webkit.org/show_bug.cgi?id=191752
<rdar://problem/46129270>

Reviewed by Dean Jackson.

Source/WebCore:

Provide isPrimary to the constructor so its value can be determined at the call site.

Test: pointerevents/ios/pointer-events-is-primary.html

* dom/PointerEvent.h:
* dom/ios/PointerEventIOS.cpp:
(WebCore::PointerEvent::create):
(WebCore::PointerEvent::PointerEvent):
(WebCore::m_isPrimary):

LayoutTests:

Add a new test that checks that adding a touch after another existing touch does not make it be
the primary touch, but that removing the first touch makes the second touch become the primary touch.

To do this we add a new ui.sequence() method that allows a series of touch actions to be performed
in a linear sequence. The test author can create a finger and call various actions on it, currently
begin(), move() and end().

When these actions are processed, we compute all "stationary" actions for each part of the sequence
so that we can provide this to the uiController.sendEventStream() function.

Finally, we add a way to track events received by the target and assert that the events that were
received match those that were expected.

* pointerevents/ios/pointer-events-is-primary-expected.txt: Added.
* pointerevents/ios/pointer-events-is-primary.html: Added.
* pointerevents/utils.js:
(prototype.handleEvent):
(prototype.assertMatchesEvents):
(const.ui.new.UIController):
(const.ui.new.UIController.prototype.finger):
(const.ui.new.UIController.prototype.pinchOut):
(const.ui.new.UIController.prototype.sequence):
(const.ui.new.UIController.prototype._runEvents):
(prototype.begin):
(prototype.move):
(prototype.end):
(prototype.stationary):
(prototype._action):

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

LayoutTests/ChangeLog
LayoutTests/pointerevents/ios/pointer-events-is-primary-expected.txt [new file with mode: 0644]
LayoutTests/pointerevents/ios/pointer-events-is-primary.html [new file with mode: 0644]
LayoutTests/pointerevents/utils.js
Source/WebCore/ChangeLog
Source/WebCore/dom/PointerEvent.h
Source/WebCore/dom/ios/PointerEventIOS.cpp

index f8ae8e2..2c9c4d7 100644 (file)
@@ -1,3 +1,40 @@
+2018-11-16  Antoine Quint  <graouts@apple.com>
+
+        [Pointer Events] event.isPrimary doesn't always represent the oldest active touch
+        https://bugs.webkit.org/show_bug.cgi?id=191752
+        <rdar://problem/46129270>
+
+        Reviewed by Dean Jackson.
+
+        Add a new test that checks that adding a touch after another existing touch does not make it be
+        the primary touch, but that removing the first touch makes the second touch become the primary touch.
+
+        To do this we add a new ui.sequence() method that allows a series of touch actions to be performed
+        in a linear sequence. The test author can create a finger and call various actions on it, currently
+        begin(), move() and end().
+
+        When these actions are processed, we compute all "stationary" actions for each part of the sequence
+        so that we can provide this to the uiController.sendEventStream() function.
+
+        Finally, we add a way to track events received by the target and assert that the events that were
+        received match those that were expected.
+
+        * pointerevents/ios/pointer-events-is-primary-expected.txt: Added.
+        * pointerevents/ios/pointer-events-is-primary.html: Added.
+        * pointerevents/utils.js:
+        (prototype.handleEvent):
+        (prototype.assertMatchesEvents):
+        (const.ui.new.UIController):
+        (const.ui.new.UIController.prototype.finger):
+        (const.ui.new.UIController.prototype.pinchOut):
+        (const.ui.new.UIController.prototype.sequence):
+        (const.ui.new.UIController.prototype._runEvents):
+        (prototype.begin):
+        (prototype.move):
+        (prototype.end):
+        (prototype.stationary):
+        (prototype._action):
+
 2018-11-16  Devin Rousso  <drousso@apple.com>
 
         Web Inspector: Audit: minor style improvements
diff --git a/LayoutTests/pointerevents/ios/pointer-events-is-primary-expected.txt b/LayoutTests/pointerevents/ios/pointer-events-is-primary-expected.txt
new file mode 100644 (file)
index 0000000..d522456
--- /dev/null
@@ -0,0 +1,3 @@
+
+PASS Oldest active touch has isPrimary = true. 
+
diff --git a/LayoutTests/pointerevents/ios/pointer-events-is-primary.html b/LayoutTests/pointerevents/ios/pointer-events-is-primary.html
new file mode 100644 (file)
index 0000000..f31f93b
--- /dev/null
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset=utf-8>
+<meta name="viewport" content="width=device-width, initial-scale=1">
+</head>
+<body>
+<script src="../../resources/testharness.js"></script>
+<script src="../../resources/testharnessreport.js"></script>
+<script src="../utils.js"></script>
+<script>
+
+'use strict';
+
+target_test((target, test) => {
+    const eventTracker = new EventTracker(target, ["pointerdown", "pointermove"]);
+
+    const one = ui.finger();
+    const two = ui.finger();
+    ui.sequence([
+        one.begin({ x: 10, y: 10 }),
+        two.begin({ x: 50, y: 50 }),
+        two.move({ x: 70, y: 70 }),
+        one.move({ x: 30, y: 30 }),
+        one.end(),
+        two.move({ x: 50, y: 50 })
+    ]).then(() => {
+        eventTracker.assertMatchesEvents([
+            { id: 1, type: "pointerdown", x: 10, y: 10, isPrimary: true },
+            { id: 2, type: "pointerdown", x: 50, y: 50, isPrimary: false },
+            { id: 2, type: "pointermove", x: 70, y: 70, isPrimary: false },
+            { id: 1, type: "pointermove", x: 30, y: 30, isPrimary: true },
+            { id: 2, type: "pointermove", x: 50, y: 50, isPrimary: true }
+        ]);
+        test.done();
+    });
+}, "Oldest active touch has isPrimary = true.");
+
+</script>
+</body>
+</html>
\ No newline at end of file
index bbf36ce..0487af7 100644 (file)
@@ -32,8 +32,61 @@ function target_test(...args)
     }, description);
 }
 
+class EventTracker
+{
+
+    constructor(target, eventNames)
+    {
+        this.target = target;
+        this.events = [];
+        this.pointerIdToTouchIdMap = {};
+
+        for (let eventName of eventNames)
+            target.addEventListener(eventName, this);
+    }
+
+    handleEvent(event)
+    {
+        if (!this.pointerIdToTouchIdMap[event.pointerId])
+            this.pointerIdToTouchIdMap[event.pointerId] = Object.keys(this.pointerIdToTouchIdMap).length + 1;
+
+        const id = this.pointerIdToTouchIdMap[event.pointerId];
+        this.events.push({
+            id,
+            type: event.type,
+            x: event.clientX,
+            y: event.clientY,
+            isPrimary: event.isPrimary
+        });
+    }
+
+    assertMatchesEvents(expectedEvents)
+    {
+        assert_true(!!this.events.length, "Event tracker saw some events.");
+        assert_equals(expectedEvents.length, this.events.length, "Expected events and actual events have the same length.");
+        for (let i = 0; i < expectedEvents.length; ++i) {
+            const expectedEvent = expectedEvents[i];
+            const actualEvent = this.events[i];
+            for (let property of Object.getOwnPropertyNames(expectedEvent))
+                assert_equals(expectedEvent[property], actualEvent[property], `Property ${property} matches for event at index ${i}.`);
+        }
+    }
+
+}
+
 const ui = new (class UIController {
 
+    constructor()
+    {
+        this.fingers = {};
+    }
+
+    finger()
+    {
+        const id = Object.keys(this.fingers).length + 1;
+        return this.fingers[id] = new Finger(id);
+    }
+
     beginTouches(options)
     {
         return this._run(`uiController.touchDownAtPoint(${options.x}, ${options.y}, ${options.numberOfTouches || 1})`);
@@ -92,18 +145,52 @@ const ui = new (class UIController {
             ]
         };
 
-        return this._runEvents({
+        return this._runEvents([{
             interpolate : "linear",
             timestep: 0.1,
             coordinateSpace : "content",
             startEvent: startEvent,
             endEvent: endEvent
-        });
+        }]);
+    }
+
+    sequence(touches)
+    {
+        const activeFingers = {};
+
+        return this._runEvents(touches.map((touches, index) => {
+            if (!Array.isArray(touches))
+                touches = [touches];
+
+            const processedIDs = {};
+
+            // Update the list of active touches.
+            touches.forEach(touch => {
+                processedIDs[touch.id] = true;
+                if (touch.phase === "ended")
+                    delete activeFingers[touch.id];
+                else
+                    activeFingers[touch.id] = { x: touch.x, y: touch.y };
+            });
+
+            // Now go through the active touches and check that they're all listed in the new touches.
+            for (let id in activeFingers) {
+                if (!processedIDs[id])
+                    touches.push(this.fingers[id].stationary(activeFingers[id]));
+            }
+
+            return {
+                inputType : "hand",
+                timeOffset : index * 0.05,
+                coordinateSpace : "content",
+                touches : touches
+            }
+        }));
     }
 
     _runEvents(events)
     {
-        return this._run(`uiController.sendEventStream('${JSON.stringify({ events: [events] })}')`);
+        return this._run(`uiController.sendEventStream('${JSON.stringify({ events })}')`);
     }
 
     _run(command)
@@ -115,3 +202,40 @@ const ui = new (class UIController {
     }
 
 })();
+
+class Finger
+{
+
+    constructor(id)
+    {
+        this.id = id;
+    }
+
+    begin(options)
+    {
+        return this._action("began", options.x || 0, options.y || 0);
+    }
+
+    move(options)
+    {
+        return this._action("moved", options.x || 0, options.y || 0);
+    }
+
+    end(options)
+    {
+        return this._action("ended", this._lastX, this._lastY);
+    }
+
+    stationary(options)
+    {
+        return this._action("stationary", options.x || 0, options.y || 0);
+    }
+
+    _action(phase, x, y)
+    {
+        this._lastX = x;
+        this._lastY = y;
+        return { inputType: "finger", id: this.id, phase, x, y };
+    }
+
+}
index 4c66254..13b35e3 100644 (file)
@@ -1,3 +1,21 @@
+2018-11-16  Antoine Quint  <graouts@apple.com>
+
+        [Pointer Events] event.isPrimary doesn't always represent the oldest active touch
+        https://bugs.webkit.org/show_bug.cgi?id=191752
+        <rdar://problem/46129270>
+
+        Reviewed by Dean Jackson.
+
+        Provide isPrimary to the constructor so its value can be determined at the call site.
+
+        Test: pointerevents/ios/pointer-events-is-primary.html
+
+        * dom/PointerEvent.h:
+        * dom/ios/PointerEventIOS.cpp:
+        (WebCore::PointerEvent::create):
+        (WebCore::PointerEvent::PointerEvent):
+        (WebCore::m_isPrimary):
+
 2018-11-16  Alex Christensen  <achristensen@webkit.org>
 
         Tweak _showSafeBrowsingWarningWithTitle SPI
index 58cfe50..cf09143 100644 (file)
@@ -60,7 +60,7 @@ public:
     }
 
 #if ENABLE(TOUCH_EVENTS) && PLATFORM(IOS_FAMILY)
-    static Ref<PointerEvent> create(const PlatformTouchEvent&, unsigned touchIndex, Ref<WindowProxy>&&);
+    static Ref<PointerEvent> create(const PlatformTouchEvent&, unsigned touchIndex, bool isPrimary, Ref<WindowProxy>&&);
 #endif
 
     virtual ~PointerEvent();
@@ -84,7 +84,7 @@ private:
     PointerEvent();
     PointerEvent(const AtomicString&, Init&&);
 #if ENABLE(TOUCH_EVENTS) && PLATFORM(IOS_FAMILY)
-    PointerEvent(const AtomicString& type, const PlatformTouchEvent&, IsCancelable isCancelable, unsigned touchIndex, Ref<WindowProxy>&&);
+    PointerEvent(const AtomicString& type, const PlatformTouchEvent&, IsCancelable isCancelable, unsigned touchIndex, bool isPrimary, Ref<WindowProxy>&&);
 #endif
 
     long m_pointerId { 0 };
index 3e984a1..c4033cb 100644 (file)
@@ -57,19 +57,19 @@ static PointerEvent::IsCancelable phaseIsCancelable(PlatformTouchPoint::TouchPha
     return PointerEvent::IsCancelable::Yes;
 }
 
-Ref<PointerEvent> PointerEvent::create(const PlatformTouchEvent& event, unsigned index, Ref<WindowProxy>&& view)
+Ref<PointerEvent> PointerEvent::create(const PlatformTouchEvent& event, unsigned index, bool isPrimary, Ref<WindowProxy>&& view)
 {
     auto phase = event.touchPhaseAtIndex(index);
-    return adoptRef(*new PointerEvent(eventType(phase), event, phaseIsCancelable(phase), index, WTFMove(view)));
+    return adoptRef(*new PointerEvent(eventType(phase), event, phaseIsCancelable(phase), index, isPrimary, WTFMove(view)));
 }
 
-PointerEvent::PointerEvent(const AtomicString& type, const PlatformTouchEvent& event, IsCancelable isCancelable, unsigned index, Ref<WindowProxy>&& view)
+PointerEvent::PointerEvent(const AtomicString& type, const PlatformTouchEvent& event, IsCancelable isCancelable, unsigned index, bool isPrimary, Ref<WindowProxy>&& view)
     : MouseEvent(type, CanBubble::Yes, isCancelable, IsComposed::Yes, event.timestamp().approximateMonotonicTime(), WTFMove(view), 0, event.touchLocationAtIndex(index), event.touchLocationAtIndex(index), { }, event.modifiers(), 0, 0, nullptr, 0, 0, nullptr, IsSimulated::No, IsTrusted::Yes)
     , m_pointerId(event.touchIdentifierAtIndex(index))
     , m_width(2 * event.radiusXAtIndex(index))
     , m_height(2 * event.radiusYAtIndex(index))
     , m_pointerType(event.touchTypeAtIndex(index) == PlatformTouchPoint::TouchType::Stylus ? "pen"_s : "touch"_s)
-    , m_isPrimary(!index)
+    , m_isPrimary(isPrimary)
 {
 }