Support programmatic paste requests on macOS
https://bugs.webkit.org/show_bug.cgi?id=202773
<rdar://problem/48957166>

Reviewed by Tim Horton.

Source/WebCore:

Adds support for programmatic paste requests on macOS. See below for more details.

Tests: editing/pasteboard/dom-paste/dom-paste-confirmation.html
       editing/pasteboard/dom-paste/dom-paste-consecutive-confirmations.html
       editing/pasteboard/dom-paste/dom-paste-rejection.html
       editing/pasteboard/dom-paste/dom-paste-requires-user-gesture.html
       editing/pasteboard/dom-paste/dom-paste-same-origin.html

* editing/EditorCommand.cpp:
(WebCore::defaultValueForSupportedPaste):
(WebCore::supportedPaste):
(WebCore::allowPasteFromDOM):
(WebCore::enabledPaste):

Fixes an existing bug uncovered by the layout test editing/execCommand/clipboard-access.html, which tests the
results of `document.queryCommandEnabled("copy")` and `document.queryCommandEnabled("paste")`. The problem here
is that document.queryCommandEnabled("paste") returns true if DOM paste access requests are enabled, regardless
of whether or not there is an active user gesture. This is inconsistent with the behavior of "copy" and "cut",
which return false in the case where there is no user gesture (and the clipboard access policy is also equal to
ClipboardAccessPolicy::RequiresUserGesture -- refer to `allowCopyCutFromDOM`).

When pasting, we only DOM paste access requests to be triggered only in the case where there is a user gesture.
This means that enabledPaste should additionally be gated on a user gesture check. For consistency with the
implementation of `enabledCopy`, we introduce a `allowPasteFromDOM` helper that is similar to
`allowCopyCutFromDOM`, and additionally check this constraint when the paste command's source is the DOM (as
opposed to a menu or key binding).

This adjustment also adds a missing canDHTMLPaste() check prior to consulting canPaste(). This ensures that when
evaluating document.queryCommandEnabled("Paste"), we'll dispatch a "beforepaste" event, similar to how
evaluating document.queryCommandEnabled("Copy") dispatches a "beforecopy" event.

* platform/LocalizedStrings.h:

Mark a function as WEBCORE_EXPORT.

Source/WebKit:

Adds support for programmatic paste requests on macOS, as well as some testing SPI in WKWebView to allow
WebKitTestRunner to grab the NSMenu used for the DOM paste request. This patch adopts the same strategy taken to
allow programmatic paste on iOS, by allowing programmatic pastes coming from the page to show platform UI which
the user must then interact with in order to proceed with the paste. See below for more details.

* Shared/WebPreferencesDefaultValues.h:

Make this available on both iOS and macOS (iOS family is omitted for now, since callout bar UI is not generally
present on non-iOS iOS-family platforms such as Apple Watch).

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

This selector is called when the user taps the Paste option in the presented NSMenu.

(-[WKWebView _activeMenu]):

Returns the currently active NSMenu. Only for testing purposes.

* UIProcess/API/Cocoa/WKWebViewInternal.h:
* UIProcess/API/Cocoa/WKWebViewPrivate.h:
* UIProcess/API/mac/WKView.mm:
(-[WKView _web_grantDOMPasteAccess]):

Same exercise as above, only for WKView instead of WKWebView.

* UIProcess/Cocoa/WebViewImpl.h:
(WebKit::WebViewImpl::domPasteMenu const):
* UIProcess/Cocoa/WebViewImpl.mm:
(-[WKDOMPasteMenuDelegate initWithWebViewImpl:]):
(-[WKDOMPasteMenuDelegate menuDidClose:]):
(-[WKDOMPasteMenuDelegate numberOfItemsInMenu:]):
(-[WKDOMPasteMenuDelegate confinementRectForMenu:onScreen:]):

Adds a new object, whose purpose is to be a delegate for the NSMenu that is presented when requesting DOM paste
access. This object is used instead of WKWebView, since API clients may end up making the WKWebView the delegate
for a different menu, in which case some implementations (either theirs or ours) of NSMenuDelegate methods would
not be called. Avoiding this would require the client to be aware that WKWebView conforms to NSMenuDelegate,
which is only declared privately.

(WebKit::WebViewImpl::handleProcessSwapOrExit):

On process swap or exit, automatically bail out of any pending DOM paste request by denying it.

(WebKit::WebViewImpl::requestDOMPasteAccess):
(WebKit::WebViewImpl::handleDOMPasteRequestWithResult):

Handle the DOM paste request by showing an NSMenu near the mouse cursor with a single option to paste.

* UIProcess/mac/PageClientImplMac.h:
* UIProcess/mac/PageClientImplMac.mm:
(WebKit::PageClientImpl::requestDOMPasteAccess):

Tools:

Adds new testing support to enable us to test programmatic paste requests on macOS.

* TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl:

Add a few new UIScriptController methods:
- activateAtPoint(x, y, callback): used to activate content underneath at (x, y), in root view coordinates
  (WKWebView on macOS, and WKContentView on iOS). On macOS, this moves the mouse to the given location and
  clicks.
- chooseMenuAction(action, callback): used to select a menu item with the given title.
- dismissMenu(): dismisses the platform menu.

Note that dismissMenu and chooseMenuAction currently only work for the DOM paste menu, but could be extended in
the future to handle the system context menu.

* TestRunnerShared/UIScriptContext/UIScriptController.cpp:
(WTR::UIScriptController::dismissMenu):
(WTR::UIScriptController::chooseMenuAction):
* TestRunnerShared/UIScriptContext/UIScriptController.h:
(WTR::UIScriptController::activateAtPoint):
* WebKitTestRunner/cocoa/TestControllerCocoa.mm:
(WTR::TestController::cocoaResetStateToConsistentValues):
* WebKitTestRunner/cocoa/TestRunnerWKWebView.h:
* WebKitTestRunner/cocoa/TestRunnerWKWebView.mm:
(-[TestRunnerWKWebView initWithFrame:configuration:]):
(-[TestRunnerWKWebView _didShowMenu]):
(-[TestRunnerWKWebView _didHideMenu]):

Make these present across both macOS and iOS. On macOS, we listen for NSMenuDidBeginTrackingNotification and
NSMenuDidEndTrackingNotification to know when a menu has been shown or dismissed.

(-[TestRunnerWKWebView dismissActiveMenu]):
(-[TestRunnerWKWebView resetInteractionCallbacks]):

Make these available on both iOS and macOS. The only interaction callbacks on macOS are currently
didShowMenuCallback and didHideMenuCallback.

(-[TestRunnerWKWebView _willHideMenu]):
* WebKitTestRunner/cocoa/UIScriptControllerCocoa.h:
* WebKitTestRunner/cocoa/UIScriptControllerCocoa.mm:
(WTR::UIScriptControllerCocoa::setDidShowMenuCallback):
(WTR::UIScriptControllerCocoa::setDidHideMenuCallback):
(WTR::UIScriptControllerCocoa::dismissMenu):
(WTR::UIScriptControllerCocoa::isShowingMenu const):

Move these implementations into UIScriptControllerCocoa, from UIScriptControllerIOS.

* WebKitTestRunner/ios/TestControllerIOS.mm:
(WTR::TestController::platformResetStateToConsistentValues):

Instead of clearing all interaction callbacks in TestControllerIOS, do it in TestControllerCocoa where it
affects both macOS and iOS.

* WebKitTestRunner/ios/UIScriptControllerIOS.h:
* WebKitTestRunner/ios/UIScriptControllerIOS.mm:
(WTR::UIScriptControllerIOS::activateAtPoint):
(WTR::UIScriptControllerIOS::singleTapAtPointWithModifiers):
(WTR::UIScriptControllerIOS::chooseMenuAction):
(WTR::UIScriptControllerIOS::rectForMenuAction const):
(WTR::UIScriptControllerIOS::setDidShowMenuCallback): Deleted.
(WTR::UIScriptControllerIOS::setDidHideMenuCallback): Deleted.
(WTR::UIScriptControllerIOS::isShowingMenu const): Deleted.

Abstract rectForMenuAction and singleTapAtPointWithModifiers out into private helper methods, such that they can
be used from within other script controller methods.

* WebKitTestRunner/mac/UIScriptControllerMac.h:
* WebKitTestRunner/mac/UIScriptControllerMac.mm:

Implement the new script controller hooks on macOS.

(WTR::UIScriptControllerMac::clearAllCallbacks):
(WTR::UIScriptControllerMac::chooseMenuAction):
(WTR::UIScriptControllerMac::activateAtPoint):

LayoutTests:

Refactors existing layout tests for programmatic paste requests on iOS, such that they now run in both iOS and
macOS. See below for more details.

* TestExpectations:
* editing/pasteboard/dom-paste/dom-paste-confirmation-expected.txt: Renamed from LayoutTests/editing/pasteboard/ios/dom-paste-confirmation-expected.txt.
* editing/pasteboard/dom-paste/dom-paste-confirmation.html: Renamed from LayoutTests/editing/pasteboard/ios/dom-paste-confirmation.html.
* editing/pasteboard/dom-paste/dom-paste-consecutive-confirmations-expected.txt: Added.
* editing/pasteboard/dom-paste/dom-paste-consecutive-confirmations.html: Renamed from LayoutTests/editing/pasteboard/ios/dom-paste-consecutive-confirmations.html.
* editing/pasteboard/dom-paste/dom-paste-rejection-expected.txt: Added.
* editing/pasteboard/dom-paste/dom-paste-rejection.html: Renamed from LayoutTests/editing/pasteboard/ios/dom-paste-rejection.html.
* editing/pasteboard/dom-paste/dom-paste-requires-user-gesture-expected.txt: Renamed from LayoutTests/editing/pasteboard/ios/dom-paste-requires-user-gesture-expected.txt.
* editing/pasteboard/dom-paste/dom-paste-requires-user-gesture.html: Renamed from LayoutTests/editing/pasteboard/ios/dom-paste-requires-user-gesture.html.
* editing/pasteboard/dom-paste/dom-paste-same-origin-expected.txt: Renamed from LayoutTests/editing/pasteboard/ios/dom-paste-same-origin-expected.txt.
* editing/pasteboard/dom-paste/dom-paste-same-origin.html: Renamed from LayoutTests/editing/pasteboard/ios/dom-paste-same-origin.html.
* editing/pasteboard/dom-paste/resources/dom-paste-helper.js: Added.

Re-word some of these layout tests' descriptions to reference "clicks or taps", instead of just "taps", and also
replace mentions of "callout bars" with platform-agnostic "menus".

(return.new.Promise.):
(async._waitForOrTriggerPasteMenu):
(async.triggerPasteMenuAfterActivatingLocation):
(async.waitForPasteMenu):

Refactor these testing helpers to support both iOS and macOS:

(1) Replace code that finds callout bar menu items and synthesizes taps on iOS, with code that instead chooses a
menu item with the given title (in this case, "Paste"). This is supported on both macOS and iOS, where we invoke
the NSMenuItem's action and dismiss the menu item, and find and tap the callout bar menu item, respectively.

(2) Implement UIScriptController::activateAtPoint, which is used as a cross-platform way of activating an
element at the given point. On iOS, this taps the given location, and on macOS, this moves the mouse to that
location and then simulates a click (mouse down and mouse up). In a subsequent patch, we should additionally use
this in the implementation of UIHelper.activateAt().

* editing/pasteboard/ios/dom-paste-consecutive-confirmations-expected.txt: Removed.
* editing/pasteboard/ios/dom-paste-rejection-expected.txt: Removed.
* editing/pasteboard/ios/resources/dom-paste-helper.js: Removed.
* platform/ios-wk2/TestExpectations:
* platform/ios/TestExpectations:
* platform/mac-wk2/TestExpectations:
* platform/win/TestExpectations:
* platform/wincairo/TestExpectations:

