Bug 1322485 - Implement browser.tabs.discard API. r=mixedpuppy
authorKevin Jones <kevinhowjones@gmail.com>
Mon, 30 Oct 2017 12:52:00 -0400
changeset 439992 41b855fe8469a4e37a3f88b0fd242f4d81bbbb3c
parent 439991 64ea2f88d57e98318c9e1a7ed8e39a7924727fb9
child 439993 726e06daf2d7af6ed318d4952d57e34e3ba77e65
push id8114
push userjlorenzo@mozilla.com
push dateThu, 02 Nov 2017 16:33:21 +0000
treeherdermozilla-beta@73e0d89a540f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy
bugs1322485
milestone58.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 1322485 - Implement browser.tabs.discard API. r=mixedpuppy
browser/base/content/tabbrowser.xml
browser/components/extensions/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_discard.js
browser/components/extensions/test/mochitest/test_ext_all_apis.html
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -2560,16 +2560,19 @@
 
             aBrowser.destroy();
 
             let notificationbox = this.getNotificationBox(aBrowser);
             this.mPanelContainer.removeChild(notificationbox);
             tab.removeAttribute("linkedpanel");
 
             this._createLazyBrowser(tab);
+
+            let evt = new CustomEvent("TabBrowserDiscarded", { bubbles: true });
+            tab.dispatchEvent(evt);
           ]]>
         </body>
       </method>
 
       <method name="addTab">
         <parameter name="aURI"/>
         <parameter name="aReferrerURI"/>
         <parameter name="aCharset"/>
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -263,16 +263,18 @@ this.tabs = class extends ExtensionAPI {
               }
             } else if (event.type == "TabPinned") {
               needed.push("pinned");
             } else if (event.type == "TabUnpinned") {
               needed.push("pinned");
             } else if (event.type == "TabBrowserInserted" &&
                        !event.detail.insertedOnTabCreation) {
               needed.push("discarded");
+            } else if (event.type == "TabBrowserDiscarded") {
+              needed.push("discarded");
             }
 
             let tab = tabManager.getWrapper(event.originalTarget);
             let changeInfo = {};
             for (let prop of needed) {
               changeInfo[prop] = tab[prop];
             }
 
@@ -302,25 +304,27 @@ this.tabs = class extends ExtensionAPI {
             }
           };
 
           windowTracker.addListener("status", statusListener);
           windowTracker.addListener("TabAttrModified", listener);
           windowTracker.addListener("TabPinned", listener);
           windowTracker.addListener("TabUnpinned", listener);
           windowTracker.addListener("TabBrowserInserted", listener);
