1 /* -*- Mode: JavaScript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
9 const kBaseUrlForContent = getRootDirectory(gTestPath).replace(
10 "chrome://mochitests/content",
14 const kContentFileName = "simple_navigator_clipboard_readText.html";
16 const kContentFileUrl = kBaseUrlForContent + kContentFileName;
18 const kApzTestNativeEventUtilsUrl =
19 "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js";
21 Services.scriptloader.loadSubScript(kApzTestNativeEventUtilsUrl, this);
23 SpecialPowers.pushPrefEnv({
24 set: [["dom.events.asyncClipboard.readText", true]],
27 const chromeDoc = window.document;
29 const kPasteMenuPopupId = "clipboardReadTextPasteMenuPopup";
30 const kPasteMenuItemId = "clipboardReadTextPasteMenuItem";
32 function promiseClickPasteButton() {
33 const pasteButton = chromeDoc.getElementById(kPasteMenuItemId);
35 // Native mouse event to execute additional code paths which wouldn't be
36 // covered by non-native events.
37 return EventUtils.promiseNativeMouseEventAndWaitForEvent({
44 function getMouseCoordsRelativeToScreenInDevicePixels() {
45 let mouseXInCSSPixels = {};
46 let mouseYInCSSPixels = {};
47 window.windowUtils.getLastOverWindowMouseLocationInCSSPixels(
54 (mouseXInCSSPixels.value + window.mozInnerScreenX) *
55 window.devicePixelRatio,
57 (mouseYInCSSPixels.value + window.mozInnerScreenY) *
58 window.devicePixelRatio,
62 function isCloselyLeftOnTopOf(aCoordsP1, aCoordsP2, aDelta) {
65 aCoordsP2.x - aCoordsP1.x < kDelta && aCoordsP2.y - aCoordsP1.y < kDelta
69 function waitForPasteMenuPopupEvent(aEventSuffix) {
70 // The element with id `kPasteMenuPopupId` is inserted dynamically, hence
71 // calling `BrowserTestUtils.waitForEvent` instead of
72 // `BrowserTestUtils.waitForPopupEvent`.
73 return BrowserTestUtils.waitForEvent(
75 "popup" + aEventSuffix,
78 return e.target.getAttribute("id") == kPasteMenuPopupId;
83 function promisePasteButtonIsShown() {
84 return waitForPasteMenuPopupEvent("shown").then(() => {
85 ok(true, "Witnessed 'popupshown' event for 'Paste' button.");
87 const pasteButton = chromeDoc.getElementById(kPasteMenuItemId);
89 return coordinatesRelativeToScreen({
97 function promisePasteButtonIsHidden() {
98 return waitForPasteMenuPopupEvent("hidden").then(() => {
99 ok(true, "Witnessed 'popuphidden' event for 'Paste' button.");
103 // @param aBrowser browser object of the content tab.
104 // @param aMultipleReadTextCalls if false, exactly one call is made, two
106 function promiseClickContentToTriggerClipboardReadText(
108 aMultipleReadTextCalls
110 const contentButtonId = aMultipleReadTextCalls
111 ? "invokeReadTextTwiceId"
112 : "invokeReadTextOnceId";
114 // `aBrowser.contentDocument` is null, therefore use `SpecialPowers.spawn`.
115 return SpecialPowers.spawn(
118 async _contentButtonId => {
119 const contentButton = content.document.getElementById(_contentButtonId);
121 // Native mouse event to execute additional code paths which wouldn't be
122 // covered by non-native events.
123 await EventUtils.promiseNativeMouseEventAndWaitForEvent({
125 target: contentButton,
130 // Creating an object in this, priviliged, scope via `eval` so that
131 // `coordinatesRelativeToScreen` below can access the object's
133 // Inside `eval`, parenthesis are needed to indicate to the JS
134 // parser that an expression, not a block statement, should be
136 const coordinateParams = content.window.eval(`({
137 target: window.document.getElementById("${_contentButtonId}"),
140 const coords = await content.wrappedJSObject.coordinatesRelativeToScreen(
149 // @param aBrowser browser object of the content tab.
150 function promiseMutatedReadTextResultFromContentElement(aBrowser) {
151 return SpecialPowers.spawn(aBrowser, [], async () => {
152 const readTextResultElement = content.document.getElementById(
156 const promiseReadTextResult = new Promise(resolve => {
157 const mutationObserver = new content.MutationObserver(
158 (aMutationRecord, aMutationObserver) => {
159 info("Observed mutation.");
160 aMutationObserver.disconnect();
161 resolve(readTextResultElement.textContent);
165 mutationObserver.observe(readTextResultElement, {
170 return await promiseReadTextResult;
174 function promiseWritingRandomTextToClipboard() {
175 const clipboardText = "X" + Math.random();
176 return navigator.clipboard.writeText(clipboardText).then(() => {
177 return clipboardText;
181 function promiseDismissPasteButton() {
182 // Native mouse event to execute additional code paths which wouldn't be
183 // covered by non-native events.
184 return EventUtils.promiseNativeMouseEvent({
186 target: chromeDoc.body,
192 add_task(async function test_paste_button_position() {
193 // Ensure there's text on the clipboard.
194 await promiseWritingRandomTextToClipboard();
196 await BrowserTestUtils.withNewTab(kContentFileUrl, async function(browser) {
197 const pasteButtonIsShown = promisePasteButtonIsShown();
198 const coordsOfClickInContentRelativeToScreenInDevicePixels = await promiseClickContentToTriggerClipboardReadText(
203 "coordsOfClickInContentRelativeToScreenInDevicePixels: " +
204 coordsOfClickInContentRelativeToScreenInDevicePixels.x +
206 coordsOfClickInContentRelativeToScreenInDevicePixels.y
209 const pasteButtonCoordsRelativeToScreenInDevicePixels = await pasteButtonIsShown;
211 "pasteButtonCoordsRelativeToScreenInDevicePixels: " +
212 pasteButtonCoordsRelativeToScreenInDevicePixels.x +
214 pasteButtonCoordsRelativeToScreenInDevicePixels.y
217 const mouseCoordsRelativeToScreenInDevicePixels = getMouseCoordsRelativeToScreenInDevicePixels();
219 "mouseCoordsRelativeToScreenInDevicePixels: " +
220 mouseCoordsRelativeToScreenInDevicePixels.x +
222 mouseCoordsRelativeToScreenInDevicePixels.y
225 // Asserting not overlapping is important; otherwise, when the
226 // "Paste" button is shown via a `mousedown` event, the following
227 // `mouseup` event could accept the "Paste" button unnoticed by the
230 isCloselyLeftOnTopOf(
231 mouseCoordsRelativeToScreenInDevicePixels,
232 pasteButtonCoordsRelativeToScreenInDevicePixels
234 "'Paste' button is closely left on top of the mouse pointer."
237 isCloselyLeftOnTopOf(
238 coordsOfClickInContentRelativeToScreenInDevicePixels,
239 pasteButtonCoordsRelativeToScreenInDevicePixels
241 "Coords of click in content are closely left on top of the 'Paste' button."
244 // To avoid disturbing subsequent tests.
245 const pasteButtonIsHidden = promisePasteButtonIsHidden();
246 await promiseClickPasteButton();
247 await pasteButtonIsHidden;
251 add_task(async function test_accepting_paste_button() {
252 // Randomized text to avoid overlappings with other tests.
253 const clipboardText = await promiseWritingRandomTextToClipboard();
255 await BrowserTestUtils.withNewTab(kContentFileUrl, async function(browser) {
256 const pasteButtonIsShown = promisePasteButtonIsShown();
257 await promiseClickContentToTriggerClipboardReadText(browser, false);
258 await pasteButtonIsShown;
259 const pasteButtonIsHidden = promisePasteButtonIsHidden();
260 const mutatedReadTextResultFromContentElement = promiseMutatedReadTextResultFromContentElement(
263 await promiseClickPasteButton();
264 await pasteButtonIsHidden;
265 await mutatedReadTextResultFromContentElement.then(value => {
268 "Resolved: " + clipboardText,
269 "Text returned from `navigator.clipboard.readText()` is as expected."
275 add_task(async function test_dismissing_paste_button() {
276 await BrowserTestUtils.withNewTab(kContentFileUrl, async function(browser) {
277 const pasteButtonIsShown = promisePasteButtonIsShown();
278 await promiseClickContentToTriggerClipboardReadText(browser, false);
279 await pasteButtonIsShown;
280 const pasteButtonIsHidden = promisePasteButtonIsHidden();
281 const mutatedReadTextResultFromContentElement = promiseMutatedReadTextResultFromContentElement(
284 await promiseDismissPasteButton();
285 await pasteButtonIsHidden;
286 await mutatedReadTextResultFromContentElement.then(value => {
290 "`navigator.clipboard.readText()` rejected after dismissing the 'Paste' button"
296 if (AppConstants.platform != "win") {
299 async function test_multiple_readText_invocations_for_same_user_activation() {
300 // Randomized text to avoid overlappings with other tests.
301 const clipboardText = await promiseWritingRandomTextToClipboard();
303 await BrowserTestUtils.withNewTab(kContentFileUrl, async function(
306 await promiseClickContentToTriggerClipboardReadText(browser, true);
307 const mutatedReadTextResultFromContentElement = promiseMutatedReadTextResultFromContentElement(
310 const pasteButtonIsHidden = promisePasteButtonIsHidden();
311 await promiseClickPasteButton();
312 await mutatedReadTextResultFromContentElement.then(value => {
315 "Resolved 1: " + clipboardText + "; Resolved 2: " + clipboardText,
316 "Two calls of `navigator.clipboard.read()` both resolved with the expected text."
320 // To avoid disturbing subsequent tests.
321 await pasteButtonIsHidden;
326 add_task(async function test_new_user_activation_shows_paste_button_again() {
327 await BrowserTestUtils.withNewTab(kContentFileUrl, async function(browser) {
328 // Ensure there's text on the clipboard.
329 await promiseWritingRandomTextToClipboard();
331 for (let i = 0; i < 2; ++i) {
332 const pasteButtonIsShown = promisePasteButtonIsShown();
333 // A click initiates a new user activation.
334 await promiseClickContentToTriggerClipboardReadText(browser, false);
335 await pasteButtonIsShown;
337 const pasteButtonIsHidden = promisePasteButtonIsHidden();
338 await promiseClickPasteButton();
339 await pasteButtonIsHidden;