Bug 1500479 - Part 1: browser.tabs.onActivated; r=mixedpuppy,rpl,JanH
authorRyan Hendrickson <ryan.hendrickson@alum.mit.edu>
Mon, 26 Nov 2018 04:16:30 +0000
changeset 504810 f5bf87ab64481b0121a4564f5aadf3a4c5a4d962
parent 504809 eb787b74bd04005a9d4bcc3099d2201461c3b0ff
child 504811 cd420001c8ea7a9bb54ae85e95d9fad271ebfcf7
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy, rpl, JanH
bugs1500479
milestone65.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 1500479 - Part 1: browser.tabs.onActivated; r=mixedpuppy,rpl,JanH Add an optional previousTabId property to the onActivated event, which is present if the previously activated tab is still open. Differential Revision: https://phabricator.services.mozilla.com/D9271
browser/components/extensions/parent/ext-browser.js
browser/components/extensions/schemas/tabs.json
browser/components/extensions/test/browser/browser_ext_tabs_events.js
browser/components/extensions/test/browser/browser_ext_tabs_update_highlighted.js
mobile/android/base/java/org/mozilla/gecko/Tabs.java
mobile/android/components/extensions/ext-tabs.js
mobile/android/components/extensions/schemas/tabs.json
mobile/android/components/extensions/test/mochitest/test_ext_tabs_events.html
--- a/browser/components/extensions/parent/ext-browser.js
+++ b/browser/components/extensions/parent/ext-browser.js
@@ -483,17 +483,17 @@ 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(() => {
           if (!nativeTab.parentNode) {
             // If the tab is already be destroyed, do nothing.
             return;
           }
-          this.emitActivated(nativeTab);
+          this.emitActivated(nativeTab, event.detail.previousTab);
         });
         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(() => {
@@ -566,21 +566,24 @@ class TabTracker extends TabTrackerBase 
     }
   }
 
   /**
    * Emits a "tab-activated" event for the given tab element.
    *
    * @param {NativeTab} nativeTab
    *        The tab element which has been activated.
+   * @param {NativeTab} previousTab
+   *        The tab element which was previously activated.
    * @private
    */
