tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit bd4fd98495758979cc35b28d2c9af6b96b456d5a
parent 9cb3e30d6196792680795d2e19fa20f766fde36b
Author: pstanciu <pstanciu@mozilla.com>
Date:   Fri, 12 Dec 2025 13:37:34 +0200

Revert "Bug 1998195 - Expose execCommand("paste") with a pop-up menu to web content; r=smaug,masayuki" for causing build bustage @nsGlobalWindowCommands.cpp

This reverts commit 57912b1d826ebd7207cdf746f284f8ae4222648c.

Diffstat:
Mdom/base/Document.cpp | 39+++++++++------------------------------
Mdom/base/nsGlobalWindowCommands.cpp | 32+++-----------------------------
Mdom/events/DataTransfer.cpp | 154++++++-------------------------------------------------------------------------
Mdom/events/DataTransfer.h | 14--------------
Mdom/events/test/clipboard/browser.toml | 15---------------
Mdom/events/test/clipboard/browser_document_command_paste.js | 550++++++++++++++++++++++++++++++++++++-------------------------------------------
Ddom/events/test/clipboard/browser_document_command_paste_contextmenu.js | 284-------------------------------------------------------------------------------
Ddom/events/test/clipboard/browser_document_command_paste_contextmenu_ext.js | 366-------------------------------------------------------------------------------
Ddom/events/test/clipboard/browser_document_command_paste_contextmenu_suppression.js | 147-------------------------------------------------------------------------------
Ddom/events/test/clipboard/browser_document_command_paste_contextmenu_suppression_ext.js | 150-------------------------------------------------------------------------------
Mdom/events/test/clipboard/head.js | 47++++-------------------------------------------
Mdom/locales/en-US/chrome/dom/dom.properties | 3---
Mdom/tests/mochitest/general/test_bug1161721.html | 18++++++------------
Meditor/libeditor/EditorBase.cpp | 1+
Meditor/libeditor/EditorCommands.cpp | 28+---------------------------
Meditor/libeditor/tests/browserscope/lib/richtext2/currentStatus.js | 6+++---
Mmodules/libpref/init/StaticPrefList.yaml | 6------
Mtesting/web-platform/meta/editing/other/exec-command-with-text-editor.tentative.html.ini | 36++++++++++++++++++++++++++++++++++++
Mtesting/web-platform/meta/editing/other/exec-command-without-editable-element.tentative.html.ini | 6------
Mtesting/web-platform/tests/editing/other/exec-command-with-text-editor.tentative.html | 2+-
20 files changed, 327 insertions(+), 1577 deletions(-)

diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp @@ -5736,22 +5736,6 @@ Document::AutoRunningExecCommandMarker::AutoRunningExecCommandMarker( } } -/** - * Returns true if calling execCommand with 'paste' arguments is allowed for the - * given subject principal. These are only allowed if the user initiated them - * (like with a mouse-click or key press). - */ -static bool IsExecCommandPasteAllowed(Document* aDocument, - nsIPrincipal& aSubjectPrincipal) { - if (StaticPrefs::dom_execCommand_paste_enabled() && aDocument && - aDocument->HasValidTransientUserGestureActivation()) { - return true; - } - - return nsContentUtils::PrincipalHasPermission(aSubjectPrincipal, - nsGkAtoms::clipboardRead); -} - bool Document::ExecCommand(const nsAString& aHTMLCommandName, bool aShowUI, const TrustedHTMLOrString& aValue, nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) { @@ -5815,14 +5799,8 @@ bool Document::ExecCommand(const nsAString& aHTMLCommandName, bool aShowUI, return false; } } else if (commandData.IsPasteCommand()) { - if (!IsExecCommandPasteAllowed(this, aSubjectPrincipal)) { - if (StaticPrefs::dom_execCommand_paste_enabled()) { - // We rejected the command because it was not performed with a valid - // user activation; therefore, we report the error to the console. - nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns, - this, nsContentUtils::eDOM_PROPERTIES, - "ExecCommandPasteDeniedNotInputDriven"); - } + if (!nsContentUtils::PrincipalHasPermission(aSubjectPrincipal, + nsGkAtoms::clipboardRead)) { return false; } } @@ -5997,7 +5975,8 @@ bool Document::QueryCommandEnabled(const nsAString& aHTMLCommandName, } if (commandData.IsPasteCommand() && - !IsExecCommandPasteAllowed(this, aSubjectPrincipal)) { + !nsContentUtils::PrincipalHasPermission(aSubjectPrincipal, + nsGkAtoms::clipboardRead)) { return false; } @@ -6212,12 +6191,12 @@ bool Document::QueryCommandSupported(const nsAString& aHTMLCommandName, } // Gecko technically supports all the clipboard commands including - // cut/copy/paste, and depending on the pref "dom.allow_cut_copy", cut and - // copy may also be disallowed to be called from non-privileged content. For - // that reason, we report the support status of corresponding command - // accordingly. + // cut/copy/paste, but non-privileged content will be unable to call + // paste, and depending on the pref "dom.allow_cut_copy", cut and copy + // may also be disallowed to be called from non-privileged content. + // For that reason, we report the support status of corresponding + // command accordingly. if (commandData.IsPasteCommand() && - !StaticPrefs::dom_execCommand_paste_enabled() && !nsContentUtils::PrincipalHasPermission(aSubjectPrincipal, nsGkAtoms::clipboardRead)) { return false; diff --git a/dom/base/nsGlobalWindowCommands.cpp b/dom/base/nsGlobalWindowCommands.cpp @@ -16,7 +16,6 @@ #include "mozilla/StaticPrefs_accessibility.h" #include "mozilla/TextEditor.h" #include "mozilla/TextEvents.h" -#include "mozilla/dom/DataTransfer.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/Selection.h" #include "mozilla/intl/WordBreaker.h" @@ -474,35 +473,10 @@ nsresult nsClipboardCommand::DoCommand(const nsACString& aCommandName, return eCopy; }(); - RefPtr<DataTransfer> dataTransfer; - if (ePaste == eventMessage) { - nsCOMPtr<nsIPrincipal> subjectPrincipal = - nsContentUtils::SubjectPrincipalOrSystemIfNativeCaller(); - MOZ_ASSERT(subjectPrincipal); - - // If we don't need to get user confirmation for clipboard access, we could - // just let nsCopySupport::FireClipboardEvent() to create DataTransfer - // instance synchronously for paste event. Otherwise, we need to spin the - // event loop to wait for the clipboard paste contextmenu to be shown and - // get user confirmation which are all handled in parent process before - // sending the paste event. - if (!nsContentUtils::PrincipalHasPermission(*subjectPrincipal, - nsGkAtoms::clipboardRead)) { - MOZ_DIAGNOSTIC_ASSERT(StaticPrefs::dom_execCommand_paste_enabled(), - "How did we get here?"); - // This will spin the event loop. - dataTransfer = DataTransfer::WaitForClipboardDataSnapshotAndCreate( - window, subjectPrincipal); - if (!dataTransfer) { - return NS_SUCCESS_DOM_NO_OPERATION; - } - } - } - bool actionTaken = false; - nsCopySupport::FireClipboardEvent( - eventMessage, Some(nsIClipboard::kGlobalClipboard), presShell, nullptr, - dataTransfer, &actionTaken); + nsCopySupport::FireClipboardEvent(eventMessage, + Some(nsIClipboard::kGlobalClipboard), + presShell, nullptr, nullptr, &actionTaken); return actionTaken ? NS_OK : NS_SUCCESS_DOM_NO_OPERATION; } diff --git a/dom/events/DataTransfer.cpp b/dom/events/DataTransfer.cpp @@ -12,7 +12,6 @@ #include "mozilla/ClipboardContentAnalysisChild.h" #include "mozilla/ClipboardReadRequestChild.h" #include "mozilla/Span.h" -#include "mozilla/SpinEventLoopUntil.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/dom/BindingUtils.h" #include "mozilla/dom/ContentChild.h" @@ -57,16 +56,6 @@ namespace mozilla::dom { -// The order of the types matters. `kFileMime` needs to be one of the first -// two types. And the order should be the same as the types order defined in -// MandatoryDataTypesAsCStrings() for Clipboard API. -static constexpr nsLiteralCString kNonPlainTextExternalFormats[] = { - nsLiteralCString(kCustomTypesMime), nsLiteralCString(kFileMime), - nsLiteralCString(kHTMLMime), nsLiteralCString(kRTFMime), - nsLiteralCString(kURLMime), nsLiteralCString(kURLDataMime), - nsLiteralCString(kTextMime), nsLiteralCString(kPNGImageMime), - nsLiteralCString(kPDFJSMime)}; - NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(DataTransfer) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(DataTransfer) @@ -192,36 +181,6 @@ DataTransfer::DataTransfer(nsISupports* aParent, EventMessage aEventMessage, "Failed to set given string to the DataTransfer object"); } -DataTransfer::DataTransfer(nsISupports* aParent, - nsIClipboard::ClipboardType aClipboardType, - nsIClipboardDataSnapshot* aClipboardDataSnapshot) - : mParent(aParent), - mEventMessage(ePaste), - mMode(ModeForEvent(ePaste)), - mClipboardType(Some(aClipboardType)) { - MOZ_ASSERT(aClipboardDataSnapshot); - - mClipboardDataSnapshot = aClipboardDataSnapshot; - mItems = new DataTransferItemList(this); - - AutoTArray<nsCString, std::size(kNonPlainTextExternalFormats)> flavors; - if (NS_FAILED(aClipboardDataSnapshot->GetFlavorList(flavors))) { - NS_WARNING("nsIClipboardDataSnapshot::GetFlavorList() failed"); - return; - } - - // Order is important for DataTransfer; ensure the returned list items follow - // the sequence specified in kNonPlainTextExternalFormats. - AutoTArray<nsCString, std::size(kNonPlainTextExternalFormats)> typesArray; - for (const auto& format : kNonPlainTextExternalFormats) { - if (flavors.Contains(format)) { - typesArray.AppendElement(format); - } - } - - CacheExternalData(typesArray, nsContentUtils::GetSystemPrincipal()); -} - DataTransfer::DataTransfer( nsISupports* aParent, EventMessage aEventMessage, const uint32_t aEffectAllowed, bool aCursorState, bool aIsExternal, @@ -276,109 +235,6 @@ JSObject* DataTransfer::WrapObject(JSContext* aCx, return DataTransfer_Binding::Wrap(aCx, this, aGivenProto); } -namespace { - -class ClipboardGetDataSnapshotCallback final - : public nsIClipboardGetDataSnapshotCallback { - public: - ClipboardGetDataSnapshotCallback(nsIGlobalObject* aGlobal, - nsIClipboard::ClipboardType aClipboardType) - : mGlobal(aGlobal), mClipboardType(aClipboardType) {} - - // This object will never be held by a cycle-collected object, so it doesn't - // need to be cycle-collected despite holding alive cycle-collected objects. - NS_DECL_ISUPPORTS - - // nsIClipboardGetDataSnapshotCallback - NS_IMETHOD OnSuccess( - nsIClipboardDataSnapshot* aClipboardDataSnapshot) override { - MOZ_ASSERT(aClipboardDataSnapshot); - mDataTransfer = MakeRefPtr<DataTransfer>( - ToSupports(mGlobal), mClipboardType, aClipboardDataSnapshot); - mComplete = true; - return NS_OK; - } - - NS_IMETHOD OnError(nsresult aResult) override { - mComplete = true; - return NS_OK; - } - - already_AddRefed<DataTransfer> TakeDataTransfer() { - MOZ_ASSERT(mComplete); - return mDataTransfer.forget(); - } - - bool IsComplete() const { return mComplete; } - - protected: - ~ClipboardGetDataSnapshotCallback() { - MOZ_ASSERT(!mDataTransfer); - MOZ_ASSERT(mComplete); - }; - - nsCOMPtr<nsIGlobalObject> mGlobal; - RefPtr<DataTransfer> mDataTransfer; - nsIClipboard::ClipboardType mClipboardType; - bool mComplete = false; -}; - -NS_IMPL_ISUPPORTS(ClipboardGetDataSnapshotCallback, - nsIClipboardGetDataSnapshotCallback) - -} // namespace - -// static -already_AddRefed<DataTransfer> -DataTransfer::WaitForClipboardDataSnapshotAndCreate( - nsPIDOMWindowOuter* aWindow, nsIPrincipal* aSubjectPrincipal) { - MOZ_ASSERT(aWindow); - MOZ_ASSERT(aSubjectPrincipal); - - nsCOMPtr<nsIClipboard> clipboardService = - do_GetService("@mozilla.org/widget/clipboard;1"); - if (!clipboardService) { - return nullptr; - } - - BrowsingContext* bc = aWindow->GetBrowsingContext(); - if (!bc) { - return nullptr; - } - - WindowContext* wc = bc->GetCurrentWindowContext(); - if (!wc) { - return nullptr; - } - - Document* doc = wc->GetExtantDoc(); - if (!doc) { - return nullptr; - } - - RefPtr<ClipboardGetDataSnapshotCallback> callback = - MakeRefPtr<ClipboardGetDataSnapshotCallback>( - doc->GetScopeObject(), nsIClipboard::kGlobalClipboard); - - AutoTArray<nsCString, std::size(kNonPlainTextExternalFormats)> types; - types.AppendElements( - Span<const nsLiteralCString>(kNonPlainTextExternalFormats)); - - nsresult rv = clipboardService->GetDataSnapshot( - types, nsIClipboard::kGlobalClipboard, wc, aSubjectPrincipal, callback); - if (NS_FAILED(rv)) { - return nullptr; - } - - if (!SpinEventLoopUntil( - "DataTransfer::WaitForClipboardDataSnapshotAndCreate"_ns, - [&]() { return callback->IsComplete(); })) { - return nullptr; - } - - return callback->TakeDataTransfer(); -} - void DataTransfer::SetDropEffect(const nsAString& aDropEffect) { // the drop effect can only be 'none', 'copy', 'move' or 'link'. for (uint32_t e = 0; e <= nsIDragService::DRAGDROP_ACTION_LINK; e++) { @@ -747,6 +603,16 @@ already_AddRefed<DataTransfer> DataTransfer::MozCloneForEvent( return dt.forget(); } +// The order of the types matters. `kFileMime` needs to be one of the first two +// types. And the order should be the same as the types order defined in +// MandatoryDataTypesAsCStrings() for Clipboard API. +static constexpr nsLiteralCString kNonPlainTextExternalFormats[] = { + nsLiteralCString(kCustomTypesMime), nsLiteralCString(kFileMime), + nsLiteralCString(kHTMLMime), nsLiteralCString(kRTFMime), + nsLiteralCString(kURLMime), nsLiteralCString(kURLDataMime), + nsLiteralCString(kTextMime), nsLiteralCString(kPNGImageMime), + nsLiteralCString(kPDFJSMime)}; + namespace { nsresult GetClipboardDataSnapshotWithContentAnalysisSync( const nsTArray<nsCString>& aFormats, diff --git a/dom/events/DataTransfer.h b/dom/events/DataTransfer.h @@ -107,8 +107,6 @@ class DataTransfer final : public nsISupports, public nsWrapperCache { nsITransferable* aTransferable); DataTransfer(nsISupports* aParent, EventMessage aEventMessage, const nsAString& aString); - DataTransfer(nsISupports* aParent, nsIClipboard::ClipboardType aClipboardType, - nsIClipboardDataSnapshot* aClipboardDataSnapshot); virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; @@ -127,18 +125,6 @@ class DataTransfer final : public nsISupports, public nsWrapperCache { const GlobalObject& aGlobal); /** - * This creates a DataTransfer by calling nsIClipboard::GetDataSnapshot() to - * obtain an nsIClipboardDataSnapshot first, in order to trigger the security - * checks, i.e. showing a paste contextmenu to request user confirmation if - * the clipboard data originated from a cross-origin page. All of that is - * handled in the parent process, so we spin the event loop here to wait for - * the result. - */ - MOZ_CAN_RUN_SCRIPT - static already_AddRefed<DataTransfer> WaitForClipboardDataSnapshotAndCreate( - nsPIDOMWindowOuter* aWindow, nsIPrincipal* aSubjectPrincipal); - - /** * The actual effect that will be used, and should always be one of the * possible values of effectAllowed. * diff --git a/dom/events/test/clipboard/browser.toml b/dom/events/test/clipboard/browser.toml @@ -11,20 +11,6 @@ support-files = [ ["browser_document_command_paste.js"] -["browser_document_command_paste_contextmenu.js"] -skip-if = [ - "headless", # bug 1989339 -] - -["browser_document_command_paste_contextmenu_ext.js"] -skip-if = [ - "headless", # bug 1989339 -] - -["browser_document_command_paste_contextmenu_suppression.js"] - -["browser_document_command_paste_contextmenu_suppression_ext.js"] - ["browser_navigator_clipboard_clickjacking.js"] support-files = ["simple_navigator_clipboard_keydown.html"] run-if = [ @@ -80,7 +66,6 @@ support-files = [ skip-if = [ "headless", # bug 1989339 ] - ["browser_navigator_clipboard_read_ext.js"] support-files = ["simple_page_ext.html"] skip-if = [ diff --git a/dom/events/test/clipboard/browser_document_command_paste.js b/dom/events/test/clipboard/browser_document_command_paste.js @@ -8,339 +8,295 @@ const kContentFileUrl = kBaseUrlForContent + "simple_page_ext.html"; -add_setup(async function init() { - await SpecialPowers.pushPrefEnv({ - // This to turn off the paste contextmenu for testing. - set: [["dom.events.testing.asyncClipboard", true]], - }); +beforeEach(async () => { + info("Write random text to clipboard"); + await promiseWritingRandomTextToClipboard(); }); -[true, false].forEach(aPrefValue => { - describe(`dom.execCommand.paste.enabled=${aPrefValue}`, () => { - it("set preference", async () => { - await SpecialPowers.pushPrefEnv({ - set: [["dom.execCommand.paste.enabled", aPrefValue]], - }); - }); +describe("test paste comment", () => { + it(`called from system principal`, async () => { + document.clearUserGestureActivation(); + ok( + document.queryCommandSupported("paste"), + "Check if the 'paste' command is supported" + ); - describe("test paste comment", () => { - beforeEach(async () => { - info("Write random text to clipboard"); - await promiseWritingRandomTextToClipboard(); - }); + // Test without editing. + ok( + !document.queryCommandEnabled("paste"), + "Check if the 'paste' command is enabled without editing" + ); + ok( + !document.execCommand("paste"), + "Check if the 'paste' command is succeed without editing" + ); + + // Test with editing. + const textArea = document.createElement("textarea"); + document.body.appendChild(textArea); + textArea.textContent = "textarea text"; + textArea.setSelectionRange(0, textArea.value.length); + textArea.focus(); + ok( + document.queryCommandEnabled("paste"), + "Check if the 'paste' command is enabled when editing" + ); + ok( + document.execCommand("paste"), + "Check if the 'paste' command is succeed when editing" + ); + textArea.remove(); + }); - it(`called from system principal`, async () => { - document.clearUserGestureActivation(); + it(`called from web content`, async () => { + await BrowserTestUtils.withNewTab(kContentFileUrl, async browser => { + await SpecialPowers.spawn(browser, [], async () => { + const doc = Cu.waiveXrays(content.document); ok( - document.queryCommandSupported("paste"), - "Check if the 'paste' command is supported" + !doc.queryCommandSupported("paste"), + `Check if the 'paste' command is supported` ); - // Test without editing. + // Test no user activation. + content.document.clearUserGestureActivation(); ok( - !document.queryCommandEnabled("paste"), - "Check if the 'paste' command is enabled without editing" + !doc.queryCommandEnabled("paste"), + "Check if the 'paste' command is enabled without user activation" ); ok( - !document.execCommand("paste"), - "Check if the 'paste' command is succeed without editing" + !doc.execCommand("paste"), + "Check if the 'paste' command is succeed without user activation" + ); + + // Test with user activation. + content.document.notifyUserGestureActivation(); + ok( + !doc.queryCommandEnabled("paste"), + "Check if the 'paste' command is enabled with user activation" + ); + ok( + !doc.execCommand("paste"), + "Check if the 'paste' command is succeed with user activation" ); // Test with editing. - const textArea = document.createElement("textarea"); - document.body.appendChild(textArea); + const textArea = content.document.createElement("textarea"); + content.document.body.appendChild(textArea); + textArea.textContent = "textarea text"; + textArea.setSelectionRange(0, textArea.value.length); + textArea.focus(); + + // Test no user activation. + content.document.clearUserGestureActivation(); + ok( + !doc.queryCommandEnabled("paste"), + "Check if the 'paste' command is enabled without user activation when editing" + ); + ok( + !doc.execCommand("paste"), + "Check if the 'paste' command is succeed without user activation when editing" + ); + + // Test with user activation. textArea.textContent = "textarea text"; textArea.setSelectionRange(0, textArea.value.length); textArea.focus(); + content.document.notifyUserGestureActivation(); ok( - document.queryCommandEnabled("paste"), - "Check if the 'paste' command is enabled when editing" + !doc.queryCommandEnabled("paste"), + "Check if the 'paste' command is enabled with user activation when editing" ); ok( - document.execCommand("paste"), - "Check if the 'paste' command is succeed when editing" + !doc.execCommand("paste"), + "Check if the 'paste' command is succeed with user activation when editing" ); - textArea.remove(); }); + }); + }); - it(`called from web content`, async () => { - await BrowserTestUtils.withNewTab(kContentFileUrl, async browser => { - await SpecialPowers.spawn(browser, [aPrefValue], async aPrefValue => { - const doc = Cu.waiveXrays(content.document); - is( - doc.queryCommandSupported("paste"), - aPrefValue, - `Check if the 'paste' command is supported` - ); - - // Test no user activation. - content.document.clearUserGestureActivation(); - ok( - !doc.queryCommandEnabled("paste"), - "Check if the 'paste' command is enabled without user activation" - ); - ok( - !doc.execCommand("paste"), - "Check if the 'paste' command is succeed without user activation" - ); + [true, false].forEach(aPermission => { + describe(`extension ${aPermission ? "with" : "without"} clipboardRead permission`, () => { + const sharedScript = function () { + this.testPasteCommand = function () { + return [ + document.queryCommandSupported("paste"), + document.queryCommandEnabled("paste"), + document.execCommand("paste"), + ]; + }; + }; - // Test with user activation. - content.document.notifyUserGestureActivation(); - is( - doc.queryCommandEnabled("paste"), - aPrefValue, - "Check if the 'paste' command is enabled with user activation" - ); - is( - doc.execCommand("paste"), - aPrefValue, - "Check if the 'paste' command is succeed with user activation" - ); + it("called from content script", async () => { + const contentScript = function () { + document + .querySelector("button") + .addEventListener("click", function (e) { + browser.test.sendMessage("result", testPasteCommand()); + }); + browser.test.sendMessage("ready", testPasteCommand()); + }; + const extensionData = { + manifest: { + content_scripts: [ + { + js: ["sharedScript.js", "contentscript.js"], + matches: ["https://example.com/*"], + }, + ], + }, + files: { + "sharedScript.js": sharedScript, + "contentscript.js": contentScript, + }, + }; + if (aPermission) { + extensionData.manifest.permissions = ["clipboardRead"]; + } - // Test with editing. - const textArea = content.document.createElement("textarea"); - content.document.body.appendChild(textArea); - textArea.textContent = "textarea text"; - textArea.setSelectionRange(0, textArea.value.length); - textArea.focus(); + // Load and start the extension. + const extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + await BrowserTestUtils.withNewTab(kContentFileUrl, async browser => { + let [supported, enabled, succeed] = + await extension.awaitMessage("ready"); + is( + supported, + aPermission, + "Check if the 'paste' command is supported" + ); + is( + enabled, + aPermission, + "Check if the 'paste' command is enabled without user activation" + ); + is( + succeed, + aPermission, + "Check if the 'paste' command is succeed without user activation" + ); - // Test no user activation. - content.document.clearUserGestureActivation(); - ok( - !doc.queryCommandEnabled("paste"), - "Check if the 'paste' command is enabled without user activation when editing" - ); - ok( - !doc.execCommand("paste"), - "Check if the 'paste' command is succeed without user activation when editing" - ); + // Click on the content to trigger user activation. + promiseClickContentElement(browser, "btn"); + [supported, enabled, succeed] = + await extension.awaitMessage("result"); + is( + enabled, + aPermission, + "Check if the 'paste' command is enabled with user activation" + ); + is( + succeed, + aPermission, + "Check if the 'paste' command is succeed with user activation" + ); + }); + await extension.unload(); + }); - // Test with user activation. + it("called from content script when editing", async () => { + const contentScript = function () { + const textArea = document.createElement("textarea"); + document.body.appendChild(textArea); + const testPasteCommandWhenEditing = function () { + // Start editing. textArea.textContent = "textarea text"; textArea.setSelectionRange(0, textArea.value.length); textArea.focus(); - content.document.notifyUserGestureActivation(); - is( - doc.queryCommandEnabled("paste"), - aPrefValue, - "Check if the 'paste' command is enabled with user activation when editing" - ); - is( - doc.execCommand("paste"), - aPrefValue, - "Check if the 'paste' command is succeed with user activation when editing" - ); - }); - }); - }); - - [true, false].forEach(aPermission => { - describe(`extension ${aPermission ? "with" : "without"} clipboardRead permission`, () => { - const sharedScript = function () { - this.testPasteCommand = function () { - return [ - document.queryCommandSupported("paste"), - document.queryCommandEnabled("paste"), - document.execCommand("paste"), - ]; - }; + return testPasteCommand(); }; - - it("called from content script", async () => { - const contentScript = function () { - document - .querySelector("button") - .addEventListener("click", function (e) { - browser.test.sendMessage("result", testPasteCommand()); - }); - browser.test.sendMessage("ready", testPasteCommand()); - }; - const extensionData = { - manifest: { - content_scripts: [ - { - js: ["sharedScript.js", "contentscript.js"], - matches: ["https://example.com/*"], - }, - ], - }, - files: { - "sharedScript.js": sharedScript, - "contentscript.js": contentScript, + document + .querySelector("button") + .addEventListener("click", function (e) { + browser.test.sendMessage("result", testPasteCommandWhenEditing()); + }); + browser.test.sendMessage("ready", testPasteCommandWhenEditing()); + }; + const extensionData = { + manifest: { + content_scripts: [ + { + js: ["sharedScript.js", "contentscript.js"], + matches: ["https://example.com/*"], }, - }; - if (aPermission) { - extensionData.manifest.permissions = ["clipboardRead"]; - } + ], + }, + files: { + "sharedScript.js": sharedScript, + "contentscript.js": contentScript, + }, + }; + if (aPermission) { + extensionData.manifest.permissions = ["clipboardRead"]; + } - // Load and start the extension. - const extension = ExtensionTestUtils.loadExtension(extensionData); - await extension.startup(); - await BrowserTestUtils.withNewTab( - kContentFileUrl, - async browser => { - let [supported, enabled, succeed] = - await extension.awaitMessage("ready"); - is( - supported, - aPrefValue || aPermission, - "Check if the 'paste' command is supported" - ); - - // Test no user activation. - is( - enabled, - aPermission, - "Check if the 'paste' command is enabled without user activation" - ); - is( - succeed, - aPermission, - "Check if the 'paste' command is succeed without user activation" - ); - - // Click on the content to trigger user activation. - promiseClickContentElement(browser, "btn"); - [supported, enabled, succeed] = - await extension.awaitMessage("result"); - is( - enabled, - aPrefValue || aPermission, - "Check if the 'paste' command is enabled with user activation" - ); - is( - succeed, - aPrefValue || aPermission, - "Check if the 'paste' command is succeed with user activation" - ); - } - ); - await extension.unload(); - }); - - it("called from content script when editing", async () => { - const contentScript = function () { - const textArea = document.createElement("textarea"); - document.body.appendChild(textArea); - const testPasteCommandWhenEditing = function () { - // Start editing. - textArea.textContent = "textarea text"; - textArea.setSelectionRange(0, textArea.value.length); - textArea.focus(); - return testPasteCommand(); - }; - document - .querySelector("button") - .addEventListener("click", function (e) { - browser.test.sendMessage( - "result", - testPasteCommandWhenEditing() - ); - }); - browser.test.sendMessage("ready", testPasteCommandWhenEditing()); - }; - const extensionData = { - manifest: { - content_scripts: [ - { - js: ["sharedScript.js", "contentscript.js"], - matches: ["https://example.com/*"], - }, - ], - }, - files: { - "sharedScript.js": sharedScript, - "contentscript.js": contentScript, - }, - }; - if (aPermission) { - extensionData.manifest.permissions = ["clipboardRead"]; - } - - // Load and start the extension. - const extension = ExtensionTestUtils.loadExtension(extensionData); - await extension.startup(); - await BrowserTestUtils.withNewTab( - kContentFileUrl, - async browser => { - let [supported, enabled, succeed] = - await extension.awaitMessage("ready"); - is( - supported, - aPrefValue || aPermission, - "Check if the 'paste' command is supported" - ); - is( - enabled, - aPermission, - "Check if the 'paste' command is enabled without user activation" - ); - is( - succeed, - aPermission, - "Check if the 'paste' command is succeed without user activation" - ); + // Load and start the extension. + const extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + await BrowserTestUtils.withNewTab(kContentFileUrl, async browser => { + let [supported, enabled, succeed] = + await extension.awaitMessage("ready"); + is( + supported, + aPermission, + "Check if the 'paste' command is supported" + ); + is( + enabled, + aPermission, + "Check if the 'paste' command is enabled without user activation" + ); + is( + succeed, + aPermission, + "Check if the 'paste' command is succeed without user activation" + ); - // Click on the content to trigger user activation. - promiseClickContentElement(browser, "btn"); - [supported, enabled, succeed] = - await extension.awaitMessage("result"); - is( - enabled, - aPrefValue || aPermission, - "Check if the 'paste' command is enabled with user activation" - ); - is( - succeed, - aPrefValue || aPermission, - "Check if the 'paste' command is succeed with user activation" - ); - } - ); - await extension.unload(); - }); + // Click on the content to trigger user activation. + promiseClickContentElement(browser, "btn"); + [supported, enabled, succeed] = + await extension.awaitMessage("result"); + is( + enabled, + aPermission, + "Check if the 'paste' command is enabled with user activation" + ); + is( + succeed, + aPermission, + "Check if the 'paste' command is succeed with user activation" + ); + }); + await extension.unload(); + }); - it("called from background script", async () => { - const backgroundScript = function () { - browser.test.sendMessage("ready", testPasteCommand()); - }; - const extensionData = { - background: [sharedScript, backgroundScript], - }; - if (aPermission) { - extensionData.manifest = { - permissions: ["clipboardRead"], - }; - } + it("called from background script", async () => { + const backgroundScript = function () { + browser.test.sendMessage("ready", testPasteCommand()); + }; + const extensionData = { + background: [sharedScript, backgroundScript], + }; + if (aPermission) { + extensionData.manifest = { + permissions: ["clipboardRead"], + }; + } - // Load and start the extension. - const extension = ExtensionTestUtils.loadExtension(extensionData); - await extension.startup(); - await BrowserTestUtils.withNewTab( - kContentFileUrl, - async browser => { - let [supported, enabled, succeed] = - await extension.awaitMessage("ready"); - is( - supported, - aPrefValue || aPermission, - "Check if the 'paste' command is supported" - ); - is( - enabled, - aPermission, - "Check if the 'paste' command is enabled" - ); - is( - succeed, - aPermission, - "Check if the 'paste' command is succeed" - ); - } - ); - await extension.unload(); - }); + // Load and start the extension. + const extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + await BrowserTestUtils.withNewTab(kContentFileUrl, async browser => { + let [supported, enabled, succeed] = + await extension.awaitMessage("ready"); + is( + supported, + aPermission, + "Check if the 'paste' command is supported" + ); + is(enabled, aPermission, "Check if the 'paste' command is enabled"); + is(succeed, aPermission, "Check if the 'paste' command is succeed"); }); + await extension.unload(); }); }); }); diff --git a/dom/events/test/clipboard/browser_document_command_paste_contextmenu.js b/dom/events/test/clipboard/browser_document_command_paste_contextmenu.js @@ -1,284 +0,0 @@ -/* -*- 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 kContentFileUrl = kBaseUrlForContent + "simple_page_ext.html"; - -function promiseExecCommandPaste(aBrowser) { - return SpecialPowers.spawn(aBrowser, [], () => { - let clipboardData = null; - content.document.addEventListener( - "paste", - e => { - clipboardData = e.clipboardData.getData("text/plain"); - }, - { once: true } - ); - - content.document.notifyUserGestureActivation(); - const execCommandResult = Cu.waiveXrays(content.document).execCommand( - "paste" - ); - - return { execCommandResult, clipboardData }; - }); -} - -function execCommandPasteWithoutWait(aBrowser) { - return SpecialPowers.spawn(aBrowser, [], () => { - SpecialPowers.executeSoon(() => { - content.document.notifyUserGestureActivation(); - const execCommandResult = Cu.waiveXrays(content.document).execCommand( - "paste" - ); - }); - }); -} - -add_setup(async function () { - await SpecialPowers.pushPrefEnv({ - set: [ - ["test.events.async.enabled", true], - // Disable the paste contextmenu delay to make the test run faster. - ["security.dialog_enable_delay", 0], - ], - }); -}); - -kPasteCommandTests.forEach(test => { - describe(test.description, () => { - it("Accepting paste contextmenu", async () => { - info(`Randomized text to avoid overlappings with other tests`); - const clipboardText = await promiseWritingRandomTextToClipboard(); - - await BrowserTestUtils.withNewTab( - kContentFileUrl, - async function (aBrowser) { - if (test.setupFn) { - info(`Setup`); - await test.setupFn(aBrowser); - } - - const pasteButtonIsShown = promisePasteButtonIsShown(); - - info(`Trigger execCommand("paste")`); - const pasteCommandResult = promiseExecCommandPaste(aBrowser); - - info(`Wait for paste context menu is shown`); - await pasteButtonIsShown; - - info(`Click paste context menu`); - const pasteButtonIsHidden = promisePasteButtonIsHidden(); - await promiseClickPasteButton(); - await pasteButtonIsHidden; - - const { execCommandResult, clipboardData } = await pasteCommandResult; - ok(execCommandResult, `execCommand("paste") should be succeed`); - is(clipboardData, clipboardText, `Check clipboard data`); - - if (test.additionalCheckFunc) { - info(`Additional checks`); - await test.additionalCheckFunc(aBrowser, clipboardText); - } - } - ); - }); - - it("Dismissing paste contextmenu", async () => { - info(`Randomized text to avoid overlappings with other tests`); - await promiseWritingRandomTextToClipboard(); - - await BrowserTestUtils.withNewTab( - kContentFileUrl, - async function (aBrowser) { - if (test.setupFn) { - info(`Setup`); - await test.setupFn(aBrowser); - } - - const pasteButtonIsShown = promisePasteButtonIsShown(); - - info(`Trigger execCommand("paste")`); - const pasteCommandResult = promiseExecCommandPaste(aBrowser); - - info(`Wait for paste context menu is shown`); - await pasteButtonIsShown; - - info(`Dismiss paste context menu`); - const pasteButtonIsHidden = promisePasteButtonIsHidden(); - await promiseDismissPasteButton(); - await pasteButtonIsHidden; - - const { execCommandResult, clipboardData } = await pasteCommandResult; - ok(!execCommandResult, `execCommand("paste") should not be succeed`); - is(clipboardData, null, `Check clipboard data`); - - if (test.additionalCheckFunc) { - info(`Additional checks`); - await test.additionalCheckFunc(aBrowser, ""); - } - } - ); - }); - - it("Tab close", async () => { - info(`Randomized text to avoid overlappings with other tests`); - await promiseWritingRandomTextToClipboard(); - - let pasteButtonIsHidden; - await BrowserTestUtils.withNewTab( - kContentFileUrl, - async function (aBrowser) { - if (test.setupFn) { - info(`Setup`); - await test.setupFn(aBrowser); - } - - const pasteButtonIsShown = promisePasteButtonIsShown(); - - info(`Trigger execCommand("paste")`); - execCommandPasteWithoutWait(aBrowser); - - info(`Wait for paste context menu is shown`); - await pasteButtonIsShown; - - pasteButtonIsHidden = promisePasteButtonIsHidden(); - info("Close tab"); - } - ); - - await pasteButtonIsHidden; - }); - - it("Tab switch", async () => { - info(`Randomized text to avoid overlappings with other tests`); - await promiseWritingRandomTextToClipboard(); - - await BrowserTestUtils.withNewTab( - kContentFileUrl, - async function (aBrowser) { - if (test.setupFn) { - info(`Setup`); - await test.setupFn(aBrowser); - } - - const pasteButtonIsShown = promisePasteButtonIsShown(); - - info(`Trigger execCommand("paste")`); - execCommandPasteWithoutWait(aBrowser); - - info(`Wait for paste context menu is shown`); - await pasteButtonIsShown; - - info("Switch tab"); - let pasteButtonIsHidden = promisePasteButtonIsHidden(); - let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); - - info(`Wait for paste context menu is hidden`); - await pasteButtonIsHidden; - - BrowserTestUtils.removeTab(tab); - } - ); - }); - - it("Window switch", async () => { - info(`Randomized text to avoid overlappings with other tests`); - await promiseWritingRandomTextToClipboard(); - - await BrowserTestUtils.withNewTab( - kContentFileUrl, - async function (aBrowser) { - if (test.setupFn) { - info(`Setup`); - await test.setupFn(aBrowser); - } - - const pasteButtonIsShown = promisePasteButtonIsShown(); - - info(`Trigger execCommand("paste")`); - execCommandPasteWithoutWait(aBrowser); - - info(`Wait for paste context menu is shown`); - await pasteButtonIsShown; - - info("Switch browser window"); - let pasteButtonIsHidden = promisePasteButtonIsHidden(); - let newWin = await BrowserTestUtils.openNewBrowserWindow(); - - info(`Wait for paste context menu is hidden`); - await pasteButtonIsHidden; - - await BrowserTestUtils.closeWindow(newWin); - } - ); - }); - - it("Tab navigate", async () => { - info(`Randomized text to avoid overlappings with other tests`); - await promiseWritingRandomTextToClipboard(); - - await BrowserTestUtils.withNewTab( - kContentFileUrl, - async function (aBrowser) { - if (test.setupFn) { - info(`Setup`); - await test.setupFn(aBrowser); - } - - const pasteButtonIsShown = promisePasteButtonIsShown(); - - info(`Trigger execCommand("paste")`); - execCommandPasteWithoutWait(aBrowser); - - info(`Wait for paste context menu is shown`); - await pasteButtonIsShown; - - info("Navigate tab"); - let pasteButtonIsHidden = promisePasteButtonIsHidden(); - aBrowser.loadURI(Services.io.newURI("https://example.com/"), { - triggeringPrincipal: - Services.scriptSecurityManager.getSystemPrincipal(), - }); - - info(`Wait for paste context menu is hidden`); - await pasteButtonIsHidden; - } - ); - }); - - it("Tab reload", async () => { - info(`Randomized text to avoid overlappings with other tests`); - await promiseWritingRandomTextToClipboard(); - - await BrowserTestUtils.withNewTab( - kContentFileUrl, - async function (aBrowser) { - if (test.setupFn) { - info(`Setup`); - await test.setupFn(aBrowser); - } - - const pasteButtonIsShown = promisePasteButtonIsShown(); - - info(`Trigger execCommand("paste")`); - execCommandPasteWithoutWait(aBrowser); - - info(`Wait for paste context menu is shown`); - await pasteButtonIsShown; - - info("Reload tab"); - let pasteButtonIsHidden = promisePasteButtonIsHidden(); - await BrowserTestUtils.reloadTab(gBrowser.selectedTab); - - info(`Wait for paste context menu is hidden`); - await pasteButtonIsHidden; - } - ); - }); - }); -}); diff --git a/dom/events/test/clipboard/browser_document_command_paste_contextmenu_ext.js b/dom/events/test/clipboard/browser_document_command_paste_contextmenu_ext.js @@ -1,366 +0,0 @@ -/* -*- 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 kContentFileUrl = kBaseUrlForContent + "simple_page_ext.html"; - -async function promiseExecCommandPasteFromExtension(aBrowser, aExtension) { - await SpecialPowers.spawn(aBrowser, [], async () => { - content.document.notifyUserGestureActivation(); - }); - - aExtension.sendMessage("paste"); - return await aExtension.awaitMessage("result"); -} - -async function execCommandPasteFromExtensionWithoutResult( - aBrowser, - aExtension -) { - await SpecialPowers.spawn(aBrowser, [], async () => { - content.document.notifyUserGestureActivation(); - }); - - aExtension.sendMessage("pasteWithoutResult"); -} - -add_setup(async function () { - await SpecialPowers.pushPrefEnv({ - set: [ - ["test.events.async.enabled", true], - // Disable the paste contextmenu delay to make the test run faster. - ["security.dialog_enable_delay", 0], - ], - }); -}); - -kPasteCommandTests.forEach(test => { - describe(test.description, () => { - const contentScript = function () { - browser.test.onMessage.addListener(async aMsg => { - if (aMsg === "paste") { - let clipboardData = null; - document.addEventListener( - "paste", - e => { - clipboardData = e.clipboardData.getData("text/plain"); - }, - { once: true } - ); - - const execCommandResult = document.execCommand("paste"); - browser.test.sendMessage("result", { - execCommandResult, - clipboardData, - }); - } else if (aMsg === "pasteWithoutResult") { - setTimeout(() => { - document.execCommand("paste"); - }, 0); - } - }); - }; - const extensionData = { - manifest: { - content_scripts: [ - { - js: ["contentscript.js"], - matches: ["https://example.com/*"], - }, - ], - }, - files: { - "contentscript.js": contentScript, - }, - }; - - it("Accepting paste contextmenu", async () => { - info(`Randomized text to avoid overlappings with other tests`); - const clipboardText = await promiseWritingRandomTextToClipboard(); - - info(`Load and start the extension`); - const extension = ExtensionTestUtils.loadExtension(extensionData); - await extension.startup(); - - await BrowserTestUtils.withNewTab( - kContentFileUrl, - async function (aBrowser) { - if (test.setupFn) { - info(`Setup`); - await test.setupFn(aBrowser); - } - - const pasteButtonIsShown = promisePasteButtonIsShown(); - - info(`Trigger execCommand("paste") from extension`); - const pasteCommandResult = promiseExecCommandPasteFromExtension( - aBrowser, - extension - ); - - info(`Wait for paste context menu is shown`); - await pasteButtonIsShown; - - info(`Click paste context menu`); - const pasteButtonIsHidden = promisePasteButtonIsHidden(); - await promiseClickPasteButton(); - await pasteButtonIsHidden; - - const { execCommandResult, clipboardData } = await pasteCommandResult; - ok(execCommandResult, `execCommand("paste") should be succeed`); - is(clipboardData, clipboardText, `Check clipboard data`); - - if (test.additionalCheckFunc) { - info(`Additional checks`); - await test.additionalCheckFunc(aBrowser, clipboardText); - } - } - ); - - info(`Unload extension`); - await extension.unload(); - }); - - it("Dismissing paste contextmenu", async () => { - info(`Randomized text to avoid overlappings with other tests`); - await promiseWritingRandomTextToClipboard(); - - info(`Load and start the extension`); - const extension = ExtensionTestUtils.loadExtension(extensionData); - await extension.startup(); - - await BrowserTestUtils.withNewTab( - kContentFileUrl, - async function (aBrowser) { - if (test.setupFn) { - info(`Setup`); - await test.setupFn(aBrowser); - } - - const pasteButtonIsShown = promisePasteButtonIsShown(); - - info(`Trigger execCommand("paste") from extension`); - const pasteCommandResult = promiseExecCommandPasteFromExtension( - aBrowser, - extension - ); - - info(`Wait for paste context menu is shown`); - await pasteButtonIsShown; - - info(`Dismiss paste context menu`); - const pasteButtonIsHidden = promisePasteButtonIsHidden(); - await promiseDismissPasteButton(); - await pasteButtonIsHidden; - - const { execCommandResult, clipboardData } = await pasteCommandResult; - ok(!execCommandResult, `execCommand("paste") should not be succeed`); - is(clipboardData, null, `Check clipboard data`); - - if (test.additionalCheckFunc) { - info(`Additional checks`); - await test.additionalCheckFunc(aBrowser, ""); - } - } - ); - - info(`Unload extension`); - await extension.unload(); - }); - - it("Tab close", async () => { - info(`Randomized text to avoid overlappings with other tests`); - await promiseWritingRandomTextToClipboard(); - - info(`Load and start the extension`); - const extension = ExtensionTestUtils.loadExtension(extensionData); - await extension.startup(); - - let pasteButtonIsHidden; - await BrowserTestUtils.withNewTab( - kContentFileUrl, - async function (aBrowser) { - if (test.setupFn) { - info(`Setup`); - await test.setupFn(aBrowser); - } - - const pasteButtonIsShown = promisePasteButtonIsShown(); - - info(`Trigger execCommand("paste") from extension`); - execCommandPasteFromExtensionWithoutResult(aBrowser, extension); - - info(`Wait for paste context menu is shown`); - await pasteButtonIsShown; - - pasteButtonIsHidden = promisePasteButtonIsHidden(); - info("Close tab"); - } - ); - - await pasteButtonIsHidden; - - info(`Unload extension`); - await extension.unload(); - }); - - it("Tab switch", async () => { - info(`Randomized text to avoid overlappings with other tests`); - await promiseWritingRandomTextToClipboard(); - - info(`Load and start the extension`); - const extension = ExtensionTestUtils.loadExtension(extensionData); - await extension.startup(); - - await BrowserTestUtils.withNewTab( - kContentFileUrl, - async function (aBrowser) { - if (test.setupFn) { - info(`Setup`); - await test.setupFn(aBrowser); - } - - const pasteButtonIsShown = promisePasteButtonIsShown(); - - info(`Trigger execCommand("paste") from extension`); - execCommandPasteFromExtensionWithoutResult(aBrowser, extension); - - info(`Wait for paste context menu is shown`); - await pasteButtonIsShown; - - info("Switch tab"); - let pasteButtonIsHidden = promisePasteButtonIsHidden(); - let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); - - info(`Wait for paste context menu is hidden`); - await pasteButtonIsHidden; - - BrowserTestUtils.removeTab(tab); - } - ); - - info(`Unload extension`); - await extension.unload(); - }); - - it("Window switch", async () => { - info(`Randomized text to avoid overlappings with other tests`); - await promiseWritingRandomTextToClipboard(); - - info(`Load and start the extension`); - const extension = ExtensionTestUtils.loadExtension(extensionData); - await extension.startup(); - - await BrowserTestUtils.withNewTab( - kContentFileUrl, - async function (aBrowser) { - if (test.setupFn) { - info(`Setup`); - await test.setupFn(aBrowser); - } - - const pasteButtonIsShown = promisePasteButtonIsShown(); - - info(`Trigger execCommand("paste") from extension`); - execCommandPasteFromExtensionWithoutResult(aBrowser, extension); - - info(`Wait for paste context menu is shown`); - await pasteButtonIsShown; - - info("Switch browser window"); - let pasteButtonIsHidden = promisePasteButtonIsHidden(); - let newWin = await BrowserTestUtils.openNewBrowserWindow(); - - info(`Wait for paste context menu is hidden`); - await pasteButtonIsHidden; - - await BrowserTestUtils.closeWindow(newWin); - } - ); - - info(`Unload extension`); - await extension.unload(); - }); - - it("Tab navigate", async () => { - info(`Randomized text to avoid overlappings with other tests`); - await promiseWritingRandomTextToClipboard(); - - info(`Load and start the extension`); - const extension = ExtensionTestUtils.loadExtension(extensionData); - await extension.startup(); - - await BrowserTestUtils.withNewTab( - kContentFileUrl, - async function (aBrowser) { - if (test.setupFn) { - info(`Setup`); - await test.setupFn(aBrowser); - } - - const pasteButtonIsShown = promisePasteButtonIsShown(); - - info(`Trigger execCommand("paste") from extension`); - execCommandPasteFromExtensionWithoutResult(aBrowser, extension); - - info(`Wait for paste context menu is shown`); - await pasteButtonIsShown; - - info("Navigate tab"); - let pasteButtonIsHidden = promisePasteButtonIsHidden(); - aBrowser.loadURI(Services.io.newURI("https://example.com/"), { - triggeringPrincipal: - Services.scriptSecurityManager.getSystemPrincipal(), - }); - - info(`Wait for paste context menu is hidden`); - await pasteButtonIsHidden; - } - ); - - info(`Unload extension`); - await extension.unload(); - }); - - it("Tab reload", async () => { - info(`Randomized text to avoid overlappings with other tests`); - await promiseWritingRandomTextToClipboard(); - - info(`Load and start the extension`); - const extension = ExtensionTestUtils.loadExtension(extensionData); - await extension.startup(); - - await BrowserTestUtils.withNewTab( - kContentFileUrl, - async function (aBrowser) { - if (test.setupFn) { - info(`Setup`); - await test.setupFn(aBrowser); - } - - const pasteButtonIsShown = promisePasteButtonIsShown(); - - info(`Trigger execCommand("paste") from extension`); - execCommandPasteFromExtensionWithoutResult(aBrowser, extension); - - info(`Wait for paste context menu is shown`); - await pasteButtonIsShown; - - info("Reload tab"); - let pasteButtonIsHidden = promisePasteButtonIsHidden(); - await BrowserTestUtils.reloadTab(gBrowser.selectedTab); - - info(`Wait for paste context menu is hidden`); - await pasteButtonIsHidden; - } - ); - - info(`Unload extension`); - await extension.unload(); - }); - }); -}); diff --git a/dom/events/test/clipboard/browser_document_command_paste_contextmenu_suppression.js b/dom/events/test/clipboard/browser_document_command_paste_contextmenu_suppression.js @@ -1,147 +0,0 @@ -/* -*- 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 kContentFileUrl = kBaseUrlForContent + "simple_page_ext.html"; -const kIsMac = navigator.platform.indexOf("Mac") > -1; - -const kSuppressionTests = [ - { - description: "Trigger paste command from keyboard shortcut", - triggerPasteFun: async () => { - await EventUtils.synthesizeAndWaitKey( - "v", - kIsMac ? { accelKey: true } : { ctrlKey: true } - ); - }, - }, - { - description: "Trigger paste command", - triggerPasteFun: async aBrowser => { - await SpecialPowers.spawn(aBrowser, [], async () => { - await SpecialPowers.doCommand(content.window, "cmd_paste"); - }); - }, - }, -]; - -async function promiseClipboardDataFromPasteEvent(aBrowser, aTriggerPasteFun) { - const promisePasteEvent = SpecialPowers.spawn(aBrowser, [], () => { - return new Promise(resolve => { - content.document.addEventListener( - "paste", - e => { - const clipboardData = e.clipboardData.getData("text/plain"); - resolve(clipboardData); - }, - { once: true } - ); - }); - }); - // Enuse the event listener is registered on remote target. - await SpecialPowers.spawn(aBrowser, [], async () => { - await new Promise(resolve => { - SpecialPowers.executeSoon(resolve); - }); - }); - const result = await aTriggerPasteFun(aBrowser); - const clipboardData = await promisePasteEvent; - return { result, clipboardData }; -} - -add_setup(async function () { - await SpecialPowers.pushPrefEnv({ - set: [ - ["test.events.async.enabled", true], - // Disable the paste contextmenu delay to make the test run faster. - ["security.dialog_enable_delay", 0], - ], - }); - - // Paste contextmenu should not be shown during the test. - let listener = function (e) { - if (e.target.getAttribute("id") == kPasteMenuPopupId) { - ok(false, "paste contextmenu should not be shown"); - } - }; - document.addEventListener("popupshown", listener); - - registerCleanupFunction(() => { - document.removeEventListener("popupshown", listener); - }); -}); - -kPasteCommandTests.forEach(test => { - describe(test.description, () => { - it("Same-origin data", async () => { - const clipboardText = "X" + Math.random(); - - await BrowserTestUtils.withNewTab( - kContentFileUrl, - async function (aBrowser) { - if (test.setupFn) { - info(`Setup`); - await test.setupFn(aBrowser); - } - - info(`Write clipboard data`); - await SpecialPowers.spawn(aBrowser, [clipboardText], async text => { - content.document.notifyUserGestureActivation(); - return content.eval(`navigator.clipboard.writeText("${text}");`); - }); - - info(`Trigger execCommand("paste")`); - const { result, clipboardData } = - await promiseClipboardDataFromPasteEvent(aBrowser, async () => { - return SpecialPowers.spawn(aBrowser, [], () => { - content.document.notifyUserGestureActivation(); - return Cu.waiveXrays(content.document).execCommand("paste"); - }); - }); - ok(result, `execCommand("paste") should be succeed`); - is(clipboardData, clipboardText, `Check clipboard data`); - - if (test.additionalCheckFunc) { - info(`Additional checks`); - await test.additionalCheckFunc(aBrowser, clipboardText); - } - } - ); - }); - - describe("cross-origin data", () => { - kSuppressionTests.forEach(subTest => { - it(subTest.description, async () => { - const clipboardText = await promiseWritingRandomTextToClipboard(); - - await BrowserTestUtils.withNewTab( - kContentFileUrl, - async function (aBrowser) { - if (test.setupFn) { - info(`Setup`); - await test.setupFn(aBrowser); - } - - info(`Trigger paste command`); - const { clipboardData } = - await promiseClipboardDataFromPasteEvent( - aBrowser, - subTest.triggerPasteFun - ); - is(clipboardData, clipboardText, `Check clipboard data`); - - if (test.additionalCheckFunc) { - info(`Additional checks`); - await test.additionalCheckFunc(aBrowser, clipboardText); - } - } - ); - }); - }); - }); - }); -}); diff --git a/dom/events/test/clipboard/browser_document_command_paste_contextmenu_suppression_ext.js b/dom/events/test/clipboard/browser_document_command_paste_contextmenu_suppression_ext.js @@ -1,150 +0,0 @@ -/* -*- 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 kContentFileUrl = kBaseUrlForContent + "simple_page_ext.html"; - -async function promiseExecCommandPasteFromExtension(aBrowser, aExtension) { - await SpecialPowers.spawn(aBrowser, [], async () => { - content.document.notifyUserGestureActivation(); - }); - - aExtension.sendMessage("paste"); - return await aExtension.awaitMessage("result"); -} - -add_setup(async function () { - await SpecialPowers.pushPrefEnv({ - set: [ - ["test.events.async.enabled", true], - // Disable the paste contextmenu delay to make the test run faster. - ["security.dialog_enable_delay", 0], - ], - }); - - // Paste contextmenu should not be shown during the test. - let listener = function (e) { - if (e.target.getAttribute("id") == kPasteMenuPopupId) { - ok(false, "paste contextmenu should not be shown"); - } - }; - document.addEventListener("popupshown", listener); - - registerCleanupFunction(() => { - document.removeEventListener("popupshown", listener); - }); -}); - -kPasteCommandTests.forEach(test => { - describe(test.description, () => { - const contentScript = function () { - browser.test.onMessage.addListener(async aMsg => { - if (aMsg === "paste") { - let clipboardData = null; - document.addEventListener( - "paste", - e => { - clipboardData = e.clipboardData.getData("text/plain"); - }, - { once: true } - ); - - const execCommandResult = document.execCommand("paste"); - browser.test.sendMessage("result", { - execCommandResult, - clipboardData, - }); - } - }); - }; - const extensionData = { - manifest: { - content_scripts: [ - { - js: ["contentscript.js"], - matches: ["https://example.com/*"], - }, - ], - }, - files: { - "contentscript.js": contentScript, - }, - }; - - it("Same-origin data", async () => { - const clipboardText = "X" + Math.random(); - - info(`Load and start the extension`); - const extension = ExtensionTestUtils.loadExtension(extensionData); - await extension.startup(); - - await BrowserTestUtils.withNewTab( - kContentFileUrl, - async function (aBrowser) { - if (test.setupFn) { - info(`Setup`); - await test.setupFn(aBrowser); - } - - info(`Write clipboard data`); - await SpecialPowers.spawn(aBrowser, [clipboardText], async text => { - content.document.notifyUserGestureActivation(); - return content.eval(`navigator.clipboard.writeText("${text}");`); - }); - - info(`Trigger execCommand("paste") from extension`); - const { execCommandResult, clipboardData } = - await promiseExecCommandPasteFromExtension(aBrowser, extension); - ok(execCommandResult, `execCommand("paste") should be succeed`); - is(clipboardData, clipboardText, `Check clipboard data`); - - if (test.additionalCheckFunc) { - info(`Additional checks`); - await test.additionalCheckFunc(aBrowser, clipboardText); - } - } - ); - - info(`Unload extension`); - await extension.unload(); - }); - - it("Extension with clipboardRead permission", async () => { - info(`Randomized text to avoid overlappings with other tests`); - const clipboardText = await promiseWritingRandomTextToClipboard(); - - info(`Load and start the extension`); - extensionData.manifest.permissions = ["clipboardRead"]; - const extension = ExtensionTestUtils.loadExtension(extensionData); - await extension.startup(); - - await BrowserTestUtils.withNewTab( - kContentFileUrl, - async function (aBrowser) { - if (test.setupFn) { - info(`Setup`); - await test.setupFn(aBrowser); - } - - info(`Trigger execCommand("paste") from extension`); - const { execCommandResult, clipboardData } = - await promiseExecCommandPasteFromExtension(aBrowser, extension); - ok(execCommandResult, `execCommand("paste") should be succeed`); - is(clipboardData, clipboardText, `Check clipboard data`); - - if (test.additionalCheckFunc) { - info(`Additional checks`); - await test.additionalCheckFunc(aBrowser, clipboardText); - } - } - ); - - info(`Unload extension`); - await extension.unload(); - }); - }); -}); diff --git a/dom/events/test/clipboard/head.js b/dom/events/test/clipboard/head.js @@ -13,43 +13,6 @@ const kBaseUrlForContent = getRootDirectory(gTestPath).replace( "https://example.com" ); -const kPasteCommandTests = [ - { description: "Test paste command without editing" }, - { - description: "Test paste command on <textarea>", - setupFn: aBrowser => { - return SpecialPowers.spawn(aBrowser, [], () => { - const textarea = content.document.createElement("textarea"); - content.document.body.appendChild(textarea); - textarea.focus(); - }); - }, - additionalCheckFunc: (aBrowser, aClipboardData) => { - return SpecialPowers.spawn(aBrowser, [aClipboardData], aClipboardData => { - const textarea = content.document.querySelector("textarea"); - is(textarea.value, aClipboardData, "check <textarea> value"); - }); - }, - }, - { - description: "Test paste command on <div contenteditable=true>", - setupFn: aBrowser => { - return SpecialPowers.spawn(aBrowser, [], () => { - const div = content.document.createElement("div"); - div.setAttribute("contenteditable", "true"); - content.document.body.appendChild(div); - div.focus(); - }); - }, - additionalCheckFunc: (aBrowser, aClipboardData) => { - return SpecialPowers.spawn(aBrowser, [aClipboardData], aClipboardData => { - const div = content.document.querySelector("div"); - is(div.innerText, aClipboardData, "check contenteditable innerText"); - }); - }, - }, -]; - Services.scriptloader.loadSubScript( "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js", this @@ -87,12 +50,10 @@ function promisePasteButtonIsShown() { ok(true, "Witnessed 'popupshown' event for 'Paste' button."); const pasteButton = document.getElementById(kPasteMenuItemId); - if (Services.prefs.getIntPref("security.dialog_enable_delay") > 0) { - ok( - pasteButton.disabled, - "Paste button should be shown with disabled by default" - ); - } + ok( + pasteButton.disabled, + "Paste button should be shown with disabled by default" + ); await BrowserTestUtils.waitForMutationCondition( pasteButton, { attributeFilter: ["disabled"] }, diff --git a/dom/locales/en-US/chrome/dom/dom.properties b/dom/locales/en-US/chrome/dom/dom.properties @@ -221,10 +221,7 @@ ServiceWorkerPostMessageStorageError=The ServiceWorker for scope ‘%S’ failed ServiceWorkerGraceTimeoutTermination=Terminating ServiceWorker for scope ‘%1$S’ with pending waitUntil/respondWith promises because of grace timeout. # LOCALIZATION NOTE (ServiceWorkerNoFetchHandler): Do not translate "Fetch". ServiceWorkerNoFetchHandler=Fetch event handlers must be added during the worker script’s initial evaluation. -# LOCALIZATION NOTE: Do not translate "document.execCommand(‘cut’/‘copy’)". ExecCommandCutCopyDeniedNotInputDriven=document.execCommand(‘cut’/‘copy’) was denied because it was not called from inside a short running user-generated event handler. -# LOCALIZATION NOTE: Do not translate "document.execCommand(‘paste’)". -ExecCommandPasteDeniedNotInputDriven=document.execCommand(‘paste’) was denied because it was not called from inside a short running user-generated event handler. ManifestIdIsInvalid=The id member did not resolve to a valid URL. ManifestIdNotSameOrigin=The id member must have the same origin as the start_url member. ManifestShouldBeObject=Manifest should be an object. diff --git a/dom/tests/mochitest/general/test_bug1161721.html b/dom/tests/mochitest/general/test_bug1161721.html @@ -19,19 +19,13 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1161721 <pre id="test"> <script type="application/javascript"> - add_task(async function test() { - await SpecialPowers.pushPrefEnv({ - set: [["dom.execCommand.paste.enabled", false]], - }); + ok(!document.queryCommandSupported("paste"), "Paste isn't supported in non-privilged JavaScript"); + ok(document.queryCommandSupported("copy"), "Copy is supported in non-privilged JavaScript"); + ok(document.queryCommandSupported("cut"), "Cut is supported in non-privilged JavaScript"); - ok(!document.queryCommandSupported("paste"), "Paste isn't supported in non-privileged JavaScript"); - ok(document.queryCommandSupported("copy"), "Copy is supported in non-privileged JavaScript"); - ok(document.queryCommandSupported("cut"), "Cut is supported in non-privileged JavaScript"); - - ok(SpecialPowers.wrap(document).queryCommandSupported("paste"), "Paste is supported in privileged JavaScript"); - ok(SpecialPowers.wrap(document).queryCommandSupported("copy"), "Copy is supported in privileged JavaScript"); - ok(SpecialPowers.wrap(document).queryCommandSupported("cut"), "Cut is supported in privileged JavaScript"); - }); + ok(SpecialPowers.wrap(document).queryCommandSupported("paste"), "Paste is supported in privilged JavaScript"); + ok(SpecialPowers.wrap(document).queryCommandSupported("copy"), "Copy is supported in privilged JavaScript"); + ok(SpecialPowers.wrap(document).queryCommandSupported("cut"), "Cut is supported in privilged JavaScript"); </script> </pre> </body> diff --git a/editor/libeditor/EditorBase.cpp b/editor/libeditor/EditorBase.cpp @@ -1928,6 +1928,7 @@ nsresult EditorBase::PasteAsAction(nsIClipboard::ClipboardType aClipboardType, // This method is not set up to pass back the new aDataTransfer // if it changes. If we need this in the future, we can change // aDataTransfer to be a RefPtr<DataTransfer>*. + MOZ_ASSERT(!aDataTransfer); AutoTrackDataTransferForPaste trackDataTransfer(*this, dataTransfer); ret = DispatchClipboardEventAndUpdateClipboard( diff --git a/editor/libeditor/EditorCommands.cpp b/editor/libeditor/EditorCommands.cpp @@ -11,7 +11,6 @@ #include "mozilla/HTMLEditor.h" #include "mozilla/Maybe.h" #include "mozilla/MozPromise.h" // for mozilla::detail::Any -#include "mozilla/dom/DataTransfer.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/Selection.h" #include "nsCommandParams.h" @@ -433,34 +432,9 @@ bool PasteCommand::IsCommandEnabled(Command aCommand, nsresult PasteCommand::DoCommand(Command aCommand, EditorBase& aEditorBase, nsIPrincipal* aPrincipal) const { - RefPtr<DataTransfer> dataTransfer; - nsCOMPtr<nsIPrincipal> subjectPrincipal = - aPrincipal ? aPrincipal - : nsContentUtils::SubjectPrincipalOrSystemIfNativeCaller(); - MOZ_ASSERT(subjectPrincipal); - - // If we don't need to get user confirmation for clipboard access, we could - // just let EditorBase::PasteAsAction() to create DataTransfer instance - // synchronously for paste event. Otherwise, we need to spin the event loop to - // wait for the clipboard paste contextmenu to be shown and get user - // confirmation which are all handled in parent process before sending the - // paste event. - if (!nsContentUtils::PrincipalHasPermission(*subjectPrincipal, - nsGkAtoms::clipboardRead)) { - MOZ_DIAGNOSTIC_ASSERT(StaticPrefs::dom_execCommand_paste_enabled(), - "How did we get here?"); - // This will spin the event loop. - nsCOMPtr<nsPIDOMWindowOuter> window = aEditorBase.GetWindow(); - dataTransfer = DataTransfer::WaitForClipboardDataSnapshotAndCreate( - window, subjectPrincipal); - if (!dataTransfer) { - return NS_SUCCESS_DOM_NO_OPERATION; - } - } - nsresult rv = aEditorBase.PasteAsAction(nsIClipboard::kGlobalClipboard, EditorBase::DispatchPasteEvent::Yes, - dataTransfer, aPrincipal); + nullptr, aPrincipal); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::PasteAsAction(nsIClipboard::" "kGlobalClipboard, DispatchPasteEvent::Yes) failed"); diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/currentStatus.js b/editor/libeditor/tests/browserscope/lib/richtext2/currentStatus.js @@ -284,9 +284,9 @@ const knownFailures = { "Q-Proposed-INCREASEFONTSIZE_TEXT-1-dM": true, "Q-Proposed-INCREASEFONTSIZE_TEXT-1-body": true, "Q-Proposed-INCREASEFONTSIZE_TEXT-1-div": true, - "Q-Proposed-PASTE_TEXT-1-dM": !SpecialPowers.getBoolPref("dom.execCommand.paste.enabled", false), - "Q-Proposed-PASTE_TEXT-1-body": !SpecialPowers.getBoolPref("dom.execCommand.paste.enabled", false), - "Q-Proposed-PASTE_TEXT-1-div": !SpecialPowers.getBoolPref("dom.execCommand.paste.enabled", false), + "Q-Proposed-PASTE_TEXT-1-dM": true, + "Q-Proposed-PASTE_TEXT-1-body": true, + "Q-Proposed-PASTE_TEXT-1-div": true, "Q-Proposed-UNBOOKMARK_TEXT-1-dM": true, "Q-Proposed-UNBOOKMARK_TEXT-1-body": true, "Q-Proposed-UNBOOKMARK_TEXT-1-div": true, diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml @@ -3021,12 +3021,6 @@ value: true mirror: always -# Whether to expose execCommand("paste") to web content. -- name: dom.execCommand.paste.enabled - type: bool - value: true - mirror: always - # Whether to expose test interfaces of various sorts - name: dom.expose_test_interfaces type: bool diff --git a/testing/web-platform/meta/editing/other/exec-command-with-text-editor.tentative.html.ini b/testing/web-platform/meta/editing/other/exec-command-with-text-editor.tentative.html.ini @@ -13,6 +13,12 @@ [In <input type="text">, execCommand("copy", false, null), abc[\]d): execCommand() should return false] expected: FAIL + [In <input type="text">, execCommand("paste", false, null), a[\]c): The command should be supported] + expected: FAIL + + [In <input type="text">, execCommand("paste", false, null), a[\]c): The command should be enabled] + expected: FAIL + [In <input type="text">, execCommand("undo", false, null), a[b\]c): The command should be enabled] expected: FAIL @@ -58,6 +64,12 @@ [In <input type="text"> in contenteditable, execCommand("copy", false, null), abc[\]d): execCommand() should return false] expected: FAIL + [In <input type="text"> in contenteditable, execCommand("paste", false, null), a[\]c): The command should be supported] + expected: FAIL + + [In <input type="text"> in contenteditable, execCommand("paste", false, null), a[\]c): The command should be enabled] + expected: FAIL + [In <input type="text"> in contenteditable, execCommand("undo", false, null), a[b\]c): The command should be enabled] expected: FAIL @@ -103,6 +115,12 @@ [In <input type="password">, execCommand("copy", false, null), a[bc\]d): execCommand() should return true] expected: FAIL + [In <input type="password">, execCommand("paste", false, null), a[\]c): The command should be supported] + expected: FAIL + + [In <input type="password">, execCommand("paste", false, null), a[\]c): The command should be enabled] + expected: FAIL + [In <input type="password">, execCommand("undo", false, null), a[b\]c): The command should be enabled] expected: FAIL @@ -157,6 +175,12 @@ [In <input type="password"> in contenteditable, execCommand("copy", false, null), a[bc\]d): execCommand() should return true] expected: FAIL + [In <input type="password"> in contenteditable, execCommand("paste", false, null), a[\]c): The command should be supported] + expected: FAIL + + [In <input type="password"> in contenteditable, execCommand("paste", false, null), a[\]c): The command should be enabled] + expected: FAIL + [In <input type="password"> in contenteditable, execCommand("undo", false, null), a[b\]c): The command should be enabled] expected: FAIL @@ -191,6 +215,12 @@ [In <textarea>, execCommand("copy", false, null), abc[\]d): execCommand() should return false] expected: FAIL + [In <textarea>, execCommand("paste", false, null), a[\]c): The command should be supported] + expected: FAIL + + [In <textarea>, execCommand("paste", false, null), a[\]c): The command should be enabled] + expected: FAIL + [In <textarea>, execCommand("undo", false, null), a[b\]c): The command should be enabled] expected: FAIL @@ -239,6 +269,12 @@ [In <textarea> in contenteditable, execCommand("copy", false, null), abc[\]d): execCommand() should return false] expected: FAIL + [In <textarea> in contenteditable, execCommand("paste", false, null), a[\]c): The command should be supported] + expected: FAIL + + [In <textarea> in contenteditable, execCommand("paste", false, null), a[\]c): The command should be enabled] + expected: FAIL + [In <textarea> in contenteditable, execCommand("redo", false, null), a[b\]c): The command should be enabled] expected: FAIL diff --git a/testing/web-platform/meta/editing/other/exec-command-without-editable-element.tentative.html.ini b/testing/web-platform/meta/editing/other/exec-command-without-editable-element.tentative.html.ini @@ -23,9 +23,3 @@ [ParentDocument.execCommand(copy, false, null) with a\[b\]c: checking event on executed document] expected: FAIL - [ChildDocument.execCommand(paste, false, null) with a[b\]c: checking event on executed document] - expected: FAIL - - [ParentDocument.execCommand(paste, false, null) with a[b\]c: checking event on executed document] - expected: FAIL - diff --git a/testing/web-platform/tests/editing/other/exec-command-with-text-editor.tentative.html b/testing/web-platform/tests/editing/other/exec-command-with-text-editor.tentative.html @@ -137,7 +137,7 @@ async function runTest(aTarget, aDescription) { beforeinputExpected: null, inputExpected: null, }, {command: "paste", param: null, - value: "a[]c", expectedValue: "abc[]c", + value: "a[]c", expectedValue: "a[bc]c", expectedExecCommandResult: true, expectedCommandSupported: true, expectedCommandEnabled: true,