REGRESSION (iOS 13): Tests that simulate multiple back-to-back single taps fail or...
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 26 Aug 2019 19:37:29 +0000 (19:37 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 26 Aug 2019 19:37:29 +0000 (19:37 +0000)
https://bugs.webkit.org/show_bug.cgi?id=201129
<rdar://problem/51857277>

Reviewed by Tim Horton.

Source/WebKit:

Adds a new SPI hook in WebKit to let clients know when a synthetic tap gesture that has ended has been reset.
See Tools/ChangeLog and LayoutTests/ChangeLog for more details.

* UIProcess/API/Cocoa/WKWebView.mm:
(-[WKWebView _doAfterResettingSingleTapGesture:]):
* UIProcess/API/Cocoa/WKWebViewPrivate.h:
* UIProcess/ios/WKContentViewInteraction.h:
* UIProcess/ios/WKContentViewInteraction.mm:
(-[WKContentView _singleTapDidReset:]):
(-[WKContentView _doAfterResettingSingleTapGesture:]):

Tools:

The tests in editing/pasteboard/ios were timing out on iOS 13 before this change. This is because they simulate
back-to-back single taps; while this is recognized as two single taps on iOS 12 and prior, only the first single
tap is recognized on iOS 13 (and the second is simply dropped on the floor). This occurs because the synthetic
single tap gesture is reset slightly later on iOS 13 compared to iOS 12, so when the second tap is dispatched,
the gesture recognizer is still in "ended" state after the first tap on iOS 13, which means the gesture isn't
capable of recognizing further touches yet.

In UIKit, a gesture recognizer is only reset once its UIGestureEnvironment's containing dependency subgraph no
longer contains gestures that are active. In iOS 12, the synthetic click gesture is a part of a dependency
subgraph that contains only itself and the normal (blocking) double tap gesture which requires the click to fail
before it can be recognized; immediately after simulating the tap, both these gestures are inactive, which
allows both of them to be reset.

However, in iOS 13, the synthetic click gesture is part of a gesture dependency graph that contains the double
tap for double click gesture, as well as the non-blocking double tap gesture, both of which are still active
immediately after sending the first tap. This change in dependencies is caused by the introduction of
UIUndoGestureInteraction's single and double three-finger tap gestures, which (in -[UIUndoGestureInteraction
gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer:]) explicitly add all other taps as failure
requirements. This effectively links the synthetic single tap gesture to most of the other gestures in
WKContentView's dependency graph by way of these tap gestures for the undo interaction.

All this means that there is now a short (~50 ms) delay after the synthetic single tap gestures is recognized,
before it can be recognized again. To account for this new delay in our test infrastructure, simply wait for
single tap gestures that have ended to reset before attempting to send subsequent single taps. We do this by
introducing WebKit testing SPI to invoke a completion handler after resetting the synthetic click gesture (only
if necessary - i.e., if the gesture is in ended state when we are about to begin simulating the tap). This
allows calls to `UIScriptController::singleTapAtPoint` to be reliably recognized as single taps without
requiring arbitrary 120 ms "human speed" delays.

This fixes a number of flaky or failing layout tests, including the tests in editing/pasteboard/ios.

* TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl:
* TestRunnerShared/UIScriptContext/UIScriptController.h:
(WTR::UIScriptController::doubleTapAtPoint):

Add a `delay` parameter to `doubleTapAtPoint`. A number of layout tests were actually simulating double click
gestures by simulating two back-to-back single taps; this is done for the purposes of being able to add a "human
speed" delay prior to the second single tap gesture. After the change to wait for the single tap gesture to
reset before attempting to simulate the next tap, this strategy no longer works, since the second gesture is
recognized only as a single tap instead of a double tap.

Instead, we add a delay parameter to `UIScriptController::doubleTapAtPoint`, which the "human speed" double tap
gestures use instead to wait after simulating the first tap.

* WebKitTestRunner/ios/HIDEventGenerator.h:
* WebKitTestRunner/ios/HIDEventGenerator.mm:
(-[HIDEventGenerator _waitFor:]):
(-[HIDEventGenerator sendTaps:location:withNumberOfTouches:delay:completionBlock:]):

Plumb the tap gesture delay through to this helper method.

(-[HIDEventGenerator tap:completionBlock:]):
(-[HIDEventGenerator doubleTap:delay:completionBlock:]):
(-[HIDEventGenerator twoFingerTap:completionBlock:]):
(-[HIDEventGenerator sendTaps:location:withNumberOfTouches:completionBlock:]): Deleted.
(-[HIDEventGenerator doubleTap:completionBlock:]): Deleted.
* WebKitTestRunner/ios/UIScriptControllerIOS.h:
* WebKitTestRunner/ios/UIScriptControllerIOS.mm:
(WTR::UIScriptControllerIOS::waitForSingleTapToReset const):

Add a new helper to wait for the content view's single tap gesture to reset if needed; call this before
attempting to simulate single taps (either using a stylus, or with a regular touch).

