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.toml40
-rw-r--r--dom/events/test/clipboard/browser_navigator_clipboard_clickjacking.js69
-rw-r--r--dom/events/test/clipboard/browser_navigator_clipboard_contextmenu_suppression.js264
-rw-r--r--dom/events/test/clipboard/browser_navigator_clipboard_contextmenu_suppression_ext.js156
-rw-r--r--dom/events/test/clipboard/browser_navigator_clipboard_read.js228
-rw-r--r--dom/events/test/clipboard/browser_navigator_clipboard_readText.js241
-rw-r--r--dom/events/test/clipboard/browser_navigator_clipboard_readText_multiple.js316
-rw-r--r--dom/events/test/clipboard/browser_navigator_clipboard_touch.js114
-rw-r--r--dom/events/test/clipboard/chrome.toml3
-rw-r--r--dom/events/test/clipboard/file_iframe.html2
-rw-r--r--dom/events/test/clipboard/file_toplevel.html10
-rw-r--r--dom/events/test/clipboard/head.js185
-rw-r--r--dom/events/test/clipboard/mochitest.toml7
-rw-r--r--dom/events/test/clipboard/simple_navigator_clipboard_keydown.html15
-rw-r--r--dom/events/test/clipboard/simple_navigator_clipboard_read.html64
-rw-r--r--dom/events/test/clipboard/simple_navigator_clipboard_readText.html47
-rw-r--r--dom/events/test/clipboard/test_async_clipboard.xhtml124
-rw-r--r--dom/events/test/clipboard/test_paste_image.html213
18 files changed, 2098 insertions, 0 deletions
diff --git a/dom/events/test/clipboard/browser.toml b/dom/events/test/clipboard/browser.toml
new file mode 100644
index 0000000000..8ced9423dd
--- /dev/null
+++ b/dom/events/test/clipboard/browser.toml
@@ -0,0 +1,40 @@
+[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_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"]
+
+["browser_navigator_clipboard_contextmenu_suppression.js"]
+support-files = [
+ "file_toplevel.html",
+ "file_iframe.html",
+]
+
+["browser_navigator_clipboard_contextmenu_suppression_ext.js"]
+support-files = [
+ "file_toplevel.html",
+ "file_iframe.html",
+]
+
+["browser_navigator_clipboard_read.js"]
+support-files = ["simple_navigator_clipboard_read.html"]
+
+["browser_navigator_clipboard_readText.js"]
+support-files = ["simple_navigator_clipboard_readText.html"]
+
+["browser_navigator_clipboard_readText_multiple.js"]
+support-files = [
+ "file_toplevel.html",
+ "file_iframe.html",
+]
+
+["browser_navigator_clipboard_touch.js"]
+support-files = ["simple_navigator_clipboard_readText.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_contextmenu_suppression.js b/dom/events/test/clipboard/browser_navigator_clipboard_contextmenu_suppression.js
new file mode 100644
index 0000000000..f504e499c9
--- /dev/null
+++ b/dom/events/test/clipboard/browser_navigator_clipboard_contextmenu_suppression.js
@@ -0,0 +1,264 @@
+/* -*- 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";
+requestLongerTimeout(2);
+
+const kBaseUrlForContent = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const kContentFileName = "file_toplevel.html";
+const kContentFileUrl = kBaseUrlForContent + kContentFileName;
+const kIsMac = navigator.platform.indexOf("Mac") > -1;
+
+async function waitForPasteContextMenu() {
+ await waitForPasteMenuPopupEvent("shown");
+ let pasteButton = document.getElementById(kPasteMenuItemId);
+ info("Wait for paste button enabled");
+ await BrowserTestUtils.waitForMutationCondition(
+ pasteButton,
+ { attributeFilter: ["disabled"] },
+ () => !pasteButton.disabled,
+ "Wait for paste button enabled"
+ );
+}
+
+async function readText(aBrowser) {
+ return SpecialPowers.spawn(aBrowser, [], async () => {
+ content.document.notifyUserGestureActivation();
+ return content.eval(`navigator.clipboard.readText();`);
+ });
+}
+
+function testPasteContextMenuSuppression(aWriteFun, aMsg) {
+ add_task(async function test_context_menu_suppression_sameorigin() {
+ await BrowserTestUtils.withNewTab(
+ kContentFileUrl,
+ async function (browser) {
+ info(`Write data by ${aMsg}`);
+ let clipboardText = await aWriteFun(browser);
+
+ info("Test read from same-origin frame");
+ let listener = function (e) {
+ if (e.target.getAttribute("id") == kPasteMenuPopupId) {
+ ok(false, "paste contextmenu should not be shown");
+ }
+ };
+ document.addEventListener("popupshown", listener);
+ is(
+ await readText(browser.browsingContext.children[0]),
+ clipboardText,
+ "read should just be resolved without paste contextmenu shown"
+ );
+ document.removeEventListener("popupshown", listener);
+ }
+ );
+ });
+
+ add_task(async function test_context_menu_suppression_crossorigin() {
+ await BrowserTestUtils.withNewTab(
+ kContentFileUrl,
+ async function (browser) {
+ info(`Write data by ${aMsg}`);
+ let clipboardText = await aWriteFun(browser);
+
+ info("Test read from cross-origin frame");
+ let pasteButtonIsShown = waitForPasteContextMenu();
+ let readTextRequest = readText(browser.browsingContext.children[1]);
+ await pasteButtonIsShown;
+
+ info("Click paste button, request should be resolved");
+ await promiseClickPasteButton();
+ is(await readTextRequest, clipboardText, "Request should be resolved");
+ }
+ );
+ });
+
+ add_task(async function test_context_menu_suppression_multiple() {
+ await BrowserTestUtils.withNewTab(
+ kContentFileUrl,
+ async function (browser) {
+ info(`Write data by ${aMsg}`);
+ let clipboardText = await aWriteFun(browser);
+
+ info("Test read from cross-origin frame");
+ let pasteButtonIsShown = waitForPasteContextMenu();
+ let readTextRequest1 = readText(browser.browsingContext.children[1]);
+ await pasteButtonIsShown;
+
+ info(
+ "Test read from same-origin frame before paste contextmenu is closed"
+ );
+ is(
+ await readText(browser.browsingContext.children[0]),
+ clipboardText,
+ "read from same-origin should just be resolved without showing paste contextmenu shown"
+ );
+
+ info("Dismiss paste button, cross-origin request should be rejected");
+ await promiseDismissPasteButton();
+ await Assert.rejects(
+ readTextRequest1,
+ /NotAllowedError/,
+ "cross-origin request should be rejected"
+ );
+ }
+ );
+ });
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.events.asyncClipboard.readText", true],
+ ["dom.events.asyncClipboard.clipboardItem", true],
+ ["test.events.async.enabled", true],
+ // Avoid paste button delay enabling making test too long.
+ ["security.dialog_enable_delay", 0],
+ ],
+ });
+});
+
+testPasteContextMenuSuppression(async aBrowser => {
+ const clipboardText = "X" + Math.random();
+ await SpecialPowers.spawn(aBrowser, [clipboardText], async text => {
+ content.document.notifyUserGestureActivation();
+ return content.eval(`navigator.clipboard.writeText("${text}");`);
+ });
+ return clipboardText;
+}, "clipboard.writeText()");
+
+testPasteContextMenuSuppression(async aBrowser => {
+ const clipboardText = "X" + Math.random();
+ await SpecialPowers.spawn(aBrowser, [clipboardText], async text => {
+ content.document.notifyUserGestureActivation();
+ return content.eval(`
+ const itemInput = new ClipboardItem({["text/plain"]: "${text}"});
+ navigator.clipboard.write([itemInput]);
+ `);
+ });
+ return clipboardText;
+}, "clipboard.write()");
+
+testPasteContextMenuSuppression(async aBrowser => {
+ const clipboardText = "X" + Math.random();
+ await SpecialPowers.spawn(aBrowser, [clipboardText], async text => {
+ let div = content.document.createElement("div");
+ div.innerText = text;
+ content.document.documentElement.appendChild(div);
+ // select text
+ content
+ .getSelection()
+ .setBaseAndExtent(div.firstChild, text.length, div.firstChild, 0);
+ });
+ // trigger keyboard shortcut to copy.
+ await EventUtils.synthesizeAndWaitKey(
+ "c",
+ kIsMac ? { accelKey: true } : { ctrlKey: true }
+ );
+ return clipboardText;
+}, "keyboard shortcut");
+
+testPasteContextMenuSuppression(async aBrowser => {
+ const clipboardText = "X" + Math.random();
+ await SpecialPowers.spawn(aBrowser, [clipboardText], async text => {
+ return content.eval(`
+ document.addEventListener("copy", function(e) {
+ e.preventDefault();
+ e.clipboardData.setData("text/plain", "${text}");
+ }, { once: true });
+ `);
+ });
+ // trigger keyboard shortcut to copy.
+ await EventUtils.synthesizeAndWaitKey(
+ "c",
+ kIsMac ? { accelKey: true } : { ctrlKey: true }
+ );
+ return clipboardText;
+}, "keyboard shortcut with custom data");
+
+testPasteContextMenuSuppression(async aBrowser => {
+ const clipboardText = "X" + Math.random();
+ await SpecialPowers.spawn(aBrowser, [clipboardText], async text => {
+ let div = content.document.createElement("div");
+ div.innerText = text;
+ content.document.documentElement.appendChild(div);
+ // select text
+ content
+ .getSelection()
+ .setBaseAndExtent(div.firstChild, text.length, div.firstChild, 0);
+ return SpecialPowers.doCommand(content, "cmd_copy");
+ });
+ return clipboardText;
+}, "copy command");
+
+async function readTypes(aBrowser) {
+ return SpecialPowers.spawn(aBrowser, [], async () => {
+ content.document.notifyUserGestureActivation();
+ let items = await content.eval(`navigator.clipboard.read();`);
+ return items[0].types;
+ });
+}
+
+add_task(async function test_context_menu_suppression_image() {
+ await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
+ await SpecialPowers.spawn(browser, [], async () => {
+ let image = content.document.createElement("img");
+ let copyImagePromise = new Promise(resolve => {
+ image.addEventListener(
+ "load",
+ e => {
+ let documentViewer = content.docShell.docViewer.QueryInterface(
+ SpecialPowers.Ci.nsIDocumentViewerEdit
+ );
+ documentViewer.setCommandNode(image);
+ documentViewer.copyImage(documentViewer.COPY_IMAGE_ALL);
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ image.src =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAD4AAABHCAIAAADQjmMaAA" +
+ "AACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3goUAwAgSAORBwAAABl0RVh0Q29tbW" +
+ "VudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAABPSURBVGje7c4BDQAACAOga//OmuMbJG" +
+ "AurTbq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6u" +
+ "rq6s31B0IqAY2/tQVCAAAAAElFTkSuQmCC";
+ content.document.documentElement.appendChild(image);
+ await copyImagePromise;
+ });
+
+ info("Test read from cross-origin frame");
+ let pasteButtonIsShown = waitForPasteContextMenu();
+ let readTypesRequest1 = readTypes(browser.browsingContext.children[1]);
+ await pasteButtonIsShown;
+
+ info("Test read from same-origin frame before paste contextmenu is closed");
+ const clipboarCacheEnabled = SpecialPowers.getBoolPref(
+ "widget.clipboard.use-cached-data.enabled",
+ false
+ );
+ // If the cached data is used, it uses type order in cached transferable.
+ SimpleTest.isDeeply(
+ await readTypes(browser.browsingContext.children[0]),
+ clipboarCacheEnabled
+ ? ["text/plain", "text/html", "image/png"]
+ : ["text/html", "text/plain", "image/png"],
+ "read from same-origin should just be resolved without showing paste contextmenu shown"
+ );
+
+ info("Dismiss paste button, cross-origin request should be rejected");
+ await promiseDismissPasteButton();
+ // XXX edgar: not sure why first promiseDismissPasteButton doesn't work on Windows opt build.
+ await promiseDismissPasteButton();
+ await Assert.rejects(
+ readTypesRequest1,
+ /NotAllowedError/,
+ "cross-origin request should be rejected"
+ );
+ });
+});
diff --git a/dom/events/test/clipboard/browser_navigator_clipboard_contextmenu_suppression_ext.js b/dom/events/test/clipboard/browser_navigator_clipboard_contextmenu_suppression_ext.js
new file mode 100644
index 0000000000..1990db05ec
--- /dev/null
+++ b/dom/events/test/clipboard/browser_navigator_clipboard_contextmenu_suppression_ext.js
@@ -0,0 +1,156 @@
+/* -*- 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";
+requestLongerTimeout(2);
+
+const kBaseUrlForContent = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const kContentFileName = "file_toplevel.html";
+const kContentFileUrl = kBaseUrlForContent + kContentFileName;
+const kIsMac = navigator.platform.indexOf("Mac") > -1;
+
+async function waitForPasteContextMenu() {
+ await waitForPasteMenuPopupEvent("shown");
+ let pasteButton = document.getElementById(kPasteMenuItemId);
+ info("Wait for paste button enabled");
+ await BrowserTestUtils.waitForMutationCondition(
+ pasteButton,
+ { attributeFilter: ["disabled"] },
+ () => !pasteButton.disabled,
+ "Wait for paste button enabled"
+ );
+}
+
+async function readText(aBrowser) {
+ return SpecialPowers.spawn(aBrowser, [], async () => {
+ content.document.notifyUserGestureActivation();
+ return content.eval(`navigator.clipboard.readText();`);
+ });
+}
+
+async function testPasteContextMenu(
+ aBrowser,
+ aClipboardText,
+ aShouldShow = true
+) {
+ let pasteButtonIsShown;
+ if (aShouldShow) {
+ pasteButtonIsShown = waitForPasteContextMenu();
+ }
+ let readTextRequest = readText(aBrowser);
+ if (aShouldShow) {
+ await pasteButtonIsShown;
+ }
+
+ info("Click paste button, request should be resolved");
+ if (aShouldShow) {
+ await promiseClickPasteButton();
+ }
+ is(await readTextRequest, aClipboardText, "Request should be resolved");
+}
+
+async function installAndStartExtension(aContentScript) {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ js: ["content_script.js"],
+ matches: ["https://example.com/*/file_toplevel.html"],
+ },
+ ],
+ },
+ files: {
+ "content_script.js": aContentScript,
+ },
+ });
+
+ await extension.startup();
+
+ return extension;
+}
+
+function testExtensionContentScript(aContentScript, aMessage) {
+ add_task(async function test_context_menu_suppression_ext() {
+ info(`${aMessage}`);
+ const extension = await installAndStartExtension(aContentScript);
+ await BrowserTestUtils.withNewTab(
+ kContentFileUrl,
+ async function (browser) {
+ const clipboardText = "X" + Math.random();
+ await SpecialPowers.spawn(browser, [clipboardText], async text => {
+ info(`Set clipboard text to ${text}`);
+ let div = content.document.createElement("div");
+ div.id = "container";
+ div.innerText = text;
+ content.document.documentElement.appendChild(div);
+ });
+
+ let writePromise = extension.awaitMessage("write-data-ready");
+ // trigger keyboard shortcut to copy.
+ await EventUtils.synthesizeAndWaitKey(
+ "c",
+ kIsMac ? { accelKey: true } : { ctrlKey: true }
+ );
+ // Wait a bit for clipboard write.
+ await writePromise;
+
+ info("Test read from same frame");
+ await testPasteContextMenu(browser, clipboardText, false);
+
+ info("Test read from same-origin subframe");
+ await testPasteContextMenu(
+ browser.browsingContext.children[0],
+ clipboardText,
+ false
+ );
+
+ info("Test read from cross-origin subframe");
+ await testPasteContextMenu(
+ browser.browsingContext.children[1],
+ clipboardText,
+ true
+ );
+ }
+ );
+
+ await extension.unload();
+ });
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.events.asyncClipboard.readText", true],
+ ["dom.events.asyncClipboard.clipboardItem", true],
+ ["test.events.async.enabled", true],
+ // Avoid paste button delay enabling making test too long.
+ ["security.dialog_enable_delay", 0],
+ ],
+ });
+});
+
+testExtensionContentScript(() => {
+ document.addEventListener("copy", function (e) {
+ e.preventDefault();
+ let div = document.getElementById("container");
+ let text = div.innerText;
+ e.clipboardData.setData("text/plain", text);
+ browser.test.sendMessage("write-data-ready");
+ });
+}, "Write data by DataTransfer API in extension");
+
+testExtensionContentScript(() => {
+ document.addEventListener("copy", async function (e) {
+ e.preventDefault();
+ let div = document.getElementById("container");
+ let text = div.innerText;
+ await navigator.clipboard.writeText(text);
+ browser.test.sendMessage("write-data-ready");
+ });
+}, "Write data by Async Clipboard API in extension");
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..e11002e7f8
--- /dev/null
+++ b/dom/events/test/clipboard/browser_navigator_clipboard_read.js
@@ -0,0 +1,228 @@
+/* -*- 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();
+ // We intentionally turn off this a11y check, because the following click
+ // is send on an arbitrary web content that is not expected to be tested
+ // by itself with the browser mochitests, therefore this rule check shall
+ // be ignored by a11y-checks suite.
+ AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
+ const coordsOfClickInContentRelativeToScreenInDevicePixels =
+ await promiseClickContentToTriggerClipboardRead(browser, false);
+ AccessibilityUtils.resetEnv();
+ 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();
+ // We intentionally turn off this a11y check, because the following click
+ // is send on an arbitrary web content that is not expected to be tested
+ // by itself with the browser mochitests, therefore this rule check shall
+ // be ignored by a11y-checks suite.
+ AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
+ await promiseClickContentToTriggerClipboardRead(browser, false);
+ AccessibilityUtils.resetEnv();
+ 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();
+ // We intentionally turn off this a11y check, because the following click
+ // is send on an arbitrary web content that is not expected to be tested
+ // by itself with the browser mochitests, therefore this rule check shall
+ // be ignored by a11y-checks suite.
+ AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
+ await promiseClickContentToTriggerClipboardRead(browser, false);
+ AccessibilityUtils.resetEnv();
+ await pasteButtonIsShown;
+ const pasteButtonIsHidden = promisePasteButtonIsHidden();
+ const mutatedReadResultFromContentElement =
+ promiseMutatedReadResultFromContentElement(browser);
+ await promiseDismissPasteButton();
+ await pasteButtonIsHidden;
+ await mutatedReadResultFromContentElement.then(value => {
+ is(
+ value,
+ "Rejected: Clipboard read operation is not allowed.",
+ "`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();
+ // We intentionally turn off this a11y check, because the following click
+ // is send on an arbitrary web content that is not expected to be tested
+ // by itself with the browser mochitests, therefore this rule check shall
+ // be ignored by a11y-checks suite.
+ AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
+ await promiseClickContentToTriggerClipboardRead(browser, true);
+ AccessibilityUtils.resetEnv();
+ 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();
+ // We intentionally turn off this a11y check, because the following click
+ // is send on an arbitrary web content that is not expected to be tested
+ // by itself with the browser mochitests, therefore this rule check shall
+ // be ignored by a11y-checks suite.
+ AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
+ // A click initiates a new user activation.
+ await promiseClickContentToTriggerClipboardRead(browser, false);
+ AccessibilityUtils.resetEnv();
+ 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..f91976a822
--- /dev/null
+++ b/dom/events/test/clipboard/browser_navigator_clipboard_readText.js
@@ -0,0 +1,241 @@
+/* -*- 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();
+ // We intentionally turn off this a11y check, because the following click
+ // is send on an arbitrary web content that is not expected to be tested
+ // by itself with the browser mochitests, therefore this rule check shall
+ // be ignored by a11y-checks suite.
+ AccessibilityUtils.setEnv({
+ mustHaveAccessibleRule: false,
+ });
+ const coordsOfClickInContentRelativeToScreenInDevicePixels =
+ await promiseClickContentToTriggerClipboardReadText(browser, false);
+ info(
+ "coordsOfClickInContentRelativeToScreenInDevicePixels: " +
+ coordsOfClickInContentRelativeToScreenInDevicePixels.x +
+ ", " +
+ coordsOfClickInContentRelativeToScreenInDevicePixels.y
+ );
+ AccessibilityUtils.resetEnv();
+
+ 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();
+ // We intentionally turn off this a11y check, because the following click
+ // is send on an arbitrary web content that is not expected to be tested
+ // by itself with the browser mochitests, therefore this rule check shall
+ // be ignored by a11y-checks suite.
+ AccessibilityUtils.setEnv({
+ mustHaveAccessibleRule: false,
+ });
+ await promiseClickContentToTriggerClipboardReadText(browser, false);
+ AccessibilityUtils.resetEnv();
+ 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();
+ // We intentionally turn off this a11y check, because the following click
+ // is send on an arbitrary web content that is not expected to be tested
+ // by itself with the browser mochitests, therefore this rule check shall
+ // be ignored by a11y-checks suite.
+ AccessibilityUtils.setEnv({
+ mustHaveAccessibleRule: false,
+ });
+ await promiseClickContentToTriggerClipboardReadText(browser, false);
+ AccessibilityUtils.resetEnv();
+ 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();
+ // We intentionally turn off this a11y check, because the following click
+ // is send on an arbitrary web content that is not expected to be tested
+ // by itself with the browser mochitests, therefore this rule check shall
+ // be ignored by a11y-checks suite.
+ AccessibilityUtils.setEnv({
+ mustHaveAccessibleRule: false,
+ });
+ await promiseClickContentToTriggerClipboardReadText(browser, true);
+ AccessibilityUtils.resetEnv();
+ 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();
+ // We intentionally turn off this a11y check, because the following click
+ // is send on an arbitrary web content that is not expected to be tested
+ // by itself with the browser mochitests, therefore this rule check shall
+ // be ignored by a11y-checks suite.
+ AccessibilityUtils.setEnv({
+ mustHaveAccessibleRule: false,
+ });
+ // A click initiates a new user activation.
+ await promiseClickContentToTriggerClipboardReadText(browser, false);
+ AccessibilityUtils.resetEnv();
+ await pasteButtonIsShown;
+
+ const pasteButtonIsHidden = promisePasteButtonIsHidden();
+ await promiseClickPasteButton();
+ await pasteButtonIsHidden;
+ }
+ });
+});
diff --git a/dom/events/test/clipboard/browser_navigator_clipboard_readText_multiple.js b/dom/events/test/clipboard/browser_navigator_clipboard_readText_multiple.js
new file mode 100644
index 0000000000..1d0d2884b7
--- /dev/null
+++ b/dom/events/test/clipboard/browser_navigator_clipboard_readText_multiple.js
@@ -0,0 +1,316 @@
+/* -*- 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 = "file_toplevel.html";
+const kContentFileUrl = kBaseUrlForContent + kContentFileName;
+
+async function waitForPasteContextMenu() {
+ await waitForPasteMenuPopupEvent("shown");
+ const pasteButton = document.getElementById(kPasteMenuItemId);
+ info("Wait for paste button enabled");
+ await BrowserTestUtils.waitForMutationCondition(
+ pasteButton,
+ { attributeFilter: ["disabled"] },
+ () => !pasteButton.disabled,
+ "Wait for paste button enabled"
+ );
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.events.asyncClipboard.readText", true],
+ ["test.events.async.enabled", true],
+ // Avoid paste button delay enabling making test too long.
+ ["security.dialog_enable_delay", 0],
+ ],
+ });
+});
+
+add_task(async function test_multiple_readText_from_same_frame_allow() {
+ // Randomized text to avoid overlapping with other tests.
+ const clipboardText = await promiseWritingRandomTextToClipboard();
+
+ await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
+ const pasteButtonIsShown = waitForPasteContextMenu();
+ const readTextRequest1 = SpecialPowers.spawn(browser, [], async () => {
+ content.document.notifyUserGestureActivation();
+ return content.eval(`navigator.clipboard.readText();`);
+ });
+ await pasteButtonIsShown;
+
+ info("readText() from same frame again before interact with paste button");
+ const readTextRequest2 = SpecialPowers.spawn(browser, [], async () => {
+ content.document.notifyUserGestureActivation();
+ return content.eval(`navigator.clipboard.readText();`);
+ });
+ // Give some time for the second request to arrive parent process.
+ await SpecialPowers.spawn(browser, [], async () => {
+ return new Promise(resolve => {
+ content.setTimeout(resolve, 0);
+ });
+ });
+
+ info("Click paste button, both request should be resolved");
+ await promiseClickPasteButton();
+ is(
+ await readTextRequest1,
+ clipboardText,
+ "First request should be resolved"
+ );
+ is(
+ await readTextRequest2,
+ clipboardText,
+ "Second request should be resolved"
+ );
+ });
+});
+
+add_task(async function test_multiple_readText_from_same_frame_deny() {
+ // Randomized text to avoid overlapping with other tests.
+ const clipboardText = await promiseWritingRandomTextToClipboard();
+
+ await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
+ const pasteButtonIsShown = waitForPasteContextMenu();
+ const readTextRequest1 = SpecialPowers.spawn(browser, [], async () => {
+ content.document.notifyUserGestureActivation();
+ return content.eval(`navigator.clipboard.readText();`);
+ });
+ await pasteButtonIsShown;
+
+ info("readText() from same frame again before interact with paste button");
+ const readTextRequest2 = SpecialPowers.spawn(browser, [], async () => {
+ content.document.notifyUserGestureActivation();
+ return content.eval(`navigator.clipboard.readText();`);
+ });
+ // Give some time for the second request to arrive parent process.
+ await SpecialPowers.spawn(browser, [], async () => {
+ return new Promise(resolve => {
+ content.setTimeout(resolve, 0);
+ });
+ });
+
+ info("Dismiss paste button, both request should be rejected");
+ await promiseDismissPasteButton();
+ await Assert.rejects(
+ readTextRequest1,
+ /NotAllowedError/,
+ "First request should be rejected"
+ );
+ await Assert.rejects(
+ readTextRequest2,
+ /NotAllowedError/,
+ "Second request should be rejected"
+ );
+ });
+});
+
+add_task(async function test_multiple_readText_from_same_origin_frame() {
+ // Randomized text to avoid overlapping with other tests.
+ const clipboardText = await promiseWritingRandomTextToClipboard();
+
+ await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
+ const pasteButtonIsShown = waitForPasteContextMenu();
+ const readTextRequest1 = SpecialPowers.spawn(browser, [], async () => {
+ content.document.notifyUserGestureActivation();
+ return content.eval(`navigator.clipboard.readText();`);
+ });
+ await pasteButtonIsShown;
+
+ info(
+ "readText() from same origin child frame again before interacting with paste button"
+ );
+ const sameOriginFrame = browser.browsingContext.children[0];
+ const readTextRequest2 = SpecialPowers.spawn(
+ sameOriginFrame,
+ [],
+ async () => {
+ content.document.notifyUserGestureActivation();
+ return content.eval(`navigator.clipboard.readText();`);
+ }
+ );
+ // Give some time for the second request to arrive parent process.
+ await SpecialPowers.spawn(sameOriginFrame, [], async () => {
+ return new Promise(resolve => {
+ content.setTimeout(resolve, 0);
+ });
+ });
+
+ info("Click paste button, both request should be resolved");
+ await promiseClickPasteButton();
+ is(
+ await readTextRequest1,
+ clipboardText,
+ "First request should be resolved"
+ );
+ is(
+ await readTextRequest2,
+ clipboardText,
+ "Second request should be resolved"
+ );
+ });
+});
+
+add_task(async function test_multiple_readText_from_cross_origin_frame() {
+ // Randomized text to avoid overlapping with other tests.
+ const clipboardText = await promiseWritingRandomTextToClipboard();
+
+ await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
+ const pasteButtonIsShown = waitForPasteContextMenu();
+ const readTextRequest1 = SpecialPowers.spawn(browser, [], async () => {
+ content.document.notifyUserGestureActivation();
+ return content.eval(`navigator.clipboard.readText();`);
+ });
+ await pasteButtonIsShown;
+
+ info(
+ "readText() from different origin child frame again before interacting with paste button"
+ );
+ const crossOriginFrame = browser.browsingContext.children[1];
+ await Assert.rejects(
+ SpecialPowers.spawn(crossOriginFrame, [], async () => {
+ content.document.notifyUserGestureActivation();
+ return content.eval(`navigator.clipboard.readText();`);
+ }),
+ /NotAllowedError/,
+ "Second request should be rejected"
+ );
+
+ info("Click paste button, both request should be resolved");
+ await promiseClickPasteButton();
+ is(
+ await readTextRequest1,
+ clipboardText,
+ "First request should be resolved"
+ );
+ });
+});
+
+add_task(async function test_multiple_readText_from_background_frame() {
+ // Randomized text to avoid overlapping with other tests.
+ const clipboardText = await promiseWritingRandomTextToClipboard();
+
+ const backgroundTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ kContentFileUrl
+ );
+
+ await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
+ const pasteButtonIsShown = waitForPasteContextMenu();
+ const readTextRequest1 = SpecialPowers.spawn(browser, [], async () => {
+ content.document.notifyUserGestureActivation();
+ return content.eval(`navigator.clipboard.readText();`);
+ });
+ await pasteButtonIsShown;
+
+ info(
+ "readText() from background tab again before interact with paste button"
+ );
+ await Assert.rejects(
+ SpecialPowers.spawn(backgroundTab.linkedBrowser, [], async () => {
+ content.document.notifyUserGestureActivation();
+ return content.eval(`navigator.clipboard.readText();`);
+ }),
+ /NotAllowedError/,
+ "Second request should be rejected"
+ );
+
+ info("Click paste button, both request should be resolved");
+ await promiseClickPasteButton();
+ is(
+ await readTextRequest1,
+ clipboardText,
+ "First request should be resolved"
+ );
+ });
+
+ await BrowserTestUtils.removeTab(backgroundTab);
+});
+
+add_task(async function test_multiple_readText_from_background_window() {
+ // Randomized text to avoid overlapping with other tests.
+ const clipboardText = await promiseWritingRandomTextToClipboard();
+
+ await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
+ const newWin = await BrowserTestUtils.openNewBrowserWindow();
+ const backgroundTab = await BrowserTestUtils.openNewForegroundTab(
+ newWin.gBrowser,
+ kContentFileUrl
+ );
+ await SimpleTest.promiseFocus(browser);
+
+ info("readText() from background window");
+ await Assert.rejects(
+ SpecialPowers.spawn(backgroundTab.linkedBrowser, [], async () => {
+ content.document.notifyUserGestureActivation();
+ return content.eval(`navigator.clipboard.readText();`);
+ }),
+ /NotAllowedError/,
+ "Request from background window should be rejected"
+ );
+
+ const pasteButtonIsShown = waitForPasteContextMenu();
+ const readTextRequest1 = SpecialPowers.spawn(browser, [], async () => {
+ content.document.notifyUserGestureActivation();
+ return content.eval(`navigator.clipboard.readText();`);
+ });
+ await pasteButtonIsShown;
+
+ info(
+ "readText() from background window again before interact with paste button"
+ );
+ await Assert.rejects(
+ SpecialPowers.spawn(backgroundTab.linkedBrowser, [], async () => {
+ content.document.notifyUserGestureActivation();
+ return content.eval(`navigator.clipboard.readText();`);
+ }),
+ /NotAllowedError/,
+ "Second request should be rejected"
+ );
+
+ info("Click paste button, both request should be resolved");
+ await promiseClickPasteButton();
+ is(
+ await readTextRequest1,
+ clipboardText,
+ "First request should be resolved"
+ );
+
+ await BrowserTestUtils.closeWindow(newWin);
+ });
+});
+
+add_task(async function test_multiple_readText_focuse_in_chrome_document() {
+ // Randomized text to avoid overlapping with other tests.
+ const clipboardText = await promiseWritingRandomTextToClipboard();
+
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ kContentFileUrl
+ );
+
+ info("Move focus to url bar");
+ win.gURLBar.focus();
+
+ info("readText() from web content");
+ await Assert.rejects(
+ SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ content.document.notifyUserGestureActivation();
+ return content.eval(`navigator.clipboard.readText();`);
+ }),
+ /NotAllowedError/,
+ "Request should be rejected when focus is not in content"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
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.toml b/dom/events/test/clipboard/chrome.toml
new file mode 100644
index 0000000000..0a3107e389
--- /dev/null
+++ b/dom/events/test/clipboard/chrome.toml
@@ -0,0 +1,3 @@
+[DEFAULT]
+
+["test_async_clipboard.xhtml"]
diff --git a/dom/events/test/clipboard/file_iframe.html b/dom/events/test/clipboard/file_iframe.html
new file mode 100644
index 0000000000..a581da6ef0
--- /dev/null
+++ b/dom/events/test/clipboard/file_iframe.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<body>Dummy page</body>
diff --git a/dom/events/test/clipboard/file_toplevel.html b/dom/events/test/clipboard/file_toplevel.html
new file mode 100644
index 0000000000..60661ca5e2
--- /dev/null
+++ b/dom/events/test/clipboard/file_toplevel.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <iframe src="file_iframe.html"></iframe>
+ <iframe src="https://example.org/browser/dom/events/test/clipboard/file_iframe.html"></iframe>
+ </body>
+</html>
diff --git a/dom/events/test/clipboard/head.js b/dom/events/test/clipboard/head.js
new file mode 100644
index 0000000000..73404ca535
--- /dev/null
+++ b/dom/events/test/clipboard/head.js
@@ -0,0 +1,185 @@
+/* -*- 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
+ );
+}
+
+async function promiseDismissPasteButton() {
+ // We intentionally turn off this a11y check, because the following click
+ // is send on the <body> to dismiss the pending popup using an alternative way
+ // of the popup dismissal, where the other way like `Esc` key is available,
+ // therefore this test can be ignored.
+ AccessibilityUtils.setEnv({
+ mustHaveAccessibleRule: false,
+ });
+ // nsXULPopupManager rollup is handled in widget code, so we have to
+ // synthesize native mouse events.
+ await 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,
+ });
+ // Move mouse away to avoid subsequence tests showing paste button in
+ // thie dismissing location.
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousemove",
+ target: document.body,
+ offsetX: 100,
+ offsetY: 100,
+ });
+ AccessibilityUtils.resetEnv();
+}
+
+// @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/mochitest.toml b/dom/events/test/clipboard/mochitest.toml
new file mode 100644
index 0000000000..7829915267
--- /dev/null
+++ b/dom/events/test/clipboard/mochitest.toml
@@ -0,0 +1,7 @@
+[DEFAULT]
+
+["test_paste_image.html"]
+skip-if = [
+ "headless", # Bug 1405869
+ "os == 'android'", # Image type isn't supported
+]
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..825089e799
--- /dev/null
+++ b/dom/events/test/clipboard/simple_navigator_clipboard_read.html
@@ -0,0 +1,64 @@
+<!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})`);
+ }
+
+ 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..ec54809077
--- /dev/null
+++ b/dom/events/test/clipboard/test_async_clipboard.xhtml
@@ -0,0 +1,124 @@
+<?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 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>
diff --git a/dom/events/test/clipboard/test_paste_image.html b/dom/events/test/clipboard/test_paste_image.html
new file mode 100644
index 0000000000..061b577657
--- /dev/null
+++ b/dom/events/test/clipboard/test_paste_image.html
@@ -0,0 +1,213 @@
+<html><head>
+<title>Test for bug 891247</title>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+
+<script class="testbody" type="application/javascript">
+ function ImageTester() {
+ var counter = 0;
+ var images = [];
+ var that = this;
+
+ this.add = function(aFile) {
+ images.push(aFile);
+ };
+
+ this.test = async function() {
+ for (var i = 0; i < images.length; i++) {
+ await testImageSize(images[i]);
+ }
+ };
+
+ this.returned = function() {
+ counter++;
+ info("returned=" + counter + " images.length=" + images.length);
+ if (counter == images.length) {
+ info("test finish");
+ }
+ };
+
+ async function testImageSize(aFile) {
+ var source = window.URL.createObjectURL(aFile);
+ var image = new Image();
+ image.src = source;
+ var imageTester = that;
+ let promise = new Promise(resolve => {
+ image.addEventListener("load", function(e) {
+ is(this.width, 62, "Check generated image width");
+ is(this.height, 71, "Check generated image height");
+
+ // This fails on OSX only.
+ if (!navigator.platform.includes("Mac")) {
+ testImageCanvas(image);
+ }
+
+ imageTester.returned();
+ resolve();
+ }, { once: true });
+ });
+
+ document.body.appendChild(image);
+ await promise;
+ };
+
+ function testImageCanvas(aImage) {
+ var canvas = drawToCanvas(aImage);
+
+ var refImage = document.getElementById('image');
+ var refCanvas = drawToCanvas(refImage);
+
+ is(canvas.toDataURL(), refCanvas.toDataURL(), "Image should map pixel-by-pixel");
+ }
+
+ function drawToCanvas(aImage) {
+ var canvas = document.createElement("CANVAS");
+ document.body.appendChild(canvas);
+ canvas.width = aImage.width;
+ canvas.height = aImage.height;
+ canvas.getContext('2d').drawImage(aImage, 0, 0);
+ return canvas;
+ }
+ }
+
+ function copyImage(aImageId) {
+ // selection of the node
+ var node = document.getElementById(aImageId);
+ var docShell = SpecialPowers.wrap(window).docShell;
+
+ // let's copy the node
+ var documentViewer = docShell.docViewer
+ .QueryInterface(SpecialPowers.Ci.nsIDocumentViewerEdit);
+ documentViewer.setCommandNode(node);
+ documentViewer.copyImage(documentViewer.COPY_IMAGE_ALL);
+ }
+
+ async function doTest(imageAsFileEnabled) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["clipboard.imageAsFile.enabled", imageAsFileEnabled]],
+ });
+
+ copyImage('image');
+
+ //--------- now check the content of the clipboard
+ var clipboard = SpecialPowers.Cc["@mozilla.org/widget/clipboard;1"]
+ .getService(SpecialPowers.Ci.nsIClipboard);
+ // does the clipboard contain text/plain data ?
+ ok(clipboard.hasDataMatchingFlavors(["text/plain"], clipboard.kGlobalClipboard),
+ "clipboard contains unicode text");
+ // does the clipboard contain text/html data ?
+ ok(clipboard.hasDataMatchingFlavors(["text/html"], clipboard.kGlobalClipboard),
+ "clipboard contains html text");
+ // does the clipboard contain image data ?
+ ok(clipboard.hasDataMatchingFlavors(["image/png"], clipboard.kGlobalClipboard),
+ "clipboard contains image");
+
+ let promise = new Promise(resolve => {
+ window.addEventListener("paste", async (e) => {
+ isDeeply(e.clipboardData.types,
+ (navigator.platform.includes("Win") && imageAsFileEnabled) ?
+ ["application/x-moz-file", "Files"] : ["text/html", "text/plain", "Files"]);
+ await onPaste(e, imageAsFileEnabled);
+ resolve();
+ }, { once: true });
+ });
+
+ var textarea = SpecialPowers.wrap(document.getElementById('textarea'));
+ textarea.focus();
+ textarea.editor.paste(clipboard.kGlobalClipboard);
+
+ await promise;
+
+ clipboard.emptyClipboard(clipboard.kGlobalClipboard);
+ }
+
+ async function onPaste(e, imageAsFileEnabled) {
+ var imageTester = new ImageTester;
+ testFiles(e, imageTester, imageAsFileEnabled);
+ testItems(e, imageTester);
+ await imageTester.test();
+ }
+
+ function testItems(e, imageTester) {
+ var items = e.clipboardData.items;
+ is(items, e.clipboardData.items,
+ "Getting @items twice should return the same object");
+ var haveFiles = false;
+ ok(items instanceof DataTransferItemList, "@items implements DataTransferItemList");
+ ok(items.length, "@items is not empty");
+ for (var i = 0; i < items.length; i++) {
+ var item = items[i];
+ ok(item instanceof DataTransferItem, "each element of @items must implement DataTransferItem");
+ if (item.kind == "file") {
+ var file = item.getAsFile();
+ ok(file instanceof File, ".getAsFile() returns a File object");
+ ok(file.size > 0, "Files shouldn't have size 0");
+ imageTester.add(file);
+ }
+ }
+ }
+
+ function testFiles(e, imageTester, imageAsFileEnabled) {
+ var files = e.clipboardData.files;
+
+ is(files, e.clipboardData.files,
+ "Getting the files array twice should return the same array");
+ is(files.length, 1, "There should be one file in the clipboard");
+ for (var i = 0; i < files.length; i++) {
+ var file = files[i];
+ ok(file instanceof File, ".files should contain only File objects");
+ ok(file.size > 0, "This file shouldn't have size 0");
+ if (navigator.platform.includes("Win") && imageAsFileEnabled) {
+ ok(file.name.startsWith("Untitled") && file.name.endsWith(".png"),
+ `Check filename, got "${file.name}"`);
+ } else {
+ is(file.name, "image.png", "Check filename");
+ }
+
+ testSlice(file);
+ imageTester.add(file);
+ // Adding the same image again so we can test concurrency
+ imageTester.add(file);
+ }
+ }
+
+ function testSlice(aFile) {
+ var blob = aFile.slice();
+ ok(blob instanceof Blob, ".slice returns a blob");
+ is(blob.size, aFile.size, "the blob has the same size");
+
+ blob = aFile.slice(123123);
+ is(blob.size, 0, ".slice overflow check");
+
+ blob = aFile.slice(123, 123141);
+ is(blob.size, aFile.size - 123, ".slice @size check");
+
+ blob = aFile.slice(123, 12);
+ is(blob.size, 0, ".slice @size check 2");
+
+ blob = aFile.slice(124, 134, "image/png");
+ is(blob.size, 10, ".slice @size check 3");
+ is(blob.type, "image/png", ".slice @type check");
+ }
+
+ add_task(async function test_imageAsFile_enabled() {
+ await doTest(true);
+ });
+
+ add_task(async function test_imageAsFile_disabled() {
+ await doTest(false);
+ });
+
+</script>
+<body>
+ <img id="image" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAD4AAABHCA
+ IAAADQjmMaAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3goUAwAgSAORBwAAABl0RVh0Q29
+ tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAABPSURBVGje7c4BDQAACAOga//OmuMbJGAurTbq
+ 6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6s31B0IqAY2/t
+ QVCAAAAAElFTkSuQmCC" />
+ <form>
+ <textarea id="textarea"></textarea>
+ </form>
+</body>
+</html>