Bug 1419195: Show items from WebExtensions in Places Library context menu r=mixedpuppy
authorPeter Simonyi <pts@petersimonyi.ca>
Thu, 17 Jan 2019 09:49:38 +0000
changeset 511354 8b35181c3ccc747106037b61a9b52adfbba5ed60
parent 511353 6b7a9a7afa56c0cc0f2399bf378cb6d12b73b86a
child 511355 bb0949deaa67c9e36d8a931aba8dd7f2cbf067c0
push id10547
push userffxbld-merge
push dateMon, 21 Jan 2019 13:03:58 +0000
treeherdermozilla-beta@24ec1916bffe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy
bugs1419195
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1419195: Show items from WebExtensions in Places Library context menu r=mixedpuppy Depends on D16413 Differential Revision: https://phabricator.services.mozilla.com/D16414
browser/components/extensions/parent/ext-menus.js
browser/components/extensions/test/browser/browser_ext_contextMenus.js
--- a/browser/components/extensions/parent/ext-menus.js
+++ b/browser/components/extensions/parent/ext-menus.js
@@ -1,14 +1,17 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
+ChromeUtils.defineModuleGetter(this, "Bookmarks",
+                               "resource://gre/modules/Bookmarks.jsm");
+
 var {
   DefaultMap,
   ExtensionError,
 } = ExtensionUtils;
 
 ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm");
 
 var {
@@ -858,35 +861,105 @@ MenuItem.prototype = {
         return false;
       }
     }
 
     return true;
   },
 };
 
+// windowTracker only looks as browser windows, but we're also interested in
+// the Library window.  Helper for menuTracker below.
+const libraryTracker = {
+  libraryWindowType: "Places:Organizer",
+
+  isLibraryWindow(window) {
+    let winType = window.document.documentElement.getAttribute("windowtype");
+    return winType === this.libraryWindowType;
+  },
+
+  init(listener) {
+    this._listener = listener;
+    Services.ww.registerNotification(this);
+
+    // See WindowTrackerBase#*browserWindows in ext-tabs-base.js for why we
+    // can't use the enumerator's windowtype filter.
+    for (let window of Services.wm.getEnumerator("")) {
+      if (window.document.readyState === "complete") {
+        if (this.isLibraryWindow(window)) {
+          this.notify(window);
+        }
+      } else {
+        window.addEventListener("load", this, {once: true});
+      }
+    }
+  },
+
+  // cleanupWindow is called on any library window that's open.
+  uninit(cleanupWindow) {
+    Services.ww.unregisterNotification(this);
+
+    for (let window of Services.wm.getEnumerator("")) {
+      window.removeEventListener("load", this);
+      try {
+        if (this.isLibraryWindow(window)) {
+          cleanupWindow(window);
+        }
+      } catch (e) {
+        Cu.reportError(e);
+      }
+    }
+  },
+
+  // Gets notifications from Services.ww.registerNotification.
+  // Defer actually doing anything until the window's loaded, though.
+  observe(window, topic) {
+    if (topic === "domwindowopened") {
+      window.addEventListener("load", this, {once: true});
+    }
+  },
+
+  // Gets the load event for new windows(registered in observe()).
+  handleEvent(event) {
+    let window = event.target.defaultView;
+    if (this.isLibraryWindow(window)) {
+      this.notify(window);
+    }
+  },
+
+  notify(window) {
+    try {
+      this._listener.call(null, window);
+    } catch (e) {
+      Cu.reportError(e);
+    }
+  },
+};
+
 // While any extensions are active, this Tracker registers to observe/listen
 // for menu events from both Tools and context menus, both content and chrome.
 const menuTracker = {
   menuIds: ["placesContext", "menu_ToolsPopup", "tabContextMenu"],
 
   register() {
     Services.obs.addObserver(this, "on-build-contextmenu");
     for (const window of windowTracker.browserWindows()) {
       this.onWindowOpen(window);
     }
     windowTracker.addOpenListener(this.onWindowOpen);
+    libraryTracker.init(this.onLibraryOpen);
   },
 
   unregister() {
     Services.obs.removeObserver(this, "on-build-contextmenu");
     for (const window of windowTracker.browserWindows()) {
       this.cleanupWindow(window);
     }
     windowTracker.removeOpenListener(this.onWindowOpen);
+    libraryTracker.uninit(this.cleanupLibrary);
   },
 
   observe(subject, topic, data) {
     subject = subject.wrappedJSObject;
     gMenuBuilder.build(subject);
   },
 
   onWindowOpen(window) {
@@ -932,16 +1005,26 @@ const menuTracker = {
         sidebarBrowser.addEventListener("load", menuTracker.onSidebarShown, {once: true});
         return;
       }
       const menu = sidebarBrowser.contentDocument.getElementById("placesContext");
       menu.addEventListener("popupshowing", menuTracker.onBookmarksContextMenu);
     }
   },
 