(WTR::UIScriptControllerIOS::singleTapAtPointWithModifiers):
(WTR::UIScriptControllerIOS::doubleTapAtPoint):
(WTR::UIScriptControllerIOS::stylusTapAtPointWithModifiers):

LayoutTests:

Adjusts a few layout tests after changes to UIScriptController::doubleTapAtPoint and
UIScriptController::singleTapAtPoint.

* editing/selection/ios/change-selection-by-tapping.html:

Tweak this test to tap the page 12 times instead of 20 (which seems to cause occasional timeouts locally, when
running all layout tests with a dozen active simulators).

* fast/events/ios/double-tap-zoom.html:
* fast/events/ios/viewport-device-width-allows-double-tap-zoom-out.html:
* fast/events/ios/viewport-shrink-to-fit-allows-double-tap.html:

Augment a few call sites of `doubleTapAtPoint` with a 0 delay. Ideally, these should just use ui-helper.js, but
we can refactor these tests as a part of folding basic-gestures.js into ui-helper.js.

* http/tests/adClickAttribution/anchor-tag-attributes-validation-expected.txt:
* http/tests/security/anchor-download-block-crossorigin-expected.txt:

Rebaseline these layout tests, due to change in line numbers.

* platform/ipad/TestExpectations:

Unskip these tests on iPad, now that they should pass.

* pointerevents/utils.js:
(const.ui.new.UIController.prototype.doubleTapToZoom):
* resources/basic-gestures.js:
(return.new.Promise.):
(return.new.Promise):

Adjust some more call sites of `doubleTapAtPoint`. Ideally, these should use just `ui-helper.js` too.

* resources/ui-helper.js:
(window.UIHelper.doubleTapAt.return.new.Promise):
(window.UIHelper.doubleTapAt):
(window.UIHelper.humanSpeedDoubleTapAt):
(window.UIHelper.humanSpeedZoomByDoubleTappingAt):

Add a delay parameter to `doubleTapAt` to specify a delay after each simulated tap. By default, this is 0, but
the `humanSpeed*` helpers add a delay of 120 milliseconds. Additionally, these helpers were previously calling
`singleTapAtPoint` twice, with a timeout in between to add a delay. Instead, call `doubleTapAtPoint` with a
nonzero delay; otherwise, we'll end up waiting in `singleTapAtPoint` for the gesture subgraph containing both
the double tap gestures and the synthetic single tap gesture to reset, which causes these two single taps to no
longer be recognized as a double tap gesture.

(window.UIHelper.zoomByDoubleTappingAt):

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

23 files changed:
LayoutTests/ChangeLog
LayoutTests/editing/selection/ios/change-selection-by-tapping.html
LayoutTests/fast/events/ios/double-tap-zoom.html
LayoutTests/fast/events/ios/viewport-device-width-allows-double-tap-zoom-out.html
LayoutTests/fast/events/ios/viewport-shrink-to-fit-allows-double-tap.html
LayoutTests/http/tests/adClickAttribution/anchor-tag-attributes-validation-expected.txt
LayoutTests/http/tests/security/anchor-download-block-crossorigin-expected.txt
LayoutTests/platform/ipad/TestExpectations
LayoutTests/pointerevents/utils.js
LayoutTests/resources/basic-gestures.js
LayoutTests/resources/ui-helper.js
Source/WebKit/ChangeLog
Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm
Source/WebKit/UIProcess/API/Cocoa/WKWebViewPrivate.h
Source/WebKit/UIProcess/ios/WKContentViewInteraction.h
Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm
Tools/ChangeLog
Tools/TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl
Tools/TestRunnerShared/UIScriptContext/UIScriptController.h
Tools/WebKitTestRunner/ios/HIDEventGenerator.h
Tools/WebKitTestRunner/ios/HIDEventGenerator.mm
Tools/WebKitTestRunner/ios/UIScriptControllerIOS.h
Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm

