Bug 1377733 - Add `discarded` property to tabs.Tab on desktop. r=zombie, r=kmag
authorKevin Jones <kevinhowjones@gmail.com>
Thu, 31 Aug 2017 16:14:26 -0600
changeset 427868 e2e21f475e05062859047774157b13e81055743f
parent 427867 ad7fe0c5020c483b3d9be2e8a2719612b62b71c0
child 427869 b1474bb83406eecbc44f7fcd591993e7ef1a18fc
push id7761
push userjlund@mozilla.com
push dateFri, 15 Sep 2017 00:19:52 +0000
treeherdermozilla-beta@c38455951db4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerszombie, kmag
bugs1377733
milestone57.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 1377733 - Add `discarded` property to tabs.Tab on desktop. r=zombie, r=kmag
browser/base/content/tabbrowser.xml
browser/components/extensions/ext-browser.js
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_discarded.js
toolkit/components/extensions/ext-tabs-base.js
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -2322,16 +2322,17 @@
               });
             }
           ]]>
         </body>
       </method>
 
       <method name="_insertBrowser">
         <parameter name="aTab"/>
+        <parameter name="aInsertedOnTabCreation"/>
         <body>
           <![CDATA[
             "use strict";
 
             // If browser is already inserted or window is closed don't do anything.
             if (aTab.linkedPanel || window.closed) {
               return;
             }
@@ -2383,17 +2384,18 @@
             // set the "nodefaultsrc" attribute that prevents a frameLoader
             // from being created as soon as the linked <browser> is inserted
             // into the DOM. We thus have to register the new outerWindowID
             // for non-remote browsers after we have called browser.loadURI().
             if (remoteType == E10SUtils.NOT_REMOTE) {
               this._outerWindowIDBrowserMap.set(browser.outerWindowID, browser);
             }
 
-            var evt = new CustomEvent("TabBrowserInserted", { bubbles: true, detail: {} });
+            var evt = new CustomEvent("TabBrowserInserted",
+              { bubbles: true, detail: { insertedOnTabCreation: aInsertedOnTabCreation } });
             aTab.dispatchEvent(evt);
           ]]>
         </body>
       </method>
 
       <method name="addTab">
         <parameter name="aURI"/>
         <parameter name="aReferrerURI"/>
@@ -2634,17 +2636,17 @@
 
               if (lazyBrowserURI) {
                 // Lazy browser must be explicitly registered so tab will appear as
                 // a switch-to-tab candidate in autocomplete.
                 this._unifiedComplete.registerOpenPage(lazyBrowserURI, aUserContextId);
                 b.registeredOpenURI = lazyBrowserURI;
               }
             } else {
-              this._insertBrowser(t);
+              this._insertBrowser(t, true);
             }
 
             // Dispatch a new tab notification.  We do this once we're
             // entirely done, so that things are in a consistent state
             // even if the event listener opens or closes tabs.
             var detail = aEventDetail || {};
             var evt = new CustomEvent("TabOpen", { bubbles: true, detail });
             t.dispatchEvent(evt);
--- a/browser/components/extensions/ext-browser.js
+++ b/browser/components/extensions/ext-browser.js
@@ -564,16 +564,20 @@ class Tab extends TabBase {
   get audible() {
     return this.nativeTab.soundPlaying;
   }
 
   get browser() {
     return this.nativeTab.linkedBrowser;
   }
 
+  get discarded() {
+    return !this.nativeTab.linkedPanel;
+  }
+
   get frameLoader() {
     // If we don't have a frameLoader yet, just return a dummy with no width and
     // height.
     return super.frameLoader || {lazyWidth: 0, lazyHeight: 0};
   }
 
   get cookieStoreId() {
     return getCookieStoreIdForTab(this, this.nativeTab);
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -257,16 +257,19 @@ this.tabs = class extends ExtensionAPI {
               }
               if (changed.includes("label")) {
                 needed.push("title");
               }
             } 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");
             }
 
             let tab = tabManager.getWrapper(event.originalTarget);
             let changeInfo = {};
             for (let prop of needed) {
               changeInfo[prop] = tab[prop];
             }
 
@@ -285,22 +288,24 @@ this.tabs = class extends ExtensionAPI {
               fireForTab(tabManager.wrapTab(tabElem), changed);
             }
           };
 
           windowTracker.addListener("status", statusListener);
           windowTracker.addListener("TabAttrModified", listener);
           windowTracker.addListener("TabPinned", listener);
           windowTracker.addListener("TabUnpinned", listener);
