Bug 1419195: Show items from WebExtensions in Places Library context menu r=mixedpuppy
☠☠ backed out by 65b6d0b670b4 ☠ ☠
authorPeter Simonyi <pts@petersimonyi.ca>
Tue, 15 Jan 2019 14:05:44 +0000
changeset 511032 c04c2376a2133c875df0e71749572688abe562a0
parent 511031 d9a81de35ac3bee7f1e69d59d90a6ad7ee82c5c9
child 511033 437003de9fffafd6656b79ea4bcbeab0aa652eef
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,103 @@ 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(this.libraryWindowType)) {
+      try {
+        window.removeEventListener("load", this);
+        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) {
@@ -924,16 +995,26 @@ const menuTracker = {
     const window = event.currentTarget.ownerGlobal;
     if (window.SidebarUI.currentID === "viewBookmarksSidebar") {
       const menu = window.SidebarUI.browser.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;
       }
@@ -958,16 +1039,20 @@ const menuTracker = {
   },
 
   onBookmarksContextMenu(event) {
     const menu = event.target;
     const tree = menu.triggerNode.parentElement;
     const cell = tree.boxObject.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"],
     },