Skip editing/pasteboard/dom-paste everywhere for now, except for macOS and iOS WebKit2.


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@250973 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index 560552d..53c6a65 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,3 +1,57 @@
+2019-10-10  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        Support programmatic paste requests on macOS
+        https://bugs.webkit.org/show_bug.cgi?id=202773
+        <rdar://problem/48957166>
+
+        Reviewed by Tim Horton.
+
+        Refactors existing layout tests for programmatic paste requests on iOS, such that they now run in both iOS and
+        macOS. See below for more details.
+
+        * TestExpectations:
+        * editing/pasteboard/dom-paste/dom-paste-confirmation-expected.txt: Renamed from LayoutTests/editing/pasteboard/ios/dom-paste-confirmation-expected.txt.
+        * editing/pasteboard/dom-paste/dom-paste-confirmation.html: Renamed from LayoutTests/editing/pasteboard/ios/dom-paste-confirmation.html.
+        * editing/pasteboard/dom-paste/dom-paste-consecutive-confirmations-expected.txt: Added.
+        * editing/pasteboard/dom-paste/dom-paste-consecutive-confirmations.html: Renamed from LayoutTests/editing/pasteboard/ios/dom-paste-consecutive-confirmations.html.
+        * editing/pasteboard/dom-paste/dom-paste-rejection-expected.txt: Added.
+        * editing/pasteboard/dom-paste/dom-paste-rejection.html: Renamed from LayoutTests/editing/pasteboard/ios/dom-paste-rejection.html.
+        * editing/pasteboard/dom-paste/dom-paste-requires-user-gesture-expected.txt: Renamed from LayoutTests/editing/pasteboard/ios/dom-paste-requires-user-gesture-expected.txt.
+        * editing/pasteboard/dom-paste/dom-paste-requires-user-gesture.html: Renamed from LayoutTests/editing/pasteboard/ios/dom-paste-requires-user-gesture.html.
+        * editing/pasteboard/dom-paste/dom-paste-same-origin-expected.txt: Renamed from LayoutTests/editing/pasteboard/ios/dom-paste-same-origin-expected.txt.
+        * editing/pasteboard/dom-paste/dom-paste-same-origin.html: Renamed from LayoutTests/editing/pasteboard/ios/dom-paste-same-origin.html.
+        * editing/pasteboard/dom-paste/resources/dom-paste-helper.js: Added.
+
+        Re-word some of these layout tests' descriptions to reference "clicks or taps", instead of just "taps", and also
+        replace mentions of "callout bars" with platform-agnostic "menus".
+
+        (return.new.Promise.):
+        (async._waitForOrTriggerPasteMenu):
+        (async.triggerPasteMenuAfterActivatingLocation):
+        (async.waitForPasteMenu):
+
+        Refactor these testing helpers to support both iOS and macOS:
+
+        (1) Replace code that finds callout bar menu items and synthesizes taps on iOS, with code that instead chooses a
+        menu item with the given title (in this case, "Paste"). This is supported on both macOS and iOS, where we invoke
+        the NSMenuItem's action and dismiss the menu item, and find and tap the callout bar menu item, respectively.
+
+        (2) Implement UIScriptController::activateAtPoint, which is used as a cross-platform way of activating an
+        element at the given point. On iOS, this taps the given location, and on macOS, this moves the mouse to that
+        location and then simulates a click (mouse down and mouse up). In a subsequent patch, we should additionally use
+        this in the implementation of UIHelper.activateAt().
+
+        * editing/pasteboard/ios/dom-paste-consecutive-confirmations-expected.txt: Removed.
+        * editing/pasteboard/ios/dom-paste-rejection-expected.txt: Removed.
+        * editing/pasteboard/ios/resources/dom-paste-helper.js: Removed.
+        * platform/ios-wk2/TestExpectations:
+        * platform/ios/TestExpectations:
+        * platform/mac-wk2/TestExpectations:
+        * platform/win/TestExpectations:
+        * platform/wincairo/TestExpectations:
+
+        Skip editing/pasteboard/dom-paste everywhere for now, except for macOS and iOS WebKit2.
+
 2019-10-10  Chris Lord  <clord@igalia.com>
 
         Flaky Test: imported/w3c/web-platform-tests/offscreen-canvas/drawing-images-to-the-canvas/2d.drawImage.floatsource.html
diff --git a/LayoutTests/TestExpectations b/LayoutTests/TestExpectations
index 01a8b2c7..cd3f1b2 100644
--- a/LayoutTests/TestExpectations
+++ b/LayoutTests/TestExpectations
@@ -56,7 +56,7 @@
 system-preview [ Skip ]
 editing/images [ Skip ]
 pointerevents/ios [ Skip ]
-editing/pasteboard/ios [ Skip ]
+editing/pasteboard/dom-paste [ Skip ]
 editing/pasteboard/mac [ Skip ]
 fast/media/ios [ Skip ]
 
diff --git a/LayoutTests/editing/pasteboard/ios/dom-paste-confirmation-expected.txt b/LayoutTests/editing/pasteboard/dom-paste/dom-paste-confirmation-expected.txt
similarity index 61%
rename from LayoutTests/editing/pasteboard/ios/dom-paste-confirmation-expected.txt
rename to LayoutTests/editing/pasteboard/dom-paste/dom-paste-confirmation-expected.txt
index 37c783a..a97bf7e 100644
--- a/LayoutTests/editing/pasteboard/ios/dom-paste-confirmation-expected.txt
+++ b/LayoutTests/editing/pasteboard/dom-paste/dom-paste-confirmation-expected.txt
@@ -1,7 +1,7 @@
 Click here to copy
 Click here to copy
 
-Verifies that a callout is shown when the page programmatically triggers paste, and that tapping the callout allows the paste to happen. To manually test, tap the text on the bottom, tap the editable area above, and then select 'Paste' in the resulting callout menu. The text 'Click here to copy' should be pasted twice in the editable area.
+Verifies that a menu is shown when the page programmatically triggers paste, and that selecting Paste in the menu allows the paste to happen. To manually test, click or tap the text on the bottom, click or tap the editable area above, and then select 'Paste' in the resulting menu. The text 'Click here to copy' should be pasted twice in the editable area.
 
 On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
 
diff --git a/LayoutTests/editing/pasteboard/ios/dom-paste-confirmation.html b/LayoutTests/editing/pasteboard/dom-paste/dom-paste-confirmation.html
similarity index 80%
rename from LayoutTests/editing/pasteboard/ios/dom-paste-confirmation.html
rename to LayoutTests/editing/pasteboard/dom-paste/dom-paste-confirmation.html
index 472f94d..4b5109c 100644
--- a/LayoutTests/editing/pasteboard/ios/dom-paste-confirmation.html
+++ b/LayoutTests/editing/pasteboard/dom-paste/dom-paste-confirmation.html
@@ -41,7 +41,7 @@
 
 const editor = document.getElementById("editor");
 
-description("Verifies that a callout is shown when the page programmatically triggers paste, and that tapping the callout allows the paste to happen. To manually test, tap the text on the bottom, tap the editable area above, and then select 'Paste' in the resulting callout menu. The text 'Click here to copy' should be pasted <strong><em>twice</em></strong> in the editable area.");
+description("Verifies that a menu is shown when the page programmatically triggers paste, and that selecting Paste in the menu allows the paste to happen. To manually test, click or tap the text on the bottom, click or tap the editable area above, and then select 'Paste' in the resulting menu. The text 'Click here to copy' should be pasted <strong><em>twice</em></strong> in the editable area.");
 
 editor.addEventListener("paste", event => shouldBeEqualToString("event.clipboardData.getData('text/plain')", "Click here to copy"));
 editor.addEventListener("click", event => {
@@ -61,7 +61,7 @@
         return;
 
     await UIHelper.activateAt(160, 125);
-    await triggerPasteMenuAfterTapAt(160, 50);
+    await triggerPasteMenuAfterActivatingLocation(160, 50);
     finishJSTest();
 });
 </script>
diff --git a/LayoutTests/editing/pasteboard/dom-paste/dom-paste-consecutive-confirmations-expected.txt b/LayoutTests/editing/pasteboard/dom-paste/dom-paste-consecutive-confirmations-expected.txt
new file mode 100644
index 0000000..5a345f0
--- /dev/null
+++ b/LayoutTests/editing/pasteboard/dom-paste/dom-paste-consecutive-confirmations-expected.txt
@@ -0,0 +1,13 @@
+
+Verifies that no menu is shown when the page programmatically triggers paste on a timer after user interaction. To test manually, click or tap the text on the bottom to copy, and then click or tap the editable area above to trigger two programmatic pastes with the menu. Check that permissions for the first programmatic paste do not affect the second programmatic paste, since it is performed on a zero-delay timer.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+PASS document.execCommand('Paste') is true
+PASS editor.textContent is "Click here to copy"
+PASS document.execCommand('Paste') is true
+PASS editor.textContent is "Click here to copy"
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/editing/pasteboard/ios/dom-paste-consecutive-confirmations.html b/LayoutTests/editing/pasteboard/dom-paste/dom-paste-consecutive-confirmations.html
similarity index 76%
rename from LayoutTests/editing/pasteboard/ios/dom-paste-consecutive-confirmations.html
rename to LayoutTests/editing/pasteboard/dom-paste/dom-paste-consecutive-confirmations.html
index daed28f..1e11aae 100644
--- a/LayoutTests/editing/pasteboard/ios/dom-paste-consecutive-confirmations.html
+++ b/LayoutTests/editing/pasteboard/dom-paste/dom-paste-consecutive-confirmations.html
@@ -41,7 +41,7 @@
 
 const editor = document.getElementById("editor");
 
