diff options
Diffstat (limited to 'dom/events/test/clipboard')
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> |