Bug 1479129 - Implement support for updateProperties.highlighted in browser.tabs.update(). r=mixedpuppy,Gijs
authorOriol Brufau <oriol-bugzilla@hotmail.com>
Thu, 23 Aug 2018 22:09:45 +0000
changeset 488282 073b8e49194afc45eca6efbc08f903128fe0d7bd
parent 488281 4a333f450838fbc7dd58146696e2ec9a11e84bdf
child 488283 3de1d6228039258820829c04fe0d1b91d96ad6db
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy, Gijs
bugs1479129
milestone63.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 1479129 - Implement support for updateProperties.highlighted in browser.tabs.update(). r=mixedpuppy,Gijs Differential Revision: https://phabricator.services.mozilla.com/D3800
browser/base/content/tabbrowser.js
browser/base/content/tabbrowser.xml
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_update_highlighted.js
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -3725,32 +3725,36 @@ window._gBrowser = {
    *          Can be from a different window as well
    * @param   aRestoreTabImmediately
    *          Can defer loading of the tab contents
    */
   duplicateTab(aTab, aRestoreTabImmediately) {
     return SessionStore.duplicateTab(window, aTab, 0, aRestoreTabImmediately);
   },
 
-  addToMultiSelectedTabs(aTab, skipPositionalAttributes) {
+  addToMultiSelectedTabs(aTab, multiSelectMayChangeMore) {
     if (aTab.multiselected) {
       return;
     }
 
     aTab.setAttribute("multiselected", "true");
     aTab.setAttribute("aria-selected", "true");
     this._multiSelectedTabsSet.add(aTab);
     this._startMultiSelectChange();
     if (this._multiSelectChangeRemovals.has(aTab)) {
       this._multiSelectChangeRemovals.delete(aTab);
     } else {
       this._multiSelectChangeAdditions.add(aTab);
     }
 
-    if (!skipPositionalAttributes) {
+    if (!multiSelectMayChangeMore) {
+      let {selectedTab} = this;
+      if (!selectedTab.multiselected) {
+        this.addToMultiSelectedTabs(selectedTab, true);
+      }
       this.tabContainer._setPositionalAttributes();
     }
   },
 
   /**
    * Adds two given tabs and all tabs between them into the (multi) selected tabs collection
    */
   addRangeToMultiSelectedTabs(aTab1, aTab2) {
@@ -3766,52 +3770,56 @@ window._gBrowser = {
       [indexOfTab1, indexOfTab2] : [indexOfTab2, indexOfTab1];
 
     for (let i = lowerIndex; i <= higherIndex; i++) {
       this.addToMultiSelectedTabs(tabs[i], true);
     }
     this.tabContainer._setPositionalAttributes();
   },
 
-  removeFromMultiSelectedTabs(aTab, updatePositionalAttributes) {
+  removeFromMultiSelectedTabs(aTab, isLastMultiSelectChange) {
     if (!aTab.multiselected) {
       return;
     }
     aTab.removeAttribute("multiselected");
     aTab.removeAttribute("aria-selected");
     this._multiSelectedTabsSet.delete(aTab);
     this._startMultiSelectChange();
     if (this._multiSelectChangeAdditions.has(aTab)) {
       this._multiSelectChangeAdditions.delete(aTab);
     } else {
       this._multiSelectChangeRemovals.add(aTab);
     }
-    if (updatePositionalAttributes) {
+    if (isLastMultiSelectChange) {
+      if (aTab.selected) {
+        this.switchToNextMultiSelectedTab();
+      }
+      this.avoidSingleSelectedTab();
       this.tabContainer._setPositionalAttributes();
     }
   },
 
-  clearMultiSelectedTabs(updatePositionalAttributes) {
+  clearMultiSelectedTabs(isLastMultiSelectChange) {
     if (this._clearMultiSelectionLocked) {
       if (this._clearMultiSelectionLockedOnce) {
         this._clearMultiSelectionLockedOnce = false;
         this._clearMultiSelectionLocked = false;
       }
       return;
     }
 
     if (this.multiSelectedTabsCount < 1) {
       return;
     }
 
     for (let tab of this.selectedTabs) {
       this.removeFromMultiSelectedTabs(tab, false);
     }
     this._lastMultiSelectedTabRef = null;
-    if (updatePositionalAttributes) {
+    if (isLastMultiSelectChange) {
       this.tabContainer._setPositionalAttributes();
     }
   },
 
   lockClearMultiSelectionOnce() {
     this._clearMultiSelectionLockedOnce = true;
     this._clearMultiSelectionLocked = true;
   },
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -2122,25 +2122,18 @@
             }
             gBrowser.addRangeToMultiSelectedTabs(lastSelectedTab, this);
             return;
           }
           if (accelKey) {
             // Ctrl (Cmd for mac) key is pressed
             if (this.multiselected) {
               gBrowser.removeFromMultiSelectedTabs(this, true);
-              if (this == gBrowser.selectedTab) {
-                gBrowser.switchToNextMultiSelectedTab();
-              }
-              gBrowser.avoidSingleSelectedTab();
             } else if (this != gBrowser.selectedTab) {
-              for (let tab of [this, gBrowser.selectedTab]) {
-                gBrowser.addToMultiSelectedTabs(tab, true);
-              }
-              gBrowser.tabContainer._setPositionalAttributes();
+              gBrowser.addToMultiSelectedTabs(this, false);
               gBrowser.lastMultiSelectedTab = this;
             }
             return;
           }
 
           const overCloseButton = event.originalTarget.getAttribute("anonid") == "close-button";
           if (gBrowser.multiSelectedTabsCount > 0 && !overCloseButton && !this._overPlayingIcon) {
             // Tabs were previously multi-selected and user clicks on a tab
--- a/browser/components/extensions/parent/ext-tabs.js
+++ b/browser/components/extensions/parent/ext-tabs.js
@@ -702,16 +702,31 @@ this.tabs = class extends ExtensionAPI {
 
           if (updateProperties.active !== null) {
             if (updateProperties.active) {
               tabbrowser.selectedTab = nativeTab;
             } else {
               // Not sure what to do here? Which tab should we select?
             }
           }
+          if (updateProperties.highlighted !== null) {
+            if (!gMultiSelectEnabled) {
+              throw new ExtensionError(`updateProperties.highlight is currently experimental and must be enabled with the ${MULTISELECT_PREFNAME} preference.`);
+            }
+            if (updateProperties.highlighted) {
+              if (!nativeTab.selected && !nativeTab.multiselected) {
+                tabbrowser.addToMultiSelectedTabs(nativeTab, false);
+                // Select the highlighted tab, this matches Chrome's behavior.
+                tabbrowser.lockClearMultiSelectionOnce();
+                tabbrowser.selectedTab = nativeTab;
+              }
+            } else {
+              tabbrowser.removeFromMultiSelectedTabs(nativeTab, true);
+            }
+          }
           if (updateProperties.muted !== null) {
             if (nativeTab.muted != updateProperties.muted) {
               nativeTab.toggleMuteAudio(extension.id);
             }
           }
           if (updateProperties.pinned !== null) {
             if (updateProperties.pinned) {
               tabbrowser.pinTab(nativeTab);
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -839,17 +839,16 @@
                 "description": "A URL to navigate the tab to."
               },
               "active": {
                 "type": "boolean",
                 "optional": true,
                 "description": "Whether the tab should be active. Does not affect whether the window is focused (see $(ref:windows.update))."
               },
               "highlighted": {
-                "unsupported": true,
                 "type": "boolean",
                 "optional": true,
                 "description": "Adds or removes the tab from the current selection."
               },
               "selected": {
                 "unsupported": true,
                 "deprecated": "Please use <em>highlighted</em>.",
                 "type": "boolean",
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -209,18 +209,19 @@ skip-if = (verify && !debug && (os == 'm
 [browser_ext_tabs_reload.js]
 [browser_ext_tabs_reload_bypass_cache.js]
 [browser_ext_tabs_saveAsPDF.js]
 skip-if = os == 'mac' # Save as PDF not supported on Mac OS X
 [browser_ext_tabs_sendMessage.js]
 [browser_ext_tabs_sharingState.js]
 [browser_ext_tabs_cookieStoreId.js]
 [browser_ext_tabs_update.js]
+[browser_ext_tabs_update_highlighted.js]
+[browser_ext_tabs_update_url.js]
 [browser_ext_tabs_zoom.js]
-[browser_ext_tabs_update_url.js]
 [browser_ext_themes_icons.js]
 [browser_ext_themes_validation.js]
 [browser_ext_url_overrides_newtab.js]
 skip-if = (os == 'linux' && debug) || (os == 'win' && debug) # Bug 1465508
 [browser_ext_user_events.js]
 [browser_ext_webRequest.js]
 [browser_ext_webNavigation_frameId0.js]
 [browser_ext_webNavigation_getFrames.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_update_highlighted.js
@@ -0,0 +1,148 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_update_highlighted() {
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      ["browser.tabs.multiselect", true],
+    ],
+  });
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "permissions": ["tabs"],
+    },
+
+    background: async function() {
+      const trackedEvents = ["onActivated", "onHighlighted"];
+      async function expectResults(fn, action) {
+        let resolve;
+        let promise = new Promise((r) => {
+          resolve = r;
+        });
+        let expected;
+        let events = [];
+        let listeners = {};
+        for (let trackedEvent of trackedEvents) {
+          listeners[trackedEvent] = (data) => {
+            events.push([trackedEvent, data]);
+            if (expected && expected.length >= events.length) {
+              resolve();
+            }
+          };
+          browser.tabs[trackedEvent].addListener(listeners[trackedEvent]);
+        }
+        let {
+          events: expectedEvents,
+          highlighted: expectedHighlighted,
+          active: expectedActive,
+        } = await fn();
+        if (events.length < expectedEvents.length) {
+          await promise;
+        }
+        let [{id: active}] = await browser.tabs.query({active: true});
+        browser.test.assertEq(
+          expectedActive, active,
+          `The expected tab is active when ${action}`);
+        let highlighted = (await browser.tabs.query({highlighted: true})).map(({id}) => id);
+        browser.test.assertEq(
+          JSON.stringify(expectedHighlighted), JSON.stringify(highlighted),
+          `The expected tabs are highlighted when ${action}`);
+        let unexpectedEvents = events.splice(expectedEvents.length);
+        browser.test.assertEq(
+          JSON.stringify(expectedEvents), JSON.stringify(events),
+          `Should get expected events when ${action}`);
+        if (unexpectedEvents.length) {
+          browser.test.fail(
+            `${unexpectedEvents.length} unexpected events when ${action}: ` +
+            JSON.stringify(unexpectedEvents));
+        }
+        for (let trackedEvent of trackedEvents) {
+          browser.tabs[trackedEvent].removeListener(listeners[trackedEvent]);
+        }
+      }
+
+      let {id: windowId} = await browser.windows.getCurrent();
+      let {id: tab1} = await browser.tabs.create({url: "about:blank?1"});
+      let {id: tab2} = await browser.tabs.create({url: "about:blank?2", active: true});
+
+      await expectResults(async () => {
+        await browser.tabs.update(tab2, {highlighted: true});
+        return {active: tab2, highlighted: [tab2], events: []};
+      }, "highlighting active tab");
+
+      await expectResults(async () => {
+        await browser.tabs.update(tab2, {highlighted: false});
+        return {active: tab2, highlighted: [tab2], events: []};
+      }, "unhighlighting active tab with no multiselection");
+
+      await expectResults(async () => {
+        await browser.tabs.update(tab1, {highlighted: true});
+        return {active: tab1, highlighted: [tab1, tab2], events: [
+          ["onActivated", {tabId: tab1, windowId}],
+          ["onHighlighted", {tabIds: [tab1, tab2], windowId}],
+        ]};
+      }, "highlighting non-highlighted tab");
+
+      await expectResults(async () => {
+        await browser.tabs.update(tab2, {highlighted: true});
+        return {active: tab1, highlighted: [tab1, tab2], events: []};
+      }, "highlighting inactive highlighted tab");
+
+      await expectResults(async () => {
+        await browser.tabs.update(tab1, {highlighted: false});
+        return {active: tab2, highlighted: [tab2], events: [
+          ["onActivated", {tabId: tab2, windowId}],
+          ["onHighlighted", {tabIds: [tab2], windowId}],
+        ]};
+      }, "unhighlighting active tab with multiselection");
+
+      await expectResults(async () => {
+        await browser.tabs.update(tab1, {highlighted: true});
+        return {active: tab1, highlighted: [tab1, tab2], events: [
+          ["onActivated", {tabId: tab1, windowId}],
+          ["onHighlighted", {tabIds: [tab1, tab2], windowId}],
+        ]};
+      }, "highlighting non-highlighted tab");
+
+      await expectResults(async () => {
+        await browser.tabs.update(tab2, {highlighted: false});
+        return {active: tab1, highlighted: [tab1], events: [
+          ["onHighlighted", {tabIds: [tab1], windowId}],
+        ]};
+      }, "unhighlighting inactive highlighted tab");
+
+      await expectResults(async () => {
+        await browser.tabs.update(tab2, {highlighted: true, active: false});
+        return {active: tab2, highlighted: [tab1, tab2], events: [
+          ["onActivated", {tabId: tab2, windowId}],
+          ["onHighlighted", {tabIds: [tab1, tab2], windowId}],
+        ]};
+      }, "highlighting and (not really) inactivating non-highlighted tab");
+
+      await expectResults(async () => {
+        await browser.tabs.update(tab1, {highlighted: true, active: true});
+        return {active: tab1, highlighted: [tab1], events: [
+          ["onActivated", {tabId: tab1, windowId}],
+          ["onHighlighted", {tabIds: [tab1], windowId}],
+        ]};
+      }, "highlighting and activating inactive highlighted tab");
+
+      await expectResults(async () => {
+        await browser.tabs.update(tab2, {active: true, highlighted: true});
+        return {active: tab2, highlighted: [tab2], events: [
+          ["onActivated", {tabId: tab2, windowId}],
+          ["onHighlighted", {tabIds: [tab2], windowId}],
+        ]};
+      }, "highlighting and activating non-highlighted tab");
+
+      await browser.tabs.remove([tab1, tab2]);
+      browser.test.notifyPass("test-finished");
+    },
+  });
+
+  await extension.startup();
+  await extension.awaitFinish("test-finished");
+  await extension.unload();
+});