[iOS] [WebKit2] Add support for honoring -[UIMenuItem dontDismiss]
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 17 Apr 2019 03:34:10 +0000 (03:34 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 17 Apr 2019 03:34:10 +0000 (03:34 +0000)
https://bugs.webkit.org/show_bug.cgi?id=196919
<rdar://problem/41630459>

Reviewed by Tim Horton.

Source/WebKit:

Adds modern WebKit support for -dontDismiss by implementing a couple of new platform hooks. Covered by a new
layout test: editing/selection/ios/selection-after-changing-text-with-callout-menu.html.

* Platform/spi/ios/UIKitSPI.h:

Declare the private -dontDismiss property of UIMenuItem.

* UIProcess/API/Cocoa/WKWebView.mm:
(-[WKWebView willFinishIgnoringCalloutBarFadeAfterPerformingAction]):

Additionally teach the web view (not just the content view) to respond to the hook. This matters in the case
where the WebKit client (most notably, Mail) overrides WKWebView methods to define custom actions in the menu
controller. This scenario is exercised by the new layout test.

* UIProcess/ios/WKContentViewInteraction.h:
* UIProcess/ios/WKContentViewInteraction.mm:
(-[WKContentView willFinishIgnoringCalloutBarFadeAfterPerformingAction]):

If an action was performed where callout bar fading was ignored, then in WebKit, don't allow selection changes
to fade the callout bar until after the next remote layer tree commit.

(-[WKContentView _updateChangedSelection:]):

Stop suppressing selection updates when showing B/I/U controls, now that we can properly honor the -dontDismiss
property. This was originally introduced in <rdar://problem/15199925>, presumably to ensure that B/I/U buttons
(which have -dontDismiss set to YES) don't trigger selection change and end up dismissing themselves; however,
if triggering B/I/U actually changes the selection rects, this also means that the selection rects on-screen
would be stale after triggering these actions. This effect is most noticeable when bolding text.

(-[WKContentView shouldAllowHidingSelectionCommands]):

Tools:

Add iOS support for several new testing hooks. See below for more detail.

* DumpRenderTree/ios/UIScriptControllerIOS.mm:
(WTR::UIScriptController::isDismissingMenu const):

Add a new script controller method to query whether the platform menu (on iOS, the callout bar) is done
dismissing. We consider the menu to be dismissing in between the `-WillHide` and `-DidHide` notifications sent
by UIKit when dismissing the callout bar (i.e. UIMenuController).

* TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl:
* TestRunnerShared/UIScriptContext/UIScriptController.cpp:
(WTR::UIScriptController::isDismissingMenu const):
* TestRunnerShared/UIScriptContext/UIScriptController.h:
* WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl:
* WebKitTestRunner/InjectedBundle/InjectedBundle.cpp:
(WTR::InjectedBundle::didReceiveMessageToPage):
* WebKitTestRunner/InjectedBundle/TestRunner.cpp:
(WTR::TestRunner::setAllowedMenuActions):

Add a new helper method to specify a list of allowed actions when bringing up the menu. On iOS, in the case of
actions supported by the platform, this matches against method selector names (for instance, "SelectAll", or
"Copy", or "Paste"). In the case of the custom actions installed via `installCustomMenuAction`, we instead match
against the name of the custom action.

(WTR::TestRunner::installCustomMenuAction):

Add a new helper method to install a custom action for the context menu (on iOS, this is the callout bar). This
takes the name of the action (which appears in a button in the callout bar), whether the action should cause
the callout bar to automatically dismiss, and finally, a JavaScript callback that is invoked when the action is
triggered.

(WTR::TestRunner::performCustomMenuAction):

Invoked when the custom menu action is triggered.

* WebKitTestRunner/InjectedBundle/TestRunner.h:
* WebKitTestRunner/TestController.cpp:
(WTR::TestController::installCustomMenuAction):
(WTR::TestController::setAllowedMenuActions):
* WebKitTestRunner/TestController.h:
* WebKitTestRunner/TestInvocation.cpp:
(WTR::TestInvocation::didReceiveMessageFromInjectedBundle):
(WTR::TestInvocation::performCustomMenuAction):

Add plumbing to call back into the injected bundle when performing the custom action.

* WebKitTestRunner/TestInvocation.h:
* WebKitTestRunner/cocoa/TestControllerCocoa.mm:
(WTR::TestController::installCustomMenuAction):
(WTR::TestController::setAllowedMenuActions):
* WebKitTestRunner/cocoa/TestRunnerWKWebView.h:
* WebKitTestRunner/cocoa/TestRunnerWKWebView.mm:
(-[TestRunnerWKWebView initWithFrame:configuration:]):
(-[TestRunnerWKWebView becomeFirstResponder]):
(-[TestRunnerWKWebView _addCustomItemToMenuControllerIfNecessary]):

Helper method that converts web view's current custom menu action info into a UIMenuItem, and adds it to the
shared menu controller. This is also invoked when the web view becomes first responder, which matches behavior
in the Mail app on iOS.

(-[TestRunnerWKWebView installCustomMenuAction:dismissesAutomatically:callback:]):
(-[TestRunnerWKWebView setAllowedMenuActions:]):
(-[TestRunnerWKWebView resetCustomMenuAction]):
(-[TestRunnerWKWebView performCustomAction:]):
(-[TestRunnerWKWebView canPerformAction:withSender:]):
(-[TestRunnerWKWebView _willHideMenu]):
(-[TestRunnerWKWebView _didHideMenu]):
* WebKitTestRunner/ios/TestControllerIOS.mm:
(WTR::TestController::platformResetStateToConsistentValues):

Reset both any custom installed actions on the shared menu controller, as well as the list of allowed actions,
if specified.

* WebKitTestRunner/ios/UIScriptControllerIOS.mm:
(WTR::UIScriptController::isDismissingMenu const):

LayoutTests:

Add a new iOS layout test that installs a custom, non-dismissing action in the callout menu that enlarges text.
The test then activates this custom menu item and checks that the selection rects after triggering this custom
action are updated, and the callout bar is still showing.

* editing/selection/ios/selection-after-changing-text-with-callout-menu-expected.txt: Added.
* editing/selection/ios/selection-after-changing-text-with-callout-menu.html: Added.

This test additionally suppresses all callout bar menu items except for the custom "Embiggen" action, to ensure
that the "Embiggen" option can be tapped from the layout test without having to navigate callout bar items by
tapping on the "Next" and "Show styles" buttons. This latter approach is very challenging to make reliable in
automation; when navigating submenus in the callout bar, the next button can't be tapped until the current
callout bar transition animation is complete, but there's no delegate method invoked or notification posted when
this happens.

* resources/ui-helper.js:
(window.UIHelper.isShowingMenu):
(window.UIHelper.isDismissingMenu):
(window.UIHelper.rectForMenuAction):
(window.UIHelper.async.chooseMenuAction):

Additionally add a few more UIHelper methods.

(window.UIHelper):

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

27 files changed:
LayoutTests/ChangeLog
LayoutTests/editing/selection/ios/selection-after-changing-text-with-callout-menu-expected.txt [new file with mode: 0644]
LayoutTests/editing/selection/ios/selection-after-changing-text-with-callout-menu.html [new file with mode: 0644]
LayoutTests/resources/ui-helper.js
Source/WebKit/ChangeLog
Source/WebKit/Platform/spi/ios/UIKitSPI.h
Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm
Source/WebKit/UIProcess/ios/WKContentViewInteraction.h
Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm
Tools/ChangeLog
Tools/DumpRenderTree/ios/UIScriptControllerIOS.mm
Tools/TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl
Tools/TestRunnerShared/UIScriptContext/UIScriptController.cpp
Tools/TestRunnerShared/UIScriptContext/UIScriptController.h
Tools/WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl
Tools/WebKitTestRunner/InjectedBundle/InjectedBundle.cpp
Tools/WebKitTestRunner/InjectedBundle/TestRunner.cpp
Tools/WebKitTestRunner/InjectedBundle/TestRunner.h
Tools/WebKitTestRunner/TestController.cpp
Tools/WebKitTestRunner/TestController.h
Tools/WebKitTestRunner/TestInvocation.cpp
Tools/WebKitTestRunner/TestInvocation.h
Tools/WebKitTestRunner/cocoa/TestControllerCocoa.mm
Tools/WebKitTestRunner/cocoa/TestRunnerWKWebView.h
Tools/WebKitTestRunner/cocoa/TestRunnerWKWebView.mm
Tools/WebKitTestRunner/ios/TestControllerIOS.mm
Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm

index b647b69..41bab9a 100644 (file)
@@ -1,3 +1,35 @@
+2019-04-16  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS] [WebKit2] Add support for honoring -[UIMenuItem dontDismiss]
+        https://bugs.webkit.org/show_bug.cgi?id=196919
+        <rdar://problem/41630459>
+
+        Reviewed by Tim Horton.
+
+        Add a new iOS layout test that installs a custom, non-dismissing action in the callout menu that enlarges text.
+        The test then activates this custom menu item and checks that the selection rects after triggering this custom
+        action are updated, and the callout bar is still showing.
+
+        * editing/selection/ios/selection-after-changing-text-with-callout-menu-expected.txt: Added.
+        * editing/selection/ios/selection-after-changing-text-with-callout-menu.html: Added.
+
+        This test additionally suppresses all callout bar menu items except for the custom "Embiggen" action, to ensure
+        that the "Embiggen" option can be tapped from the layout test without having to navigate callout bar items by
+        tapping on the "Next" and "Show styles" buttons. This latter approach is very challenging to make reliable in
+        automation; when navigating submenus in the callout bar, the next button can't be tapped until the current
+        callout bar transition animation is complete, but there's no delegate method invoked or notification posted when
+        this happens.
+
+        * resources/ui-helper.js:
+        (window.UIHelper.isShowingMenu):
+        (window.UIHelper.isDismissingMenu):
+        (window.UIHelper.rectForMenuAction):
+        (window.UIHelper.async.chooseMenuAction):
+
+        Additionally add a few more UIHelper methods.
+
+        (window.UIHelper):
+
 2019-04-16  John Wilander  <wilander@apple.com>
 
         Set test conditions closer to conversion redirect in LayoutTests/http/tests/adClickAttribution/send-attribution-conversion-request.html