-description("Verifies that no callout is shown when the page programmatically triggers paste on a timer after user interaction. To test manually, click the text on the bottom to copy, and then click the editable area above to trigger two programmatic pastes with the callout bar. Check that permissions for the first programmatic paste do not affect the second programmatic paste, since it is performed on a zero-delay timer.");
+description("Verifies that no menu is shown when the page programmatically triggers paste on a timer after user interaction. To test manually, click or tap the text on the bottom to copy, and then click or tap the editable area above to trigger two programmatic pastes with the menu. Check that permissions for the first programmatic paste do not affect the second programmatic paste, since it is performed on a zero-delay timer.");
 
 async function waitForAndTapPasteMenuTwice() {
     return new Promise(resolve => {
@@ -57,11 +57,10 @@
                 uiController.didShowMenuCallback = tapPasteMenuAction;
 
                 function tapPasteMenuAction() {
-                    const rect = uiController.rectForMenuAction("Paste");
-                    uiController.singleTapAtPoint(rect.left + rect.width / 2, rect.top + rect.height / 2, incrementProgress);
+                    uiController.chooseMenuAction("Paste", incrementProgress);
                 }
 
-                uiController.singleTapAtPoint(160, 50, incrementProgress);
+                uiController.activateAtPoint(160, 50, incrementProgress);
             })()`, resolve);
     });
 }
diff --git a/LayoutTests/editing/pasteboard/dom-paste/dom-paste-rejection-expected.txt b/LayoutTests/editing/pasteboard/dom-paste/dom-paste-rejection-expected.txt
new file mode 100644
index 0000000..222f7d9
--- /dev/null
+++ b/LayoutTests/editing/pasteboard/dom-paste/dom-paste-rejection-expected.txt
@@ -0,0 +1,14 @@
+
+Verifies that a menu is shown when the page programmatically triggers paste, and that dismissing the menu prevents the paste from happening. To manually test, click or tap the text on the bottom, click or tap the editable area above, and then dismiss the resulting menu by interacting elsewhere. The text 'Click here to copy' should not be pasted, and the menu should disappear.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+PASS document.queryCommandSupported('Paste') is true
+PASS document.queryCommandEnabled('Paste') is true
+PASS document.execCommand('Paste') is false
+PASS document.execCommand('Paste') is false
+PASS editor.textContent is ""
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/editing/pasteboard/ios/dom-paste-rejection.html b/LayoutTests/editing/pasteboard/dom-paste/dom-paste-rejection.html
similarity index 79%
rename from LayoutTests/editing/pasteboard/ios/dom-paste-rejection.html
rename to LayoutTests/editing/pasteboard/dom-paste/dom-paste-rejection.html
index 3836e79..80a8eb9 100644
--- a/LayoutTests/editing/pasteboard/ios/dom-paste-rejection.html
+++ b/LayoutTests/editing/pasteboard/dom-paste/dom-paste-rejection.html
@@ -41,7 +41,7 @@
 
 const editor = document.getElementById("editor");
 
-description("Verifies that a callout is shown when the page programmatically triggers paste, and that dismissing the callout prevents the paste from happening. To manually test, tap the text on the bottom, tap the editable area above, and then dismiss the resulting callout menu by scrolling or tapping elsewhere. The text 'Click here to copy' should <strong>not</strong> be pasted, and the callout bar should disappear.");
+description("Verifies that a menu is shown when the page programmatically triggers paste, and that dismissing the menu prevents the paste from happening. To manually test, click or tap the text on the bottom, click or tap the editable area above, and then dismiss the resulting menu by interacting elsewhere. The text 'Click here to copy' should <strong>not</strong> be pasted, and the menu should disappear.");
 
 editor.addEventListener("paste", event => shouldBeEqualToString("event.clipboardData.getData('text/plain')", "Click here to copy"));
 editor.addEventListener("click", event => {
@@ -60,7 +60,7 @@
         return;
 
     await UIHelper.activateAt(160, 125);
-    await triggerPasteMenuAfterTapAt(160, 50, false);
+    await triggerPasteMenuAfterActivatingLocation(160, 50, false);
     finishJSTest();
 });
 </script>
diff --git a/LayoutTests/editing/pasteboard/ios/dom-paste-requires-user-gesture-expected.txt b/LayoutTests/editing/pasteboard/dom-paste/dom-paste-requires-user-gesture-expected.txt
similarity index 64%
rename from LayoutTests/editing/pasteboard/ios/dom-paste-requires-user-gesture-expected.txt
rename to LayoutTests/editing/pasteboard/dom-paste/dom-paste-requires-user-gesture-expected.txt
index f100ba6..5853a23 100644
--- a/LayoutTests/editing/pasteboard/ios/dom-paste-requires-user-gesture-expected.txt
+++ b/LayoutTests/editing/pasteboard/dom-paste/dom-paste-requires-user-gesture-expected.txt
@@ -1,6 +1,6 @@
 Click here to copy
 
-Verifies that no callout is shown when the page programmatically triggers paste outside the scope of user interaction. This test requires WebKitTestRunner.
+Verifies that no menu is shown when the page programmatically triggers paste outside the scope of user interaction. This test requires WebKitTestRunner.
 
 On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
 
diff --git a/LayoutTests/editing/pasteboard/ios/dom-paste-requires-user-gesture.html b/LayoutTests/editing/pasteboard/dom-paste/dom-paste-requires-user-gesture.html
similarity index 88%
rename from LayoutTests/editing/pasteboard/ios/dom-paste-requires-user-gesture.html
rename to LayoutTests/editing/pasteboard/dom-paste/dom-paste-requires-user-gesture.html
index 127904f..5865dc6 100644
--- a/LayoutTests/editing/pasteboard/ios/dom-paste-requires-user-gesture.html
+++ b/LayoutTests/editing/pasteboard/dom-paste/dom-paste-requires-user-gesture.html
@@ -41,7 +41,7 @@
 
 const editor = document.getElementById("editor");
 
-description("Verifies that no callout is shown when the page programmatically triggers paste outside the scope of user interaction. This test requires WebKitTestRunner.");
+description("Verifies that no menu is shown when the page programmatically triggers paste outside the scope of user interaction. This test requires WebKitTestRunner.");
 
 function checkDone() {
     if (!window.doneCount)
@@ -63,7 +63,7 @@
 
 addEventListener("load", async () => {
     await UIHelper.activateAt(160, 125);
-    await triggerPasteMenuAfterTapAt(160, 50);
+    await triggerPasteMenuAfterActivatingLocation(160, 50);
     checkDone();
 });
 </script>
diff --git a/LayoutTests/editing/pasteboard/ios/dom-paste-same-origin-expected.txt b/LayoutTests/editing/pasteboard/dom-paste/dom-paste-same-origin-expected.txt
similarity index 77%
rename from LayoutTests/editing/pasteboard/ios/dom-paste-same-origin-expected.txt
rename to LayoutTests/editing/pasteboard/dom-paste/dom-paste-same-origin-expected.txt
index 6aab17b..018f669 100644
--- a/LayoutTests/editing/pasteboard/ios/dom-paste-same-origin-expected.txt
+++ b/LayoutTests/editing/pasteboard/dom-paste/dom-paste-same-origin-expected.txt
@@ -1,7 +1,7 @@
 Click here to copy
 Click here to copy
 Click here to copy
-Verifies that programmatic paste is allowed when copied data is from the same origin. To manually test, tap the text on the bottom to programmatically copy, and then tap the editable area and check that the text is pasted twice.
+Verifies that programmatic paste is allowed when copied data is from the same origin. To manually test, click or tap the text on the bottom to programmatically copy, and then click or tap the editable area and check that the text is pasted twice.
 
 On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
 
diff --git a/LayoutTests/editing/pasteboard/ios/dom-paste-same-origin.html b/LayoutTests/editing/pasteboard/dom-paste/dom-paste-same-origin.html
similarity index 90%
rename from LayoutTests/editing/pasteboard/ios/dom-paste-same-origin.html
rename to LayoutTests/editing/pasteboard/dom-paste/dom-paste-same-origin.html
index aca0d69..899138c 100644
--- a/LayoutTests/editing/pasteboard/ios/dom-paste-same-origin.html
+++ b/LayoutTests/editing/pasteboard/dom-paste/dom-paste-same-origin.html
@@ -37,7 +37,7 @@
 const copy = document.getElementById("copy");
 const editor = document.getElementById("editor");
 
-description("Verifies that programmatic paste is allowed when copied data is from the same origin. To manually test, tap the text on the bottom to programmatically copy, and then tap the editable area and check that the text is pasted <em>twice</em>.");
+description("Verifies that programmatic paste is allowed when copied data is from the same origin. To manually test, click or tap the text on the bottom to programmatically copy, and then click or tap the editable area and check that the text is pasted <em>twice</em>.");
 
 copy.addEventListener('click', () => {
     getSelection().selectAllChildren(copy);
diff --git a/LayoutTests/editing/pasteboard/dom-paste/resources/dom-paste-helper.js b/LayoutTests/editing/pasteboard/dom-paste/resources/dom-paste-helper.js
new file mode 100644
index 0000000..c843c61
--- /dev/null
+++ b/LayoutTests/editing/pasteboard/dom-paste/resources/dom-paste-helper.js
@@ -0,0 +1,40 @@
+
+async function _waitForOrTriggerPasteMenu(x, y, proceedWithPaste, shouldActivate) {
+    return new Promise(resolve => {
+        testRunner.runUIScript(`
+            (() => {
+                doneCount = 0;
+                function checkDone() {
+                    if (++doneCount === (${shouldActivate} ? 3 : 2))
+                        uiController.uiScriptComplete();
+                }
+
+                uiController.didHideMenuCallback = checkDone;
+
+                function resolveDOMPasteRequest() {
+                    if (${proceedWithPaste})
+                        uiController.chooseMenuAction("Paste", checkDone);
+                    else {
+                        uiController.dismissMenu();
+                        checkDone();
+                    }
+                }
+
+                if (uiController.isShowingMenu)
+                    resolveDOMPasteRequest();
+                else
+                    uiController.didShowMenuCallback = resolveDOMPasteRequest;
+
+                if (${shouldActivate})
+                    uiController.activateAtPoint(${x}, ${y}, checkDone);
+            })()`, resolve);
+    });
+}
+
+async function triggerPasteMenuAfterActivatingLocation(x, y, proceedWithPaste = true) {
+    return _waitForOrTriggerPasteMenu(x, y, proceedWithPaste, true);
+}
+
+async function waitForPasteMenu(proceedWithPaste = true) {
+    return _waitForOrTriggerPasteMenu(null, null, proceedWithPaste, false);
+}
diff --git a/LayoutTests/editing/pasteboard/ios/dom-paste-consecutive-confirmations-expected.txt b/LayoutTests/editing/pasteboard/ios/dom-paste-consecutive-confirmations-expected.txt
deleted file mode 100644
index 056b480..0000000
--- a/LayoutTests/editing/pasteboard/ios/dom-paste-consecutive-confirmations-expected.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-
-Verifies that no callout is shown when the page programmatically triggers paste on a timer after user interaction. To test manually, click the text on the bottom to copy, and then click the editable area above to trigger two programmatic pastes with the callout bar. Check that permissions for the first programmatic paste do not affect the second programmatic paste, since it is performed on a zero-delay timer.
-
-On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
-
-PASS document.execCommand('Paste') is true
-PASS editor.textContent is "Click here to copy"
-PASS document.execCommand('Paste') is true
-PASS editor.textContent is "Click here to copy"
-PASS successfullyParsed is true
-
-TEST COMPLETE
-
diff --git a/LayoutTests/editing/pasteboard/ios/dom-paste-rejection-expected.txt b/LayoutTests/editing/pasteboard/ios/dom-paste-rejection-expected.txt
deleted file mode 100644
index c3adad7..0000000
--- a/LayoutTests/editing/pasteboard/ios/dom-paste-rejection-expected.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-
-Verifies that a callout is shown when the page programmatically triggers paste, and that dismissing the callout prevents the paste from happening. To manually test, tap the text on the bottom, tap the editable area above, and then dismiss the resulting callout menu by scrolling or tapping elsewhere. The text 'Click here to copy' should not be pasted, and the callout bar should disappear.
-
-On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
-
-PASS document.queryCommandSupported('Paste') is true
-PASS document.queryCommandEnabled('Paste') is true
-PASS document.execCommand('Paste') is false
-PASS document.execCommand('Paste') is false
-PASS editor.textContent is ""
-PASS successfullyParsed is true
-
-TEST COMPLETE
-
diff --git a/LayoutTests/editing/pasteboard/ios/resources/dom-paste-helper.js b/LayoutTests/editing/pasteboard/ios/resources/dom-paste-helper.js
deleted file mode 100644
index ef9bc41..0000000
--- a/LayoutTests/editing/pasteboard/ios/resources/dom-paste-helper.js
+++ /dev/null
@@ -1,41 +0,0 @@
-
-async function _waitForOrTriggerPasteMenu(x, y, proceedWithPaste, shouldTap) {
-    return new Promise(resolve => {
-        testRunner.runUIScript(`
-            (() => {
-                doneCount = 0;
-                function checkDone() {
-                    if (++doneCount === (${shouldTap} ? 3 : 2))
-                        uiController.uiScriptComplete();
-                }
-
-                uiController.didHideMenuCallback = checkDone;
-
-                function tapPasteMenuAction() {
-                    if (${proceedWithPaste}) {
-                        const rect = uiController.rectForMenuAction("Paste");
-                        uiController.singleTapAtPoint(rect.left + rect.width / 2, rect.top + rect.height / 2, checkDone);
-                    } else {
-                        uiController.resignFirstResponder();
-                        checkDone();
-                    }
-                }
-
-                if (uiController.isShowingMenu)
-                    tapPasteMenuAction();
-                else
-                    uiController.didShowMenuCallback = tapPasteMenuAction;
-
-                if (${shouldTap})
-                    uiController.singleTapAtPoint(${x}, ${y}, checkDone);
-            })()`, resolve);
-    });
-}
-
-async function triggerPasteMenuAfterTapAt(x, y, proceedWithPaste = true) {
-    return _waitForOrTriggerPasteMenu(x, y, proceedWithPaste, true);
-}
-
-async function waitForPasteMenu(proceedWithPaste = true) {
-    return _waitForOrTriggerPasteMenu(null, null, proceedWithPaste, false);
-}
diff --git a/LayoutTests/platform/ios-wk2/TestExpectations b/LayoutTests/platform/ios-wk2/TestExpectations
index d7e98a9..f08ef97 100644
--- a/LayoutTests/platform/ios-wk2/TestExpectations
+++ b/LayoutTests/platform/ios-wk2/TestExpectations
@@ -21,7 +21,7 @@
 editing/caret/ios [ Pass ]
 editing/find [ Pass ]
 editing/input/ios [ Pass ]
-editing/pasteboard/ios [ Pass ]
+editing/pasteboard/dom-paste [ Pass ]
 editing/undo-manager [ Pass ]
 
 accessibility/set-selected-text-range-after-newline.html [ Pass ]
@@ -1363,7 +1363,6 @@
 # problem with blur handling
 mathml/focus-event-handling.html [ Failure ]
 
-webkit.org/b/201898 editing/pasteboard/ios/dom-paste-same-origin.html [ Failure ]
 # <rdar://problem/51756254>REGRESSION (r244582-r244596) Layout tests fast/scrolling/ios/touch-scroll-visibility-hidden.html fast/scrolling/ios/touch-scroll-pointer-events-none.html are failing
 fast/scrolling/ios/touch-scroll-pointer-events-none.html [ Failure ]
 fast/scrolling/ios/touch-scroll-visibility-hidden.html [ Failure ]
diff --git a/LayoutTests/platform/ios/TestExpectations b/LayoutTests/platform/ios/TestExpectations
index 6ea0bc2..ff66a57 100644
--- a/LayoutTests/platform/ios/TestExpectations
+++ b/LayoutTests/platform/ios/TestExpectations
@@ -3366,8 +3366,6 @@
 # <rdar://problem/52962272> fast/scrolling/ios/body-overflow-hidden.html is an Image failure
 fast/scrolling/ios/body-overflow-hidden.html [ Pass ImageOnlyFailure ]
 
-webkit.org/b/201898 editing/pasteboard/ios/dom-paste-same-origin.html [ Failure ]
-
 webkit.org/b/201899 editing/pasteboard/paste-does-not-fire-promises-while-sanitizing-web-content.html [ Failure ]
 
 webkit.org/b/201900 webrtc/datachannel/mdns-ice-candidates.html [ Failure ]
diff --git a/LayoutTests/platform/mac-wk2/TestExpectations b/LayoutTests/platform/mac-wk2/TestExpectations
index e7fd49e..3828dbc 100644
--- a/LayoutTests/platform/mac-wk2/TestExpectations
+++ b/LayoutTests/platform/mac-wk2/TestExpectations
@@ -10,6 +10,7 @@
 compositing/layer-creation/clipping-scope [ Pass ]
 editing/find [ Pass ]
 editing/undo-manager [ Pass ]
+editing/pasteboard/dom-paste [ Pass ]
 fast/forms/select/mac-wk2 [ Pass ]
 fast/visual-viewport/tiled-drawing [ Pass ]
 fast/web-share [ Pass ]
diff --git a/LayoutTests/platform/win/TestExpectations b/LayoutTests/platform/win/TestExpectations
index 383e35d..7329662 100644
--- a/LayoutTests/platform/win/TestExpectations
+++ b/LayoutTests/platform/win/TestExpectations
@@ -1173,7 +1173,7 @@
 ###### Pasteboard
 ###### These tests are very flaky.
 editing/pasteboard/ [ Pass Failure ]
-editing/pasteboard/ios [ Skip ]
+editing/pasteboard/dom-paste/ [ Skip ]
 [ Debug ] editing/pasteboard/copy-crash.html [ Skip ] # Debug Assertion
 [ Debug ] editing/pasteboard/copy-crash-with-extraneous-attribute.html [ Skip ] # Debug Assertion
 [ Debug ] editing/pasteboard/testcase-9507.html [ Skip ] # Debug Assertion
diff --git a/LayoutTests/platform/wincairo/TestExpectations b/LayoutTests/platform/wincairo/TestExpectations
index fda76c2..e1b7ee1 100644
--- a/LayoutTests/platform/wincairo/TestExpectations
+++ b/LayoutTests/platform/wincairo/TestExpectations
@@ -1306,7 +1306,7 @@
 editing/pasteboard/ [ Pass Failure ImageOnlyFailure ]
 editing/pasteboard/copy-paste-across-shadow-boundaries-with-style-2.html [ Skip ]
 editing/pasteboard/drag-and-drop-color-input-events.html [ Skip ]
-editing/pasteboard/ios/ [ Skip ]
+editing/pasteboard/dom-paste/ [ Skip ]
 editing/pasteboard/paste-image-as-blob-url.html [ Skip ]
 # TODO eventSender.contextClick() needs to return a JS array of the context menu items.
 webkit.org/b/62597 editing/pasteboard/copy-standalone-image-crash.html [ Skip ]
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index ed3f98e..a1f7d31 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,46 @@
+2019-10-10  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        Support programmatic paste requests on macOS
+        https://bugs.webkit.org/show_bug.cgi?id=202773
+        <rdar://problem/48957166>
+
+        Reviewed by Tim Horton.
+
+        Adds support for programmatic paste requests on macOS. See below for more details.
+
+        Tests: editing/pasteboard/dom-paste/dom-paste-confirmation.html
+               editing/pasteboard/dom-paste/dom-paste-consecutive-confirmations.html
+               editing/pasteboard/dom-paste/dom-paste-rejection.html
+               editing/pasteboard/dom-paste/dom-paste-requires-user-gesture.html
+               editing/pasteboard/dom-paste/dom-paste-same-origin.html
+
+        * editing/EditorCommand.cpp:
+        (WebCore::defaultValueForSupportedPaste):
+        (WebCore::supportedPaste):
+        (WebCore::allowPasteFromDOM):
+        (WebCore::enabledPaste):
+
+        Fixes an existing bug uncovered by the layout test editing/execCommand/clipboard-access.html, which tests the
+        results of `document.queryCommandEnabled("copy")` and `document.queryCommandEnabled("paste")`. The problem here
+        is that document.queryCommandEnabled("paste") returns true if DOM paste access requests are enabled, regardless
+        of whether or not there is an active user gesture. This is inconsistent with the behavior of "copy" and "cut",
+        which return false in the case where there is no user gesture (and the clipboard access policy is also equal to
+        ClipboardAccessPolicy::RequiresUserGesture -- refer to `allowCopyCutFromDOM`).
+
+        When pasting, we only DOM paste access requests to be triggered only in the case where there is a user gesture.
+        This means that enabledPaste should additionally be gated on a user gesture check. For consistency with the
+        implementation of `enabledCopy`, we introduce a `allowPasteFromDOM` helper that is similar to
+        `allowCopyCutFromDOM`, and additionally check this constraint when the paste command's source is the DOM (as
+        opposed to a menu or key binding).
+
+        This adjustment also adds a missing canDHTMLPaste() check prior to consulting canPaste(). This ensures that when
+        evaluating document.queryCommandEnabled("Paste"), we'll dispatch a "beforepaste" event, similar to how
+        evaluating document.queryCommandEnabled("Copy") dispatches a "beforecopy" event.
+
+        * platform/LocalizedStrings.h:
+
+        Mark a function as WEBCORE_EXPORT.
+
 2019-10-10  Eric Carlson  <eric.carlson@apple.com>
 
         [GTK][WPE] Lots of media related tests crashing or flaky after r250918 - [ Mac WK2 ] Layout Test fast/mediastream/MediaStreamTrack-getSettings.html is a flaky failure
diff --git a/Source/WebCore/editing/EditorCommand.cpp b/Source/WebCore/editing/EditorCommand.cpp
index 4e5aa61..30e822e 100644
--- a/Source/WebCore/editing/EditorCommand.cpp
+++ b/Source/WebCore/editing/EditorCommand.cpp
@@ -1235,13 +1235,21 @@
     return client ? client->canCopyCut(frame, defaultValue) : defaultValue;
 }
 
+static bool defaultValueForSupportedPaste(Frame& frame)
+{
+    auto& settings = frame.settings();
+    if (settings.javaScriptCanAccessClipboard() && settings.DOMPasteAllowed())
+        return true;
+
+    return settings.domPasteAccessRequestsEnabled();
+}
+
 static bool supportedPaste(Frame* frame)
 {
     if (!frame)
         return false;
 
-    auto& settings = frame->settings();
-    bool defaultValue = (settings.javaScriptCanAccessClipboard() && settings.DOMPasteAllowed()) || settings.domPasteAccessRequestsEnabled();
+    bool defaultValue = defaultValueForSupportedPaste(*frame);
 
     EditorClient* client = frame->editor().client();
     return client ? client->canPaste(frame, defaultValue) : defaultValue;
@@ -1370,9 +1378,26 @@
     return selection.isCaretOrRange() && selection.isContentRichlyEditable() && selection.rootEditableElement();
 }
 
-static bool enabledPaste(Frame& frame, Event*, EditorCommandSource)
+static bool allowPasteFromDOM(Frame& frame)
 {
-    return frame.editor().canPaste();
+    auto& settings = frame.settings();
+    if (settings.javaScriptCanAccessClipboard() && settings.DOMPasteAllowed())
+        return true;
+
+    return settings.domPasteAccessRequestsEnabled() && UserGestureIndicator::processingUserGesture();
+}
+
+static bool enabledPaste(Frame& frame, Event*, EditorCommandSource source)
+{
+    switch (source) {
+    case CommandFromMenuOrKeyBinding:
+        return frame.editor().canDHTMLPaste() || frame.editor().canPaste();
+    case CommandFromDOM:
+    case CommandFromDOMWithUserInterface:
+        return allowPasteFromDOM(frame) && (frame.editor().canDHTMLPaste() || frame.editor().canPaste());
+    }
+    ASSERT_NOT_REACHED();
+    return false;
 }
 
 static bool enabledRangeInEditableText(Frame& frame, Event*, EditorCommandSource)
diff --git a/Source/WebCore/platform/LocalizedStrings.h b/Source/WebCore/platform/LocalizedStrings.h
index 8a7ac88..94a0d63 100644
--- a/Source/WebCore/platform/LocalizedStrings.h
+++ b/Source/WebCore/platform/LocalizedStrings.h
@@ -68,7 +68,7 @@
     String contextMenuItemTagStop();
     String contextMenuItemTagReload();
     String contextMenuItemTagCut();
-    String contextMenuItemTagPaste();
+    WEBCORE_EXPORT String contextMenuItemTagPaste();
 #if PLATFORM(GTK)
     String contextMenuItemTagDelete();
     String contextMenuItemTagInputMethods();
diff --git a/Source/WebKit/ChangeLog b/Source/WebKit/ChangeLog
index 95f0cc9..8c636f9 100644
--- a/Source/WebKit/ChangeLog
+++ b/Source/WebKit/ChangeLog
@@ -1,3 +1,64 @@
+2019-10-10  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        Support programmatic paste requests on macOS
+        https://bugs.webkit.org/show_bug.cgi?id=202773
+        <rdar://problem/48957166>
+
+        Reviewed by Tim Horton.
+
+        Adds support for programmatic paste requests on macOS, as well as some testing SPI in WKWebView to allow
+        WebKitTestRunner to grab the NSMenu used for the DOM paste request. This patch adopts the same strategy taken to
+        allow programmatic paste on iOS, by allowing programmatic pastes coming from the page to show platform UI which
+        the user must then interact with in order to proceed with the paste. See below for more details.
+
+        * Shared/WebPreferencesDefaultValues.h:
+
+        Make this available on both iOS and macOS (iOS family is omitted for now, since callout bar UI is not generally
+        present on non-iOS iOS-family platforms such as Apple Watch).
+
+        * UIProcess/API/Cocoa/WKWebView.mm:
+        (-[WKWebView _web_grantDOMPasteAccess]):
+
+        This selector is called when the user taps the Paste option in the presented NSMenu.
+
+        (-[WKWebView _activeMenu]):
+
+        Returns the currently active NSMenu. Only for testing purposes.
+
+        * UIProcess/API/Cocoa/WKWebViewInternal.h:
+        * UIProcess/API/Cocoa/WKWebViewPrivate.h:
+        * UIProcess/API/mac/WKView.mm:
+        (-[WKView _web_grantDOMPasteAccess]):
+
+        Same exercise as above, only for WKView instead of WKWebView.
+
+        * UIProcess/Cocoa/WebViewImpl.h:
+        (WebKit::WebViewImpl::domPasteMenu const):
+        * UIProcess/Cocoa/WebViewImpl.mm:
+        (-[WKDOMPasteMenuDelegate initWithWebViewImpl:]):
+        (-[WKDOMPasteMenuDelegate menuDidClose:]):
+        (-[WKDOMPasteMenuDelegate numberOfItemsInMenu:]):
+        (-[WKDOMPasteMenuDelegate confinementRectForMenu:onScreen:]):
+
+        Adds a new object, whose purpose is to be a delegate for the NSMenu that is presented when requesting DOM paste
+        access. This object is used instead of WKWebView, since API clients may end up making the WKWebView the delegate
+        for a different menu, in which case some implementations (either theirs or ours) of NSMenuDelegate methods would
+        not be called. Avoiding this would require the client to be aware that WKWebView conforms to NSMenuDelegate,
+        which is only declared privately.
+
+        (WebKit::WebViewImpl::handleProcessSwapOrExit):
+
+        On process swap or exit, automatically bail out of any pending DOM paste request by denying it.
+
+        (WebKit::WebViewImpl::requestDOMPasteAccess):
+        (WebKit::WebViewImpl::handleDOMPasteRequestWithResult):
+
+        Handle the DOM paste request by showing an NSMenu near the mouse cursor with a single option to paste.
+
+        * UIProcess/mac/PageClientImplMac.h:
+        * UIProcess/mac/PageClientImplMac.mm:
+        (WebKit::PageClientImpl::requestDOMPasteAccess):
+
 2019-10-10  youenn fablet  <youenn@apple.com>
 
         Remove unified plan runtime flag
diff --git a/Source/WebKit/Shared/WebPreferencesDefaultValues.h b/Source/WebKit/Shared/WebPreferencesDefaultValues.h
index cbbcdc1..49c5bb0 100644
--- a/Source/WebKit/Shared/WebPreferencesDefaultValues.h
+++ b/Source/WebKit/Shared/WebPreferencesDefaultValues.h
@@ -256,7 +256,7 @@
 #define DEFAULT_CUSTOM_PASTEBOARD_DATA_ENABLED false
 #endif
 
-#if PLATFORM(IOS)
+#if PLATFORM(IOS) || PLATFORM(MAC)
 #define DEFAULT_DOM_PASTE_ACCESS_REQUESTS_ENABLED true
 #else
 #define DEFAULT_DOM_PASTE_ACCESS_REQUESTS_ENABLED false
diff --git a/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm b/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm
index a5cfb72..ba14c27 100644
--- a/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm
+++ b/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm
@@ -3640,6 +3640,11 @@
     _impl->setFrameSize(NSSizeToCGSize(size));
 }
 
+- (void)_web_grantDOMPasteAccess
+{
+    _impl->handleDOMPasteRequestWithResult(WebCore::DOMPasteAccessResponse::GrantedForGesture);
+}
+
 ALLOW_DEPRECATED_IMPLEMENTATIONS_BEGIN
 - (void)renewGState
 ALLOW_DEPRECATED_IMPLEMENTATIONS_END
@@ -7314,6 +7319,13 @@
     _impl->doAfterProcessingAllPendingMouseEvents(action);
 }
 
+- (NSMenu *)_activeMenu
+{
+    // FIXME: Only the DOM paste access menu is supported for now. In the future, it could be
+    // extended to recognize the regular context menu as well.
+    return _impl->domPasteMenu();
+}
+
 #endif // PLATFORM(MAC)
 
 - (void)_requestActiveNowPlayingSessionInfo:(void(^)(BOOL, BOOL, NSString*, double, double, NSInteger))callback
diff --git a/Source/WebKit/UIProcess/API/Cocoa/WKWebViewInternal.h b/Source/WebKit/UIProcess/API/Cocoa/WKWebViewInternal.h
index c50a7e4..435080f 100644
--- a/Source/WebKit/UIProcess/API/Cocoa/WKWebViewInternal.h
+++ b/Source/WebKit/UIProcess/API/Cocoa/WKWebViewInternal.h
@@ -189,6 +189,10 @@
 - (WKPageRef)_pageForTesting;
 - (WebKit::WebPageProxy*)_page;
 
+#if PLATFORM(MAC)
+- (void)_web_grantDOMPasteAccess;
+#endif
+
 @end
 
 WKWebView* fromWebPageProxy(WebKit::WebPageProxy&);
diff --git a/Source/WebKit/UIProcess/API/Cocoa/WKWebViewPrivate.h b/Source/WebKit/UIProcess/API/Cocoa/WKWebViewPrivate.h
index cd205d0..094fe25 100644
--- a/Source/WebKit/UIProcess/API/Cocoa/WKWebViewPrivate.h
+++ b/Source/WebKit/UIProcess/API/Cocoa/WKWebViewPrivate.h
@@ -543,6 +543,7 @@
 - (void)_insertText:(id)string replacementRange:(NSRange)replacementRange WK_API_AVAILABLE(macos(10.12.3));
 - (NSRect)_candidateRect WK_API_AVAILABLE(macos(10.13));
 @property (nonatomic, readwrite, setter=_setUseSystemAppearance:) BOOL _useSystemAppearance WK_API_AVAILABLE(macos(10.14));
+@property (nonatomic, readonly) NSMenu *_activeMenu WK_API_AVAILABLE(macos(WK_MAC_TBA));
 
 - (void)_setHeaderBannerHeight:(int)height WK_API_AVAILABLE(macos(10.12.3));
 - (void)_setFooterBannerHeight:(int)height WK_API_AVAILABLE(macos(10.12.3));
diff --git a/Source/WebKit/UIProcess/API/mac/WKView.mm b/Source/WebKit/UIProcess/API/mac/WKView.mm
index 72f88ca..68ce0c8 100644
--- a/Source/WebKit/UIProcess/API/mac/WKView.mm
+++ b/Source/WebKit/UIProcess/API/mac/WKView.mm
@@ -891,6 +891,11 @@
     return _data->_impl->namesOfPromisedFilesDroppedAtDestination(dropDestination);
 }
 
+- (void)_web_grantDOMPasteAccess
+{
+    _data->_impl->handleDOMPasteRequestWithResult(WebCore::DOMPasteAccessResponse::GrantedForGesture);
+}
+
 - (void)maybeInstallIconLoadingClient
 {
     ALLOW_DEPRECATED_DECLARATIONS_BEGIN
diff --git a/Source/WebKit/UIProcess/Cocoa/WebViewImpl.h b/Source/WebKit/UIProcess/Cocoa/WebViewImpl.h
index 6c5f4f3..33d7a9a 100644
--- a/Source/WebKit/UIProcess/Cocoa/WebViewImpl.h
+++ b/Source/WebKit/UIProcess/Cocoa/WebViewImpl.h
@@ -31,6 +31,7 @@
 #include "ShareableBitmap.h"
 #include "WKLayoutMode.h"
 #include "_WKOverlayScrollbarStyle.h"
+#include <WebCore/DOMPasteAccess.h>
 #include <WebCore/FocusDirection.h>
 #include <WebCore/ScrollTypes.h>
 #include <WebCore/TextIndicatorWindow.h>
@@ -38,6 +39,7 @@
 #include <WebKit/WKDragDestinationAction.h>
 #include <pal/spi/cocoa/AVKitSPI.h>
 #include <wtf/BlockPtr.h>
+#include <wtf/CompletionHandler.h>
 #include <wtf/RetainPtr.h>
 #include <wtf/WeakObjCPtr.h>
 #include <wtf/WeakPtr.h>
@@ -47,10 +49,12 @@
 
 OBJC_CLASS NSAccessibilityRemoteUIElement;
 OBJC_CLASS NSImmediateActionGestureRecognizer;
+OBJC_CLASS NSMenu;
 OBJC_CLASS NSTextInputContext;
 OBJC_CLASS NSView;
 OBJC_CLASS WKAccessibilitySettingsObserver;
 OBJC_CLASS WKBrowsingContextController;
+OBJC_CLASS WKDOMPasteMenuDelegate;
 OBJC_CLASS WKEditorUndoTarget;
 OBJC_CLASS WKFullScreenWindowController;
 OBJC_CLASS WKImmediateActionController;
@@ -114,6 +118,8 @@
 - (void)_web_didPerformDragOperation:(BOOL)handled;
 #endif
 
+- (void)_web_grantDOMPasteAccess;
+
 @optional
 - (void)_web_didAddMediaControlsManager:(id)controlsManager;
 - (void)_web_didRemoveMediaControlsManager;
@@ -600,6 +606,10 @@
     void takeFocus(WebCore::FocusDirection);
     void clearPromisedDragImage();
 
+    void requestDOMPasteAccess(const WebCore::IntRect&, const String& originIdentifier, CompletionHandler<void(WebCore::DOMPasteAccessResponse)>&&);
+    void handleDOMPasteRequestWithResult(WebCore::DOMPasteAccessResponse);
+    NSMenu *domPasteMenu() const { return m_domPasteMenu.get(); }
+
 private:
 #if HAVE(TOUCH_BAR)
     void setUpTextTouchBar(NSTouchBar *);
@@ -794,6 +804,9 @@
     NSInteger m_initialNumberOfValidItemsForDrop { 0 };
 #endif
 
+    RetainPtr<NSMenu> m_domPasteMenu;
+    RetainPtr<WKDOMPasteMenuDelegate> m_domPasteMenuDelegate;
+    CompletionHandler<void(WebCore::DOMPasteAccessResponse)> m_domPasteRequestHandler;
 };
     
 } // namespace WebKit
diff --git a/Source/WebKit/UIProcess/Cocoa/WebViewImpl.mm b/Source/WebKit/UIProcess/Cocoa/WebViewImpl.mm
index 642f166..1262717 100644
--- a/Source/WebKit/UIProcess/Cocoa/WebViewImpl.mm
+++ b/Source/WebKit/UIProcess/Cocoa/WebViewImpl.mm
@@ -92,6 +92,7 @@
 #import <WebCore/LegacyNSPasteboardTypes.h>
 #import <WebCore/LoaderNSURLExtras.h>
 #import <WebCore/LocalizedStrings.h>
+#import <WebCore/Pasteboard.h>
 #import <WebCore/PlatformEventFactoryMac.h>
 #import <WebCore/PromisedAttachmentInfo.h>
 #import <WebCore/TextAlternativeWithRange.h>
@@ -879,6 +880,45 @@
 
 @end
 
+@interface WKDOMPasteMenuDelegate : NSObject<NSMenuDelegate>
+- (instancetype)initWithWebViewImpl:(WebKit::WebViewImpl&)impl;
+@end
+
+@implementation WKDOMPasteMenuDelegate {
+    WeakPtr<WebKit::WebViewImpl> _impl;
+}
+
+- (instancetype)initWithWebViewImpl:(WebKit::WebViewImpl&)impl
+{
+    if (!(self = [super init]))
+        return nil;
+
+    _impl = makeWeakPtr(impl);
+    return self;
+}
+
+- (void)menuDidClose:(NSMenu *)menu
+{
+    dispatch_async(dispatch_get_main_queue(), [impl = _impl] {
+        if (impl)
+            impl->handleDOMPasteRequestWithResult(WebCore::DOMPasteAccessResponse::DeniedForGesture);
+    });
+}
+
+- (NSInteger)numberOfItemsInMenu:(NSMenu *)menu
+{
+    return 1;
+}
+
+- (NSRect)confinementRectForMenu:(NSMenu *)menu onScreen:(NSScreen *)screen
+{
+    auto confinementRect = WebCore::enclosingIntRect(NSRect { NSEvent.mouseLocation, menu.size });
+    confinementRect.move(0, -confinementRect.height());
+    return confinementRect;
+}
+
+@end
+
 namespace WebKit {
 
 NSTouchBar *WebViewImpl::makeTouchBar()
@@ -1417,6 +1457,8 @@
 
     updateRemoteAccessibilityRegistration(false);
     flushPendingMouseEventCallbacks();
+
+    handleDOMPasteRequestWithResult(WebCore::DOMPasteAccessResponse::DeniedForGesture);
 }
 
 void WebViewImpl::processWillSwap()
@@ -4263,6 +4305,39 @@
     return [NSArray arrayWithObject:[path lastPathComponent]];
 }
 
+void WebViewImpl::requestDOMPasteAccess(const WebCore::IntRect&, const String& originIdentifier, CompletionHandler<void(WebCore::DOMPasteAccessResponse)>&& completion)
+{
+    ASSERT(!m_domPasteRequestHandler);
+    handleDOMPasteRequestWithResult(WebCore::DOMPasteAccessResponse::DeniedForGesture);
+
+    NSData *data = [NSPasteboard.generalPasteboard dataForType:@(WebCore::PasteboardCustomData::cocoaType())];
+    auto buffer = WebCore::SharedBuffer::create(data);
+    if (WebCore::PasteboardCustomData::fromSharedBuffer(buffer.get()).origin == originIdentifier) {
+        completion(WebCore::DOMPasteAccessResponse::GrantedForGesture);
+        return;
+    }
+
+    m_domPasteMenuDelegate = adoptNS([[WKDOMPasteMenuDelegate alloc] initWithWebViewImpl:*this]);
+    m_domPasteRequestHandler = WTFMove(completion);
+    m_domPasteMenu = adoptNS([[NSMenu alloc] initWithTitle:WebCore::contextMenuItemTagPaste()]);
+
+    [m_domPasteMenu setDelegate:m_domPasteMenuDelegate.get()];
+    [m_domPasteMenu setAllowsContextMenuPlugIns:NO];
+    [m_domPasteMenu insertItemWithTitle:WebCore::contextMenuItemTagPaste() action:@selector(_web_grantDOMPasteAccess) keyEquivalent:emptyString() atIndex:0];
+    [NSMenu popUpContextMenu:m_domPasteMenu.get() withEvent:m_lastMouseDownEvent.get() forView:m_view.getAutoreleased()];
+}
+
+void WebViewImpl::handleDOMPasteRequestWithResult(WebCore::DOMPasteAccessResponse response)
+{
+    if (auto handler = std::exchange(m_domPasteRequestHandler, { }))
+        handler(response);
+    [m_domPasteMenu removeAllItems];
+    [m_domPasteMenu update];
+    [m_domPasteMenu cancelTracking];
+    m_domPasteMenu = nil;
+    m_domPasteMenuDelegate = nil;
+}
+
 static RetainPtr<CGImageRef> takeWindowSnapshot(CGSWindowID windowID, bool captureAtNominalResolution)
 {
     CGSWindowCaptureOptions options = kCGSCaptureIgnoreGlobalClipShape;
diff --git a/Source/WebKit/UIProcess/mac/PageClientImplMac.h b/Source/WebKit/UIProcess/mac/PageClientImplMac.h
index a7fdb0b..8016b56 100644
--- a/Source/WebKit/UIProcess/mac/PageClientImplMac.h
+++ b/Source/WebKit/UIProcess/mac/PageClientImplMac.h
@@ -213,7 +213,7 @@
     void willRecordNavigationSnapshot(WebBackForwardListItem&) override;
     void didRemoveNavigationGestureSnapshot() override;
 
-    void requestDOMPasteAccess(const WebCore::IntRect&, const String& originIdentifier, CompletionHandler<void(WebCore::DOMPasteAccessResponse)>&& completion) final { completion(WebCore::DOMPasteAccessResponse::DeniedForGesture); }
+    void requestDOMPasteAccess(const WebCore::IntRect&, const String&, CompletionHandler<void(WebCore::DOMPasteAccessResponse)>&&) final;
 
     NSView *activeView() const;
     NSWindow *activeWindow() const;
diff --git a/Source/WebKit/UIProcess/mac/PageClientImplMac.mm b/Source/WebKit/UIProcess/mac/PageClientImplMac.mm
index a912953..22653d7 100644
--- a/Source/WebKit/UIProcess/mac/PageClientImplMac.mm
+++ b/Source/WebKit/UIProcess/mac/PageClientImplMac.mm
@@ -955,6 +955,11 @@
     m_impl->takeFocus(direction);
 }
 
+void PageClientImpl::requestDOMPasteAccess(const WebCore::IntRect& elementRect, const String& originIdentifier, CompletionHandler<void(WebCore::DOMPasteAccessResponse)>&& completion)
+{
+    m_impl->requestDOMPasteAccess(elementRect, originIdentifier, WTFMove(completion));
+}
+
 } // namespace WebKit
 
 #endif // PLATFORM(MAC)
diff --git a/Tools/ChangeLog b/Tools/ChangeLog
index fbb8433..c8edfc3 100644
--- a/Tools/ChangeLog
+++ b/Tools/ChangeLog
@@ -1,3 +1,85 @@
+2019-10-10  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        Support programmatic paste requests on macOS
+        https://bugs.webkit.org/show_bug.cgi?id=202773
+        <rdar://problem/48957166>
+
+        Reviewed by Tim Horton.
+
+        Adds new testing support to enable us to test programmatic paste requests on macOS.
+
+        * TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl:
+
+        Add a few new UIScriptController methods:
+        - activateAtPoint(x, y, callback): used to activate content underneath at (x, y), in root view coordinates
+          (WKWebView on macOS, and WKContentView on iOS). On macOS, this moves the mouse to the given location and
+          clicks.
+        - chooseMenuAction(action, callback): used to select a menu item with the given title.
+        - dismissMenu(): dismisses the platform menu.
+
+        Note that dismissMenu and chooseMenuAction currently only work for the DOM paste menu, but could be extended in
+        the future to handle the system context menu.
+
+        * TestRunnerShared/UIScriptContext/UIScriptController.cpp:
+        (WTR::UIScriptController::dismissMenu):
+        (WTR::UIScriptController::chooseMenuAction):
+        * TestRunnerShared/UIScriptContext/UIScriptController.h:
+        (WTR::UIScriptController::activateAtPoint):
+        * WebKitTestRunner/cocoa/TestControllerCocoa.mm:
+        (WTR::TestController::cocoaResetStateToConsistentValues):
+        * WebKitTestRunner/cocoa/TestRunnerWKWebView.h:
+        * WebKitTestRunner/cocoa/TestRunnerWKWebView.mm:
+        (-[TestRunnerWKWebView initWithFrame:configuration:]):
+        (-[TestRunnerWKWebView _didShowMenu]):
+        (-[TestRunnerWKWebView _didHideMenu]):
+
+        Make these present across both macOS and iOS. On macOS, we listen for NSMenuDidBeginTrackingNotification and
+        NSMenuDidEndTrackingNotification to know when a menu has been shown or dismissed.
+
+        (-[TestRunnerWKWebView dismissActiveMenu]):
+        (-[TestRunnerWKWebView resetInteractionCallbacks]):
+
+        Make these available on both iOS and macOS. The only interaction callbacks on macOS are currently
+        didShowMenuCallback and didHideMenuCallback.
+
+        (-[TestRunnerWKWebView _willHideMenu]):
+        * WebKitTestRunner/cocoa/UIScriptControllerCocoa.h:
+        * WebKitTestRunner/cocoa/UIScriptControllerCocoa.mm:
+        (WTR::UIScriptControllerCocoa::setDidShowMenuCallback):
+        (WTR::UIScriptControllerCocoa::setDidHideMenuCallback):
+        (WTR::UIScriptControllerCocoa::dismissMenu):
+        (WTR::UIScriptControllerCocoa::isShowingMenu const):
+
+        Move these implementations into UIScriptControllerCocoa, from UIScriptControllerIOS.
+
+        * WebKitTestRunner/ios/TestControllerIOS.mm:
+        (WTR::TestController::platformResetStateToConsistentValues):
+
+        Instead of clearing all interaction callbacks in TestControllerIOS, do it in TestControllerCocoa where it
+        affects both macOS and iOS.
+
+        * WebKitTestRunner/ios/UIScriptControllerIOS.h:
+        * WebKitTestRunner/ios/UIScriptControllerIOS.mm:
+        (WTR::UIScriptControllerIOS::activateAtPoint):
+        (WTR::UIScriptControllerIOS::singleTapAtPointWithModifiers):
+        (WTR::UIScriptControllerIOS::chooseMenuAction):
+        (WTR::UIScriptControllerIOS::rectForMenuAction const):
+        (WTR::UIScriptControllerIOS::setDidShowMenuCallback): Deleted.
+        (WTR::UIScriptControllerIOS::setDidHideMenuCallback): Deleted.
+        (WTR::UIScriptControllerIOS::isShowingMenu const): Deleted.
+
+        Abstract rectForMenuAction and singleTapAtPointWithModifiers out into private helper methods, such that they can
+        be used from within other script controller methods.
+
+        * WebKitTestRunner/mac/UIScriptControllerMac.h:
+        * WebKitTestRunner/mac/UIScriptControllerMac.mm:
+
+        Implement the new script controller hooks on macOS.
+
+        (WTR::UIScriptControllerMac::clearAllCallbacks):
+        (WTR::UIScriptControllerMac::chooseMenuAction):
+        (WTR::UIScriptControllerMac::activateAtPoint):
+
 2019-10-10  Jonathan Bedard  <jbedard@apple.com>
 
         results.webkit.org: Increase default limit for test results (Follow-up fix)
diff --git a/Tools/TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl b/Tools/TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl
index d8dbe6e..c0a9238 100644
--- a/Tools/TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl
+++ b/Tools/TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl
@@ -63,6 +63,8 @@
     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 activateAtPoint(long x, long y, object callback);
+
     void longPressAtPoint(long x, long y, object callback);
 
     void stylusDownAtPoint(long x, long y, float azimuthAngle, float altitudeAngle, float pressure, object callback);
@@ -238,6 +240,8 @@
     readonly attribute boolean isShowingMenu;
     readonly attribute object menuRect;
     object rectForMenuAction(DOMString action);
+    void chooseMenuAction(DOMString action, object callback);
+    void dismissMenu();
 
     readonly attribute boolean isShowingPopover;
     attribute object willPresentPopoverCallback;
diff --git a/Tools/TestRunnerShared/UIScriptContext/UIScriptController.cpp b/Tools/TestRunnerShared/UIScriptContext/UIScriptController.cpp
index f5b715e..800ccdb 100644
--- a/Tools/TestRunnerShared/UIScriptContext/UIScriptController.cpp
+++ b/Tools/TestRunnerShared/UIScriptContext/UIScriptController.cpp
@@ -231,4 +231,12 @@
     clearAllCallbacks();
 }
 
+void UIScriptController::dismissMenu()
+{
+}
+
+void UIScriptController::chooseMenuAction(JSStringRef, JSValueRef)
+{
+}
+
 }
diff --git a/Tools/TestRunnerShared/UIScriptContext/UIScriptController.h b/Tools/TestRunnerShared/UIScriptContext/UIScriptController.h
index 1029041..b060db5 100644
--- a/Tools/TestRunnerShared/UIScriptContext/UIScriptController.h
+++ b/Tools/TestRunnerShared/UIScriptContext/UIScriptController.h
@@ -105,6 +105,9 @@
     virtual void becomeFirstResponder() { notImplemented(); }
     virtual void resignFirstResponder() { notImplemented(); }
 
+    virtual void chooseMenuAction(JSStringRef, JSValueRef);
+    virtual void dismissMenu();
+
     virtual void firstResponderSuppressionForWebView(bool) { notImplemented(); }
     virtual void makeWindowContentViewFirstResponder() { notImplemented(); }
     virtual bool isWindowContentViewFirstResponder() const { notImplemented(); return false; }
@@ -142,6 +145,8 @@
     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(); }
 
+    virtual void activateAtPoint(long x, long y, JSValueRef callback) { notImplemented(); }
+
     // Keyboard
 
     virtual void enterText(JSStringRef) { notImplemented(); }
diff --git a/Tools/WebKitTestRunner/cocoa/TestControllerCocoa.mm b/Tools/WebKitTestRunner/cocoa/TestControllerCocoa.mm
index da9f349..612760c 100644
--- a/Tools/WebKitTestRunner/cocoa/TestControllerCocoa.mm
+++ b/Tools/WebKitTestRunner/cocoa/TestControllerCocoa.mm
@@ -262,6 +262,8 @@
         // Toggle on before the test, and toggle off after the test.
         if (options.shouldShowSpellCheckingDots)
             [platformView toggleContinuousSpellChecking:nil];
+
+        [platformView resetInteractionCallbacks];
     }
 
     [globalWebsiteDataStoreDelegateClient setAllowRaisingQuota: true];
diff --git a/Tools/WebKitTestRunner/cocoa/TestRunnerWKWebView.h b/Tools/WebKitTestRunner/cocoa/TestRunnerWKWebView.h
index d216404..e0af99d 100644
--- a/Tools/WebKitTestRunner/cocoa/TestRunnerWKWebView.h
+++ b/Tools/WebKitTestRunner/cocoa/TestRunnerWKWebView.h
@@ -41,8 +41,6 @@
 @property (nonatomic, copy) void (^didEndZoomingCallback)(void);
 @property (nonatomic, copy) void (^didShowKeyboardCallback)(void);
 @property (nonatomic, copy) void (^didHideKeyboardCallback)(void);
-@property (nonatomic, copy) void (^didShowMenuCallback)(void);
-@property (nonatomic, copy) void (^didHideMenuCallback)(void);
 @property (nonatomic, copy) void (^willPresentPopoverCallback)(void);
 @property (nonatomic, copy) void (^didDismissPopoverCallback)(void);
 @property (nonatomic, copy) void (^didEndScrollingCallback)(void);
@@ -54,7 +52,6 @@
 - (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;
 - (void)_didEndRotation;
@@ -62,7 +59,6 @@
 @property (nonatomic, assign) UIEdgeInsets overrideSafeAreaInsets;
 
 @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;
@@ -70,7 +66,13 @@
 
 #endif
 
+@property (nonatomic, readonly, getter=isShowingMenu) BOOL showingMenu;
+@property (nonatomic, copy) void (^didShowMenuCallback)(void);
+@property (nonatomic, copy) void (^didHideMenuCallback)(void);
 @property (nonatomic, retain, setter=_setStableStateOverride:) NSNumber *_stableStateOverride;
 @property (nonatomic, setter=_setScrollingUpdatesDisabledForTesting:) BOOL _scrollingUpdatesDisabledForTesting;
 
+- (void)dismissActiveMenu;
+- (void)resetInteractionCallbacks;
+
 @end
diff --git a/Tools/WebKitTestRunner/cocoa/TestRunnerWKWebView.mm b/Tools/WebKitTestRunner/cocoa/TestRunnerWKWebView.mm
index 632ba0e..52ced83 100644
--- a/Tools/WebKitTestRunner/cocoa/TestRunnerWKWebView.mm
+++ b/Tools/WebKitTestRunner/cocoa/TestRunnerWKWebView.mm
@@ -84,11 +84,14 @@
 }
 #endif
 
-#if PLATFORM(IOS_FAMILY)
 - (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration
 {
     if (self = [super initWithFrame:frame configuration:configuration]) {
         NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+#if PLATFORM(MAC)
+        [center addObserver:self selector:@selector(_didShowMenu) name:NSMenuDidBeginTrackingNotification object:nil];
+        [center addObserver:self selector:@selector(_didHideMenu) name:NSMenuDidEndTrackingNotification object:nil];
+#else
         [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];
@@ -97,6 +100,7 @@
         [center addObserver:self selector:@selector(_willPresentPopover) name:@"UIPopoverControllerWillPresentPopoverNotification" object:nil];
         [center addObserver:self selector:@selector(_didDismissPopover) name:@"UIPopoverControllerDidDismissPopoverNotification" object:nil];
         self.UIDelegate = self;
+#endif
     }
     return self;
 }
@@ -113,6 +117,69 @@
     [super dealloc];
 }
 
+- (void)_didShowMenu
+{
+    if (self.showingMenu)
+        return;
+
+    self.showingMenu = YES;
+    if (self.didShowMenuCallback)
+        self.didShowMenuCallback();
+}
+
+- (void)_didHideMenu
+{
+#if PLATFORM(IOS_FAMILY)
+    self.dismissingMenu = NO;
+#endif
+
+    if (!self.showingMenu)
+        return;
+
+    self.showingMenu = NO;
+    if (self.didHideMenuCallback)
+        self.didHideMenuCallback();
+}
+
+- (void)dismissActiveMenu
+{
+#if PLATFORM(IOS_FAMILY)
+    [self resignFirstResponder];
+#else
+    auto menu = retainPtr(self._activeMenu);
+    [menu removeAllItems];
+    [menu update];
+    [menu cancelTracking];
+#endif
+}
+
+- (void)resetInteractionCallbacks
+{
+    self.didShowMenuCallback = nil;
+    self.didHideMenuCallback = nil;
+#if PLATFORM(IOS_FAMILY)
+    self.didStartFormControlInteractionCallback = nil;
+    self.didEndFormControlInteractionCallback = nil;
+    self.didShowForcePressPreviewCallback = nil;
+    self.didDismissForcePressPreviewCallback = nil;
+    self.willBeginZoomingCallback = nil;
+    self.didEndZoomingCallback = nil;
+    self.didShowKeyboardCallback = nil;
+    self.didHideKeyboardCallback = nil;
+    self.willPresentPopoverCallback = nil;
+    self.didDismissPopoverCallback = nil;
+    self.didEndScrollingCallback = nil;
+    self.rotationDidEndCallback = nil;
+#endif // PLATFORM(IOS_FAMILY)
+}
+
+#if PLATFORM(IOS_FAMILY)
+
+- (void)_willHideMenu
+{
+    self.dismissingMenu = YES;
+}
+
 - (void)didStartFormControlInteraction
 {
     _isInteractingWithFormControl = YES;
@@ -228,24 +295,6 @@
     return canPerformActionByDefault;
 }
 
-- (void)resetInteractionCallbacks
-{
-    self.didStartFormControlInteractionCallback = nil;
-    self.didEndFormControlInteractionCallback = nil;
-    self.didShowForcePressPreviewCallback = nil;
-    self.didDismissForcePressPreviewCallback = nil;
-    self.willBeginZoomingCallback = nil;
-    self.didEndZoomingCallback = nil;
-    self.didShowKeyboardCallback = nil;
-    self.didHideKeyboardCallback = nil;
-    self.didShowMenuCallback = nil;
-    self.didHideMenuCallback = nil;
-    self.willPresentPopoverCallback = nil;
-    self.didDismissPopoverCallback = nil;
-    self.didEndScrollingCallback = nil;
-    self.rotationDidEndCallback = nil;
-}
-
 - (void)zoomToScale:(double)scale animated:(BOOL)animated completionHandler:(void (^)(void))completionHandler
 {
     ASSERT(!self.zoomToScaleCompletionHandler);
@@ -281,33 +330,6 @@
         self.didHideKeyboardCallback();
 }
 
-- (void)_didShowMenu
-{
-    if (self.showingMenu)
-        return;
-
-    self.showingMenu = YES;
-    if (self.didShowMenuCallback)
-        self.didShowMenuCallback();
-}
-
-- (void)_willHideMenu
-{
-    self.dismissingMenu = YES;
-}
-
-- (void)_didHideMenu
-{
-    self.dismissingMenu = NO;
-
-    if (!self.showingMenu)
-        return;
-
-    self.showingMenu = NO;
-    if (self.didHideMenuCallback)
-        self.didHideMenuCallback();
-}
-
 - (void)_willPresentPopover
 {
     if (self.showingPopover)
diff --git a/Tools/WebKitTestRunner/cocoa/UIScriptControllerCocoa.h b/Tools/WebKitTestRunner/cocoa/UIScriptControllerCocoa.h
index dd57dc0..3e4bfd8 100644
--- a/Tools/WebKitTestRunner/cocoa/UIScriptControllerCocoa.h
+++ b/Tools/WebKitTestRunner/cocoa/UIScriptControllerCocoa.h
@@ -49,6 +49,11 @@
     JSRetainPtr<JSStringRef> firstRedoLabel() const override;
     NSUndoManager *platformUndoManager() const override;
 
+    void setDidShowMenuCallback(JSValueRef) override;
+    void setDidHideMenuCallback(JSValueRef) override;
+    void dismissMenu() override;
+    bool isShowingMenu() const override;
+
 protected:
     explicit UIScriptControllerCocoa(UIScriptContext&);
     TestRunnerWKWebView *webView() const;
diff --git a/Tools/WebKitTestRunner/cocoa/UIScriptControllerCocoa.mm b/Tools/WebKitTestRunner/cocoa/UIScriptControllerCocoa.mm
index dcefa76..0f4eb07 100644
--- a/Tools/WebKitTestRunner/cocoa/UIScriptControllerCocoa.mm
+++ b/Tools/WebKitTestRunner/cocoa/UIScriptControllerCocoa.mm
@@ -166,4 +166,34 @@
     return platformContentView().undoManager;
 }
 
+void UIScriptControllerCocoa::setDidShowMenuCallback(JSValueRef callback)
+{
+    UIScriptController::setDidShowMenuCallback(callback);
+    webView().didShowMenuCallback = ^{
+        if (!m_context)
+            return;
+        m_context->fireCallback(CallbackTypeDidShowMenu);
+    };
+}
+
+void UIScriptControllerCocoa::setDidHideMenuCallback(JSValueRef callback)
+{
+    UIScriptController::setDidHideMenuCallback(callback);
+    webView().didHideMenuCallback = ^{
+        if (!m_context)
+            return;
+        m_context->fireCallback(CallbackTypeDidHideMenu);
+    };
+}
+
+void UIScriptControllerCocoa::dismissMenu()
+{
+    [webView() dismissActiveMenu];
+}
+
+bool UIScriptControllerCocoa::isShowingMenu() const
+{
+    return webView().showingMenu;
+}
+
 } // namespace WTR
diff --git a/Tools/WebKitTestRunner/ios/TestControllerIOS.mm b/Tools/WebKitTestRunner/ios/TestControllerIOS.mm
index 978a0ee..f1f1657 100644
--- a/Tools/WebKitTestRunner/ios/TestControllerIOS.mm
+++ b/Tools/WebKitTestRunner/ios/TestControllerIOS.mm
@@ -167,7 +167,6 @@
         webView.overrideSafeAreaInsets = UIEdgeInsetsZero;
         [webView _clearOverrideLayoutParameters];
         [webView _clearInterfaceOrientationOverride];
-        [webView resetInteractionCallbacks];
         [webView resetCustomMenuAction];
         [webView setAllowedMenuActions:nil];
 
diff --git a/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.h b/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.h
index 02a61e6..6ba262a 100644
--- a/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.h
+++ b/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.h
@@ -26,9 +26,15 @@
 #pragma once
 
 #import "UIScriptControllerCocoa.h"
+#import <wtf/BlockPtr.h>
 
 #if PLATFORM(IOS_FAMILY)
 
+namespace WebCore {
+class FloatPoint;
+class FloatRect;
+}
+
 namespace WTR {
 
 class UIScriptControllerIOS : public UIScriptControllerCocoa {
@@ -67,6 +73,8 @@
     void typeCharacterUsingHardwareKeyboard(JSStringRef character, JSValueRef) override;
     void keyDown(JSStringRef character, JSValueRef modifierArray) override;
 
+    void activateAtPoint(long x, long y, JSValueRef callback) override;
+
     void rawKeyDown(JSStringRef) override;
     void rawKeyUp(JSStringRef) override;
 
@@ -112,7 +120,7 @@
     JSObjectRef rectForMenuAction(JSStringRef) const override;
     JSObjectRef menuRect() const override;
     bool isDismissingMenu() const override;
-    bool isShowingMenu() const override;
+    void chooseMenuAction(JSStringRef, JSValueRef) override;
     void setSafeAreaInsets(double top, double right, double bottom, double left) override;
     void beginBackSwipe(JSValueRef) override;
     void completeBackSwipe(JSValueRef) override;
@@ -135,8 +143,6 @@
     void setDidEndZoomingCallback(JSValueRef) override;
     void setDidShowKeyboardCallback(JSValueRef) override;
     void setDidHideKeyboardCallback(JSValueRef) override;
-    void setDidShowMenuCallback(JSValueRef) override;
-    void setDidHideMenuCallback(JSValueRef) override;
     void setWillPresentPopoverCallback(JSValueRef) override;
     void setDidDismissPopoverCallback(JSValueRef) override;
     void setDidEndScrollingCallback(JSValueRef) override;
@@ -144,6 +150,8 @@
 
 private:
     void waitForSingleTapToReset() const;
+    WebCore::FloatRect rectForMenuAction(CFStringRef) const;
+    void singleTapAtPointWithModifiers(WebCore::FloatPoint location, Vector<String>&& modifierFlags, BlockPtr<void()>&&);
 };
 
 }
diff --git a/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm b/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm
index 3243edf..6e61224 100644
--- a/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm
+++ b/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm
@@ -39,6 +39,7 @@
 #import <JavaScriptCore/JavaScriptCore.h>
 #import <JavaScriptCore/OpaqueJSString.h>
 #import <UIKit/UIKit.h>
+#import <WebCore/FloatPoint.h>
 #import <WebCore/FloatRect.h>
 #import <WebKit/WKWebViewPrivate.h>
 #import <WebKit/WebKit.h>
@@ -264,6 +265,11 @@
     singleTapAtPointWithModifiers(x, y, nullptr, callback);
 }
 
+void UIScriptControllerIOS::activateAtPoint(long x, long y, JSValueRef callback)
+{
+    singleTapAtPoint(x, y, callback);
+}
+
 void UIScriptControllerIOS::waitForSingleTapToReset() const
 {
     bool doneWaitingForSingleTapToReset = false;
@@ -288,25 +294,28 @@
 void UIScriptControllerIOS::singleTapAtPointWithModifiers(long x, long y, JSValueRef modifierArray, JSValueRef callback)
 {
     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
+    singleTapAtPointWithModifiers(WebCore::FloatPoint(x, y), parseModifierArray(m_context->jsContext(), modifierArray), makeBlockPtr([this, protectedThis = makeRefPtr(*this), callbackID] {
+        if (!m_context)
+            return;
+        m_context->asyncTaskComplete(callbackID);
+    }));
+}
 
+void UIScriptControllerIOS::singleTapAtPointWithModifiers(WebCore::FloatPoint location, Vector<String>&& modifierFlags, BlockPtr<void()>&& block)
+{
     waitForSingleTapToReset();
 
-    auto modifierFlags = parseModifierArray(m_context->jsContext(), modifierArray);
     for (auto& modifierFlag : modifierFlags)
         [[HIDEventGenerator sharedHIDEventGenerator] keyDown:modifierFlag];
 
-    [[HIDEventGenerator sharedHIDEventGenerator] tap:globalToContentCoordinates(webView(), x, y) completionBlock:^{
+    [[HIDEventGenerator sharedHIDEventGenerator] tap:globalToContentCoordinates(webView(), location.x(), location.y()) completionBlock:[this, protectedThis = makeRefPtr(*this), modifierFlags = WTFMove(modifierFlags), block = WTFMove(block)] () mutable {
         if (!m_context)
             return;
         for (size_t i = modifierFlags.size(); i; ) {
             --i;
             [[HIDEventGenerator sharedHIDEventGenerator] keyUp:modifierFlags[i]];
         }
-        [[HIDEventGenerator sharedHIDEventGenerator] sendMarkerHIDEventWithCompletionBlock:^{
-            if (!m_context)
-                return;
-            m_context->asyncTaskComplete(callbackID);
-        }];
+        [[HIDEventGenerator sharedHIDEventGenerator] sendMarkerHIDEventWithCompletionBlock:block.get()];
     }];
 }
 
@@ -924,24 +933,18 @@
     };
 }
 
-void UIScriptControllerIOS::setDidShowMenuCallback(JSValueRef callback)
+void UIScriptControllerIOS::chooseMenuAction(JSStringRef jsAction, JSValueRef callback)
 {
-    UIScriptController::setDidShowMenuCallback(callback);
-    webView().didShowMenuCallback = ^{
-        if (!m_context)
-            return;
-        m_context->fireCallback(CallbackTypeDidShowMenu);
-    };
-}
+    auto action = adoptCF(JSStringCopyCFString(kCFAllocatorDefault, jsAction));
+    auto rect = rectForMenuAction(action.get());
+    if (rect.isEmpty())
+        return;
 
-void UIScriptControllerIOS::setDidHideMenuCallback(JSValueRef callback)
-{
-    UIScriptController::setDidHideMenuCallback(callback);
-    webView().didHideMenuCallback = ^{
-        if (!m_context)
-            return;
-        m_context->fireCallback(CallbackTypeDidHideMenu);
-    };
+    unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
+    singleTapAtPointWithModifiers(rect.center(), { }, makeBlockPtr([this, protectedThis = makeRef(*this), callbackID] {
+        if (m_context)
+            m_context->asyncTaskComplete(callbackID);
+    }));
 }
 
 bool UIScriptControllerIOS::isShowingPopover() const
@@ -972,19 +975,27 @@
 JSObjectRef UIScriptControllerIOS::rectForMenuAction(JSStringRef jsAction) const
 {
     auto action = adoptCF(JSStringCopyCFString(kCFAllocatorDefault, jsAction));
+    auto rect = rectForMenuAction(action.get());
+    if (rect.isEmpty())
+        return nullptr;
 
+    return m_context->objectFromRect(rect);
+}
+
+WebCore::FloatRect UIScriptControllerIOS::rectForMenuAction(CFStringRef action) const
+{
     UIWindow *windowForButton = nil;
     UIButton *buttonForAction = nil;
     UIView *calloutBar = UICalloutBar.activeCalloutBar;
     if (!calloutBar.window)
-        return nullptr;
+        return { };
 
     for (UIButton *button in findAllViewsInHierarchyOfType(calloutBar, UIButton.class)) {
         NSString *buttonTitle = [button titleForState:UIControlStateNormal];
         if (!buttonTitle.length)
             continue;
 
-        if (![buttonTitle isEqualToString:(__bridge NSString *)action.get()])
+        if (![buttonTitle isEqualToString:(__bridge NSString *)action])
             continue;
 
         buttonForAction = button;
@@ -993,10 +1004,10 @@
     }
 
     if (!buttonForAction)
-        return nullptr;
+        return { };
 
     CGRect rectInRootViewCoordinates = [buttonForAction convertRect:buttonForAction.bounds toView:platformContentView()];
-    return m_context->objectFromRect(WebCore::FloatRect(rectInRootViewCoordinates.origin.x, rectInRootViewCoordinates.origin.y, rectInRootViewCoordinates.size.width, rectInRootViewCoordinates.size.height));
+    return WebCore::FloatRect(rectInRootViewCoordinates.origin.x, rectInRootViewCoordinates.origin.y, rectInRootViewCoordinates.size.width, rectInRootViewCoordinates.size.height);
 }
 
 JSObjectRef UIScriptControllerIOS::menuRect() const
@@ -1014,11 +1025,6 @@
     return webView().dismissingMenu;
 }
 
-bool UIScriptControllerIOS::isShowingMenu() const
-{
-    return webView().showingMenu;
-}
-
 void UIScriptControllerIOS::setDidEndScrollingCallback(JSValueRef callback)
 {
     UIScriptController::setDidEndScrollingCallback(callback);
diff --git a/Tools/WebKitTestRunner/mac/UIScriptControllerMac.h b/Tools/WebKitTestRunner/mac/UIScriptControllerMac.h
index 049d971..2550f78 100644
--- a/Tools/WebKitTestRunner/mac/UIScriptControllerMac.h
+++ b/Tools/WebKitTestRunner/mac/UIScriptControllerMac.h
@@ -51,6 +51,11 @@
     bool isWindowContentViewFirstResponder() const override;
     void toggleCapsLock(JSValueRef) override;
     NSView *platformContentView() const override;
+    void clearAllCallbacks() override;
+
+    void chooseMenuAction(JSStringRef, JSValueRef) override;
+
+    void activateAtPoint(long x, long y, JSValueRef callback) override;
 };
 
 } // namespace WTR
diff --git a/Tools/WebKitTestRunner/mac/UIScriptControllerMac.mm b/Tools/WebKitTestRunner/mac/UIScriptControllerMac.mm
index 68ef878..08c6683 100644
--- a/Tools/WebKitTestRunner/mac/UIScriptControllerMac.mm
+++ b/Tools/WebKitTestRunner/mac/UIScriptControllerMac.mm
@@ -26,6 +26,7 @@
 #import "config.h"
 #import "UIScriptControllerMac.h"
 
+#import "EventSenderProxy.h"
 #import "EventSerializerMac.h"
 #import "PlatformWebView.h"
 #import "SharedEventStreamsMac.h"
@@ -118,6 +119,36 @@
     }];
 }
 
+void UIScriptControllerMac::clearAllCallbacks()
+{
+    [webView() resetInteractionCallbacks];
+}
+
+void UIScriptControllerMac::chooseMenuAction(JSStringRef jsAction, JSValueRef callback)
+{
+    unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
+
+    auto action = adoptCF(JSStringCopyCFString(kCFAllocatorDefault, jsAction));
+    __block NSUInteger matchIndex = NSNotFound;
+    auto activeMenu = retainPtr(webView()._activeMenu);
+    [[activeMenu itemArray] enumerateObjectsUsingBlock:^(NSMenuItem *item, NSUInteger index, BOOL *stop) {
+        if ([item.title isEqualToString:(__bridge NSString *)action.get()])
+            matchIndex = index;
+    }];
+
+    if (matchIndex != NSNotFound) {
+        [activeMenu performActionForItemAtIndex:matchIndex];
+        [activeMenu removeAllItems];
+        [activeMenu update];
+        [activeMenu cancelTracking];
+    }
+
+    dispatch_async(dispatch_get_main_queue(), ^{
+        if (m_context)
+            m_context->asyncTaskComplete(callbackID);
+    });
+}
+
 void UIScriptControllerMac::beginBackSwipe(JSValueRef callback)
 {
     playBackEvents(webView(), m_context, beginSwipeBackEventStream(), callback);
@@ -174,4 +205,24 @@
     return webView();
 }
 
+void UIScriptControllerMac::activateAtPoint(long x, long y, JSValueRef callback)
+{
+    auto* eventSender = TestController::singleton().eventSenderProxy();
+    if (!eventSender) {
+        ASSERT_NOT_REACHED();
+        return;
+    }
+
+    unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
+
+    eventSender->mouseMoveTo(x, y);
+    eventSender->mouseDown(0, 0);
+    eventSender->mouseUp(0, 0);
+
+    dispatch_async(dispatch_get_main_queue(), ^{
+        if (m_context)
+            m_context->asyncTaskComplete(callbackID);
+    });
+}
+
 } // namespace WTR