summaryrefslogtreecommitdiffstats
path: root/dom/events/test/clipboard
diff options
context:
space:
mode:
Diffstat (limited to 'dom/events/test/clipboard')
-rw-r--r--dom/events/test/clipboard/browser.ini21
-rw-r--r--dom/events/test/clipboard/browser_navigator_clipboard_clickjacking.js69
-rw-r--r--dom/events/test/clipboard/browser_navigator_clipboard_read.js198
-rw-r--r--dom/events/test/clipboard/browser_navigator_clipboard_readText.js201
-rw-r--r--dom/events/test/clipboard/browser_navigator_clipboard_touch.js114
-rw-r--r--dom/events/test/clipboard/chrome.ini3
-rw-r--r--dom/events/test/clipboard/head.js169
-rw-r--r--dom/events/test/clipboard/simple_navigator_clipboard_keydown.html15
-rw-r--r--dom/events/test/clipboard/simple_navigator_clipboard_read.html65
-rw-r--r--dom/events/test/clipboard/simple_navigator_clipboard_readText.html47
-rw-r--r--dom/events/test/clipboard/test_async_clipboard.xhtml130
11 files changed, 1032 insertions, 0 deletions
diff --git a/dom/events/test/clipboard/browser.ini b/dom/events/test/clipboard/browser.ini
new file mode 100644
index 0000000000..eb452c1443
--- /dev/null
+++ b/dom/events/test/clipboard/browser.ini
@@ -0,0 +1,21 @@
+[DEFAULT]
+support-files =
+ head.js
+ !/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js
+ !/gfx/layers/apz/test/mochitest/apz_test_utils.js
+
+[browser_navigator_clipboard_readText.js]
+support-files =
+ simple_navigator_clipboard_readText.html
+[browser_navigator_clipboard_read.js]
+support-files =
+ simple_navigator_clipboard_read.html
+[browser_navigator_clipboard_touch.js]
+support-files =
+ simple_navigator_clipboard_readText.html
+[browser_navigator_clipboard_clickjacking.js]
+skip-if =
+ os == "win" # The popupmenus dismiss when access keys for disabled items are pressed on windows
+ os == "mac" && verify
+support-files =
+ simple_navigator_clipboard_keydown.html
diff --git a/dom/events/test/clipboard/browser_navigator_clipboard_clickjacking.js b/dom/events/test/clipboard/browser_navigator_clipboard_clickjacking.js
new file mode 100644
index 0000000000..cd3e97f274
--- /dev/null
+++ b/dom/events/test/clipboard/browser_navigator_clipboard_clickjacking.js
@@ -0,0 +1,69 @@
+/* -*- Mode: JavaScript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const kBaseUrlForContent = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+const kContentFileName = "simple_navigator_clipboard_keydown.html";
+
+const kContentFileUrl = kBaseUrlForContent + kContentFileName;
+
+const kApzTestNativeEventUtilsUrl =
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js";
+
+Services.scriptloader.loadSubScript(kApzTestNativeEventUtilsUrl, this);
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.events.asyncClipboard.readText", true]],
+ });
+});
+
+add_task(async function test_paste_button_clickjacking() {
+ await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
+ const pasteButtonIsShown = promisePasteButtonIsShown();
+
+ // synthesize key to trigger readText() to bring up paste popup.
+ EventUtils.synthesizeKey("p", {}, window);
+ await waitForPasteMenuPopupEvent("shown");
+
+ const pastePopup = document.getElementById(kPasteMenuPopupId);
+ const pasteButton = document.getElementById(kPasteMenuItemId);
+ ok(
+ pasteButton.disabled,
+ "Paste button should be shown with disabled by default"
+ );
+
+ let accesskey = pasteButton.getAttribute("accesskey");
+ let delay = Services.prefs.getIntPref("security.dialog_enable_delay") * 3;
+ while (delay > 0) {
+ // There's no other way to allow some time to pass and ensure we're
+ // genuinely testing that these keypresses postpone the enabling of
+ // the paste button, so disable this check for this line:
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 100));
+ ok(pasteButton.disabled, "Paste button should still be disabled");
+ EventUtils.synthesizeKey(accesskey, {}, window);
+ is(pastePopup.state, "open", "Paste popup should still be opened");
+ delay = delay - 100;
+ }
+
+ await BrowserTestUtils.waitForMutationCondition(
+ pasteButton,
+ { attributeFilter: ["disabled"] },
+ () => !pasteButton.disabled,
+ "Wait for paste button enabled"
+ );
+
+ const pasteButtonIsHidden = promisePasteButtonIsHidden();
+ EventUtils.synthesizeKey(accesskey, {}, window);
+ await pasteButtonIsHidden;
+ });
+});
diff --git a/dom/events/test/clipboard/browser_navigator_clipboard_read.js b/dom/events/test/clipboard/browser_navigator_clipboard_read.js
new file mode 100644
index 0000000000..5b3af1d116
--- /dev/null
+++ b/dom/events/test/clipboard/browser_navigator_clipboard_read.js
@@ -0,0 +1,198 @@
+/* -*- Mode: JavaScript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const kBaseUrlForContent = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+const kContentFileName = "simple_navigator_clipboard_read.html";
+
+const kContentFileUrl = kBaseUrlForContent + kContentFileName;
+
+const kApzTestNativeEventUtilsUrl =
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js";
+
+Services.scriptloader.loadSubScript(kApzTestNativeEventUtilsUrl, this);
+
+// @param aBrowser browser object of the content tab.
+// @param aMultipleReadTextCalls if false, exactly one call is made, two
+// otherwise.
+function promiseClickContentToTriggerClipboardRead(
+ aBrowser,
+ aMultipleReadTextCalls
+) {
+ return promiseClickContentElement(
+ aBrowser,
+ aMultipleReadTextCalls ? "invokeReadTwiceId" : "invokeReadOnceId"
+ );
+}
+
+// @param aBrowser browser object of the content tab.
+function promiseMutatedReadResultFromContentElement(aBrowser) {
+ return promiseMutatedTextContentFromContentElement(aBrowser, "readResultId");
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.events.asyncClipboard.clipboardItem", true],
+ ["test.events.async.enabled", true],
+ ],
+ });
+});
+
+add_task(async function test_paste_button_position() {
+ // Ensure there's text on the clipboard.
+ await promiseWritingRandomTextToClipboard();
+
+ await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
+ const pasteButtonIsShown = promisePasteButtonIsShown();
+ const coordsOfClickInContentRelativeToScreenInDevicePixels =
+ await promiseClickContentToTriggerClipboardRead(browser, false);
+ info(
+ "coordsOfClickInContentRelativeToScreenInDevicePixels: " +
+ coordsOfClickInContentRelativeToScreenInDevicePixels.x +
+ ", " +
+ coordsOfClickInContentRelativeToScreenInDevicePixels.y
+ );
+
+ const pasteButtonCoordsRelativeToScreenInDevicePixels =
+ await pasteButtonIsShown;
+ info(
+ "pasteButtonCoordsRelativeToScreenInDevicePixels: " +
+ pasteButtonCoordsRelativeToScreenInDevicePixels.x +
+ ", " +
+ pasteButtonCoordsRelativeToScreenInDevicePixels.y
+ );
+
+ const mouseCoordsRelativeToScreenInDevicePixels =
+ getMouseCoordsRelativeToScreenInDevicePixels();
+ info(
+ "mouseCoordsRelativeToScreenInDevicePixels: " +
+ mouseCoordsRelativeToScreenInDevicePixels.x +
+ ", " +
+ mouseCoordsRelativeToScreenInDevicePixels.y
+ );
+
+ // Asserting not overlapping is important; otherwise, when the
+ // "Paste" button is shown via a `mousedown` event, the following
+ // `mouseup` event could accept the "Paste" button unnoticed by the
+ // user.
+ ok(
+ isCloselyLeftOnTopOf(
+ mouseCoordsRelativeToScreenInDevicePixels,
+ pasteButtonCoordsRelativeToScreenInDevicePixels
+ ),
+ "'Paste' button is closely left on top of the mouse pointer."
+ );
+ ok(
+ isCloselyLeftOnTopOf(
+ coordsOfClickInContentRelativeToScreenInDevicePixels,
+ pasteButtonCoordsRelativeToScreenInDevicePixels
+ ),
+ "Coords of click in content are closely left on top of the 'Paste' button."
+ );
+
+ // To avoid disturbing subsequent tests.
+ const pasteButtonIsHidden = promisePasteButtonIsHidden();
+ await promiseClickPasteButton();
+ await pasteButtonIsHidden;
+ });
+});
+
+add_task(async function test_accepting_paste_button() {
+ // Randomized text to avoid overlappings with other tests.
+ const clipboardText = await promiseWritingRandomTextToClipboard();
+
+ await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
+ const pasteButtonIsShown = promisePasteButtonIsShown();
+ await promiseClickContentToTriggerClipboardRead(browser, false);
+ await pasteButtonIsShown;
+ const pasteButtonIsHidden = promisePasteButtonIsHidden();
+ const mutatedReadResultFromContentElement =
+ promiseMutatedReadResultFromContentElement(browser);
+ await promiseClickPasteButton();
+ await pasteButtonIsHidden;
+ await mutatedReadResultFromContentElement.then(value => {
+ is(
+ value,
+ "Resolved: " + clipboardText,
+ "Text returned from `navigator.clipboard.read()` is as expected."
+ );
+ });
+ });
+});
+
+add_task(async function test_dismissing_paste_button() {
+ await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
+ const pasteButtonIsShown = promisePasteButtonIsShown();
+ await promiseClickContentToTriggerClipboardRead(browser, false);
+ await pasteButtonIsShown;
+ const pasteButtonIsHidden = promisePasteButtonIsHidden();
+ const mutatedReadResultFromContentElement =
+ promiseMutatedReadResultFromContentElement(browser);
+ await promiseDismissPasteButton();
+ await pasteButtonIsHidden;
+ await mutatedReadResultFromContentElement.then(value => {
+ is(
+ value,
+ "Rejected: The user dismissed the 'Paste' button.",
+ "`navigator.clipboard.read()` rejected after dismissing the 'Paste' button"
+ );
+ });
+ });
+});
+
+add_task(
+ async function test_multiple_read_invocations_for_same_user_activation() {
+ // Randomized text to avoid overlappings with other tests.
+ const clipboardText = await promiseWritingRandomTextToClipboard();
+
+ await BrowserTestUtils.withNewTab(
+ kContentFileUrl,
+ async function (browser) {
+ const pasteButtonIsShown = promisePasteButtonIsShown();
+ await promiseClickContentToTriggerClipboardRead(browser, true);
+ await pasteButtonIsShown;
+ const mutatedReadResultFromContentElement =
+ promiseMutatedReadResultFromContentElement(browser);
+ const pasteButtonIsHidden = promisePasteButtonIsHidden();
+ await promiseClickPasteButton();
+ await mutatedReadResultFromContentElement.then(value => {
+ is(
+ value,
+ "Resolved 1: " + clipboardText + "; Resolved 2: " + clipboardText,
+ "Two calls of `navigator.clipboard.read()` both resolved with the expected text."
+ );
+ });
+
+ // To avoid disturbing subsequent tests.
+ await pasteButtonIsHidden;
+ }
+ );
+ }
+);
+
+add_task(async function test_new_user_activation_shows_paste_button_again() {
+ await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
+ // Ensure there's text on the clipboard.
+ await promiseWritingRandomTextToClipboard();
+
+ for (let i = 0; i < 2; ++i) {
+ const pasteButtonIsShown = promisePasteButtonIsShown();
+ // A click initiates a new user activation.
+ await promiseClickContentToTriggerClipboardRead(browser, false);
+ await pasteButtonIsShown;
+
+ const pasteButtonIsHidden = promisePasteButtonIsHidden();
+ await promiseClickPasteButton();
+ await pasteButtonIsHidden;
+ }
+ });
+});
diff --git a/dom/events/test/clipboard/browser_navigator_clipboard_readText.js b/dom/events/test/clipboard/browser_navigator_clipboard_readText.js
new file mode 100644
index 0000000000..7563fa8a21
--- /dev/null
+++ b/dom/events/test/clipboard/browser_navigator_clipboard_readText.js
@@ -0,0 +1,201 @@
+/* -*- Mode: JavaScript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const kBaseUrlForContent = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+const kContentFileName = "simple_navigator_clipboard_readText.html";
+
+const kContentFileUrl = kBaseUrlForContent + kContentFileName;
+
+const kApzTestNativeEventUtilsUrl =
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js";
+
+Services.scriptloader.loadSubScript(kApzTestNativeEventUtilsUrl, this);
+
+// @param aBrowser browser object of the content tab.
+// @param aMultipleReadTextCalls if false, exactly one call is made, two
+// otherwise.
+function promiseClickContentToTriggerClipboardReadText(
+ aBrowser,
+ aMultipleReadTextCalls
+) {
+ return promiseClickContentElement(
+ aBrowser,
+ aMultipleReadTextCalls ? "invokeReadTextTwiceId" : "invokeReadTextOnceId"
+ );
+}
+
+// @param aBrowser browser object of the content tab.
+function promiseMutatedReadTextResultFromContentElement(aBrowser) {
+ return promiseMutatedTextContentFromContentElement(
+ aBrowser,
+ "readTextResultId"
+ );
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.events.asyncClipboard.readText", true],
+ ["test.events.async.enabled", true],
+ ],
+ });
+});
+
+add_task(async function test_paste_button_position() {
+ // Ensure there's text on the clipboard.
+ await promiseWritingRandomTextToClipboard();
+
+ await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
+ const pasteButtonIsShown = promisePasteButtonIsShown();
+ const coordsOfClickInContentRelativeToScreenInDevicePixels =
+ await promiseClickContentToTriggerClipboardReadText(browser, false);
+ info(
+ "coordsOfClickInContentRelativeToScreenInDevicePixels: " +
+ coordsOfClickInContentRelativeToScreenInDevicePixels.x +
+ ", " +
+ coordsOfClickInContentRelativeToScreenInDevicePixels.y
+ );
+
+ const pasteButtonCoordsRelativeToScreenInDevicePixels =
+ await pasteButtonIsShown;
+ info(
+ "pasteButtonCoordsRelativeToScreenInDevicePixels: " +
+ pasteButtonCoordsRelativeToScreenInDevicePixels.x +
+ ", " +
+ pasteButtonCoordsRelativeToScreenInDevicePixels.y
+ );
+
+ const mouseCoordsRelativeToScreenInDevicePixels =
+ getMouseCoordsRelativeToScreenInDevicePixels();
+ info(
+ "mouseCoordsRelativeToScreenInDevicePixels: " +
+ mouseCoordsRelativeToScreenInDevicePixels.x +
+ ", " +
+ mouseCoordsRelativeToScreenInDevicePixels.y
+ );
+
+ // Asserting not overlapping is important; otherwise, when the
+ // "Paste" button is shown via a `mousedown` event, the following
+ // `mouseup` event could accept the "Paste" button unnoticed by the
+ // user.
+ ok(
+ isCloselyLeftOnTopOf(
+ mouseCoordsRelativeToScreenInDevicePixels,
+ pasteButtonCoordsRelativeToScreenInDevicePixels
+ ),
+ "'Paste' button is closely left on top of the mouse pointer."
+ );
+ ok(
+ isCloselyLeftOnTopOf(
+ coordsOfClickInContentRelativeToScreenInDevicePixels,
+ pasteButtonCoordsRelativeToScreenInDevicePixels
+ ),
+ "Coords of click in content are closely left on top of the 'Paste' button."
+ );
+
+ // To avoid disturbing subsequent tests.
+ const pasteButtonIsHidden = promisePasteButtonIsHidden();
+ await promiseClickPasteButton();
+ await pasteButtonIsHidden;
+ });
+});
+
+add_task(async function test_accepting_paste_button() {
+ // Randomized text to avoid overlappings with other tests.
+ const clipboardText = await promiseWritingRandomTextToClipboard();
+
+ await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
+ const pasteButtonIsShown = promisePasteButtonIsShown();
+ await promiseClickContentToTriggerClipboardReadText(browser, false);
+ await pasteButtonIsShown;
+ const pasteButtonIsHidden = promisePasteButtonIsHidden();
+ const mutatedReadTextResultFromContentElement =
+ promiseMutatedReadTextResultFromContentElement(browser);
+ await promiseClickPasteButton();
+ await pasteButtonIsHidden;
+ await mutatedReadTextResultFromContentElement.then(value => {
+ is(
+ value,
+ "Resolved: " + clipboardText,
+ "Text returned from `navigator.clipboard.readText()` is as expected."
+ );
+ });
+ });
+});
+
+add_task(async function test_dismissing_paste_button() {
+ await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
+ const pasteButtonIsShown = promisePasteButtonIsShown();
+ await promiseClickContentToTriggerClipboardReadText(browser, false);
+ await pasteButtonIsShown;
+ const pasteButtonIsHidden = promisePasteButtonIsHidden();
+ const mutatedReadTextResultFromContentElement =
+ promiseMutatedReadTextResultFromContentElement(browser);
+ await promiseDismissPasteButton();
+ await pasteButtonIsHidden;
+ await mutatedReadTextResultFromContentElement.then(value => {
+ is(
+ value,
+ "Rejected.",
+ "`navigator.clipboard.readText()` rejected after dismissing the 'Paste' button"
+ );
+ });
+ });
+});
+
+add_task(
+ async function test_multiple_readText_invocations_for_same_user_activation() {
+ // Randomized text to avoid overlappings with other tests.
+ const clipboardText = await promiseWritingRandomTextToClipboard();
+
+ await BrowserTestUtils.withNewTab(
+ kContentFileUrl,
+ async function (browser) {
+ const pasteButtonIsShown = promisePasteButtonIsShown();
+ await promiseClickContentToTriggerClipboardReadText(browser, true);
+ await pasteButtonIsShown;
+ const mutatedReadTextResultFromContentElement =
+ promiseMutatedReadTextResultFromContentElement(browser);
+ const pasteButtonIsHidden = promisePasteButtonIsHidden();
+ await promiseClickPasteButton();
+ await mutatedReadTextResultFromContentElement.then(value => {
+ is(
+ value,
+ "Resolved 1: " + clipboardText + "; Resolved 2: " + clipboardText,
+ "Two calls of `navigator.clipboard.read()` both resolved with the expected text."
+ );
+ });
+
+ // To avoid disturbing subsequent tests.
+ await pasteButtonIsHidden;
+ }
+ );
+ }
+);
+
+add_task(async function test_new_user_activation_shows_paste_button_again() {
+ await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
+ // Ensure there's text on the clipboard.
+ await promiseWritingRandomTextToClipboard();
+
+ for (let i = 0; i < 2; ++i) {
+ const pasteButtonIsShown = promisePasteButtonIsShown();
+ // A click initiates a new user activation.
+ await promiseClickContentToTriggerClipboardReadText(browser, false);
+ await pasteButtonIsShown;
+
+ const pasteButtonIsHidden = promisePasteButtonIsHidden();
+ await promiseClickPasteButton();
+ await pasteButtonIsHidden;
+ }
+ });
+});
diff --git a/dom/events/test/clipboard/browser_navigator_clipboard_touch.js b/dom/events/test/clipboard/browser_navigator_clipboard_touch.js
new file mode 100644
index 0000000000..aeea9a612d
--- /dev/null
+++ b/dom/events/test/clipboard/browser_navigator_clipboard_touch.js
@@ -0,0 +1,114 @@
+/* -*- Mode: JavaScript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const kBaseUrlForContent = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const kContentFileUrl =
+ kBaseUrlForContent + "simple_navigator_clipboard_readText.html";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
+ this
+);
+
+// @param aBrowser browser object of the content tab.
+// @param aContentElementId the ID of the element to be tapped.
+function promiseTouchTapContent(aBrowser, aContentElementId) {
+ return SpecialPowers.spawn(
+ aBrowser,
+ [aContentElementId],
+ async _contentElementId => {
+ await content.wrappedJSObject.waitUntilApzStable();
+
+ const contentElement = content.document.getElementById(_contentElementId);
+ let promise = new Promise(resolve => {
+ contentElement.addEventListener(
+ "click",
+ function (e) {
+ resolve({ x: e.screenX, y: e.screenY });
+ },
+ { once: true }
+ );
+ });
+
+ EventUtils.synthesizeTouchAtCenter(contentElement, {}, content.window);
+
+ return promise;
+ }
+ );
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.events.asyncClipboard.readText", true],
+ ["test.events.async.enabled", true],
+ ],
+ });
+});
+
+add_task(async function test_paste_button_position_touch() {
+ // Ensure there's text on the clipboard.
+ await promiseWritingRandomTextToClipboard();
+
+ await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
+ const pasteButtonIsShown = promisePasteButtonIsShown();
+ const coordsOfClickInContentRelativeToScreenInDevicePixels =
+ await promiseTouchTapContent(browser, "invokeReadTextOnceId");
+ info(
+ "coordsOfClickInContentRelativeToScreenInDevicePixels: " +
+ coordsOfClickInContentRelativeToScreenInDevicePixels.x +
+ ", " +
+ coordsOfClickInContentRelativeToScreenInDevicePixels.y
+ );
+
+ const pasteButtonCoordsRelativeToScreenInDevicePixels =
+ await pasteButtonIsShown;
+ info(
+ "pasteButtonCoordsRelativeToScreenInDevicePixels: " +
+ pasteButtonCoordsRelativeToScreenInDevicePixels.x +
+ ", " +
+ pasteButtonCoordsRelativeToScreenInDevicePixels.y
+ );
+
+ const mouseCoordsRelativeToScreenInDevicePixels =
+ getMouseCoordsRelativeToScreenInDevicePixels();
+ info(
+ "mouseCoordsRelativeToScreenInDevicePixels: " +
+ mouseCoordsRelativeToScreenInDevicePixels.x +
+ ", " +
+ mouseCoordsRelativeToScreenInDevicePixels.y
+ );
+
+ // Asserting not overlapping is important; otherwise, when the
+ // "Paste" button is shown via a `mousedown` event, the following
+ // `mouseup` event could accept the "Paste" button unnoticed by the
+ // user.
+ ok(
+ isCloselyLeftOnTopOf(
+ mouseCoordsRelativeToScreenInDevicePixels,
+ pasteButtonCoordsRelativeToScreenInDevicePixels
+ ),
+ "'Paste' button is closely left on top of the mouse pointer."
+ );
+ ok(
+ isCloselyLeftOnTopOf(
+ coordsOfClickInContentRelativeToScreenInDevicePixels,
+ pasteButtonCoordsRelativeToScreenInDevicePixels
+ ),
+ "Coords of click in content are closely left on top of the 'Paste' button."
+ );
+
+ // To avoid disturbing subsequent tests.
+ const pasteButtonIsHidden = promisePasteButtonIsHidden();
+ await promiseClickPasteButton();
+ await pasteButtonIsHidden;
+ });
+});
diff --git a/dom/events/test/clipboard/chrome.ini b/dom/events/test/clipboard/chrome.ini
new file mode 100644
index 0000000000..ce646f98ef
--- /dev/null
+++ b/dom/events/test/clipboard/chrome.ini
@@ -0,0 +1,3 @@
+[DEFAULT]
+
+[test_async_clipboard.xhtml]
diff --git a/dom/events/test/clipboard/head.js b/dom/events/test/clipboard/head.js
new file mode 100644
index 0000000000..0db1e0abf4
--- /dev/null
+++ b/dom/events/test/clipboard/head.js
@@ -0,0 +1,169 @@
+/* -*- Mode: JavaScript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const kPasteMenuPopupId = "clipboardReadPasteMenuPopup";
+const kPasteMenuItemId = "clipboardReadPasteMenuItem";
+
+function promiseWritingRandomTextToClipboard() {
+ const clipboardText = "X" + Math.random();
+ return navigator.clipboard.writeText(clipboardText).then(() => {
+ return clipboardText;
+ });
+}
+
+function promiseBrowserReflow() {
+ return new Promise(resolve =>
+ requestAnimationFrame(() => requestAnimationFrame(resolve))
+ );
+}
+
+function waitForPasteMenuPopupEvent(aEventSuffix) {
+ // The element with id `kPasteMenuPopupId` is inserted dynamically, hence
+ // calling `BrowserTestUtils.waitForEvent` instead of
+ // `BrowserTestUtils.waitForPopupEvent`.
+ return BrowserTestUtils.waitForEvent(
+ document,
+ "popup" + aEventSuffix,
+ false /* capture */,
+ e => {
+ return e.target.getAttribute("id") == kPasteMenuPopupId;
+ }
+ );
+}
+
+function promisePasteButtonIsShown() {
+ return waitForPasteMenuPopupEvent("shown").then(async () => {
+ ok(true, "Witnessed 'popupshown' event for 'Paste' button.");
+
+ const pasteButton = document.getElementById(kPasteMenuItemId);
+ ok(
+ pasteButton.disabled,
+ "Paste button should be shown with disabled by default"
+ );
+ await BrowserTestUtils.waitForMutationCondition(
+ pasteButton,
+ { attributeFilter: ["disabled"] },
+ () => !pasteButton.disabled,
+ "Wait for paste button enabled"
+ );
+
+ return promiseBrowserReflow().then(() => {
+ return coordinatesRelativeToScreen({
+ target: pasteButton,
+ offsetX: 0,
+ offsetY: 0,
+ });
+ });
+ });
+}
+
+function promisePasteButtonIsHidden() {
+ return waitForPasteMenuPopupEvent("hidden").then(() => {
+ ok(true, "Witnessed 'popuphidden' event for 'Paste' button.");
+ return promiseBrowserReflow();
+ });
+}
+
+function promiseClickPasteButton() {
+ const pasteButton = document.getElementById(kPasteMenuItemId);
+ let promise = BrowserTestUtils.waitForEvent(pasteButton, "click");
+ EventUtils.synthesizeMouseAtCenter(pasteButton, {});
+ return promise;
+}
+
+function getMouseCoordsRelativeToScreenInDevicePixels() {
+ let mouseXInCSSPixels = {};
+ let mouseYInCSSPixels = {};
+ window.windowUtils.getLastOverWindowPointerLocationInCSSPixels(
+ mouseXInCSSPixels,
+ mouseYInCSSPixels
+ );
+
+ return {
+ x:
+ (mouseXInCSSPixels.value + window.mozInnerScreenX) *
+ window.devicePixelRatio,
+ y:
+ (mouseYInCSSPixels.value + window.mozInnerScreenY) *
+ window.devicePixelRatio,
+ };
+}
+
+function isCloselyLeftOnTopOf(aCoordsP1, aCoordsP2, aDelta = 10) {
+ return (
+ Math.abs(aCoordsP2.x - aCoordsP1.x) < aDelta &&
+ Math.abs(aCoordsP2.y - aCoordsP1.y) < aDelta
+ );
+}
+
+function promiseDismissPasteButton() {
+ // nsXULPopupManager rollup is handled in widget code, so we have to
+ // synthesize native mouse events.
+ return EventUtils.promiseNativeMouseEvent({
+ type: "click",
+ target: document.body,
+ // Relies on the assumption that the center of chrome document doesn't
+ // overlay with the paste button showed for clipboard readText request.
+ atCenter: true,
+ });
+}
+
+// @param aBrowser browser object of the content tab.
+// @param aContentElementId the ID of the element to be clicked.
+function promiseClickContentElement(aBrowser, aContentElementId) {
+ return SpecialPowers.spawn(
+ aBrowser,
+ [aContentElementId],
+ async _contentElementId => {
+ const contentElement = content.document.getElementById(_contentElementId);
+ let promise = new Promise(resolve => {
+ contentElement.addEventListener(
+ "click",
+ function (e) {
+ resolve({ x: e.screenX, y: e.screenY });
+ },
+ { once: true }
+ );
+ });
+
+ EventUtils.synthesizeMouseAtCenter(contentElement, {}, content.window);
+
+ return promise;
+ }
+ );
+}
+
+// @param aBrowser browser object of the content tab.
+// @param aContentElementId the ID of the element to observe.
+function promiseMutatedTextContentFromContentElement(
+ aBrowser,
+ aContentElementId
+) {
+ return SpecialPowers.spawn(
+ aBrowser,
+ [aContentElementId],
+ async _contentElementId => {
+ const contentElement = content.document.getElementById(_contentElementId);
+
+ const promiseTextContentResult = new Promise(resolve => {
+ const mutationObserver = new content.MutationObserver(
+ (aMutationRecord, aMutationObserver) => {
+ info("Observed mutation.");
+ aMutationObserver.disconnect();
+ resolve(contentElement.textContent);
+ }
+ );
+
+ mutationObserver.observe(contentElement, {
+ childList: true,
+ });
+ });
+
+ return await promiseTextContentResult;
+ }
+ );
+}
diff --git a/dom/events/test/clipboard/simple_navigator_clipboard_keydown.html b/dom/events/test/clipboard/simple_navigator_clipboard_keydown.html
new file mode 100644
index 0000000000..15fcdfba2c
--- /dev/null
+++ b/dom/events/test/clipboard/simple_navigator_clipboard_keydown.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <script>
+ function onLoad() {
+ document.addEventListener("keydown", function() {
+ navigator.clipboard.readText();
+ });
+ }
+ </script>
+ </head>
+ <body onload="onLoad()">
+ </body>
+</html>
diff --git a/dom/events/test/clipboard/simple_navigator_clipboard_read.html b/dom/events/test/clipboard/simple_navigator_clipboard_read.html
new file mode 100644
index 0000000000..89f38a3240
--- /dev/null
+++ b/dom/events/test/clipboard/simple_navigator_clipboard_read.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <!-- Required by the .js part of the test. In a more ideal world, the script
+ could be loaded in the .js part; however, currently, that causes other
+ problems, which would require other changes in test framework code. -->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js"></script>
+ <script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script>
+
+ <script>
+ function onLoad() {
+ const readResult = document.getElementById("readResultId");
+
+ async function getClipboardText() {
+ let items = await navigator.clipboard.read();
+ if (items.length != 1) {
+ throw Error(`incorrect number of clipboard item (${items.length})`);
+ return;
+ }
+
+ let item = items[0];
+ for (let type of item.types) {
+ if (type == "text/plain") {
+ let blob = await item.getType(type);
+ return await blob.text();
+ }
+ }
+
+ throw Error("no text/plain type");
+ }
+
+ const b1 = document.getElementById("invokeReadOnceId");
+ b1.addEventListener("click", async () => {
+ getClipboardText().then(text => {
+ readResult.textContent = `Resolved: ${text}`;
+ }, (e) => { readResult.textContent = `Rejected: ${e.message}`});
+ });
+
+ const b2 = document.getElementById("invokeReadTwiceId");
+ b2.addEventListener("click", async () => {
+ const t1 = getClipboardText();
+ const t2 = getClipboardText();
+
+ const r1 = await t1.then(text => {
+ return `Resolved 1: ${text}`;
+ }, (e) => { return `Rejected 1: ${e.message}`;});
+
+ const r2 = await t2.then(text => {
+ return "Resolved 2: " + text;
+ }, (e) => { return `Rejected 2: ${e.message}`;});
+
+ readResult.textContent = r1 + "; " + r2;
+ });
+ }
+ </script>
+ </head>
+ <body onload="onLoad()">
+ <button id="invokeReadOnceId">1</button>
+ <button id="invokeReadTwiceId">2</button>
+ <div id="readResultId"/>
+ </body>
+</html>
diff --git a/dom/events/test/clipboard/simple_navigator_clipboard_readText.html b/dom/events/test/clipboard/simple_navigator_clipboard_readText.html
new file mode 100644
index 0000000000..0b85371091
--- /dev/null
+++ b/dom/events/test/clipboard/simple_navigator_clipboard_readText.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <!-- Required by the .js part of the test. In a more ideal world, the script
+ could be loaded in the .js part; however, currently, that causes other
+ problems, which would require other changes in test framework code. -->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js"></script>
+ <script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script>
+
+ <script>
+ function onLoad() {
+ const readTextResult = document.getElementById("readTextResultId");
+
+ const b1 = document.getElementById("invokeReadTextOnceId");
+ b1.addEventListener("click", async () => {
+ navigator.clipboard.readText().then(text => {
+ readTextResult.textContent = "Resolved: " + text;
+ }, () => { readTextResult.textContent = "Rejected." });
+ });
+
+ const b2 = document.getElementById("invokeReadTextTwiceId");
+ b2.addEventListener("click", async () => {
+ const t1 = navigator.clipboard.readText();
+ const t2 = navigator.clipboard.readText();
+
+ const r1 = await t1.then(text => {
+ return "Resolved 1: " + text;
+ }, () => { return "Rejected: 1";});
+
+ const r2 = await t2.then(text => {
+ return "Resolved 2: " + text;
+ }, () => { return "Rejected: 2";});
+
+ readTextResult.textContent = r1 + "; " + r2;
+ });
+ }
+ </script>
+ </head>
+ <body onload="onLoad()">
+ <button id="invokeReadTextOnceId">1</button>
+ <button id="invokeReadTextTwiceId">2</button>
+ <div id="readTextResultId"/>
+ </body>
+</html>
diff --git a/dom/events/test/clipboard/test_async_clipboard.xhtml b/dom/events/test/clipboard/test_async_clipboard.xhtml
new file mode 100644
index 0000000000..787821efc3
--- /dev/null
+++ b/dom/events/test/clipboard/test_async_clipboard.xhtml
@@ -0,0 +1,130 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Async clipboard APIs Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+ SimpleTest.waitForExplicitFinish();
+
+ const Services = SpecialPowers.Services;
+ const { AppConstants } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+ );
+ const { PlacesUtils } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/PlacesUtils.jsm"
+ );
+
+ const kTextPlainMimeType = "text/plain";
+
+ function clearClipboard() {
+ Services.clipboard.emptyClipboard(Services.clipboard.kGlobalClipboard);
+ }
+
+ async function testRead() {
+ let expected = "x";
+ await SimpleTest.promiseClipboardChange(expected, () => {
+ SpecialPowers.clipboardCopyString(expected);
+ }, kTextPlainMimeType);
+ let items = await navigator.clipboard.read();
+ is(items.length, 1, "read() read exactly one item");
+ const actual = await items[0].getType(kTextPlainMimeType).then(blob => blob.text());
+ is(actual, expected, "read() read the right thing");
+ }
+
+ async function testWrite() {
+ await SimpleTest.promiseClipboardChange("", () => {
+ clearClipboard();
+ });
+
+ let expected = "x";
+ // eslint-disable-next-line no-undef
+ let item = new ClipboardItem({[kTextPlainMimeType]: expected});
+ await navigator.clipboard.write([item]);
+ let actual = SpecialPowers.getClipboardData(kTextPlainMimeType);
+ is(actual, expected, "write() wrote the right thing");
+ }
+
+ async function testReadText() {
+ let expected = "x";
+ await SimpleTest.promiseClipboardChange(expected, () => {
+ SpecialPowers.clipboardCopyString(expected);
+ }, kTextPlainMimeType);
+ let actual = await navigator.clipboard.readText();
+ is(actual, expected, "readText() read the right thing");
+ }
+
+ async function testWriteText() {
+ await SimpleTest.promiseClipboardChange("", () => {
+ clearClipboard();
+ });
+
+ let expected = "x";
+ await navigator.clipboard.writeText(expected);
+ let actual = SpecialPowers.getClipboardData(kTextPlainMimeType);
+ is(actual, expected, "writeText() wrote the right thing");
+ }
+
+ async function testNoContentsRead() {
+ await SimpleTest.promiseClipboardChange("", () => {
+ clearClipboard();
+ });
+
+ const items = await navigator.clipboard.read();
+
+ // Bug 1756955: at least on Ubuntu 20.04, clearing the clipboard leads to
+ // one item with no types.
+ if (!items.length ||
+ (items.length == 1 && !items[0].types.length)) {
+ ok(true, "read() read the right thing from empty clipboard");
+ } else {
+ ok(false, "read() read the wrong thing from empty clipboard");
+ }
+ }
+
+ async function testNoContentsReadText() {
+ await SimpleTest.promiseClipboardChange("", () => {
+ clearClipboard();
+ });
+ let actual = await navigator.clipboard.readText();
+ is(actual, "", "readText() read the right thing from empty clipboard");
+ }
+
+ function runTest() {
+ (async function() {
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["dom.events.asyncClipboard.clipboardItem", true],
+ ]});
+ await testRead();
+ await testReadText();
+ await testWrite();
+ await testWriteText();
+
+ await testNoContentsRead();
+ await testNoContentsReadText();
+
+ SimpleTest.finish();
+ })();
+ }
+ ]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<p id="display">
+</p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+</body>
+
+</window>