diff --git a/LayoutTests/editing/selection/ios/selection-after-changing-text-with-callout-menu-expected.txt b/LayoutTests/editing/selection/ios/selection-after-changing-text-with-callout-menu-expected.txt
new file mode 100644 (file)
index 0000000..b58b9f8
--- /dev/null
@@ -0,0 +1,19 @@
+The quick brown fox jumped over the lazy dog.
+This test verifies that interacting with a menu action item on iOS that is marked with 'dontDismiss' does not cause the callout menu to dismiss, even if the action changes the selection. To test manually, select the text and tap an item in the callout menu that shouldn't automatically dismiss the menu but changes the selection rects (for instance, Bold). Verify that the callout menu remains visible after tapping this item.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+PASS selectionRects[0].top is 101
+PASS selectionRects[0].width is 320
+PASS selectionRects[0].left is 1
+PASS selectionRects[0].height is 29
+PASS selectionRects[1].top is 130
+PASS selectionRects[1].width is 172
+PASS selectionRects[1].left is 1
+PASS selectionRects[1].height is 30
+PASS dismissingMenu is false
+PASS showingMenu is true
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/editing/selection/ios/selection-after-changing-text-with-callout-menu.html b/LayoutTests/editing/selection/ios/selection-after-changing-text-with-callout-menu.html
new file mode 100644 (file)
index 0000000..7d195ef
--- /dev/null
@@ -0,0 +1,68 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ useFlexibleViewport=true ] -->
+<html>
+<head>
+<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
+<script src="../../../resources/ui-helper.js"></script>
+<script src="../../../resources/js-test.js"></script>
+<style>
+body, html {
+    width: 100%;
+    height: 100%;
+    margin: 0;
+}
+
+#editor {
+    width: 320px;
+    height: 200px;
+    border: solid 1px tomato;
+    margin-top: 100px;
+}
+
+.big {
+    font-size: 24px;
+}
+</style>
+<script>
+jsTestIsAsync = true;
+
+if (window.testRunner) {
+    testRunner.setAllowedMenuActions(["Embiggen"]);
+    testRunner.installCustomMenuAction("Embiggen", false, () => document.querySelector("#editor").classList.add("big"));
+}
+
+addEventListener("load", runTest);
+async function runTest() {
+    description("This test verifies that interacting with a menu action item on iOS that is marked with 'dontDismiss' does not cause the callout menu to dismiss, even if the action changes the selection. To test manually, select the text and tap an item in the callout menu that shouldn't automatically dismiss the menu but changes the selection rects (for instance, Bold). Verify that the callout menu remains visible after tapping this item.");
+
+    await UIHelper.activateElementAndWaitForInputSession(editor);
+    await UIHelper.keyDown("a", ["metaKey"]);
+    await UIHelper.waitForMenuToShow();
+    await UIHelper.chooseMenuAction("Embiggen");
+
+    selectionRects = null;
+    while (!selectionRects || selectionRects.length != 2)
+        selectionRects = await UIHelper.getUISelectionViewRects();
+    dismissingMenu = await UIHelper.isDismissingMenu();
+    showingMenu = await UIHelper.isShowingMenu();
+
+    shouldBe("selectionRects[0].top", "101");
+    shouldBe("selectionRects[0].width", "320");
+    shouldBe("selectionRects[0].left", "1");
+    shouldBe("selectionRects[0].height", "29");
+    shouldBe("selectionRects[1].top", "130");
+    shouldBe("selectionRects[1].width", "172");
+    shouldBe("selectionRects[1].left", "1");
+    shouldBe("selectionRects[1].height", "30");
+    shouldBe("dismissingMenu", "false");
+    shouldBe("showingMenu", "true");
+
+    finishJSTest();
+}
+</script>
+</head>
+<body>
+<div id="editor" contenteditable>The quick brown fox jumped over the lazy dog.</div>
+<div id="description"></div>
+<div id="console"></div>
+</body>
+</html>
index 2c9e290..30ab8b5 100644 (file)
@@ -827,6 +827,20 @@ window.UIHelper = class UIHelper {
         });
     }
 
