Bug 1579059 - Update nsContextMenu to fix selection text properties; r=mkmelin
authorGeoff Lankow <geoff@darktrojan.net>
Fri, 22 Nov 2019 22:10:57 +1300
changeset 28307 247abc27cb5243b3cc136046593c2bb5a62dad64
parent 28306 0211c8540f8473d9e538d27e39f4a922cbf033a3
child 28308 c272f21f60cef07fc06efcbcabc5880014c90bce
push id16759
push usergeoff@darktrojan.net
push dateTue, 03 Dec 2019 22:20:21 +0000
treeherdercomm-central@247abc27cb52 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmkmelin
bugs1579059
Bug 1579059 - Update nsContextMenu to fix selection text properties; r=mkmelin
mail/base/content/nsContextMenu.js
mail/components/extensions/test/browser/browser.ini
mail/components/extensions/test/browser/browser_ext_menus.js
mail/components/extensions/test/browser/data/content.html
--- a/mail/base/content/nsContextMenu.js
+++ b/mail/base/content/nsContextMenu.js
@@ -32,17 +32,17 @@ XPCOMUtils.defineLazyGetter(this, "PageM
 });
 
 var gSpellChecker = new InlineSpellChecker();
 
 function nsContextMenu(aXulMenu, aIsShift) {
   this.target = null;
   this.menu = null;
   this.onTextInput = false;
-  this.onEditableArea = false;
+  this.onEditable = false;
   this.onImage = false;
   this.onLoadedImage = false;
   this.onCanvas = false;
   this.onVideo = false;
   this.onAudio = false;
   this.onPlayableMedia = false;
   this.onLink = false;
   this.onMailtoLink = false;
@@ -82,17 +82,20 @@ nsContextMenu.prototype = {
     this.setTarget(document.popupNode);
     this.setMessageTargets(document.popupNode);
 
     if (!this.inThreadPane && this.messagepaneIsBlank) {
       this.shouldDisplay = false;
       return;
     }
 
-    this.isContentSelected = this.isContentSelection();
+    this.selectionInfo = BrowserUtils.getSelectionDetails(window);
+    this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed;
+    this.textSelected = this.selectionInfo.text;
+    this.isTextSelected = !!this.textSelected.length;
 
     this.hasPageMenu = false;
     if (!aIsShift) {
       let menuObject = PageMenuParent.maybeBuild(this.target);
       this.hasPageMenu = PageMenuParent.addToPopup(menuObject, null, aPopup);
 
       let subject = {
         menu: aPopup,
@@ -103,22 +106,23 @@ nsContextMenu.prototype = {
         inFrame: this.inFrame,
         isTextSelected: this.isTextSelected,
         onTextInput: this.onTextInput,
         onLink: this.onLink,
         onImage: this.onImage,
         onVideo: this.onVideo,
         onAudio: this.onAudio,
         onCanvas: this.onCanvas,
-        onEditableArea: this.onEditableArea,
+        onEditable: this.onEditable,
         srcUrl: this.mediaURL,
         pageUrl: this.browser ? this.browser.currentURI.spec : undefined,
+        linkText: this.onLink ? this.linkText() : undefined,
         linkUrl: this.linkURL,
         selectionText: this.isTextSelected
-          ? this.selectionInfo.text
+          ? this.selectionInfo.fullText
           : undefined,
       };
       if (document.popupNode.closest("tree") == gFolderDisplay.tree) {
         subject.displayedFolder = gFolderDisplay.view.displayedFolder;
         subject.selectedMessages = gFolderDisplay.selectedMessages;
       }
       subject.wrappedJSObject = subject;
 
@@ -153,20 +157,17 @@ nsContextMenu.prototype = {
   },
   initPageMenuSeparator() {
     this.showItem("page-menu-separator", this.hasPageMenu);
   },
   initSpellingItems() {
     let canSpell = gSpellChecker.canSpellCheck;
     let onMisspelling = gSpellChecker.overMisspelling;
     this.showItem("mailContext-spell-check-enabled", canSpell);
-    this.showItem(
-      "mailContext-spell-separator",
-      canSpell || this.onEditableArea
-    );
+    this.showItem("mailContext-spell-separator", canSpell || this.onEditable);
     if (canSpell) {
       document
         .getElementById("mailContext-spell-check-enabled")
         .setAttribute("checked", gSpellChecker.enabled);
     }
 
     this.showItem("mailContext-spell-add-to-dictionary", onMisspelling);
 
@@ -192,17 +193,17 @@ nsContextMenu.prototype = {
       let dictMenu = document.getElementById(
         "mailContext-spell-dictionaries-menu"
       );
       let dictSep = document.getElementById(
         "mailContext-spell-language-separator"
       );
       gSpellChecker.addDictionaryListToMenu(dictMenu, dictSep);
       this.showItem("mailContext-spell-add-dictionaries-main", false);
-    } else if (this.onEditableArea) {
+    } else if (this.onEditable) {
       // when there is no spellchecker but we might be able to spellcheck
       // add the add to dictionaries item. This will ensure that people
       // with no dictionaries will be able to download them
       this.showItem("mailContext-spell-add-dictionaries-main", true);
     } else {
       this.showItem("mailContext-spell-add-dictionaries-main", false);
     }
   },
@@ -600,33 +601,33 @@ nsContextMenu.prototype = {
         this.shouldDisplay = false;
       }
       return;
     }
     this.onImage = false;
     this.onLoadedImage = false;
     this.onMetaDataItem = false;
     this.onTextInput = false;
-    this.onEditableArea = false;
+    this.onEditable = false;
     this.imageURL = "";
     this.onLink = false;
     this.onVideo = false;
     this.onAudio = false;
     this.mediaURL = "";
     this.linkURL = "";
     this.linkURI = null;
     this.linkProtocol = null;
     this.onMathML = false;
 
     this.target = aNode;
 
     // Set up early the right flags for editable / not editable.
     let editFlags = SpellCheckHelper.isEditable(this.target, window);
     this.onTextInput = (editFlags & SpellCheckHelper.TEXTINPUT) !== 0;
-    this.onEditableArea = (editFlags & SpellCheckHelper.EDITABLE) !== 0;
+    this.onEditable = (editFlags & SpellCheckHelper.EDITABLE) !== 0;
 
     // First, do checks for nodes that never have children.
     if (this.target.nodeType == Node.ELEMENT_NODE) {
       if (
         this.target instanceof Ci.nsIImageLoadingContent &&
         this.target.currentURI
       ) {
         this.onImage = true;
@@ -642,17 +643,17 @@ nsContextMenu.prototype = {
         this.imageURL = this.target.currentURI.spec;
       } else if (this.target instanceof HTMLInputElement) {
         this.onTextInput = this.isTargetATextBox(this.target);
       } else if (
         editFlags &
         (SpellCheckHelper.INPUT | SpellCheckHelper.TEXTAREA)
       ) {
         if (!this.target.readOnly) {
-          this.onEditableArea = true;
+          this.onEditable = true;
           gSpellChecker.init(this.target.editor);
           gSpellChecker.initFromEvent(
             document.popupRangeParent,
             document.popupRangeOffset
           );
         }
       } else if (editFlags & SpellCheckHelper.CONTENTEDITABLE) {
         let targetWin = this.target.ownerGlobal;
--- a/mail/components/extensions/test/browser/browser.ini
+++ b/mail/components/extensions/test/browser/browser.ini
@@ -17,13 +17,14 @@ tags = webextensions
 [browser_ext_browserAction.js]
 [browser_ext_commands_execute_browser_action.js]
 [browser_ext_commands_getAll.js]
 [browser_ext_commands_onCommand.js]
 [browser_ext_commands_update.js]
 [browser_ext_composeAction.js]
 [browser_ext_mailTabs.js]
 [browser_ext_menus.js]
+support-files = data/content.html
 [browser_ext_messageDisplay.js]
 [browser_ext_messageDisplayAction.js]
 [browser_ext_quickFilter.js]
 [browser_ext_windows.js]
 [browser_ext_windows_types.js]
--- a/mail/components/extensions/test/browser/browser_ext_menus.js
+++ b/mail/components/extensions/test/browser/browser_ext_menus.js
@@ -5,27 +5,35 @@
 let gAccount, gFolders;
 
 const { mailTestUtils } = ChromeUtils.import(
   "resource://testing-common/mailnews/mailTestUtils.js"
 );
 
 const treeClick = mailTestUtils.treeClick.bind(null, EventUtils, window);
 
-async function checkEvent(extension, menuIds, contexts) {
-  let [event, tab] = await extension.awaitMessage("onShown");
-  is(event.menuIds.length, menuIds.length);
+async function checkShownEvent(extension, menuIds, contexts) {
+  let [info, tab] = await extension.awaitMessage("onShown");
+  is(info.menuIds.length, menuIds.length);
   for (let i = 0; i < menuIds.length; i++) {
-    is(event.menuIds[i], menuIds[i]);
+    is(info.menuIds[i], menuIds[i]);
+  }
+  is(info.contexts.length, contexts.length);
+  for (let i = 0; i < contexts.length; i++) {
+    is(info.contexts[i], contexts[i]);
   }
-  is(event.contexts.length, contexts.length);
-  for (let i = 0; i < contexts.length; i++) {
-    is(event.contexts[i], contexts[i]);
+  return [info, tab];
+}
+
+async function checkClickedEvent(extension, properties) {
+  let [info, tab] = await extension.awaitMessage("onClicked");
+  for (let [name, value] of Object.entries(properties)) {
+    is(info[name], value);
   }
-  return [event, tab];
+  return [info, tab];
 }
 
 function createExtension() {
   return ExtensionTestUtils.loadExtension({
     async background() {
       for (let context of [
         "audio",
         "browser_action",
@@ -46,29 +54,35 @@ function createExtension() {
           title: context,
           contexts: [context],
         });
       }
 
       browser.menus.onShown.addListener((...args) => {
         browser.test.sendMessage("onShown", args);
       });
+
+      browser.menus.onClicked.addListener((...args) => {
+        browser.test.sendMessage("onClicked", args);
+      });
     },
     manifest: {
       applications: {
         gecko: {
           id: "test1@mochi.test",
         },
       },
       permissions: ["accountsRead", "menus", "messagesRead"],
     },
   });
 }
 
 add_task(async function set_up() {
+  await Services.search.init();
+
   gAccount = createAccount();
   gFolders = [...gAccount.incomingServer.rootFolder.subFolders];
   createMessages(gFolders[0], 10);
 });
 
 add_task(async function test_folder_pane() {
   let extension = createExtension();
   await extension.startup();
@@ -77,25 +91,25 @@ add_task(async function test_folder_pane
   treeClick(folderTree, 1, 0, {});
   treeClick(folderTree, 1, 0, { type: "contextmenu" });
 
   let menu = document.getElementById("folderPaneContext");
   await BrowserTestUtils.waitForEvent(menu, "popupshown");
   ok(menu.querySelector("#test1_mochi_test-menuitem-_folder_pane"));
   menu.hidePopup();
 
-  let [event] = await checkEvent(
+  let [info] = await checkShownEvent(
     extension,
     ["folder_pane"],
     ["folder_pane", "all"]
   );
-  is(event.selectedFolder.accountId, gAccount.key);
-  is(event.selectedFolder.path, "/Trash");
-  ok(!event.displayedFolder);
-  ok(!event.selectedMessages);
+  is(info.selectedFolder.accountId, gAccount.key);
+  is(info.selectedFolder.path, "/Trash");
+  ok(!info.displayedFolder);
+  ok(!info.selectedMessages);
 
   await extension.unload();
 });
 
 add_task(async function test_thread_pane() {
   let extension = createExtension();
   await extension.startup();
 
@@ -103,45 +117,45 @@ add_task(async function test_thread_pane
   treeClick(threadTree, 1, 1, {});
   treeClick(threadTree, 1, 1, { type: "contextmenu" });
 
   let menu = document.getElementById("mailContext");
   await BrowserTestUtils.waitForEvent(menu, "popupshown");
   ok(menu.querySelector("#test1_mochi_test-menuitem-_message_list"));
   menu.hidePopup();
 
-  let [event] = await checkEvent(
+  let [info] = await checkShownEvent(
     extension,
     ["message_list"],
     ["message_list", "all"]
   );
-  is(event.displayedFolder.accountId, gAccount.key);
-  is(event.displayedFolder.path, "/Trash");
-  is(event.selectedMessages.cursor, null);
-  ok(!event.selectedFolder);
+  is(info.displayedFolder.accountId, gAccount.key);
+  is(info.displayedFolder.path, "/Trash");
+  is(info.selectedMessages.cursor, null);
+  ok(!info.selectedFolder);
 
   await extension.unload();
 });
 
 add_task(async function test_tab() {
   async function checkTabEvent(index, active, mailTab) {
     EventUtils.synthesizeMouseAtCenter(
       tabs[index],
       { type: "contextmenu" },
       window
     );
 
     await BrowserTestUtils.waitForEvent(menu, "popupshown");
     ok(menu.querySelector("#test1_mochi_test-menuitem-_tab"));
     menu.hidePopup();
 
-    let [event, tab] = await checkEvent(extension, ["tab"], ["tab"]);
-    ok(!event.selectedFolder);
-    ok(!event.displayedFolder);
-    ok(!event.selectedMessages);
+    let [info, tab] = await checkShownEvent(extension, ["tab"], ["tab"]);
+    ok(!info.selectedFolder);
+    ok(!info.displayedFolder);
+    ok(!info.selectedMessages);
     is(tab.active, active);
     is(tab.index, index);
     is(tab.mailTab, mailTab);
   }
 
   let extension = createExtension();
   await extension.startup();
 
@@ -157,8 +171,88 @@ add_task(async function test_tab() {
   await checkTabEvent(1, false, false);
   await checkTabEvent(2, false, false);
   await checkTabEvent(3, true, true);
 
   await extension.unload();
 
   tabmail.closeOtherTabs(tabmail.tabModes.folder.tabs[0]);
 });
+
+add_task(async function test_selection() {
+  let extension = createExtension();
+  await extension.startup();
+
+  if (window.IsMessagePaneCollapsed()) {
+    window.MsgToggleMessagePane();
+  }
+
+  let oldPref = Services.prefs.getStringPref("mailnews.start_page.url");
+  Services.prefs.setStringPref(
+    "mailnews.start_page.url",
+    "http://mochi.test:8888/browser/comm/mail/components/extensions/test/browser/data/content.html"
+  );
+
+  let tabmail = document.getElementById("tabmail");
+  let menu = document.getElementById("mailContext");
+
+  window.loadStartPage();
+  await BrowserTestUtils.waitForEvent(
+    tabmail.selectedBrowser,
+    "DOMContentLoaded"
+  );
+
+  let { contentDocument, contentWindow } = tabmail.selectedBrowser;
+
+  let text = contentDocument.querySelector("p");
+  EventUtils.synthesizeMouseAtCenter(text, {}, contentWindow);
+  contentWindow.getSelection().selectAllChildren(text);
+  EventUtils.synthesizeMouseAtCenter(
+    text,
+    { type: "contextmenu" },
+    contentWindow
+  );
+
+  await BrowserTestUtils.waitForEvent(menu, "popupshown");
+  ok(menu.querySelector("#test1_mochi_test-menuitem-_selection"));
+  await checkShownEvent(extension, ["selection"], ["selection", "all"]);
+
+  EventUtils.synthesizeMouseAtCenter(
+    menu.querySelector("#test1_mochi_test-menuitem-_selection"),
+    {}
+  );
+  await checkClickedEvent(extension, {
+    selectionText: "This is text.",
+  });
+
+  let link = contentDocument.querySelector("a");
+  EventUtils.synthesizeMouseAtCenter(
+    link,
+    { type: "contextmenu" },
+    contentWindow
+  );
+
+  await BrowserTestUtils.waitForEvent(menu, "popupshown");
+  ok(menu.querySelector("#test1_mochi_test-menuitem-_selection"));
+  await checkShownEvent(
+    extension,
+    ["link", "selection"],
+    ["link", "selection", "all"]
+  );
+
+  EventUtils.synthesizeMouseAtCenter(
+    menu.querySelector(`menu[id^="test1_mochi_test-menuitem"]`),
+    {}
+  );
+  await BrowserTestUtils.waitForEvent(menu, "popupshown");
+  EventUtils.synthesizeMouseAtCenter(
+    menu.querySelector("#test1_mochi_test-menuitem-_link"),
+    {}
+  );
+  await checkClickedEvent(extension, {
+    linkUrl: "http://mochi.test:8888/",
+    linkText: "This is a link with text.",
+    selectionText: "This is text.",
+  });
+
+  await extension.unload();
+  Services.prefs.setStringPref("mailnews.start_page.url", oldPref);
+});
new file mode 100644
--- /dev/null
+++ b/mail/components/extensions/test/browser/data/content.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8"/>
+  <title></title>
+</head>
+<body>
+  <p>This is text.</p>
+  <p><a href="http://mochi.test:8888/">This is a link with text.</a></p>
+</body>
+</html>