-  emitActivated(nativeTab) {
+  emitActivated(nativeTab, previousTab = undefined) {
     this.emit("tab-activated", {
       tabId: this.getId(nativeTab),
+      previousTabId: previousTab && !previousTab.closing ? this.getId(previousTab) : undefined,
       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.
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -1580,16 +1580,22 @@
             "type": "object",
             "name": "activeInfo",
             "properties": {
               "tabId": {
                 "type": "integer",
                 "minimum": 0,
                 "description": "The ID of the tab that has become active."
               },
+              "previousTabId": {
+                "type": "integer",
+                "minimum": 0,
+                "optional": true,
+                "description": "The ID of the tab that was previously active, if that tab is still open."
+              },
               "windowId": {
                 "type": "integer",
                 "minimum": 0,
                 "description": "The ID of the window the active tab changed inside of."
               }
             }
           }
         ]
--- a/browser/components/extensions/test/browser/browser_ext_tabs_events.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_events.js
@@ -369,18 +369,19 @@ add_task(async function testTabCreateRel
 
   await extension.awaitMessage("tabRemoved");
   await extension.unload();
 
   BrowserTestUtils.removeTab(openerTab);
 });
 
 add_task(async function testLastTabRemoval() {
+  const CLOSE_WINDOW_PREF = "browser.tabs.closeWindowWithLastTab";
   await SpecialPowers.pushPrefEnv({set: [
-    ["browser.tabs.closeWindowWithLastTab", false],
+    [CLOSE_WINDOW_PREF, false],
   ]});
 
   async function background() {
     let windowId;
     browser.tabs.onCreated.addListener(tab => {
       browser.test.assertEq(windowId, tab.windowId,
                             "expecting onCreated after onRemoved on the same window");
       browser.test.sendMessage("tabCreated", `${tab.width}x${tab.height}`);
@@ -404,9 +405,87 @@ add_task(async function testLastTabRemov
   const expectedDims = `${oldBrowser.clientWidth}x${oldBrowser.clientHeight}`;
   BrowserTestUtils.removeTab(newWin.gBrowser.selectedTab);
 
   const actualDims = await extension.awaitMessage("tabCreated");
   is(actualDims, expectedDims, "created tab reports a size same to the removed last tab");
 
   await extension.unload();
   await BrowserTestUtils.closeWindow(newWin);
+  SpecialPowers.clearUserPref(CLOSE_WINDOW_PREF);
 });
+
+add_task(async function testTabActivationEvent() {
+  async function background() {
+    function makeExpectable() {
+      let expectation = null, resolver = null;
+      const expectable = param => {
+        if (expectation === null) {
+          browser.test.fail("unexpected call to expectable");
+        } else {
+          try {
+            resolver(expectation(param));
+          } catch (e) {
+            resolver(Promise.reject(e));
+          } finally {
+            expectation = null;
+          }
+        }
+      };
+      expectable.expect = e => {
+        expectation = e;
+        return new Promise(r => { resolver = r; });
+      };
+      return expectable;
+    }
+    try {
+      const listener = makeExpectable();
+      browser.tabs.onActivated.addListener(listener);
+
+      const [, {tabs: [tab1]}] = await Promise.all([
+        listener.expect(info => {
+          browser.test.assertEq(undefined, info.previousTabId, "previousTabId should not be defined when window is first opened");
+        }),
+        browser.windows.create({url: "about:blank"}),
+      ]);
+      const [, tab2] = await Promise.all([
+        listener.expect(info => {
+          browser.test.assertEq(tab1.id, info.previousTabId, "Got expected previousTabId");
+        }),
+        browser.tabs.create({url: "about:blank"}),
+      ]);
+
+      await Promise.all([
+        listener.expect(info => {
+          browser.test.assertEq(tab1.id, info.tabId, "Got expected tabId");
+          browser.test.assertEq(tab2.id, info.previousTabId, "Got expected previousTabId");
+        }),
+        browser.tabs.update(tab1.id, {active: true}),
+      ]);
+
+      await Promise.all([
+        listener.expect(info => {
+          browser.test.assertEq(tab2.id, info.tabId, "Got expected tabId");
+          browser.test.assertEq(undefined, info.previousTabId, "previousTabId should not be defined when previous tab was closed");
+        }),
+        browser.tabs.remove(tab1.id),
+      ]);
+
+      await browser.tabs.remove(tab2.id);
+
+      browser.test.notifyPass("tabs-events");
+    } catch (e) {
+      browser.test.fail(`${e} :: ${e.stack}`);
+      browser.test.notifyFail("tabs-events");
+    }
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "permissions": ["tabs"],
+    },
+    background,
+  });
+
+  await extension.startup();
+  await extension.awaitFinish("tabs-events");
+  await extension.unload();
+});
--- a/browser/components/extensions/test/browser/browser_ext_tabs_update_highlighted.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_update_highlighted.js
@@ -75,38 +75,38 @@ add_task(async function test_update_high
       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}],
+          ["onActivated", {tabId: tab1, previousTabId: tab2, 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}],
+          ["onActivated", {tabId: tab2, previousTabId: tab1, 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}],
+          ["onActivated", {tabId: tab1, previousTabId: tab2, 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}],
@@ -118,25 +118,25 @@ add_task(async function test_update_high
         return {active: tab1, highlighted: [tab1, tab2], events: [
           ["onHighlighted", {tabIds: [tab1, tab2], windowId}],
         ]};
       }, "highlighting without activating non-highlighted tab");
 
       await expectResults(async () => {
         await browser.tabs.update(tab2, {highlighted: true, active: true});
         return {active: tab2, highlighted: [tab2], events: [
-          ["onActivated", {tabId: tab2, windowId}],
+          ["onActivated", {tabId: tab2, previousTabId: tab1, windowId}],
           ["onHighlighted", {tabIds: [tab2], windowId}],
         ]};
       }, "highlighting and activating inactive highlighted tab");
 
       await expectResults(async () => {
         await browser.tabs.update(tab1, {active: true, highlighted: true});
         return {active: tab1, highlighted: [tab1], events: [
-          ["onActivated", {tabId: tab1, windowId}],
+          ["onActivated", {tabId: tab1, previousTabId: tab2, windowId}],
           ["onHighlighted", {tabIds: [tab1], windowId}],
         ]};
       }, "highlighting and activating non-highlighted tab");
 
       await browser.tabs.remove([tab1, tab2]);
       browser.test.notifyPass("test-finished");
     },
   });
--- a/mobile/android/base/java/org/mozilla/gecko/Tabs.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Tabs.java
@@ -338,18 +338,21 @@ public class Tabs implements BundleEvent
         notifyListeners(tab, TabEvents.SELECTED);
 
         if (oldTab != null) {
             mPreviouslySelectedTabId = oldTab.getId();
             notifyListeners(oldTab, TabEvents.UNSELECTED);
         }
 
         // Pass a message to Gecko to update tab state in BrowserApp.
-        final GeckoBundle data = new GeckoBundle(1);
+        final GeckoBundle data = new GeckoBundle(2);
         data.putInt("id", tab.getId());
