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