index 7ecf09d..7ed1813 100644 (file)
@@ -1,3 +1,58 @@
+2019-08-26  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        REGRESSION (iOS 13): Tests that simulate multiple back-to-back single taps fail or time out
+        https://bugs.webkit.org/show_bug.cgi?id=201129
+        <rdar://problem/51857277>
+
+        Reviewed by Tim Horton.
+
+        Adjusts a few layout tests after changes to UIScriptController::doubleTapAtPoint and
+        UIScriptController::singleTapAtPoint.
+
+        * editing/selection/ios/change-selection-by-tapping.html:
+
+        Tweak this test to tap the page 12 times instead of 20 (which seems to cause occasional timeouts locally, when
+        running all layout tests with a dozen active simulators).
+
+        * fast/events/ios/double-tap-zoom.html:
+        * fast/events/ios/viewport-device-width-allows-double-tap-zoom-out.html:
+        * fast/events/ios/viewport-shrink-to-fit-allows-double-tap.html:
+
+        Augment a few call sites of `doubleTapAtPoint` with a 0 delay. Ideally, these should just use ui-helper.js, but
+        we can refactor these tests as a part of folding basic-gestures.js into ui-helper.js.
+
+        * http/tests/adClickAttribution/anchor-tag-attributes-validation-expected.txt:
+        * http/tests/security/anchor-download-block-crossorigin-expected.txt:
+
+        Rebaseline these layout tests, due to change in line numbers.
+
+        * platform/ipad/TestExpectations:
+
+        Unskip these tests on iPad, now that they should pass.
+
+        * pointerevents/utils.js:
+        (const.ui.new.UIController.prototype.doubleTapToZoom):
+        * resources/basic-gestures.js:
+        (return.new.Promise.):
+        (return.new.Promise):
+
+        Adjust some more call sites of `doubleTapAtPoint`. Ideally, these should use just `ui-helper.js` too.
+
+        * resources/ui-helper.js:
+        (window.UIHelper.doubleTapAt.return.new.Promise):
+        (window.UIHelper.doubleTapAt):
+        (window.UIHelper.humanSpeedDoubleTapAt):
+        (window.UIHelper.humanSpeedZoomByDoubleTappingAt):
+
+        Add a delay parameter to `doubleTapAt` to specify a delay after each simulated tap. By default, this is 0, but
+        the `humanSpeed*` helpers add a delay of 120 milliseconds. Additionally, these helpers were previously calling
+        `singleTapAtPoint` twice, with a timeout in between to add a delay. Instead, call `doubleTapAtPoint` with a
+        nonzero delay; otherwise, we'll end up waiting in `singleTapAtPoint` for the gesture subgraph containing both
+        the double tap gestures and the synthetic single tap gesture to reset, which causes these two single taps to no
+        longer be recognized as a double tap gesture.
+
+        (window.UIHelper.zoomByDoubleTappingAt):
+
 2019-08-26  Jiewen Tan  <jiewen_tan@apple.com>
 
         [WebAuthn] Support HID authenticators on iOS
index edd75c3..d57cdf6 100644 (file)
@@ -40,7 +40,7 @@ addEventListener("load", async () => {
     description("Verifies that rapidly tapping to change selection doesn't hang due to IPC deadlock. To verify manually, focus the editable text and tap repeatedly in different parts of the editable area to change selection; check that this does not result in sporadic 1-second IPC hangs.");
 
     await UIHelper.activateElementAndWaitForInputSession(document.getElementById("editor"));
-    for (let i = 0; i < 5; ++i) {
+    for (let i = 0; i < 3; ++i) {
         for (const [x, y] of [[40, 40], [220, 40], [40, 240], [220, 240]])
             await tapAndWaitForSelectionChange(x, y);
     }
index 3723de6..ff7cd18 100644 (file)
@@ -9,7 +9,7 @@
                 uiController.uiScriptComplete('Zoomed to scale ' + uiController.zoomScale);
             };
 
-            uiController.doubleTapAtPoint(50, 50, function() {});
+            uiController.doubleTapAtPoint(50, 50, 0, function() {});
         })();
     </script>
     <script>
index b227235..303532b 100644 (file)
@@ -8,7 +8,7 @@
             uiController.didEndZoomingCallback = function() {
                 uiController.uiScriptComplete(uiController.zoomScale);
             };
-            uiController.doubleTapAtPoint(15, 15, function() {});
+            uiController.doubleTapAtPoint(15, 15, 0, function() {});
         })();
     </script>
     <script>
index f38a1d7..01607a6 100644 (file)
@@ -16,7 +16,7 @@
                     uiController.didEndZoomingCallback = function() {
                         uiController.uiScriptComplete(uiController.zoomScale);
                     };