+          windowTracker.addListener("TabBrowserDiscarded", listener);
 
           tabTracker.on("tab-isarticle", isArticleChangeListener);
 
           return () => {
             windowTracker.removeListener("status", statusListener);
             windowTracker.removeListener("TabAttrModified", listener);
             windowTracker.removeListener("TabPinned", listener);
             windowTracker.removeListener("TabUnpinned", listener);
             windowTracker.removeListener("TabBrowserInserted", listener);
+            windowTracker.removeListener("TabBrowserDiscarded", listener);
             tabTracker.off("tab-isarticle", isArticleChangeListener);
           };
         }).api(),
 
         create(createProperties) {
           return new Promise((resolve, reject) => {
             let window = createProperties.windowId !== null ?
               windowTracker.getWindow(createProperties.windowId, context) :
@@ -441,16 +445,27 @@ this.tabs = class extends ExtensionAPI {
           }
 
           for (let tabId of tabs) {
             let nativeTab = tabTracker.getTab(tabId);
             nativeTab.ownerGlobal.gBrowser.removeTab(nativeTab);
           }
         },
 
+        async discard(tabIds) {
+          if (!Array.isArray(tabIds)) {
+            tabIds = [tabIds];
+          }
+          let tabs = tabIds.map(tabId => tabTracker.getTab(tabId));
+
+          for (let tab of tabs) {
+            tab.ownerGlobal.gBrowser.discardBrowser(tab.linkedBrowser);
+          }
+        },
+
         async update(tabId, updateProperties) {
           let nativeTab = getTabOrActive(tabId);
 
           let tabbrowser = nativeTab.ownerGlobal.gBrowser;
 
           if (updateProperties.url !== null) {
             let url = context.uri.resolve(updateProperties.url);
 
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -875,16 +875,32 @@
             "type": "function",
             "name": "callback",
             "optional": true,
             "parameters": []
           }
         ]
       },
       {
+        "name": "discard",
+        "type": "function",
+        "description": "discards one or more tabs.",
+        "async": true,
+        "parameters": [
+          {
+            "name": "tabIds",
+            "description": "The tab or list of tabs to discard.",
+            "choices": [
+              {"type": "integer", "minimum": 0},
+              {"type": "array", "items": {"type": "integer", "minimum": 0}}
+            ]
+          }
+        ]
+      },
+      {
         "name": "detectLanguage",
         "type": "function",
         "description": "Detects the primary language of the content in a tab.",
         "async": "callback",
         "parameters": [
           {
             "type": "integer",
             "name": "tabId",
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -133,16 +133,18 @@ skip-if = (os == 'win' && !debug) # bug 
 skip-if = !e10s || debug || asan
 [browser_ext_tab_runtimeConnect.js]
 [browser_ext_tabs_audio.js]
 [browser_ext_tabs_captureVisibleTab.js]
 [browser_ext_tabs_create.js]
 skip-if = os == "linux" && debug && bits == 32 # Bug 1350189
 [browser_ext_tabs_create_invalid_url.js]
 [browser_ext_tabs_detectLanguage.js]
+[browser_ext_tabs_discard.js]
+skip-if = !e10s
 [browser_ext_tabs_discarded.js]
 [browser_ext_tabs_duplicate.js]
 [browser_ext_tabs_events.js]
 [browser_ext_tabs_executeScript.js]
 [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]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_discard.js
@@ -0,0 +1,62 @@
+/* -*- 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_discarded() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "permissions": ["tabs"],
+    },
+
+    background: async function() {
+      let tabs = await browser.tabs.query({currentWindow: true});
+      tabs.sort((tab1, tab2) => tab1.index - tab2.index);
+
+      async function finishTest() {
+        try {
+          await browser.tabs.discard(tabs[0].id);
+          await browser.tabs.discard(tabs[2].id);
+          browser.test.succeed("attempting to discard an already discarded tab or the active tab should not throw error");
+        } catch (e) {
+          browser.test.fail("attempting to discard an already discarded tab or the active tab should not throw error");
+        }
+        let discardedTab = await browser.tabs.get(tabs[2].id);
+        browser.test.assertEq(false, discardedTab.discarded, "attempting to discard the active tab should not have succeeded");
+
+        await browser.test.assertRejects(browser.tabs.discard(999999999), /Invalid tab ID/, "attempt to discard invalid tabId should throw");
+        await browser.test.assertRejects(browser.tabs.discard([999999999, tabs[1].id]), /Invalid tab ID/, "attempt to discard a valid and invalid tabId should throw");
+        discardedTab = await browser.tabs.get(tabs[1].id);
+        browser.test.assertEq(false, discardedTab.discarded, "tab is still not discarded");
+
+        browser.test.notifyPass("test-finished");
+      }
+
+      browser.tabs.onUpdated.addListener(async function(tabId, updatedInfo) {
+        if ("discarded" in updatedInfo) {
+          browser.test.assertEq(tabId, tabs[0].id, "discarding tab triggered onUpdated");
+          let discardedTab = await browser.tabs.get(tabs[0].id);
+          browser.test.assertEq(true, discardedTab.discarded, "discarded tab discard property");
+
+          await finishTest();
+        }
+      });
+
+      browser.tabs.discard(tabs[0].id);
+    },
+  });
+
+  BrowserTestUtils.loadURI(gBrowser.browsers[0], "http://example.com");
+  await BrowserTestUtils.browserLoaded(gBrowser.browsers[0]);
+  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();
+
+  await BrowserTestUtils.removeTab(tab1);
+  await BrowserTestUtils.removeTab(tab2);
+});
+
--- a/browser/components/extensions/test/mochitest/test_ext_all_apis.html
+++ b/browser/components/extensions/test/mochitest/test_ext_all_apis.html
@@ -21,16 +21,17 @@ let expectedBackgroundApisTargetSpecific
   "tabs.TabStatus",
   "tabs.WindowType",
   "tabs.ZoomSettingsMode",
   "tabs.ZoomSettingsScope",
   "tabs.connect",
   "tabs.create",
   "tabs.detectLanguage",
   "tabs.duplicate",
+  "tabs.discard",
   "tabs.executeScript",
   "tabs.get",
   "tabs.getCurrent",
   "tabs.getZoom",
   "tabs.getZoomSettings",
   "tabs.insertCSS",
   "tabs.move",
   "tabs.onActivated",