+        if (oldTab != null && mTabs.containsKey(oldTab.getId())) {
+            data.putInt("previousTabId", oldTab.getId());
+        }
         mEventDispatcher.dispatch("Tab:Selected", data);
         EventDispatcher.getInstance().dispatch("Tab:Selected", data);
         return tab;
     }
 
     public synchronized boolean selectLastTab() {
         if (mOrder.isEmpty()) {
             return false;
--- a/mobile/android/components/extensions/ext-tabs.js
+++ b/mobile/android/components/extensions/ext-tabs.js
@@ -91,17 +91,21 @@ this.tabs = class extends ExtensionAPI {
       return tab;
     }
 
     let self = {
       tabs: {
         onActivated: makeGlobalEvent(context, "tabs.onActivated", "Tab:Selected", (fire, data) => {
           let tab = tabManager.get(data.id);
 
-          fire.async({tabId: tab.id, windowId: tab.windowId});
+          fire.async({
+            tabId: tab.id,
+            previousTabId: data.previousTabId,
+            windowId: tab.windowId,
+          });
         }),
 
         onCreated: new EventManager({
           context,
           name: "tabs.onCreated",
           register: fire => {
             let listener = (eventName, event) => {
               fire.async(tabManager.convert(event.nativeTab));
--- a/mobile/android/components/extensions/schemas/tabs.json
+++ b/mobile/android/components/extensions/schemas/tabs.json
@@ -1204,16 +1204,22 @@
             "type": "object",
             "name": "activeInfo",
             "properties": {
               "tabId": {
                 "type": "integer",
                 "minimum": 0,
                 "description": "The ID of the tab that has become active."
               },
+              "previousTabId": {
+                "type": "integer",
+                "minimum": 0,
+                "optional": true,
+                "description": "The ID of the tab that was previously active, if that tab is still open."
+              },
               "windowId": {
                 "type": "integer",
                 "minimum": 0,
                 "description": "The ID of the window the active tab changed inside of."
               }
             }
           }
         ]
--- a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_events.html
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_events.html
@@ -145,12 +145,92 @@ add_task(async function testTabRemovalEv
 
     background,
   });
 
   await extension.startup();
   await extension.awaitFinish("tabs-events");
   await extension.unload();
 });
+
+add_task(async function testTabActivationEvent() {
+  async function background() {
+    function makeExpectable() {
+      let expectation = null, resolver = null;
+      const expectable = param => {
+        if (expectation === null) {
+          browser.test.fail("unexpected call to expectable");
+        } else {
+          try {
+            resolver(expectation(param));
+          } catch (e) {
+            resolver(Promise.reject(e));
+          } finally {
+            expectation = null;
+          }
+        }
+      };
+      expectable.expect = e => {
+        expectation = e;
+        return new Promise(r => { resolver = r; });
+      };
+      return expectable;
+    }
+    try {
+      const listener = makeExpectable();
+      browser.tabs.onActivated.addListener(listener);
+
+      const [tab0] = await browser.tabs.query({active: true});
+      const [, tab1] = await Promise.all([
+        listener.expect(info => {
+          browser.test.assertEq(tab0.id, info.previousTabId, "Got expected previousTabId");
+        }),
+        browser.tabs.create({url: "about:blank"}),
+      ]);
+      const [, tab2] = await Promise.all([
+        listener.expect(info => {
+          browser.test.assertEq(tab1.id, info.previousTabId, "Got expected previousTabId");
+        }),
+        browser.tabs.create({url: "about:blank"}),
+      ]);
+
+      await Promise.all([
+        listener.expect(info => {
+          browser.test.assertEq(tab1.id, info.tabId, "Got expected tabId");
+          browser.test.assertEq(tab2.id, info.previousTabId, "Got expected previousTabId");
+        }),
+        browser.tabs.update(tab1.id, {active: true}),
+      ]);
+
+      await Promise.all([
+        listener.expect(info => {
+          browser.test.assertEq(tab2.id, info.tabId, "Got expected tabId");
+          browser.test.assertEq(undefined, info.previousTabId, "previousTabId should not be defined when previous tab was closed");
+        }),
+        browser.tabs.remove(tab1.id),
+      ]);
+
+      browser.tabs.onActivated.removeListener(listener);
+      await browser.tabs.remove(tab2.id);
+
+      browser.test.notifyPass("tabs-events");
+    } catch (e) {
+      browser.test.fail(`${e} :: ${e.stack}`);
+      browser.test.notifyFail("tabs-events");
+    }
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "permissions": ["tabs"],
+    },
+
+    background,
+  });
+
+  await extension.startup();
+  await extension.awaitFinish("tabs-events");
+  await extension.unload();
+});
 </script>
 
 </body>
 </html>