+  onLibraryOpen(window) {
+    const menu = window.document.getElementById("placesContext");
+    menu.addEventListener("popupshowing", menuTracker.onBookmarksContextMenu);
+  },
+
+  cleanupLibrary(window) {
+    const menu = window.document.getElementById("placesContext");
+    menu.removeEventListener("popupshowing", menuTracker.onBookmarksContextMenu);
+  },
+
   handleEvent(event) {
     const menu = event.target;
 
     if (menu.id === "placesContext") {
       const trigger = menu.triggerNode;
       if (!trigger._placesNode) {
         return;
       }
@@ -966,16 +1049,20 @@ const menuTracker = {
   },
 
   onBookmarksContextMenu(event) {
     const menu = event.target;
     const tree = menu.triggerNode.parentElement;
     const cell = tree.getCellAt(event.x, event.y);
     const node = tree.view.nodeForTreeIndex(cell.row);
 
+    if (!node.bookmarkGuid || Bookmarks.isVirtualRootItem(node.bookmarkGuid)) {
+      return;
+    }
+
     gMenuBuilder.build({
       menu,
       bookmarkId: node.bookmarkGuid,
       onBookmark: true,
     });
   },
 };
 
--- a/browser/components/extensions/test/browser/browser_ext_contextMenus.js
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus.js
@@ -1,16 +1,18 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/browser/components/places/tests/browser/head.js",
   this);
-/* globals withSidebarTree, synthesizeClickOnSelectedTreeCell */
+/* globals withSidebarTree, synthesizeClickOnSelectedTreeCell,
+ * promiseLibrary, promiseLibraryClosed
+ */
 
 const PAGE = "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html";
 
 add_task(async function() {
   let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
 
   gBrowser.selectedTab = tab1;
 
@@ -601,16 +603,85 @@ add_task(async function test_bookmark_si
     closeChromeContextMenu("placesContext", menuItem, sidebar.contentWindow);
     await extension.awaitMessage("test-finish");
     await extension.unload();
 
     BrowserTestUtils.removeTab(tab);
   });
 });
 
+function bookmarkFolderContextMenuExtension() {
+  return ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["contextMenus", "bookmarks"],
+    },
+    async background() {
+      const title = "Example";
+      let newBookmark = await browser.bookmarks.create({
+        title,
+        parentId: "toolbar_____",
+      });
+      await new Promise(resolve =>
+        browser.contextMenus.create({
+          title: "Get bookmark",
+          contexts: ["bookmark"],
+        }, resolve));
+      browser.contextMenus.onClicked.addListener(async (info) => {
+        browser.test.assertEq(newBookmark.id, info.bookmarkId, "Bookmark ID matches");
+
+        let [bookmark] = await browser.bookmarks.get(info.bookmarkId);
+        browser.test.assertEq(title, bookmark.title, "Bookmark title matches");
+        browser.test.assertFalse(info.hasOwnProperty("pageUrl"), "Context menu does not expose pageUrl");
+        await browser.bookmarks.remove(info.bookmarkId);
+        browser.test.sendMessage("test-finish");
+      });
+      browser.test.sendMessage("bookmark-created", newBookmark.id);
+    },
+  });
+}
+
+add_task(async function test_organizer_contextmenu() {
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+  let library = await promiseLibrary("BookmarksToolbar");
+
+  let menu = library.document.getElementById("placesContext");
+  let mainTree = library.document.getElementById("placeContent");
+  let leftTree = library.document.getElementById("placesList");
+
+  let tests = [
+    [mainTree, bookmarkContextMenuExtension],
+    [mainTree, bookmarkFolderContextMenuExtension],
+    [leftTree, bookmarkFolderContextMenuExtension],
+  ];
+
+  if (AppConstants.DEBUG) {
+    // Avoid intermittent leak - bug 1520047
+    tests.pop();
+  }
+
+  for (let [tree, makeExtension] of tests) {
+    let extension = makeExtension();
+    await extension.startup();
+    let bookmarkGuid = await extension.awaitMessage("bookmark-created");
+
+    tree.selectItems([bookmarkGuid]);
+    let shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+    synthesizeClickOnSelectedTreeCell(tree, {type: "contextmenu"});
+    await shown;
+
+    let menuItem = menu.getElementsByAttribute("label", "Get bookmark")[0];
+    closeChromeContextMenu("placesContext", menuItem, library);
+    await extension.awaitMessage("test-finish");
+    await extension.unload();
+  }
+
+  await promiseLibraryClosed(library);
+  BrowserTestUtils.removeTab(tab);
+});
+
 add_task(async function test_bookmark_context_requires_permission() {
   const bookmarksToolbar = document.getElementById("PersonalToolbar");
   setToolbarVisibility(bookmarksToolbar, true);
 
   const extension = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: ["contextMenus"],
     },