Bug 1957916 - avoid extra Content Analysis call when pasting into Google Sheets r=edgar
If aDataTransfer is non-null we should always use it in EditorBase::PasteAsAction(), even if we aren't dispatching the event. This correctly handles a corner case when the focus is changed by the paste handler (around line 1929) and PasteAsAction() calls itself again. Differential Revision: https://phabricator.services.mozilla.com/D244235
This commit is contained in:
@@ -1855,13 +1855,10 @@ nsresult EditorBase::PasteAsAction(nsIClipboard::ClipboardType aClipboardType,
|
|||||||
// the clipboard event and the call to HandlePaste below. This prevents
|
// the clipboard event and the call to HandlePaste below. This prevents
|
||||||
// race conditions with Content Analysis on like we see in bug 1918027.
|
// race conditions with Content Analysis on like we see in bug 1918027.
|
||||||
// Note that this is not needed if we're not going to dispatch the paste
|
// Note that this is not needed if we're not going to dispatch the paste
|
||||||
// event.
|
// event and no aDataTransfer was passed in.
|
||||||
RefPtr<DataTransfer> dataTransfer;
|
RefPtr<DataTransfer> dataTransfer = aDataTransfer;
|
||||||
if (aDispatchPasteEvent == DispatchPasteEvent::Yes) {
|
if (!aDataTransfer && aDispatchPasteEvent == DispatchPasteEvent::Yes) {
|
||||||
dataTransfer = aDataTransfer
|
dataTransfer = CreateDataTransferForPaste(ePaste, aClipboardType);
|
||||||
? RefPtr<DataTransfer>(aDataTransfer)
|
|
||||||
: RefPtr<DataTransfer>(CreateDataTransferForPaste(
|
|
||||||
ePaste, aClipboardType));
|
|
||||||
}
|
}
|
||||||
AutoEditActionDataSetter editActionData(*this, EditAction::ePaste,
|
AutoEditActionDataSetter editActionData(*this, EditAction::ePaste,
|
||||||
aPrincipal);
|
aPrincipal);
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ support-files = [
|
|||||||
"clipboard_paste_prompt.html",
|
"clipboard_paste_prompt.html",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
["browser_clipboard_paste_redirect_focus_in_paste_event_listener.js"]
|
||||||
|
support-files = [
|
||||||
|
"clipboard_paste_redirect_focus_in_paste_event_listener.html",
|
||||||
|
]
|
||||||
|
|
||||||
["browser_clipboard_read_async_content_analysis.js"]
|
["browser_clipboard_read_async_content_analysis.js"]
|
||||||
support-files = [
|
support-files = [
|
||||||
"clipboard_read_async.html",
|
"clipboard_read_async.html",
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
let mockCA = makeMockContentAnalysis();
|
||||||
|
|
||||||
|
add_setup(async function test_setup() {
|
||||||
|
mockCA = await mockContentAnalysisService(mockCA);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Based on editor/libeditor/tests/test_paste_redirect_focus_in_paste_event_listener.html
|
||||||
|
|
||||||
|
const PAGE_URL =
|
||||||
|
"https://example.com/browser/toolkit/components/contentanalysis/tests/browser/clipboard_paste_redirect_focus_in_paste_event_listener.html";
|
||||||
|
const CLIPBOARD_TEXT_STRING = "plain text";
|
||||||
|
|
||||||
|
function setClipboardData(clipboardString) {
|
||||||
|
const trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
|
||||||
|
Ci.nsITransferable
|
||||||
|
);
|
||||||
|
trans.init(null);
|
||||||
|
trans.addDataFlavor("text/plain");
|
||||||
|
const str = Cc["@mozilla.org/supports-string;1"].createInstance(
|
||||||
|
Ci.nsISupportsString
|
||||||
|
);
|
||||||
|
str.data = clipboardString;
|
||||||
|
trans.setTransferData("text/plain", str);
|
||||||
|
|
||||||
|
// Write to clipboard.
|
||||||
|
Services.clipboard.setData(trans, null, Ci.nsIClipboard.kGlobalClipboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testPasteWithElementId(elementId, browser) {
|
||||||
|
let resultPromise = SpecialPowers.spawn(browser, [], () => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
content.document.addEventListener(
|
||||||
|
"testresult",
|
||||||
|
event => {
|
||||||
|
resolve(event.detail.result);
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Paste into content
|
||||||
|
await SpecialPowers.spawn(browser, [elementId], async elementId => {
|
||||||
|
content.document.getElementById(elementId).focus();
|
||||||
|
});
|
||||||
|
await BrowserTestUtils.synthesizeKey("v", { accelKey: true }, browser);
|
||||||
|
let result = await resultPromise;
|
||||||
|
is(result, undefined, "Got unexpected result from page");
|
||||||
|
|
||||||
|
is(
|
||||||
|
mockCA.calls.length,
|
||||||
|
1,
|
||||||
|
"Correct number of calls to Content Analysis"
|
||||||
|
);
|
||||||
|
assertContentAnalysisRequest(
|
||||||
|
mockCA.calls[0],
|
||||||
|
CLIPBOARD_TEXT_STRING,
|
||||||
|
mockCA.calls[0].userActionId,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
mockCA.clearCalls();
|
||||||
|
//TODO
|
||||||
|
let value = await getElementValue(browser, elementId);
|
||||||
|
is(
|
||||||
|
value,
|
||||||
|
CLIPBOARD_TEXT_STRING,
|
||||||
|
"element has correct value"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertContentAnalysisRequest(
|
||||||
|
request,
|
||||||
|
expectedText,
|
||||||
|
expectedUserActionId,
|
||||||
|
expectedRequestsCount
|
||||||
|
) {
|
||||||
|
is(request.url.spec, PAGE_URL, "request has correct URL");
|
||||||
|
is(
|
||||||
|
request.analysisType,
|
||||||
|
Ci.nsIContentAnalysisRequest.eBulkDataEntry,
|
||||||
|
"request has correct analysisType"
|
||||||
|
);
|
||||||
|
is(
|
||||||
|
request.reason,
|
||||||
|
Ci.nsIContentAnalysisRequest.eClipboardPaste,
|
||||||
|
"request has correct reason"
|
||||||
|
);
|
||||||
|
is(
|
||||||
|
request.operationTypeForDisplay,
|
||||||
|
Ci.nsIContentAnalysisRequest.eClipboard,
|
||||||
|
"request has correct operationTypeForDisplay"
|
||||||
|
);
|
||||||
|
is(request.filePath, "", "request filePath should match");
|
||||||
|
is(request.textContent, expectedText, "request textContent should match");
|
||||||
|
is(
|
||||||
|
request.userActionRequestsCount,
|
||||||
|
expectedRequestsCount,
|
||||||
|
"request userActionRequestsCount should match"
|
||||||
|
);
|
||||||
|
is(
|
||||||
|
request.userActionId,
|
||||||
|
expectedUserActionId,
|
||||||
|
"request userActionId should match"
|
||||||
|
);
|
||||||
|
ok(request.userActionId.length, "request userActionId should not be empty");
|
||||||
|
is(request.printDataHandle, 0, "request printDataHandle should not be 0");
|
||||||
|
is(request.printDataSize, 0, "request printDataSize should not be 0");
|
||||||
|
ok(!!request.requestToken.length, "request requestToken should not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must be called from inside SpecialPowers.spawn()
|
||||||
|
function getElementValue(elementId) {
|
||||||
|
let elem = content.document.getElementById(elementId);
|
||||||
|
let tagName = elem.tagName.toLowerCase();
|
||||||
|
if (tagName === "input" || tagName === "textarea") {
|
||||||
|
return elem.value;
|
||||||
|
}
|
||||||
|
return elem.textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must be called from inside SpecialPowers.spawn()
|
||||||
|
function setElementValue(elementId, value) {
|
||||||
|
let elem = content.document.getElementById(elementId);
|
||||||
|
let tagName = elem.tagName.toLowerCase();
|
||||||
|
if (tagName === "input" || tagName === "textarea") {
|
||||||
|
elem.value = value;
|
||||||
|
} else {
|
||||||
|
elem.innerHTML = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add_task(async function testClipboardPasteWithRedirectFocus() {
|
||||||
|
mockCA.setupForTest(true);
|
||||||
|
|
||||||
|
setClipboardData(CLIPBOARD_TEXT_STRING);
|
||||||
|
|
||||||
|
const transferable =
|
||||||
|
SpecialPowers.Cc["@mozilla.org/widget/transferable;1"].createInstance(SpecialPowers.Ci.nsITransferable);
|
||||||
|
transferable.init(
|
||||||
|
SpecialPowers.wrap(window).docShell.QueryInterface(SpecialPowers.Ci.nsILoadContext)
|
||||||
|
);
|
||||||
|
const supportString =
|
||||||
|
SpecialPowers.Cc["@mozilla.org/supports-string;1"].createInstance(SpecialPowers.Ci.nsISupportsString);
|
||||||
|
supportString.data = "plain text";
|
||||||
|
transferable.setTransferData("text/plain", supportString);
|
||||||
|
|
||||||
|
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL);
|
||||||
|
let browser = tab.linkedBrowser;
|
||||||
|
|
||||||
|
for (const command of [
|
||||||
|
"cmd_paste",
|
||||||
|
"cmd_pasteNoFormatting",
|
||||||
|
"cmd_pasteQuote",
|
||||||
|
"cmd_pasteTransferable"
|
||||||
|
]) {
|
||||||
|
for (const editableSelector of [
|
||||||
|
"#src > input",
|
||||||
|
"#src > textarea",
|
||||||
|
"#src > div[contenteditable]"
|
||||||
|
]) {
|
||||||
|
await SpecialPowers.spawn(browser, [editableSelector], async (editableSelector) => {
|
||||||
|
const editableElement = content.document.querySelector(editableSelector);
|
||||||
|
await (async () => {
|
||||||
|
const input = content.document.querySelector("#dest > input");
|
||||||
|
editableElement.focus();
|
||||||
|
editableElement.addEventListener(
|
||||||
|
"paste",
|
||||||
|
() => input.focus(),
|
||||||
|
{once: true}
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
await BrowserTestUtils.synthesizeKey("v", { accelKey: true }, browser);
|
||||||
|
await SpecialPowers.spawn(browser, [command, editableSelector], async (command, editableSelector) => {
|
||||||
|
// Must be called from inside SpecialPowers.spawn()
|
||||||
|
function getElementValue(elem) {
|
||||||
|
let tagName = elem.tagName.toLowerCase();
|
||||||
|
if (tagName === "input" || tagName === "textarea") {
|
||||||
|
return elem.value;
|
||||||
|
}
|
||||||
|
return elem.textContent;
|
||||||
|
}
|
||||||
|
function setElementValue(elem, value) {
|
||||||
|
let tagName = elem.tagName.toLowerCase();
|
||||||
|
if (tagName === "input" || tagName === "textarea") {
|
||||||
|
elem.value = value;
|
||||||
|
} else {
|
||||||
|
elem.innerHTML = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editableElement = content.document.querySelector(editableSelector);
|
||||||
|
const editableElementDesc = `<${
|
||||||
|
editableElement.tagName.toLocaleLowerCase()
|
||||||
|
}${editableElement.hasAttribute("contenteditable") ? " contenteditable" : ""}>`;
|
||||||
|
const input = content.document.querySelector("#dest > input");
|
||||||
|
is(
|
||||||
|
getElementValue(editableElement).replace(/\n/g, ""),
|
||||||
|
"",
|
||||||
|
`${command}: ${
|
||||||
|
editableElementDesc
|
||||||
|
} should not have the pasted text because focus is redirected to <input> in a "paste" event listener`
|
||||||
|
);
|
||||||
|
is(
|
||||||
|
input.value.replace("> ", ""),
|
||||||
|
"plain text",
|
||||||
|
`${command}: new focused <input> (moved from ${
|
||||||
|
editableElementDesc
|
||||||
|
}) should have the pasted text`
|
||||||
|
);
|
||||||
|
|
||||||
|
setElementValue(editableElement, "");
|
||||||
|
input.value = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
is(
|
||||||
|
mockCA.calls.length,
|
||||||
|
1,
|
||||||
|
"Correct number of calls to Content Analysis"
|
||||||
|
);
|
||||||
|
assertContentAnalysisRequest(
|
||||||
|
mockCA.calls[0],
|
||||||
|
CLIPBOARD_TEXT_STRING,
|
||||||
|
mockCA.calls[0].userActionId,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
mockCA.clearCalls();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BrowserTestUtils.removeTab(tab);
|
||||||
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<!-- Based on editor/libeditor/tests/test_paste_redirect_focus_in_paste_event_listener.html -->
|
||||||
|
<title>Testing handling "paste" command when a "paste" event listener moves focus</title>
|
||||||
|
<script>
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/*SimpleTest.waitForExplicitFinish();
|
||||||
|
SimpleTest.waitForFocus(async () => {
|
||||||
|
info("Waiting for initializing clipboard...");
|
||||||
|
await SimpleTest.promiseClipboardChange(
|
||||||
|
"plain text",
|
||||||
|
() => SpecialPowers.clipboardCopyString("plain text")
|
||||||
|
);
|
||||||
|
|
||||||
|
const transferable =
|
||||||
|
SpecialPowers.Cc["@mozilla.org/widget/transferable;1"].createInstance(SpecialPowers.Ci.nsITransferable);
|
||||||
|
transferable.init(
|
||||||
|
SpecialPowers.wrap(window).docShell.QueryInterface(SpecialPowers.Ci.nsILoadContext)
|
||||||
|
);
|
||||||
|
const supportString =
|
||||||
|
SpecialPowers.Cc["@mozilla.org/supports-string;1"].createInstance(SpecialPowers.Ci.nsISupportsString);
|
||||||
|
supportString.data = "plain text";
|
||||||
|
transferable.setTransferData("text/plain", supportString);
|
||||||
|
|
||||||
|
function getValue(aElement) {
|
||||||
|
if (aElement.tagName.toLowerCase() == "input" ||
|
||||||
|
aElement.tagName.toLowerCase() == "textarea") {
|
||||||
|
return aElement.value;
|
||||||
|
}
|
||||||
|
return aElement.textContent;
|
||||||
|
}
|
||||||
|
function setValue(aElement, aValue) {
|
||||||
|
if (aElement.tagName.toLowerCase() == "input" ||
|
||||||
|
aElement.tagName.toLowerCase() == "textarea") {
|
||||||
|
aElement.value = aValue;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
aElement.innerHTML = aValue === "" ? "<br>" : aValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const command of [
|
||||||
|
"cmd_paste",
|
||||||
|
"cmd_pasteNoFormatting",
|
||||||
|
"cmd_pasteQuote",
|
||||||
|
"cmd_pasteTransferable"
|
||||||
|
]) {
|
||||||
|
for (const editableSelector of [
|
||||||
|
"#src > input",
|
||||||
|
"#src > textarea",
|
||||||
|
"#src > div[contenteditable]"
|
||||||
|
]) {
|
||||||
|
const editableElement = document.querySelector(editableSelector);
|
||||||
|
const editableElementDesc = `<${
|
||||||
|
editableElement.tagName.toLocaleLowerCase()
|
||||||
|
}${editableElement.hasAttribute("contenteditable") ? " contenteditable" : ""}>`;
|
||||||
|
(() => {
|
||||||
|
const input = document.querySelector("#dest > input");
|
||||||
|
editableElement.focus();
|
||||||
|
editableElement.addEventListener(
|
||||||
|
"paste",
|
||||||
|
() => input.focus(),
|
||||||
|
{once: true}
|
||||||
|
);
|
||||||
|
SpecialPowers.doCommand(window, command, transferable);
|
||||||
|
is(
|
||||||
|
getValue(editableElement).replace(/\n/g, ""),
|
||||||
|
"",
|
||||||
|
`${command}: ${
|
||||||
|
editableElementDesc
|
||||||
|
} should not have the pasted text because focus is redirected to <input> in a "paste" event listener`
|
||||||
|
);
|
||||||
|
is(
|
||||||
|
input.value.replace("> ", ""),
|
||||||
|
"plain text",
|
||||||
|
`${command}: new focused <input> (moved from ${
|
||||||
|
editableElementDesc
|
||||||
|
}) should have the pasted text`
|
||||||
|
);
|
||||||
|
setValue(editableElement, "");
|
||||||
|
input.value = "";
|
||||||
|
})();
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
const contentEditable = document.querySelector("#dest > div[contenteditable]");
|
||||||
|
editableElement.focus();
|
||||||
|
editableElement.addEventListener(
|
||||||
|
"paste",
|
||||||
|
() => contentEditable.focus(),
|
||||||
|
{once: true}
|
||||||
|
);
|
||||||
|
SpecialPowers.doCommand(window, command, transferable);
|
||||||
|
is(
|
||||||
|
getValue(editableElement).replace(/\n/g, ""),
|
||||||
|
"",
|
||||||
|
`${command}: ${
|
||||||
|
editableElementDesc
|
||||||
|
} should not have the pasted text because focus is redirected to <div contenteditable> in a "paste" event listener`
|
||||||
|
);
|
||||||
|
is(
|
||||||
|
contentEditable.textContent.replace(/\n/g, "").replace("> ", ""),
|
||||||
|
"plain text",
|
||||||
|
`${command}: new focused <div contenteditable> (moved from ${
|
||||||
|
editableElementDesc
|
||||||
|
}) should have the pasted text`
|
||||||
|
);
|
||||||
|
setValue(editableElement, "");
|
||||||
|
contentEditable.innerHTML = "<br>";
|
||||||
|
})();
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
const button = document.querySelector("#dest > button");
|
||||||
|
editableElement.focus();
|
||||||
|
editableElement.addEventListener(
|
||||||
|
"paste",
|
||||||
|
() => button.focus(),
|
||||||
|
{once: true}
|
||||||
|
);
|
||||||
|
SpecialPowers.doCommand(window, command, transferable);
|
||||||
|
is(
|
||||||
|
getValue(editableElement).replace(/\n/g, ""),
|
||||||
|
"",
|
||||||
|
`${command}: ${
|
||||||
|
editableElementDesc
|
||||||
|
} should not have the pasted text because focus is redirected to <button> in a "paste" event listener`
|
||||||
|
);
|
||||||
|
setValue(editableElement, "");
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleTest.finish();
|
||||||
|
});*/
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="src"><input><textarea></textarea><div contenteditable><br></div></div>
|
||||||
|
<div id="dest"><input><div contenteditable><br></div><button>button</button></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user