Bug 1474440 - Implement support for the 'onHighlighted' API for multiselect tabs
MozReview-Commit-ID: 8aOmdj0AB3e
--- a/browser/components/extensions/parent/ext-browser.js
+++ b/browser/components/extensions/parent/ext-browser.js
@@ -280,16 +280,17 @@ class TabTracker extends TabTrackerBase
this.adoptedTabs = new WeakMap();
this._handleWindowOpen = this._handleWindowOpen.bind(this);
this._handleWindowClose = this._handleWindowClose.bind(this);
windowTracker.addListener("TabClose", this);
windowTracker.addListener("TabOpen", this);
windowTracker.addListener("TabSelect", this);
+ windowTracker.addListener("TabMultiSelect", this);
windowTracker.addOpenListener(this._handleWindowOpen);
windowTracker.addCloseListener(this._handleWindowClose);
Services.mm.addMessageListener("Reader:UpdateReaderButton", this);
/* eslint-disable mozilla/balanced-listeners */
this.on("tab-detached", this._handleTabDestroyed);
this.on("tab-removed", this._handleTabDestroyed);
@@ -451,16 +452,26 @@ class TabTracker extends TabTrackerBase
case "TabSelect":
// Because we are delaying calling emitCreated above, we also need to
// delay sending this event because it shouldn't fire before onCreated.
Promise.resolve().then(() => {
this.emitActivated(nativeTab);
});
break;
+
+ case "TabMultiSelect":
+ if (this.has("tabs-highlighted")) {
+ // Because we are delaying calling emitCreated above, we also need to
+ // delay sending this event because it shouldn't fire before onCreated.
+ Promise.resolve().then(() => {
+ this.emitHighlighted(event.target.ownerGlobal);
+ });
+ }
+ break;
}
}
/**
* @param {Object} message
* The message to handle.
* @private
*/
@@ -509,16 +520,19 @@ class TabTracker extends TabTrackerBase
this.on("tab-detached", listener);
} else {
for (let nativeTab of window.gBrowser.tabs) {
this.emitCreated(nativeTab);
}
// emitActivated to trigger tab.onActivated/tab.onHighlighted for a newly opened window.
this.emitActivated(window.gBrowser.tabs[0]);
+ if (this.has("tabs-highlighted")) {
+ this.emitHighlighted(window);
+ }
}
}
/**
* A private method which is called whenever a browser window is closed,
* and dispatches the necessary events for it.
*
* @param {DOMWindow} window
@@ -544,16 +558,29 @@ class TabTracker extends TabTrackerBase
*/
emitActivated(nativeTab) {
this.emit("tab-activated", {
tabId: this.getId(nativeTab),
windowId: windowTracker.getId(nativeTab.ownerGlobal)});
}
/**
+ * Emits a "tabs-highlighted" event for the given tab element.
+ *
+ * @param {ChromeWindow} window
+ * The window in which the active tab or the set of multiselected tabs changed.
+ * @private
+ */
+ emitHighlighted(window) {
+ let tabIds = window.gBrowser.selectedTabs.map(tab => this.getId(tab));
+ let windowId = windowTracker.getId(window);
+ this.emit("tabs-highlighted", {tabIds, windowId});
+ }
+
+ /**
* Emits a "tab-attached" event for the given tab element.
*
* @param {NativeTab} nativeTab
* The tab element in the window to which the tab is being attached.
* @private
*/
emitAttached(nativeTab) {
let newWindowId = windowTracker.getId(nativeTab.ownerGlobal);
--- a/browser/components/extensions/parent/ext-tabs.js
+++ b/browser/components/extensions/parent/ext-tabs.js
@@ -406,33 +406,27 @@ this.tabs = class extends ExtensionAPI {
tabTracker.on("tab-created", listener);
return () => {
tabTracker.off("tab-created", listener);
};
},
}).api(),
- /**
- * Since multiple tabs currently can't be highlighted, onHighlighted
- * essentially acts an alias for self.tabs.onActivated but returns
- * the tabId in an array to match the API.
- * @see https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/Tabs/onHighlighted
- */
onHighlighted: new EventManager({
context,
name: "tabs.onHighlighted",
register: fire => {
let listener = (eventName, event) => {
- fire.async({tabIds: [event.tabId], windowId: event.windowId});
+ fire.async(event);
};
- tabTracker.on("tab-activated", listener);
+ tabTracker.on("tabs-highlighted", listener);
return () => {
- tabTracker.off("tab-activated", listener);
+ tabTracker.off("tabs-highlighted", listener);
};
},
}).api(),
onAttached: new EventManager({
context,
name: "tabs.onAttached",
register: fire => {
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -193,16 +193,17 @@ skip-if = (verify && !debug && (os == 'm
[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]
+[browser_ext_tabs_onHighlighted.js]
[browser_ext_tabs_onUpdated.js]
[browser_ext_tabs_onUpdated_filter.js]
[browser_ext_tabs_opener.js]
[browser_ext_tabs_printPreview.js]
[browser_ext_tabs_query.js]
[browser_ext_tabs_readerMode.js]
[browser_ext_tabs_reload.js]
[browser_ext_tabs_reload_bypass_cache.js]
--- a/browser/components/extensions/test/browser/browser_ext_tabs_highlight.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_highlight.js
@@ -21,17 +21,17 @@ add_task(async function test_highlighted
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);
}
let highlightedTabs = await browser.tabs.query({currentWindow: true, highlighted: true});
browser.test.assertEq(
highlightedIndices.concat(activeIndex).sort((a, b) => a - b).join(),
- highlightedTabs.map(tab => tab.index).sort((a, b) => a - b).join(),
+ highlightedTabs.map(tab => tab.index).join(),
"Check tabs.query with highlighted:true provides the expected tabs");
}
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]});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_onHighlighted.js
@@ -0,0 +1,122 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+add_task(async function test_onHighlighted() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.tabs.multiselect", true],
+ ],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "permissions": ["tabs"],
+ },
+
+ background: async function() {
+ async function expectHighlighted(fn, action) {
+ let resolve;
+ let promise = new Promise((r) => {
+ resolve = r;
+ });
+ let expected;
+ let events = [];
+ let listener = (highlightInfo) => {
+ events.push(highlightInfo);
+ if (expected && expected.length >= events.length) {
+ resolve();
+ }
+ };
+ browser.tabs.onHighlighted.addListener(listener);
+ expected = await fn() || [];
+ if (events.length < expected.length) {
+ await promise;
+ }
+ let unexpected = events.splice(expected.length);
+ browser.test.assertEq(
+ JSON.stringify(expected), JSON.stringify(events),
+ `Should get ${expected.length} expected onHighlighted events when ${action}`);
+ if (unexpected.length) {
+ browser.test.fail(
+ `${unexpected.length} unexpected onHighlighted events when ${action}: ` +
+ JSON.stringify(unexpected));
+ }
+ browser.tabs.onHighlighted.removeListener(listener);
+ }
+
+ let [{id, windowId}] = await browser.tabs.query({active: true, currentWindow: true});
+ let windows = [windowId];
+ let tabs = [id];
+
+ await expectHighlighted(async () => {
+ let tab = await browser.tabs.create({active: true, url: "about:blank?1"});
+ tabs.push(tab.id);
+ return [{tabIds: [tabs[1]], windowId: windows[0]}];
+ }, "creating a new active tab");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.update(tabs[0], {active: true});
+ return [{tabIds: [tabs[0]], windowId: windows[0]}];
+ }, "selecting former tab");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.highlight({tabs: [0, 1]});
+ return [{tabIds: [tabs[0], tabs[1]], windowId: windows[0]}];
+ }, "highlighting both tabs");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.highlight({tabs: [1, 0]});
+ return [{tabIds: [tabs[0], tabs[1]], windowId: windows[0]}];
+ }, "highlighting same tabs but changing selected one");
+
+ await expectHighlighted(async () => {
+ let tab = await browser.tabs.create({active: false, url: "about:blank?2"});
+ tabs.push(tab.id);
+ }, "create a new inactive tab");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.highlight({tabs: [2, 0, 1]});
+ return [{tabIds: [tabs[0], tabs[1], tabs[2]], windowId: windows[0]}];
+ }, "highlighting all tabs");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.move(tabs[1], {index: 0});
+ }, "reordering tabs");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.highlight({tabs: [0]});
+ return [{tabIds: [tabs[1]], windowId: windows[0]}];
+ }, "highlighting moved tab");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.highlight({tabs: [0]});
+ }, "highlighting again");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.highlight({tabs: [2, 1, 0]});
+ return [{tabIds: [tabs[1], tabs[0], tabs[2]], windowId: windows[0]}];
+ }, "highlighting all tabs");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.highlight({tabs: [2, 0, 1]});
+ }, "highlighting same tabs with different order");
+
+ await expectHighlighted(async () => {
+ let window = await browser.windows.create({tabId: tabs[2]});
+ windows.push(window.id);
+ // Bug 1481185: on Chrome it's [tabs[1], tabs[0]] instead of [tabs[0]]
+ return [{tabIds: [tabs[0]], windowId: windows[0]},
+ {tabIds: [tabs[2]], windowId: windows[1]}];
+ }, "moving selected tab into a new window");
+
+ await browser.tabs.remove(tabs.slice(1));
+ browser.test.notifyPass("test-finished");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("test-finished");
+ await extension.unload();
+});
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -210,16 +210,27 @@ const ONCE_MAP = Symbol("onceMap");
class EventEmitter {
constructor() {
this[LISTENERS] = new Map();
this[ONCE_MAP] = new WeakMap();
}
/**
+ * Checks whether there is some listener for the given event.
+ *
+ * @param {string} event
+ * The name of the event to listen for.
+ * @returns {boolean}
+ */
+ has(event) {
+ return this[LISTENERS].has(event);
+ }
+
+ /**
* Adds the given function as a listener for the given event.
*
* The listener function may optionally return a Promise which
* resolves when it has completed all operations which event
* dispatchers may need to block on.
*
* @param {string} event
* The name of the event to listen for.