-                    uiController.doubleTapAtPoint(15, 60, function() {});
+                    uiController.doubleTapAtPoint(15, 60, 0, function() {});
                 })();`;
         }
 
index 7ae41e5..bf807e8 100644 (file)
@@ -1,14 +1,14 @@
-CONSOLE MESSAGE: line 192: adcampaignid must have a non-negative value less than or equal to 63 for Ad Click Attribution.
-CONSOLE MESSAGE: line 192: adcampaignid must have a non-negative value less than or equal to 63 for Ad Click Attribution.
-CONSOLE MESSAGE: line 192: adcampaignid can not be converted to a non-negative integer which is required for Ad Click Attribution.
-CONSOLE MESSAGE: line 192: adcampaignid can not be converted to a non-negative integer which is required for Ad Click Attribution.
-CONSOLE MESSAGE: line 192: adcampaignid can not be converted to a non-negative integer which is required for Ad Click Attribution.
-CONSOLE MESSAGE: line 192: addestination could not be converted to a valid HTTP-family URL.
-CONSOLE MESSAGE: line 192: addestination could not be converted to a valid HTTP-family URL.
-CONSOLE MESSAGE: line 192: addestination could not be converted to a valid HTTP-family URL.
-CONSOLE MESSAGE: line 192: Both adcampaignid and addestination need to be set for Ad Click Attribution to work.
-CONSOLE MESSAGE: line 192: Both adcampaignid and addestination need to be set for Ad Click Attribution to work.
-CONSOLE MESSAGE: line 192: addestination can not be the same site as the current website.
+CONSOLE MESSAGE: line 182: adcampaignid must have a non-negative value less than or equal to 63 for Ad Click Attribution.
+CONSOLE MESSAGE: line 182: adcampaignid must have a non-negative value less than or equal to 63 for Ad Click Attribution.
+CONSOLE MESSAGE: line 182: adcampaignid can not be converted to a non-negative integer which is required for Ad Click Attribution.
+CONSOLE MESSAGE: line 182: adcampaignid can not be converted to a non-negative integer which is required for Ad Click Attribution.
+CONSOLE MESSAGE: line 182: adcampaignid can not be converted to a non-negative integer which is required for Ad Click Attribution.
+CONSOLE MESSAGE: line 182: addestination could not be converted to a valid HTTP-family URL.
+CONSOLE MESSAGE: line 182: addestination could not be converted to a valid HTTP-family URL.
+CONSOLE MESSAGE: line 182: addestination could not be converted to a valid HTTP-family URL.
+CONSOLE MESSAGE: line 182: Both adcampaignid and addestination need to be set for Ad Click Attribution to work.
+CONSOLE MESSAGE: line 182: Both adcampaignid and addestination need to be set for Ad Click Attribution to work.
+CONSOLE MESSAGE: line 182: addestination can not be the same site as the current website.
 Test for validity of ad click attribution attributes on anchor tags.
 
 On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
index 759d568..345e07e 100644 (file)
@@ -1,4 +1,4 @@
-CONSOLE MESSAGE: line 165: The download attribute on anchor was ignored because its href URL has a different security origin.
+CONSOLE MESSAGE: line 155: The download attribute on anchor was ignored because its href URL has a different security origin.
 Tests that the download attribute is ignored if the link is cross origin.
 
 It should navigate the subframe instead of downloading the file.
index 2e81884..77c95aa 100644 (file)
@@ -55,12 +55,6 @@ http/tests/paymentrequest/payment-response-retry-method.https.html [ Skip ]
 http/tests/paymentrequest/rejects_if_not_active.https.html [ Skip ]
 http/tests/paymentrequest/updateWith-method-pmi-handling.https.html [ Skip ]
 
-# <rdar://problem/51857277> iOS 13 iPad: editing/pasteboard/ios/dom-paste-* layout tests timing out
-editing/pasteboard/ios/dom-paste-confirmation.html [ Skip ]
-editing/pasteboard/ios/dom-paste-consecutive-confirmations.html [ Skip ]
-editing/pasteboard/ios/dom-paste-rejection.html [ Skip ]
-editing/pasteboard/ios/dom-paste-requires-user-gesture.html [ Skip ]
-
 # <rdar://problem/51862629> REGRESSION (r244239) [ iPad Sim ] Layout Test fast/canvas/canvas-too-large-to-draw.html is failing
 fast/canvas/canvas-too-large-to-draw.html [ Failure ]
 
index 20878e8..6cfc3a9 100644 (file)
@@ -126,7 +126,7 @@ const ui = new (class UIController {
     doubleTapToZoom(options)
     {
         const durationInSeconds = 0.35;
-        return new Promise(resolve => this._run(`uiController.doubleTapAtPoint(${options.x}, ${options.y})`).then(() =>
+        return new Promise(resolve => this._run(`uiController.doubleTapAtPoint(${options.x}, ${options.y}, 0)`).then(() =>
             setTimeout(resolve, durationInSeconds * 1000)
         ));
         return this._run();
index 3c19ad2..d3e5a0c 100644 (file)
@@ -32,7 +32,7 @@ function doubleTapAtPoint(x, y)
     return new Promise(resolve => {
         testRunner.runUIScript(`
             (function() {
-                uiController.doubleTapAtPoint(${x}, ${y}, function() {
+                uiController.doubleTapAtPoint(${x}, ${y}, 0, function() {
                     uiController.uiScriptComplete();
                 });
             })();`, resolve);
index 3b58dc6..e75c8e8 100644 (file)
@@ -50,7 +50,7 @@ window.UIHelper = class UIHelper {
         });
     }
 
-    static doubleTapAt(x, y)
+    static doubleTapAt(x, y, delay = 0)
     {
         console.assert(this.isIOSFamily());
 
@@ -68,7 +68,7 @@ window.UIHelper = class UIHelper {
 
         return new Promise((resolve) => {
             testRunner.runUIScript(`
-                uiController.doubleTapAtPoint(${x}, ${y}, function() {
+                uiController.doubleTapAtPoint(${x}, ${y}, ${delay}, function() {
                     uiController.uiScriptComplete();
                 });`, resolve);
         });
@@ -91,12 +91,7 @@ window.UIHelper = class UIHelper {
             return Promise.resolve();
         }
 
-        return new Promise(async (resolve) => {
-            await UIHelper.tapAt(x, y);
-            await new Promise(resolveAfterDelay => setTimeout(resolveAfterDelay, 120));
-            await UIHelper.tapAt(x, y);
-            resolve();
-        });
+        return UIHelper.doubleTapAt(x, y, 0.12);
     }
 
     static humanSpeedZoomByDoubleTappingAt(x, y)
@@ -117,17 +112,12 @@ window.UIHelper = class UIHelper {
         }
 
         return new Promise(async (resolve) => {
-            await UIHelper.tapAt(x, y);
-            await new Promise(resolveAfterDelay => setTimeout(resolveAfterDelay, 120));
-            await new Promise((resolveAfterZoom) => {
-                testRunner.runUIScript(`
-                    uiController.didEndZoomingCallback = () => {
-                        uiController.didEndZoomingCallback = null;
-                        uiController.uiScriptComplete(uiController.zoomScale);
-                    };
-                    uiController.singleTapAtPoint(${x}, ${y}, () => {});`, resolveAfterZoom);
-            });
-            resolve();
+            testRunner.runUIScript(`
+                uiController.didEndZoomingCallback = () => {
+                    uiController.didEndZoomingCallback = null;
+                    uiController.uiScriptComplete(uiController.zoomScale);
+                };
+                uiController.doubleTapAtPoint(${x}, ${y}, 0.12, () => { });`, resolve);
         });
     }
 
@@ -153,7 +143,7 @@ window.UIHelper = class UIHelper {
                     uiController.didEndZoomingCallback = null;
                     uiController.uiScriptComplete(uiController.zoomScale);
                 };
-                uiController.doubleTapAtPoint(${x}, ${y}, () => {});`, resolve);
+                uiController.doubleTapAtPoint(${x}, ${y}, 0, () => { });`, resolve);
         });
     }
 
index 2fe5db5..5eaac19 100644 (file)
@@ -1,3 +1,22 @@
+2019-08-26  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        REGRESSION (iOS 13): Tests that simulate multiple back-to-back single taps fail or time out
+        https://bugs.webkit.org/show_bug.cgi?id=201129
+        <rdar://problem/51857277>
+
+        Reviewed by Tim Horton.
+
+        Adds a new SPI hook in WebKit to let clients know when a synthetic tap gesture that has ended has been reset.
+        See Tools/ChangeLog and LayoutTests/ChangeLog for more details.
+
+        * UIProcess/API/Cocoa/WKWebView.mm:
+        (-[WKWebView _doAfterResettingSingleTapGesture:]):
+        * UIProcess/API/Cocoa/WKWebViewPrivate.h:
+        * UIProcess/ios/WKContentViewInteraction.h:
+        * UIProcess/ios/WKContentViewInteraction.mm:
+        (-[WKContentView _singleTapDidReset:]):
+        (-[WKContentView _doAfterResettingSingleTapGesture:]):
+
 2019-08-26  Brent Fulgham  <bfulgham@apple.com>
 
         [FTW] Go back to ID2D1Bitmap as our NativeImage type
index 8c401d9..4b4297f 100644 (file)
@@ -7002,6 +7002,11 @@ static WebCore::UserInterfaceLayoutDirection toUserInterfaceLayoutDirection(UISe
     };
 }
 
+- (void)_doAfterResettingSingleTapGesture:(dispatch_block_t)action
+{
+    [_contentView _doAfterResettingSingleTapGesture:action];
+}
+
 - (void)_doAfterReceivingEditDragSnapshotForTesting:(dispatch_block_t)action
 {
     [_contentView _doAfterReceivingEditDragSnapshotForTesting:action];
index 739bcad..bd5eb7f 100644 (file)
@@ -481,6 +481,7 @@ typedef NS_OPTIONS(NSUInteger, _WKRectEdge) {
 - (void)_didShowForcePressPreview WK_API_AVAILABLE(ios(10.3));
 - (void)_didDismissForcePressPreview WK_API_AVAILABLE(ios(10.3));
 - (void)_doAfterNextStablePresentationUpdate:(dispatch_block_t)updateBlock WK_API_AVAILABLE(ios(10.3));
+- (void)_doAfterResettingSingleTapGesture:(dispatch_block_t)action WK_API_AVAILABLE(ios(WK_IOS_TBA));
 
 @property (nonatomic, readonly) NSArray<NSValue *> *_uiTextSelectionRects WK_API_AVAILABLE(ios(10.3));
 @property (nonatomic, readonly) CGRect _uiTextCaretRect WK_API_AVAILABLE(ios(10.3));
index 60cbcc1..da8bbfd 100644 (file)
@@ -385,6 +385,8 @@ struct WKAutoCorrectionData {
 #if ENABLE(PLATFORM_DRIVEN_TEXT_CHECKING)
     std::unique_ptr<WebKit::TextCheckingController> _textCheckingController;
 #endif
+
+    Vector<BlockPtr<void()>> _actionsToPerformAfterResettingSingleTapGestureRecognizer;
 }
 
 @end
@@ -555,6 +557,7 @@ FOR_EACH_PRIVATE_WKCONTENTVIEW_ACTION(DECLARE_WKCONTENTVIEW_ACTION_FOR_WEB_VIEW)
 - (void)selectFormAccessoryPickerRow:(NSInteger)rowIndex;
 - (void)setTimePickerValueToHour:(NSInteger)hour minute:(NSInteger)minute;
 - (NSDictionary *)_contentsOfUserInterfaceItem:(NSString *)userInterfaceItem;
+- (void)_doAfterResettingSingleTapGesture:(dispatch_block_t)action;
 - (void)_doAfterReceivingEditDragSnapshotForTesting:(dispatch_block_t)action;
 
 @property (nonatomic, readonly) NSString *textContentTypeForTesting;
index fd4b509..176bbe3 100644 (file)
@@ -2450,6 +2450,9 @@ static void cancelPotentialTapIfNecessary(WKContentView* contentView)
             _page->touchWithIdentifierWasRemoved(pointerId);
     }
 #endif
+    auto actionsToPerform = std::exchange(_actionsToPerformAfterResettingSingleTapGestureRecognizer, { });
+    for (auto action : actionsToPerform)
+        action();
 }
 
 - (void)_doubleTapDidFail:(UITapGestureRecognizer *)gestureRecognizer
@@ -7541,6 +7544,15 @@ static WebEventFlags webEventFlagsForUIKeyModifierFlags(UIKeyModifierFlags flags
 
 @implementation WKContentView (WKTesting)
 
+- (void)_doAfterResettingSingleTapGesture:(dispatch_block_t)action
+{
+    if ([_singleTapGestureRecognizer state] != UIGestureRecognizerStateEnded) {
+        action();
+        return;
+    }
+    _actionsToPerformAfterResettingSingleTapGestureRecognizer.append(makeBlockPtr(action));
+}
+
 - (void)_doAfterReceivingEditDragSnapshotForTesting:(dispatch_block_t)action
 {
 #if ENABLE(DRAG_SUPPORT)
index 920586b..a3d9578 100644 (file)
@@ -1,3 +1,78 @@
+2019-08-26  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        REGRESSION (iOS 13): Tests that simulate multiple back-to-back single taps fail or time out
+        https://bugs.webkit.org/show_bug.cgi?id=201129
+        <rdar://problem/51857277>
+
+        Reviewed by Tim Horton.
+
+        The tests in editing/pasteboard/ios were timing out on iOS 13 before this change. This is because they simulate
+        back-to-back single taps; while this is recognized as two single taps on iOS 12 and prior, only the first single
+        tap is recognized on iOS 13 (and the second is simply dropped on the floor). This occurs because the synthetic
+        single tap gesture is reset slightly later on iOS 13 compared to iOS 12, so when the second tap is dispatched,
+        the gesture recognizer is still in "ended" state after the first tap on iOS 13, which means the gesture isn't
+        capable of recognizing further touches yet.
+
+        In UIKit, a gesture recognizer is only reset once its UIGestureEnvironment's containing dependency subgraph no
+        longer contains gestures that are active. In iOS 12, the synthetic click gesture is a part of a dependency
+        subgraph that contains only itself and the normal (blocking) double tap gesture which requires the click to fail
+        before it can be recognized; immediately after simulating the tap, both these gestures are inactive, which
+        allows both of them to be reset.
+
+        However, in iOS 13, the synthetic click gesture is part of a gesture dependency graph that contains the double
+        tap for double click gesture, as well as the non-blocking double tap gesture, both of which are still active
+        immediately after sending the first tap. This change in dependencies is caused by the introduction of
+        UIUndoGestureInteraction's single and double three-finger tap gestures, which (in -[UIUndoGestureInteraction
+        gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer:]) explicitly add all other taps as failure
+        requirements. This effectively links the synthetic single tap gesture to most of the other gestures in
+        WKContentView's dependency graph by way of these tap gestures for the undo interaction.
+
+        All this means that there is now a short (~50 ms) delay after the synthetic single tap gestures is recognized,
+        before it can be recognized again. To account for this new delay in our test infrastructure, simply wait for
+        single tap gestures that have ended to reset before attempting to send subsequent single taps. We do this by
+        introducing WebKit testing SPI to invoke a completion handler after resetting the synthetic click gesture (only
+        if necessary - i.e., if the gesture is in ended state when we are about to begin simulating the tap). This
+        allows calls to `UIScriptController::singleTapAtPoint` to be reliably recognized as single taps without
+        requiring arbitrary 120 ms "human speed" delays.
+
+        This fixes a number of flaky or failing layout tests, including the tests in editing/pasteboard/ios.
+
+        * TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl:
+        * TestRunnerShared/UIScriptContext/UIScriptController.h:
+        (WTR::UIScriptController::doubleTapAtPoint):
+
+        Add a `delay` parameter to `doubleTapAtPoint`. A number of layout tests were actually simulating double click
+        gestures by simulating two back-to-back single taps; this is done for the purposes of being able to add a "human
+        speed" delay prior to the second single tap gesture. After the change to wait for the single tap gesture to
+        reset before attempting to simulate the next tap, this strategy no longer works, since the second gesture is
+        recognized only as a single tap instead of a double tap.
+
+        Instead, we add a delay parameter to `UIScriptController::doubleTapAtPoint`, which the "human speed" double tap
+        gestures use instead to wait after simulating the first tap.
+
+        * WebKitTestRunner/ios/HIDEventGenerator.h:
+        * WebKitTestRunner/ios/HIDEventGenerator.mm:
+        (-[HIDEventGenerator _waitFor:]):
+        (-[HIDEventGenerator sendTaps:location:withNumberOfTouches:delay:completionBlock:]):
+
+        Plumb the tap gesture delay through to this helper method.
+
+        (-[HIDEventGenerator tap:completionBlock:]):
+        (-[HIDEventGenerator doubleTap:delay:completionBlock:]):
+        (-[HIDEventGenerator twoFingerTap:completionBlock:]):
+        (-[HIDEventGenerator sendTaps:location:withNumberOfTouches:completionBlock:]): Deleted.
+        (-[HIDEventGenerator doubleTap:completionBlock:]): Deleted.
+        * WebKitTestRunner/ios/UIScriptControllerIOS.h:
+        * WebKitTestRunner/ios/UIScriptControllerIOS.mm:
+        (WTR::UIScriptControllerIOS::waitForSingleTapToReset const):
+
+        Add a new helper to wait for the content view's single tap gesture to reset if needed; call this before
+        attempting to simulate single taps (either using a stylus, or with a regular touch).
+
+        (WTR::UIScriptControllerIOS::singleTapAtPointWithModifiers):
+        (WTR::UIScriptControllerIOS::doubleTapAtPoint):
+        (WTR::UIScriptControllerIOS::stylusTapAtPointWithModifiers):
+
 2019-08-26  Jonathan Bedard  <jbedard@apple.com>
 
         results.webkit.org: Allow clicking on the tooltip arrow
index 7fffc77..6d94987 100644 (file)
@@ -57,7 +57,7 @@ interface UIScriptController {
     void liftUpAtPoint(long x, long y, long touchCount, object callback);
     void singleTapAtPoint(long x, long y, object callback);
     void singleTapAtPointWithModifiers(long x, long y, object modifierArray, object callback);
-    void doubleTapAtPoint(long x, long y, object callback);
+    void doubleTapAtPoint(long x, long y, float delay, object callback);
     void dragFromPointToPoint(long startX, long startY, long endX, long endY, double durationSeconds, object callback);
 
     void longPressAtPoint(long x, long y, object callback);
index f813300..ffe507f 100644 (file)
@@ -135,7 +135,7 @@ public:
     virtual void liftUpAtPoint(long x, long y, long touchCount, JSValueRef callback) { notImplemented(); }
     virtual void singleTapAtPoint(long x, long y, JSValueRef callback) { notImplemented(); }
     virtual void singleTapAtPointWithModifiers(long x, long y, JSValueRef modifierArray, JSValueRef callback) { notImplemented(); }
-    virtual void doubleTapAtPoint(long x, long y, JSValueRef callback) { notImplemented(); }
+    virtual void doubleTapAtPoint(long x, long y, float delay, JSValueRef callback) { notImplemented(); }
     virtual void dragFromPointToPoint(long startX, long startY, long endX, long endY, double durationSeconds, JSValueRef callback) { notImplemented(); }
     virtual void longPressAtPoint(long x, long y, JSValueRef callback) { notImplemented(); }
 
index a523e3b..08fb5c7 100644 (file)
@@ -87,7 +87,7 @@ RetainPtr<IOHIDEventRef> createHIDKeyEvent(NSString *, uint64_t timestamp, bool
 
 // Taps
 - (void)tap:(CGPoint)location completionBlock:(void (^)(void))completionBlock;
-- (void)doubleTap:(CGPoint)location completionBlock:(void (^)(void))completionBlock;
+- (void)doubleTap:(CGPoint)location delay:(NSTimeInterval)delay completionBlock:(void (^)(void))completionBlock;
 - (void)twoFingerTap:(CGPoint)location completionBlock:(void (^)(void))completionBlock;
 
 // Long Press
index 89991c2..76c5e16 100644 (file)
@@ -712,7 +712,21 @@ static InterpolationType interpolationFromString(NSString *string)
     [self sendMarkerHIDEventWithCompletionBlock:completionBlock];
 }
 
-- (void)sendTaps:(int)tapCount location:(CGPoint)location withNumberOfTouches:(int)touchCount completionBlock:(void (^)(void))completionBlock
+- (void)_waitFor:(NSTimeInterval)delay
+{
+    if (delay <= 0)
+        return;
+
+    bool doneWaitingForDelay = false;
+    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), [&doneWaitingForDelay] {
+        doneWaitingForDelay = true;
+    });
+
+    while (!doneWaitingForDelay)
+        [NSRunLoop.currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:NSDate.distantFuture];
+}
+
+- (void)sendTaps:(int)tapCount location:(CGPoint)location withNumberOfTouches:(int)touchCount delay:(NSTimeInterval)delay completionBlock:(void (^)(void))completionBlock
 {
     struct timespec doubleDelay = { 0, static_cast<long>(multiTapInterval * nanosecondsPerSecond) };
     struct timespec pressDelay = { 0, static_cast<long>(fingerLiftDelay * nanosecondsPerSecond) };
@@ -723,6 +737,8 @@ static InterpolationType interpolationFromString(NSString *string)
         [self liftUp:location touchCount:touchCount];
         if (i + 1 != tapCount) 
             nanosleep(&doubleDelay, 0);
+
+        [self _waitFor:delay];
     }
     
     [self sendMarkerHIDEventWithCompletionBlock:completionBlock];
@@ -730,17 +746,17 @@ static InterpolationType interpolationFromString(NSString *string)
 
 - (void)tap:(CGPoint)location completionBlock:(void (^)(void))completionBlock
 {
-    [self sendTaps:1 location:location withNumberOfTouches:1 completionBlock:completionBlock];
+    [self sendTaps:1 location:location withNumberOfTouches:1 delay:0 completionBlock:completionBlock];
 }
 
-- (void)doubleTap:(CGPoint)location completionBlock:(void (^)(void))completionBlock
+- (void)doubleTap:(CGPoint)location delay:(NSTimeInterval)delay completionBlock:(void (^)(void))completionBlock
 {
-    [self sendTaps:2 location:location withNumberOfTouches:1 completionBlock:completionBlock];
+    [self sendTaps:2 location:location withNumberOfTouches:1 delay:delay completionBlock:completionBlock];
 }
 
 - (void)twoFingerTap:(CGPoint)location completionBlock:(void (^)(void))completionBlock
 {
-    [self sendTaps:1 location:location withNumberOfTouches:2 completionBlock:completionBlock];
+    [self sendTaps:1 location:location withNumberOfTouches:2 delay:0 completionBlock:completionBlock];
 }
 
 - (void)longPress:(CGPoint)location completionBlock:(void (^)(void))completionBlock
index d8d4331..77dd55c 100644 (file)
@@ -52,7 +52,7 @@ public:
     void liftUpAtPoint(long x, long y, long touchCount, JSValueRef) override;
     void singleTapAtPoint(long x, long y, JSValueRef) override;
     void singleTapAtPointWithModifiers(long x, long y, JSValueRef modifierArray, JSValueRef) override;
-    void doubleTapAtPoint(long x, long y, JSValueRef) override;
+    void doubleTapAtPoint(long x, long y, float delay, JSValueRef) override;
     void stylusDownAtPoint(long x, long y, float azimuthAngle, float altitudeAngle, float pressure, JSValueRef) override;
     void stylusMoveToPoint(long x, long y, float azimuthAngle, float altitudeAngle, float pressure, JSValueRef) override;
     void stylusUpAtPoint(long x, long y, JSValueRef) override;
@@ -139,6 +139,9 @@ public:
     void setDidDismissPopoverCallback(JSValueRef) override;
     void setDidEndScrollingCallback(JSValueRef) override;
     void clearAllCallbacks() override;
+
+private:
+    void waitForSingleTapToReset() const;
 };
 
 }
index 3e0bdec..e10d9ec 100644 (file)
@@ -263,10 +263,21 @@ void UIScriptControllerIOS::singleTapAtPoint(long x, long y, JSValueRef callback
     singleTapAtPointWithModifiers(x, y, nullptr, callback);
 }
 
+void UIScriptControllerIOS::waitForSingleTapToReset() const
+{
+    bool doneWaitingForSingleTapToReset = false;
+    [webView() _doAfterResettingSingleTapGesture:[&doneWaitingForSingleTapToReset] {
+        doneWaitingForSingleTapToReset = true;
+    }];
+    TestController::singleton().runUntil(doneWaitingForSingleTapToReset, 0.5_s);
+}
+
 void UIScriptControllerIOS::singleTapAtPointWithModifiers(long x, long y, JSValueRef modifierArray, JSValueRef callback)
 {
     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
 
+    waitForSingleTapToReset();
+
     auto modifierFlags = parseModifierArray(m_context->jsContext(), modifierArray);
     for (auto& modifierFlag : modifierFlags)
         [[HIDEventGenerator sharedHIDEventGenerator] keyDown:modifierFlag];
@@ -286,11 +297,11 @@ void UIScriptControllerIOS::singleTapAtPointWithModifiers(long x, long y, JSValu
     }];
 }
 
-void UIScriptControllerIOS::doubleTapAtPoint(long x, long y, JSValueRef callback)
+void UIScriptControllerIOS::doubleTapAtPoint(long x, long y, float delay, JSValueRef callback)
 {
     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
 
-    [[HIDEventGenerator sharedHIDEventGenerator] doubleTap:globalToContentCoordinates(webView(), x, y) completionBlock:^{
+    [[HIDEventGenerator sharedHIDEventGenerator] doubleTap:globalToContentCoordinates(webView(), x, y) delay:delay completionBlock:^{
         if (!m_context)
             return;
         m_context->asyncTaskComplete(callbackID);
@@ -342,6 +353,8 @@ void UIScriptControllerIOS::stylusTapAtPointWithModifiers(long x, long y, float
 {
     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
 
+    waitForSingleTapToReset();
+
     auto modifierFlags = parseModifierArray(m_context->jsContext(), modifierArray);
     for (auto& modifierFlag : modifierFlags)
         [[HIDEventGenerator sharedHIDEventGenerator] keyDown:modifierFlag];