+    static isShowingMenu()
+    {
+        return new Promise(resolve => {
+            testRunner.runUIScript(`uiController.isShowingMenu`, result => resolve(result === "true"));
+        });
+    }
+
+    static isDismissingMenu()
+    {
+        return new Promise(resolve => {
+            testRunner.runUIScript(`uiController.isDismissingMenu`, result => resolve(result === "true"));
+        });
+    }
+
     static menuRect()
     {
         return new Promise(resolve => {
@@ -838,4 +852,23 @@ window.UIHelper = class UIHelper {
     {
         return new Promise(resolve => testRunner.runUIScript(`uiController.setHardwareKeyboardAttached(${attached ? "true" : "false"})`, resolve));
     }
+
+    static rectForMenuAction(action)
+    {
+        return new Promise(resolve => {
+            testRunner.runUIScript(`
+                const rect = uiController.rectForMenuAction("${action}");
+                uiController.uiScriptComplete(rect ? JSON.stringify(rect) : "");
+            `, stringResult => {
+                resolve(stringResult.length ? JSON.parse(stringResult) : null);
+            });
+        });
+    }
+
+    static async chooseMenuAction(action)
+    {
+        const menuRect = await this.rectForMenuAction(action);
+        if (menuRect)
+            await this.activateAt(menuRect.left + menuRect.width / 2, menuRect.top + menuRect.height / 2);
+    }
 }
index 0eae98e..5d9e047 100644 (file)
@@ -1,3 +1,42 @@
+2019-04-16  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS] [WebKit2] Add support for honoring -[UIMenuItem dontDismiss]
+        https://bugs.webkit.org/show_bug.cgi?id=196919
+        <rdar://problem/41630459>
+
+        Reviewed by Tim Horton.
+
+        Adds modern WebKit support for -dontDismiss by implementing a couple of new platform hooks. Covered by a new
+        layout test: editing/selection/ios/selection-after-changing-text-with-callout-menu.html.
+
+        * Platform/spi/ios/UIKitSPI.h:
+
+        Declare the private -dontDismiss property of UIMenuItem.
+
+        * UIProcess/API/Cocoa/WKWebView.mm:
+        (-[WKWebView willFinishIgnoringCalloutBarFadeAfterPerformingAction]):
+
+        Additionally teach the web view (not just the content view) to respond to the hook. This matters in the case
+        where the WebKit client (most notably, Mail) overrides WKWebView methods to define custom actions in the menu
+        controller. This scenario is exercised by the new layout test.
+
+        * UIProcess/ios/WKContentViewInteraction.h:
+        * UIProcess/ios/WKContentViewInteraction.mm:
+        (-[WKContentView willFinishIgnoringCalloutBarFadeAfterPerformingAction]):
+
+        If an action was performed where callout bar fading was ignored, then in WebKit, don't allow selection changes
+        to fade the callout bar until after the next remote layer tree commit.
+
+        (-[WKContentView _updateChangedSelection:]):
+
+        Stop suppressing selection updates when showing B/I/U controls, now that we can properly honor the -dontDismiss
+        property. This was originally introduced in <rdar://problem/15199925>, presumably to ensure that B/I/U buttons
+        (which have -dontDismiss set to YES) don't trigger selection change and end up dismissing themselves; however,
+        if triggering B/I/U actually changes the selection rects, this also means that the selection rects on-screen
+        would be stale after triggering these actions. This effect is most noticeable when bolding text.
+
+        (-[WKContentView shouldAllowHidingSelectionCommands]):
+
 2019-04-16  Ross Kirsling  <ross.kirsling@sony.com>
 
         Unreviewed non-unified build fix after r244307.
index 41cb810..00d4222 100644 (file)
@@ -48,6 +48,7 @@
 #import <UIKit/UIKeyboardIntl.h>
 #import <UIKit/UIKeyboard_Private.h>
 #import <UIKit/UILongPressGestureRecognizer_Private.h>
+#import <UIKit/UIMenuController_Private.h>
 #import <UIKit/UIPeripheralHost.h>
 #import <UIKit/UIPeripheralHost_Private.h>
 #import <UIKit/UIPickerContentView_Private.h>
@@ -987,6 +988,10 @@ typedef NS_OPTIONS(NSUInteger, UIDragOperation)
 
 #endif
 
+@interface UIMenuItem (UIMenuController_SPI)
+@property (nonatomic) BOOL dontDismiss;
+@end
+
 @interface UICalloutBar : UIView
 + (UICalloutBar *)activeCalloutBar;
 + (void)fadeSharedCalloutBar;
index 481cab1..b9b1e80 100644 (file)
@@ -1553,6 +1553,11 @@ FOR_EACH_WKCONTENTVIEW_ACTION(FORWARD_ACTION_TO_WKCONTENTVIEW)
     return [super targetForAction:action withSender:sender];
 }
 
+- (void)willFinishIgnoringCalloutBarFadeAfterPerformingAction
+{
+    [_contentView willFinishIgnoringCalloutBarFadeAfterPerformingAction];
+}
+
 static inline CGFloat floorToDevicePixel(CGFloat input, float deviceScaleFactor)
 {
     return CGFloor(input * deviceScaleFactor) / deviceScaleFactor;
index 92ba204..41d855d 100644 (file)
@@ -319,6 +319,7 @@ struct WKAutoCorrectionData {
     BOOL _focusRequiresStrongPasswordAssistance;
 
     BOOL _hasSetUpInteractions;
+    NSUInteger _ignoreSelectionCommandFadeCount;
     CompletionHandler<void(WebCore::DOMPasteAccessResponse)> _domPasteRequestHandler;
     BlockPtr<void(UIWKAutocorrectionContext *)> _pendingAutocorrectionContextHandler;
 
@@ -449,6 +450,8 @@ FOR_EACH_PRIVATE_WKCONTENTVIEW_ACTION(DECLARE_WKCONTENTVIEW_ACTION_FOR_WEB_VIEW)
 - (WKFormInputSession *)_formInputSession;
 - (void)_didChangeWebViewEditability;
 
+- (void)willFinishIgnoringCalloutBarFadeAfterPerformingAction;
+
 // UIWebFormAccessoryDelegate protocol
 - (void)accessoryDone;
 
index b672979..a0d3f74 100644 (file)
@@ -3880,6 +3880,15 @@ static void selectionChangedWithTouch(WKContentView *view, const WebCore::IntPoi
     [self.inputDelegate selectionDidChange:self];
 }
 
+- (void)willFinishIgnoringCalloutBarFadeAfterPerformingAction
+{
+    _ignoreSelectionCommandFadeCount++;
+    _page->callAfterNextPresentationUpdate([weakSelf = WeakObjCPtr<WKContentView>(self)] (auto) {
+        if (auto strongSelf = weakSelf.get())
+            strongSelf->_ignoreSelectionCommandFadeCount--;
+    });
+}
+
 - (void)_didChangeWebViewEditability
 {
     if ([_formAccessoryView respondsToSelector:@selector(setNextPreviousItemsVisible:)])
@@ -5647,8 +5656,7 @@ static BOOL allPasteboardItemOriginsMatchOrigin(UIPasteboard *pasteboard, const
         // FIXME: We need to figure out what to do if the selection is changed by Javascript.
         if (_textSelectionAssistant) {
             _markedText = (_page->editorState().hasComposition) ? _page->editorState().markedText : String();
-            if (!_showingTextStyleOptions)
-                [_textSelectionAssistant selectionChanged];
+            [_textSelectionAssistant selectionChanged];
         }
 
         _selectionNeedsUpdate = NO;
@@ -5670,6 +5678,12 @@ static BOOL allPasteboardItemOriginsMatchOrigin(UIPasteboard *pasteboard, const
     }
 }
 
+- (BOOL)shouldAllowHidingSelectionCommands
+{
+    ASSERT(_ignoreSelectionCommandFadeCount >= 0);
+    return !_ignoreSelectionCommandFadeCount;
+}
+
 - (BOOL)_shouldSuppressSelectionCommands
 {
     return !!_suppressSelectionAssistantReasons;
index 74aa74b..df54d7b 100644 (file)
@@ -1,3 +1,87 @@
+2019-04-16  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS] [WebKit2] Add support for honoring -[UIMenuItem dontDismiss]
+        https://bugs.webkit.org/show_bug.cgi?id=196919
+        <rdar://problem/41630459>
+
+        Reviewed by Tim Horton.
+
+        Add iOS support for several new testing hooks. See below for more detail.
+
+        * DumpRenderTree/ios/UIScriptControllerIOS.mm:
+        (WTR::UIScriptController::isDismissingMenu const):
+
+        Add a new script controller method to query whether the platform menu (on iOS, the callout bar) is done
+        dismissing. We consider the menu to be dismissing in between the `-WillHide` and `-DidHide` notifications sent
+        by UIKit when dismissing the callout bar (i.e. UIMenuController).
+
+        * TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl:
+        * TestRunnerShared/UIScriptContext/UIScriptController.cpp:
+        (WTR::UIScriptController::isDismissingMenu const):
+        * TestRunnerShared/UIScriptContext/UIScriptController.h:
+        * WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl:
+        * WebKitTestRunner/InjectedBundle/InjectedBundle.cpp:
+        (WTR::InjectedBundle::didReceiveMessageToPage):
+        * WebKitTestRunner/InjectedBundle/TestRunner.cpp:
+        (WTR::TestRunner::setAllowedMenuActions):
+
+        Add a new helper method to specify a list of allowed actions when bringing up the menu. On iOS, in the case of
+        actions supported by the platform, this matches against method selector names (for instance, "SelectAll", or
+        "Copy", or "Paste"). In the case of the custom actions installed via `installCustomMenuAction`, we instead match
+        against the name of the custom action.
+
+        (WTR::TestRunner::installCustomMenuAction):
+
+        Add a new helper method to install a custom action for the context menu (on iOS, this is the callout bar). This
+        takes the name of the action (which appears in a button in the callout bar), whether the action should cause
+        the callout bar to automatically dismiss, and finally, a JavaScript callback that is invoked when the action is
+        triggered.
+
+        (WTR::TestRunner::performCustomMenuAction):
+
+        Invoked when the custom menu action is triggered.
+
+        * WebKitTestRunner/InjectedBundle/TestRunner.h:
+        * WebKitTestRunner/TestController.cpp:
+        (WTR::TestController::installCustomMenuAction):
+        (WTR::TestController::setAllowedMenuActions):
+        * WebKitTestRunner/TestController.h:
+        * WebKitTestRunner/TestInvocation.cpp:
+        (WTR::TestInvocation::didReceiveMessageFromInjectedBundle):
+        (WTR::TestInvocation::performCustomMenuAction):
+
+        Add plumbing to call back into the injected bundle when performing the custom action.
+
+        * WebKitTestRunner/TestInvocation.h:
+        * WebKitTestRunner/cocoa/TestControllerCocoa.mm:
+        (WTR::TestController::installCustomMenuAction):
+        (WTR::TestController::setAllowedMenuActions):
+        * WebKitTestRunner/cocoa/TestRunnerWKWebView.h:
+        * WebKitTestRunner/cocoa/TestRunnerWKWebView.mm:
+        (-[TestRunnerWKWebView initWithFrame:configuration:]):
+        (-[TestRunnerWKWebView becomeFirstResponder]):
+        (-[TestRunnerWKWebView _addCustomItemToMenuControllerIfNecessary]):
+
+        Helper method that converts web view's current custom menu action info into a UIMenuItem, and adds it to the
+        shared menu controller. This is also invoked when the web view becomes first responder, which matches behavior
+        in the Mail app on iOS.
+
+        (-[TestRunnerWKWebView installCustomMenuAction:dismissesAutomatically:callback:]):
+        (-[TestRunnerWKWebView setAllowedMenuActions:]):
+        (-[TestRunnerWKWebView resetCustomMenuAction]):
+        (-[TestRunnerWKWebView performCustomAction:]):
+        (-[TestRunnerWKWebView canPerformAction:withSender:]):
+        (-[TestRunnerWKWebView _willHideMenu]):
+        (-[TestRunnerWKWebView _didHideMenu]):
+        * WebKitTestRunner/ios/TestControllerIOS.mm:
+        (WTR::TestController::platformResetStateToConsistentValues):
+
+        Reset both any custom installed actions on the shared menu controller, as well as the list of allowed actions,
+        if specified.
+
+        * WebKitTestRunner/ios/UIScriptControllerIOS.mm:
+        (WTR::UIScriptController::isDismissingMenu const):
+
 2019-04-16  Megan Gardner  <megan_gardner@apple.com>
 
         Allow sharing from imageSheet on an image document
index e1cbd6c..ba1f3ed 100644 (file)
@@ -381,6 +381,11 @@ bool UIScriptController::isShowingMenu() const
     return false;
 }
 
+bool UIScriptController::isDismissingMenu() const
+{
+    return false;
+}
+
 void UIScriptController::platformSetDidEndScrollingCallback()
 {
 }
index 17a544c..3b69cde 100644 (file)
@@ -227,6 +227,7 @@ interface UIScriptController {
 
     attribute object didShowMenuCallback;
     attribute object didHideMenuCallback;
+    readonly attribute boolean isDismissingMenu;
     readonly attribute boolean isShowingMenu;
     readonly attribute object menuRect;
     object rectForMenuAction(DOMString action);
index adc7260..39efca1 100644 (file)
@@ -598,6 +598,11 @@ JSObjectRef UIScriptController::rectForMenuAction(JSStringRef) const
     return nullptr;
 }
 
+bool UIScriptController::isDismissingMenu() const
+{
+    return false;
+}
+
 bool UIScriptController::isShowingMenu() const
 {
     return false;
index 560a3d2..0bedc04 100644 (file)
@@ -175,6 +175,7 @@ public:
     void setWillPresentPopoverCallback(JSValueRef);
     JSValueRef willPresentPopoverCallback() const;
 
+    bool isDismissingMenu() const;
     bool isShowingMenu() const;
     JSObjectRef rectForMenuAction(JSStringRef action) const;
     JSObjectRef menuRect() const;
index e7b5813..5059b0c 100644 (file)
@@ -272,6 +272,10 @@ interface TestRunner {
 
     void accummulateLogsForChannel(DOMString channel);
 
+    // Contextual menu actions
+    void setAllowedMenuActions(object actions);
+    void installCustomMenuAction(DOMString name, boolean dismissesAutomatically, object callback);
+
     // Gamepad
     void setMockGamepadDetails(unsigned long index, DOMString id, unsigned long axisCount, unsigned long buttonCount);
     void setMockGamepadAxisValue(unsigned long index, unsigned long axisIndex, double value);
index 49cdcf8..eecd30c 100644 (file)
@@ -445,6 +445,11 @@ void InjectedBundle::didReceiveMessageToPage(WKBundlePageRef page, WKStringRef m
         m_testRunner->didGetApplicationManifest();
         return;
     }
+
+    if (WKStringIsEqualToUTF8CString(messageName, "PerformCustomMenuAction")) {
+        m_testRunner->performCustomMenuAction();
+        return;
+    }
     
     WKRetainPtr<WKStringRef> errorMessageName(AdoptWK, WKStringCreateWithUTF8CString("Error"));
     WKRetainPtr<WKStringRef> errorMessageBody(AdoptWK, WKStringCreateWithUTF8CString("Unknown"));
index 50b999e..14af3ce 100644 (file)
@@ -793,6 +793,7 @@ enum {
     TextDidChangeInTextFieldCallbackID,
     TextFieldDidBeginEditingCallbackID,
     TextFieldDidEndEditingCallbackID,
+    CustomMenuActionCallbackID,
     FirstUIScriptCallbackID = 100
 };
 
@@ -1342,6 +1343,52 @@ void TestRunner::runUIScriptCallback(unsigned callbackID, JSStringRef result)
     callTestRunnerCallback(callbackID, 1, &resultValue);
 }
 
+void TestRunner::setAllowedMenuActions(JSValueRef actions)
+{
+    WKRetainPtr<WKStringRef> messageName(AdoptWK, WKStringCreateWithUTF8CString("SetAllowedMenuActions"));
+    WKRetainPtr<WKMutableArrayRef> messageBody(AdoptWK, WKMutableArrayCreate());
+
+    auto page = InjectedBundle::singleton().page()->page();
+    auto mainFrame = WKBundlePageGetMainFrame(page);
+    auto context = WKBundleFrameGetJavaScriptContext(mainFrame);
+    auto lengthPropertyName = adopt(JSStringCreateWithUTF8CString("length"));
+    auto actionsArray = JSValueToObject(context, actions, nullptr);
+    auto lengthValue = JSObjectGetProperty(context, actionsArray, lengthPropertyName.get(), nullptr);
+    if (!JSValueIsNumber(context, lengthValue))
+        return;
+
+    auto length = static_cast<size_t>(JSValueToNumber(context, lengthValue, 0));
+    for (size_t i = 0; i < length; ++i) {
+        auto value = JSObjectGetPropertyAtIndex(context, actionsArray, i, 0);
+        if (!JSValueIsString(context, value))
+            continue;
+
+        auto actionName = adopt(JSValueToStringCopy(context, value, 0));
+        WKRetainPtr<WKStringRef> action(AdoptWK, WKStringCreateWithJSString(actionName.get()));
+        WKArrayAppendItem(messageBody.get(), action.get());
+    }
+
+    WKBundlePagePostMessage(page, messageName.get(), messageBody.get());
+}
+
+void TestRunner::installCustomMenuAction(JSStringRef name, bool dismissesAutomatically, JSValueRef callback)
+{
+    cacheTestRunnerCallback(CustomMenuActionCallbackID, callback);
+
+    WKRetainPtr<WKStringRef> messageName(AdoptWK, WKStringCreateWithUTF8CString("InstallCustomMenuAction"));
+    WKRetainPtr<WKMutableDictionaryRef> messageBody(AdoptWK, WKMutableDictionaryCreate());
+
+    WKRetainPtr<WKStringRef> nameKey(AdoptWK, WKStringCreateWithUTF8CString("name"));
+    WKRetainPtr<WKStringRef> nameValue(AdoptWK, WKStringCreateWithJSString(name));
+    WKDictionarySetItem(messageBody.get(), nameKey.get(), nameValue.get());
+
+    WKRetainPtr<WKStringRef> dismissesAutomaticallyKey(AdoptWK, WKStringCreateWithUTF8CString("dismissesAutomatically"));
+    WKRetainPtr<WKBooleanRef> dismissesAutomaticallyValue(AdoptWK, WKBooleanCreate(dismissesAutomatically));
+    WKDictionarySetItem(messageBody.get(), dismissesAutomaticallyKey.get(), dismissesAutomaticallyValue.get());
+
+    WKBundlePagePostMessage(InjectedBundle::singleton().page()->page(), messageName.get(), messageBody.get());
+}
+
 void TestRunner::installDidBeginSwipeCallback(JSValueRef callback)
 {
     cacheTestRunnerCallback(DidBeginSwipeCallbackID, callback);
@@ -2465,6 +2512,11 @@ void TestRunner::didGetApplicationManifest()
     callTestRunnerCallback(GetApplicationManifestCallbackID);
 }
 
+void TestRunner::performCustomMenuAction()
+{
+    callTestRunnerCallback(CustomMenuActionCallbackID);
+}
+
 size_t TestRunner::userScriptInjectedCount() const
 {
     return InjectedBundle::singleton().userScriptInjectedCount();
index 09a95ea..7701b90 100644 (file)
@@ -355,6 +355,11 @@ public:
     void runUIScript(JSStringRef script, JSValueRef callback);
     void runUIScriptCallback(unsigned callbackID, JSStringRef result);
 
+    // Contextual menu actions
+    void setAllowedMenuActions(JSValueRef);
+    void installCustomMenuAction(JSStringRef name, bool dismissesAutomatically, JSValueRef callback);
+    void performCustomMenuAction();
+
     void installDidBeginSwipeCallback(JSValueRef);
     void installWillEndSwipeCallback(JSValueRef);
     void installDidEndSwipeCallback(JSValueRef);
index ae547b6..046a5f7 100644 (file)
@@ -3511,6 +3511,14 @@ bool TestController::canDoServerTrustEvaluationInNetworkProcess() const
     return false;
 }
 
+void TestController::installCustomMenuAction(const String&, bool)
+{
+}
+
+void TestController::setAllowedMenuActions(const Vector<String>&)
+{
+}
+
 #endif
 
 void TestController::sendDisplayConfigurationChangedMessageForTesting()
index 689526d..722dd08 100644 (file)
@@ -296,6 +296,9 @@ public:
     UIKeyboardInputMode *overriddenKeyboardInputMode() const { return m_overriddenKeyboardInputMode.get(); }
 #endif
 
+    void setAllowedMenuActions(const Vector<String>&);
+    void installCustomMenuAction(const String& name, bool dismissesAutomatically);
+
     bool canDoServerTrustEvaluationInNetworkProcess() const;
     uint64_t serverTrustEvaluationCallbackCallsCount() const { return m_serverTrustEvaluationCallbackCallsCount; }
 
index 0699cc7..9b2003b 100644 (file)
@@ -768,6 +768,27 @@ void TestInvocation::didReceiveMessageFromInjectedBundle(WKStringRef messageName
         return;
     }
 
+    if (WKStringIsEqualToUTF8CString(messageName, "InstallCustomMenuAction")) {
+        auto messageBodyDictionary = static_cast<WKDictionaryRef>(messageBody);
+        WKRetainPtr<WKStringRef> nameKey(AdoptWK, WKStringCreateWithUTF8CString("name"));
+        WKRetainPtr<WKStringRef> name = static_cast<WKStringRef>(WKDictionaryGetItemForKey(messageBodyDictionary, nameKey.get()));
+        WKRetainPtr<WKStringRef> dismissesAutomaticallyKey(AdoptWK, WKStringCreateWithUTF8CString("dismissesAutomatically"));
+        auto dismissesAutomatically = static_cast<WKBooleanRef>(WKDictionaryGetItemForKey(messageBodyDictionary, dismissesAutomaticallyKey.get()));
+        TestController::singleton().installCustomMenuAction(toWTFString(name.get()), WKBooleanGetValue(dismissesAutomatically));
+        return;
+    }
+
+    if (WKStringIsEqualToUTF8CString(messageName, "SetAllowedMenuActions")) {
+        auto messageBodyArray = static_cast<WKArrayRef>(messageBody);
+        auto size = WKArrayGetSize(messageBodyArray);
+        Vector<String> actions;
+        actions.reserveInitialCapacity(size);
+        for (size_t index = 0; index < size; ++index)
+            actions.append(toWTFString(static_cast<WKStringRef>(WKArrayGetItemAtIndex(messageBodyArray, index))));
+        TestController::singleton().setAllowedMenuActions(actions);
+        return;
+    }
+
     if (WKStringIsEqualToUTF8CString(messageName, "SetOpenPanelFileURLs")) {
         TestController::singleton().setOpenPanelFileURLs(static_cast<WKArrayRef>(messageBody));
         return;
@@ -1785,4 +1806,10 @@ void TestInvocation::dumpAdClickAttribution()
     m_shouldDumpAdClickAttribution = true;
 }
 
+void TestInvocation::performCustomMenuAction()
+{
+    WKRetainPtr<WKStringRef> messageName = adoptWK(WKStringCreateWithUTF8CString("PerformCustomMenuAction"));
+    WKPagePostMessageToInjectedBundle(TestController::singleton().mainWebView()->page(), messageName.get(), 0);
+}
+
 } // namespace WTR
index 2bdbaaa..9d3cf2f 100644 (file)
@@ -88,6 +88,7 @@ public:
     bool canOpenWindows() const { return m_canOpenWindows; }
 
     void dumpAdClickAttribution();
+    void performCustomMenuAction();
 
 private:
     WKRetainPtr<WKMutableDictionaryRef> createTestSettingsDictionary();
index bd5f39f..4087735 100644 (file)
@@ -400,6 +400,32 @@ bool TestController::canDoServerTrustEvaluationInNetworkProcess() const
 #endif
 }
 
+void TestController::installCustomMenuAction(const String& name, bool dismissesAutomatically)
+{
+#if PLATFORM(IOS_FAMILY)
+    auto* invocation = m_currentInvocation.get();
+    [m_mainWebView->platformView() installCustomMenuAction:name dismissesAutomatically:dismissesAutomatically callback:[invocation] {
+        if (TestController::singleton().isCurrentInvocation(invocation))
+            invocation->performCustomMenuAction();
+    }];
+#else
+    UNUSED_PARAM(name);
+    UNUSED_PARAM(dismissesAutomatically);
+#endif
+}
+
+void TestController::setAllowedMenuActions(const Vector<String>& actions)
+{
+#if PLATFORM(IOS_FAMILY)
+    auto actionNames = adoptNS([[NSMutableArray<NSString *> alloc] initWithCapacity:actions.size()]);
+    for (auto action : actions)
+        [actionNames addObject:action];
+    [m_mainWebView->platformView() setAllowedMenuActions:actionNames.get()];
+#else
+    UNUSED_PARAM(actions);
+#endif
+}
+
 bool TestController::isDoingMediaCapture() const
 {
     return m_mainWebView->platformView()._mediaCaptureState != _WKMediaCaptureStateNone;
index 96e5152..d216404 100644 (file)
 @property (nonatomic, copy) void (^rotationDidEndCallback)(void);
 @property (nonatomic, copy) NSString *accessibilitySpeakSelectionContent;
 
+- (void)setAllowedMenuActions:(NSArray<NSString *> *)actions;
+
+- (void)resetCustomMenuAction;
+- (void)installCustomMenuAction:(NSString *)name dismissesAutomatically:(BOOL)dismissesAutomatically callback:(dispatch_block_t)callback;
+
 - (void)resetInteractionCallbacks;
 - (void)zoomToScale:(double)scale animated:(BOOL)animated completionHandler:(void (^)(void))completionHandler;
 - (void)accessibilityRetrieveSpeakSelectionContentWithCompletionHandler:(void (^)(void))completionHandler;
@@ -58,6 +63,7 @@
 
 @property (nonatomic, readonly, getter=isShowingKeyboard) BOOL showingKeyboard;
 @property (nonatomic, readonly, getter=isShowingMenu) BOOL showingMenu;
+@property (nonatomic, readonly, getter=isDismissingMenu) BOOL dismissingMenu;
 @property (nonatomic, readonly, getter=isShowingPopover) BOOL showingPopover;
 @property (nonatomic, assign) BOOL usesSafariLikeRotation;
 @property (nonatomic, readonly, getter=isInteractingWithFormControl) BOOL interactingWithFormControl;
index 258080e..9029689 100644 (file)
@@ -29,6 +29,8 @@
 #import "WebKitTestRunnerDraggingInfo.h"
 #import <WebKit/WKUIDelegatePrivate.h>
 #import <wtf/Assertions.h>
+#import <wtf/BlockPtr.h>
+#import <wtf/Optional.h>
 #import <wtf/RetainPtr.h>
 
 #if PLATFORM(IOS_FAMILY)
 @end
 #endif
 
+struct CustomMenuActionInfo {
+    RetainPtr<NSString> name;
+    BOOL dismissesAutomatically { NO };
+    BlockPtr<void()> callback;
+};
+
 @interface TestRunnerWKWebView () <WKUIDelegatePrivate> {
     RetainPtr<NSNumber> m_stableStateOverride;
     BOOL _isInteractingWithFormControl;
     BOOL _scrollingUpdatesDisabled;
+    Optional<CustomMenuActionInfo> _customMenuActionInfo;
+    RetainPtr<NSArray<NSString *>> _allowedMenuActions;
 }
 
 @property (nonatomic, copy) void (^zoomToScaleCompletionHandler)(void);
 @property (nonatomic, copy) void (^retrieveSpeakSelectionContentCompletionHandler)(void);
 @property (nonatomic, getter=isShowingKeyboard, setter=setIsShowingKeyboard:) BOOL showingKeyboard;
 @property (nonatomic, getter=isShowingMenu, setter=setIsShowingMenu:) BOOL showingMenu;
+@property (nonatomic, getter=isDismissingMenu, setter=setIsDismissingMenu:) BOOL dismissingMenu;
 @property (nonatomic, getter=isShowingPopover, setter=setIsShowingPopover:) BOOL showingPopover;
 
 @end
@@ -79,6 +90,7 @@ IGNORE_WARNINGS_END
         [center addObserver:self selector:@selector(_invokeShowKeyboardCallbackIfNecessary) name:UIKeyboardDidShowNotification object:nil];
         [center addObserver:self selector:@selector(_invokeHideKeyboardCallbackIfNecessary) name:UIKeyboardDidHideNotification object:nil];
         [center addObserver:self selector:@selector(_didShowMenu) name:UIMenuControllerDidShowMenuNotification object:nil];
+        [center addObserver:self selector:@selector(_willHideMenu) name:UIMenuControllerWillHideMenuNotification object:nil];
         [center addObserver:self selector:@selector(_didHideMenu) name:UIMenuControllerDidHideMenuNotification object:nil];
         [center addObserver:self selector:@selector(_willPresentPopover) name:@"UIPopoverControllerWillPresentPopoverNotification" object:nil];
         [center addObserver:self selector:@selector(_didDismissPopover) name:@"UIPopoverControllerDidDismissPopoverNotification" object:nil];
@@ -132,6 +144,88 @@ IGNORE_WARNINGS_END
         self.didDismissForcePressPreviewCallback();
 }
 
+- (BOOL)becomeFirstResponder
+{
+    BOOL wasFirstResponder = self.isFirstResponder;
+    BOOL becameFirstResponder = [super becomeFirstResponder];
+    if (!wasFirstResponder && becameFirstResponder)
+        [self _addCustomItemToMenuControllerIfNecessary];
+    return becameFirstResponder;
+}
+
+- (void)_addCustomItemToMenuControllerIfNecessary
+{
+    if (!_customMenuActionInfo)
+        return;
+
+    auto item = adoptNS([[UIMenuItem alloc] initWithTitle:_customMenuActionInfo->name.get() action:@selector(performCustomAction:)]);
+    [item setDontDismiss:!_customMenuActionInfo->dismissesAutomatically];
+    UIMenuController *controller = UIMenuController.sharedMenuController;
+    controller.menuItems = @[ item.get() ];
+    [controller update];
+}
+
+- (void)installCustomMenuAction:(NSString *)name dismissesAutomatically:(BOOL)dismissesAutomatically callback:(dispatch_block_t)callback
+{
+    _customMenuActionInfo = {{ name, dismissesAutomatically, callback }};
+    [self _addCustomItemToMenuControllerIfNecessary];
+}
+
+- (void)setAllowedMenuActions:(NSArray<NSString *> *)actions
+{
+    _allowedMenuActions = actions;
+}
+
+- (void)resetCustomMenuAction
+{
+    _customMenuActionInfo.reset();
+    UIMenuController.sharedMenuController.menuItems = @[ ];
+}
+
+- (void)performCustomAction:(id)sender
+{
+    if (!_customMenuActionInfo)
+        return;
+
+    if (!_customMenuActionInfo->callback) {
+        ASSERT_NOT_REACHED();
+        return;
+    }
+
+    _customMenuActionInfo->callback();
+}
+
+- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
+{
+    BOOL isCustomAction = action == @selector(performCustomAction:);
+    BOOL canPerformActionByDefault = [super canPerformAction:action withSender:sender];
+    if (isCustomAction)
+        canPerformActionByDefault = _customMenuActionInfo.hasValue();
+
+    if (canPerformActionByDefault && _allowedMenuActions && sender == UIMenuController.sharedMenuController) {
+        BOOL isAllowed = NO;
+        if (isCustomAction) {
+            for (NSString *allowedAction in _allowedMenuActions.get()) {
+                if ([[_customMenuActionInfo->name lowercaseString] isEqualToString:allowedAction.lowercaseString]) {
+                    isAllowed = YES;
+                    break;
+                }
+            }
+        } else {
+            for (NSString *allowedAction in _allowedMenuActions.get()) {
+                NSString *lowercaseSelectorName = [[allowedAction lowercaseString] stringByAppendingString:@":"];
+                if ([NSStringFromSelector(action).lowercaseString isEqualToString:lowercaseSelectorName]) {
+                    isAllowed = YES;
+                    break;
+                }
+            }
+        }
+        if (!isAllowed)
+            return NO;
+    }
+    return canPerformActionByDefault;
+}
+
 - (void)resetInteractionCallbacks
 {
     self.didStartFormControlInteractionCallback = nil;
@@ -195,8 +289,15 @@ IGNORE_WARNINGS_END
         self.didShowMenuCallback();
 }
 
+- (void)_willHideMenu
+{
+    self.dismissingMenu = YES;
+}
+
 - (void)_didHideMenu
 {
+    self.dismissingMenu = NO;
+
     if (!self.showingMenu)
         return;
 
index 3eee005..45eb5ee 100644 (file)
@@ -163,6 +163,8 @@ void TestController::platformResetStateToConsistentValues(const TestOptions& opt
         [webView _clearOverrideLayoutParameters];
         [webView _clearInterfaceOrientationOverride];
         [webView resetInteractionCallbacks];
+        [webView resetCustomMenuAction];
+        [webView setAllowedMenuActions:nil];
 
         UIScrollView *scrollView = webView.scrollView;
         [scrollView _removeAllAnimations:YES];
index 748c3a1..191bc99 100644 (file)
@@ -971,6 +971,11 @@ JSObjectRef UIScriptController::menuRect() const
     return m_context->objectFromRect(WebCore::FloatRect(rectInRootViewCoordinates.origin.x, rectInRootViewCoordinates.origin.y, rectInRootViewCoordinates.size.width, rectInRootViewCoordinates.size.height));
 }
 
+bool UIScriptController::isDismissingMenu() const
+{
+    return TestController::singleton().mainWebView()->platformView().dismissingMenu;
+}
+
 bool UIScriptController::isShowingMenu() const
 {
     return TestController::singleton().mainWebView()->platformView().showingMenu;