Bug 1464862 - Expose multiselected status to "tabs.Tab.highlighted", and allow to change it via "browser.tabs.highlight()" draft
authorOriol Brufau <oriol-bugzilla@hotmail.com>
Sat, 23 Jun 2018 21:46:58 +0200
changeset 810731 cd54aae51507b39215e7669ebc52717015b4616d
parent 809889 5bbe54174894e1b71aaf83006c2744bb8836816e
child 810733 94a0fdd6aefa6d447c01ec15214de84f569f5236
child 810808 61cc6b6008d193da6e632c8cb7f1b526acfd99c2
child 810871 24c8f00738d6c81225948aa26bbb3363533feb03
push id114075
push userbmo:oriol-bugzilla@hotmail.com
push dateTue, 26 Jun 2018 10:27:08 +0000
bugs1464862
milestone62.0a1
Bug 1464862 - Expose multiselected status to "tabs.Tab.highlighted", and allow to change it via "browser.tabs.highlight()" MozReview-Commit-ID: H2SiqM5ksCH
browser/components/extensions/parent/ext-browser.js
browser/components/extensions/parent/ext-tabs.js
browser/components/extensions/schemas/tabs.json
browser/components/extensions/test/browser/browser-common.ini
browser/components/extensions/test/browser/browser_ext_tabs_highlight.js
mobile/android/components/extensions/ext-utils.js
toolkit/components/extensions/parent/ext-tabs-base.js
--- a/browser/components/extensions/parent/ext-browser.js
+++ b/browser/components/extensions/parent/ext-browser.js
@@ -702,16 +702,21 @@ class Tab extends TabBase {
   get pinned() {
     return this.nativeTab.pinned;
   }
 
   get active() {
     return this.nativeTab.selected;
   }
 
+  get highlighted() {
+    let {selected, multiselected} = this.nativeTab;
+    return selected || multiselected;
+  }
+
   get selected() {
     return this.nativeTab.selected;
   }
 
   get status() {
     if (this.nativeTab.getAttribute("busy") === "true") {
       return "loading";
     }
--- a/browser/components/extensions/parent/ext-tabs.js
+++ b/browser/components/extensions/parent/ext-tabs.js
@@ -342,17 +342,17 @@ this.tabs = class extends ExtensionAPI {
 
   static onUninstall(id) {
     tabHidePopup.clearConfirmation(id);
   }
 
   getAPI(context) {
     let {extension} = context;
 
-    let {tabManager} = extension;
+    let {tabManager, windowManager} = extension;
 
     function getTabOrActive(tabId) {
       if (tabId !== null) {
         return tabTracker.getTab(tabId);
       }
       return tabTracker.activeTab;
     }
 
@@ -1230,13 +1230,42 @@ this.tabs = class extends ExtensionAPI {
             }
           }
           if (hidden.length > 0) {
             let win = Services.wm.getMostRecentWindow("navigator:browser");
             tabHidePopup.open(win, extension.id);
           }
           return hidden;
         },
+
+        highlight(highlightInfo) {
+          if (!Services.prefs.getBoolPref("browser.tabs.multiselect")) {
+            throw new ExtensionError("Multiple tab selection is not enabled.");
+          }
+          let {windowId, tabs} = highlightInfo;
+          if (windowId == null) {
+            windowId = Window.WINDOW_ID_CURRENT;
+          }
+          let window = windowTracker.getWindow(windowId, context);
+          if (!Array.isArray(tabs)) {
+            tabs = [tabs];
+          } else if (tabs.length == 0) {
+            throw new ExtensionError("No highlighted tab.");
+          }
+          tabs = tabs.map((tabIndex) => {
+            let tab = window.gBrowser.tabs[tabIndex];
+            if (!tab) {
+              throw new ExtensionError("No tab at index: " + tabIndex);
+            }
+            return tab;
+          });
+          window.gBrowser.clearMultiSelectedTabs();
+          window.gBrowser.selectedTab = tabs[0];
+          for (let tab of tabs) {
+            window.gBrowser.addToMultiSelectedTabs(tab);
+          }
+          return windowManager.convert(window, {populate: true});
+        },
       },
     };
     return self;
   }
 };
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -764,17 +764,16 @@
           }
         ]
       },
       {
         "name": "highlight",
         "type": "function",
         "description": "Highlights the given tabs.",
         "async": "callback",
-        "unsupported": "true",
         "parameters": [
           {
             "type": "object",
             "name": "highlightInfo",
             "properties": {
                "windowId": {
                  "type": "integer",
                  "optional": true,
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -172,16 +172,17 @@ skip-if = (verify && !debug && (os == 'm
 [browser_ext_tabs_executeScript_good.js]
 [browser_ext_tabs_executeScript_bad.js]
 [browser_ext_tabs_executeScript_multiple.js]
 [browser_ext_tabs_executeScript_no_create.js]
 [browser_ext_tabs_executeScript_runAt.js]
 [browser_ext_tabs_getCurrent.js]
 [browser_ext_tabs_hide.js]
 [browser_ext_tabs_hide_update.js]
+[browser_ext_tabs_highlight.js]
 [browser_ext_tabs_insertCSS.js]
 [browser_ext_tabs_lastAccessed.js]
 [browser_ext_tabs_lazy.js]
 [browser_ext_tabs_removeCSS.js]
 [browser_ext_tabs_move_array.js]
 [browser_ext_tabs_move_window.js]
 [browser_ext_tabs_move_window_multiple.js]
 [browser_ext_tabs_move_window_pinned.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_highlight.js
@@ -0,0 +1,80 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* global gBrowser SessionStore */
+"use strict";
+
+add_task(async function test_highlighted() {
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      ["browser.tabs.multiselect", true],
+    ],
+  });
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "permissions": ["tabs"],
+    },
+
+    background: async function() {
+      async function testHighlighted(activeIndex, highlightedIndices) {
+        let tabs = await browser.tabs.query({currentWindow: true});
+        for (let {index, active, highlighted} of tabs) {
+          browser.test.assertEq(index == activeIndex, active, "Check Tab.active: " + index);
+          let expected = highlightedIndices.includes(index) || index == activeIndex;
+          browser.test.assertEq(expected, highlighted, "Check Tab.highlighted: " + index);
+        }
+      }
+
+      browser.test.log("Check that last tab is active, and no other is highlighted");
+      await testHighlighted(2, []);
+
+      browser.test.log("Highlight first and second tabs");
+      await browser.tabs.highlight({tabs: [0, 1]});
+      await testHighlighted(0, [1]);
+
+      browser.test.log("Highlight second and first tabs");
+      await browser.tabs.highlight({tabs: [1, 0]});
+      await testHighlighted(1, [0]);
+
+      browser.test.log("Test that highlight fails for invalid data");
+      await browser.test.assertRejects(
+        browser.tabs.highlight({tabs: []}),
+        /No highlighted tab/,
+        "Attempt to highlight no tab should throw");
+      await browser.test.assertRejects(
+        browser.tabs.highlight({windowId: 999999999, tabs: 0}),
+        /Invalid window ID: 999999999/,
+        "Attempt to highlight tabs in invalid window should throw");
+      await browser.test.assertRejects(
+        browser.tabs.highlight({tabs: 999999999}),
+        /No tab at index: 999999999/,
+        "Attempt to highlight invalid tab index should throw");
+      await browser.test.assertRejects(
+        browser.tabs.highlight({tabs: [2, 999999999]}),
+        /No tab at index: 999999999/,
+        "Attempt to highlight invalid tab index should throw");
+
+      browser.test.log("Highlighted tabs shouldn't be affected by failures above");
+      await testHighlighted(1, [0]);
+
+      browser.test.log("Highlight last tab");
+      let window = await browser.tabs.highlight({tabs: 2});
+      await testHighlighted(2, []);
+
+      browser.test.assertEq(3, window.tabs.length, "Returned window should be populated");
+
+      browser.test.notifyPass("test-finished");
+    },
+  });
+
+  let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
+  let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
+
+  await extension.startup();
+  await extension.awaitFinish("test-finished");
+  await extension.unload();
+
+  BrowserTestUtils.removeTab(tab1);
+  BrowserTestUtils.removeTab(tab2);
+});
+
--- a/mobile/android/components/extensions/ext-utils.js
+++ b/mobile/android/components/extensions/ext-utils.js
@@ -515,16 +515,20 @@ class Tab extends TabBase {
       // all the active tabs.
       if (tabTracker.extensionPopupTab === this.nativeTab) {
         return false;
       }
     }
     return this.nativeTab.getActive();
   }
 
+  get highlighted() {
+    return this.active;
+  }
+
   get selected() {
     return this.nativeTab.getActive();
   }
 
   get status() {
     if (this.browser.webProgress.isLoadingDocument) {
       return "loading";
     }
--- a/toolkit/components/extensions/parent/ext-tabs-base.js
+++ b/toolkit/components/extensions/parent/ext-tabs-base.js
@@ -395,22 +395,22 @@ class TabBase {
    *        @abstract
    */
   get active() {
     throw new Error("Not implemented");
   }
 
   /**
    * @property {boolean} highlighted
-   *        Alias for `active`.
+   *        Returns true if the tab is highlighted.
    *        @readonly
    *        @abstract
    */
   get highlighted() {
-    return this.active;
+    throw new Error("Not implemented");
   }
 
   /**
    * @property {boolean} selected
    *        An alias for `active`.
    *        @readonly
    *        @abstract
    */