+          windowTracker.addListener("TabBrowserInserted", listener);
 
           return () => {
             windowTracker.removeListener("status", statusListener);
             windowTracker.removeListener("TabAttrModified", listener);
             windowTracker.removeListener("TabPinned", listener);
             windowTracker.removeListener("TabUnpinned", listener);
+            windowTracker.removeListener("TabBrowserInserted", listener);
           };
         }).api(),
 
         create(createProperties) {
           return new Promise((resolve, reject) => {
             let window = createProperties.windowId !== null ?
               windowTracker.getWindow(createProperties.windowId, context) :
               windowTracker.topWindow;
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -66,16 +66,17 @@
           "pinned": {"type": "boolean", "description": "Whether the tab is pinned."},
           "lastAccessed": {"type": "integer", "optional": true, "description": "The last time the tab was accessed as the number of milliseconds since epoch."},
           "audible": {"type": "boolean", "optional": true, "description": "Whether the tab has produced sound over the past couple of seconds (but it might not be heard if also muted). Equivalent to whether the speaker audio indicator is showing."},
           "mutedInfo": {"$ref": "MutedInfo", "optional": true, "description": "Current tab muted state and the reason for the last state change."},
           "url": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL the tab is displaying. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
           "title": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The title of the tab. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
           "favIconUrl": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL of the tab's favicon. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission. It may also be an empty string if the tab is loading."},
           "status": {"type": "string", "optional": true, "description": "Either <em>loading</em> or <em>complete</em>."},
+          "discarded": {"type": "boolean", "optional": true, "description": "True while the tab is not loaded with content."},
           "incognito": {"type": "boolean", "description": "Whether the tab is in an incognito window."},
           "width": {"type": "integer", "optional": true, "description": "The width of the tab in pixels."},
           "height": {"type": "integer", "optional": true, "description": "The height of the tab in pixels."},
           "sessionId": {"type": "string", "optional": true, "description": "The session ID used to uniquely identify a Tab obtained from the $(ref:sessions) API."},
           "cookieStoreId": {"type": "string", "optional": true, "description": "The CookieStoreId used for the tab."}
         }
       },
       {
@@ -584,16 +585,21 @@
                 "optional": true,
                 "description": "Whether the tabs are in the last focused window."
               },
               "status": {
                 "$ref": "TabStatus",
                 "optional": true,
                 "description": "Whether the tabs have completed loading."
               },
+              "discarded": {
+                "type": "boolean",
+                "optional": true,
+                "description": "True while the tabs are not loaded with content."
+              },
               "title": {
                 "type": "string",
                 "optional": true,
                 "description": "Match page titles against a pattern."
               },
               "url": {
                 "choices": [
                   {"type": "string"},
@@ -1190,16 +1196,21 @@
             "name": "changeInfo",
             "description": "Lists the changes to the state of the tab that was updated.",
             "properties": {
               "status": {
                 "type": "string",
                 "optional": true,
                 "description": "The status of the tab. Can be either <em>loading</em> or <em>complete</em>."
               },
+              "discarded": {
+                "type": "boolean",
+                "optional": true,
+                "description": "True while the tab is not loaded with content."
+              },
               "url": {
                 "type": "string",
                 "optional": true,
                 "description": "The tab's URL if it has changed."
               },
               "pinned": {
                 "type": "boolean",
                 "optional": true,
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -122,16 +122,17 @@ skip-if = debug || asan # Bug 1354681
 [browser_ext_sidebarAction_windows.js]
 [browser_ext_simple.js]
 [browser_ext_tab_runtimeConnect.js]
 [browser_ext_tabs_audio.js]
 [browser_ext_tabs_captureVisibleTab.js]
 [browser_ext_tabs_create.js]
 [browser_ext_tabs_create_invalid_url.js]
 [browser_ext_tabs_detectLanguage.js]
+[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]
 [browser_ext_tabs_executeScript_runAt.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_discarded.js
@@ -0,0 +1,66 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* global gBrowser SessionStore */
+"use strict";
+
+let lazyTabState = {entries: [{url: "http://example.com/", title: "Example Domain"}]};
+
+add_task(async function test_discarded() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "permissions": ["tabs"],
+    },
+
+    background: async function() {
+      let onCreatedTabData = [];
+      let discardedEventData = [];
+
+      async function finishTest() {
+        browser.test.assertEq(0, discardedEventData.length, "number of discarded events fired");
+
+        onCreatedTabData.sort((data1, data2) => data1.index - data2.index);
+        browser.test.assertEq(false, onCreatedTabData[0].discarded, "non-lazy tab onCreated discard property");
+        browser.test.assertEq(true, onCreatedTabData[1].discarded, "lazy tab onCreated discard property");
+
+        let tabs = await browser.tabs.query({currentWindow: true});
+        tabs.sort((tab1, tab2) => tab1.index - tab2.index);
+
+        browser.test.assertEq(false, tabs[1].discarded, "non-lazy tab query discard property");
+        browser.test.assertEq(true, tabs[2].discarded, "lazy tab query discard property");
+
+        let updatedTab = await browser.tabs.update(tabs[2].id, {active: true});
+        browser.test.assertEq(false, updatedTab.discarded, "lazy to non-lazy update discard property");
+        browser.test.assertEq(false, discardedEventData[0], "lazy to non-lazy onUpdated discard property");
+
+        browser.test.notifyPass("test-finished");
+      }
+
+      browser.tabs.onUpdated.addListener(function(tabId, updatedInfo) {
+        if ("discarded" in updatedInfo) {
+          discardedEventData.push(updatedInfo.discarded);
+        }
+      });
+
+      browser.tabs.onCreated.addListener(function(tab) {
+        onCreatedTabData.push({discarded: tab.discarded, index: tab.index});
+        if (onCreatedTabData.length == 2) {
+          finishTest();
+        }
+      });
+    },
+  });
+
+  await extension.startup();
+
+  let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com");
+
+  let tab2 = BrowserTestUtils.addTab(gBrowser, "about:blank", {createLazyBrowser: true});
+  SessionStore.setTabState(tab2, JSON.stringify(lazyTabState));
+
+  await extension.awaitFinish("test-finished");
+  await extension.unload();
+
+  await BrowserTestUtils.removeTab(tab1);
+  await BrowserTestUtils.removeTab(tab2);
+});
+
--- a/toolkit/components/extensions/ext-tabs-base.js
+++ b/toolkit/components/extensions/ext-tabs-base.js
@@ -486,16 +486,17 @@ class TabBase {
     let result = {
       id: this.id,
       index: this.index,
       windowId: this.windowId,
       highlighted: this.selected,
       active: this.selected,
       pinned: this.pinned,
       status: this.status,
+      discarded: this.discarded,
       incognito: this.incognito,
       width: this.width,
       height: this.height,
       lastAccessed: this.lastAccessed,
       audible: this.audible,
       mutedInfo: this.mutedInfo,
     };