Bug 1420681 - add forceDiscard property to tabs.discard - V4 draft
authorKevin Jones <kevinhowjones@gmail.com>
Wed, 29 Nov 2017 16:15:02 -0700
changeset 710997 374c3c5d3a65457d55868c2122875feb9f8a3038
parent 705276 698d4d2ed8c11406c4a6d4f82e6af9c5c49debaf
child 743718 8e165094643b451f1d3c9e0aad04072f343b2dea
push id92969
push userallassopraise@gmail.com
push dateTue, 12 Dec 2017 23:50:19 +0000
bugs1420681
milestone59.0a1
Bug 1420681 - add forceDiscard property to tabs.discard - V4 MozReview-Commit-ID: 8tHxOngj1ln
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/browser/file_onbeforeunload.html
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -445,24 +445,30 @@ this.tabs = class extends ExtensionAPI {
           }
 
           for (let tabId of tabs) {
             let nativeTab = tabTracker.getTab(tabId);
             nativeTab.ownerGlobal.gBrowser.removeTab(nativeTab);
           }
         },
 
-        async discard(tabIds) {
+        async discard(tabIds, discardProperties) {
           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);
+            tab.ownerGlobal.gBrowser.discardBrowser(tab.linkedBrowser, discardProperties &&
+                                                    discardProperties.forceDiscard);
+          }
+
+          // For Chrome compatibility.
+          if (tabs.length == 1 && !tabs[0].linkedPanel) {
+            return tabManager.convert(tabs[0]);
           }
         },
 
         async update(tabId, updateProperties) {
           let nativeTab = getTabOrActive(tabId);
 
           let tabbrowser = nativeTab.ownerGlobal.gBrowser;
 
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -878,25 +878,49 @@
             "parameters": []
           }
         ]
       },
       {
         "name": "discard",
         "type": "function",
         "description": "discards one or more tabs.",
-        "async": true,
+        "async": "callback",
         "parameters": [
           {
             "name": "tabIds",
             "description": "The tab or list of tabs to discard.",
             "choices": [
               {"type": "integer", "minimum": 0},
               {"type": "array", "items": {"type": "integer", "minimum": 0}}
             ]
+          },
+          {
+            "type": "object",
+            "name": "discardProperties",
+            "optional": true,
+            "properties": {
+              "forceDiscard": {
+                "type": "boolean",
+                "optional": true,
+                "description": "Forces discard regardless of whether an onbeforeunload handler wants to prompt.  Will suppress display of prompt, but will run all onbeforeunload handlers.  Default is `false`."
+              }
+            }
+          },
+          {
+            "type": "function",
+            "name": "callback",
+            "optional": true,
+            "parameters": [
+              {
+                "type": "object",
+                "name": "tab",
+                "description": "Tab that was discarded, undefined if discard was unsuccessful."
+              }
+            ]
           }
         ]
       },
       {
         "name": "detectLanguage",
         "type": "function",
         "description": "Detects the primary language of the content in a tab.",
         "async": "callback",
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -19,16 +19,17 @@ support-files =
   file_bypass_cache.sjs
   file_language_fr_en.html
   file_language_ja.html
   file_language_tlh.html
   file_dummy.html
   file_title.html
   file_inspectedwindow_reload_target.sjs
   file_indexedDB.html
+  file_onbeforeunload.html
   file_serviceWorker.html
   locale/chrome.manifest
   webNav_createdTarget.html
   webNav_createdTargetSource.html
   webNav_createdTargetSource_subframe.html
   serviceWorker.js
   searchSuggestionEngine.xml
   searchSuggestionEngine.sjs
--- a/browser/components/extensions/test/browser/browser_ext_tabs_discard.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_discard.js
@@ -1,62 +1,120 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 /* global gBrowser SessionStore */
 "use strict";
 
+const BASE_URL = "http://mochi.test:8888/browser/browser/components/extensions/test/browser/";
+
 add_task(async function test_discarded() {
+  await SpecialPowers.pushPrefEnv({
+    "set": [
+      ["dom.require_user_interaction_for_beforeunload", false],
+    ],
+  });
+
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "permissions": ["tabs"],
     },
 
     background: async function() {
+      function awaitDiscardTab(tabIds, param1) {
+        return new Promise(resolve => {
+          let count = 0;
+          let expectedTabIds = Array.isArray(tabIds) ? tabIds : [tabIds];
+          browser.tabs.onUpdated.addListener(async function onUpdatedListener(updatedTabId, updatedInfo) {
+            if ("discarded" in updatedInfo && expectedTabIds.includes(updatedTabId)) {
+              if (++count == expectedTabIds.length) {
+                browser.tabs.onUpdated.removeListener(onUpdatedListener);
+                resolve();
+              }
+            }
+          });
+          browser.tabs.discard(tabIds, param1);
+        });
+      }
+
+      function awaitDiscardTabCallback(params) {
+        return new Promise(resolve => {
+          let callback = tab => {
+            resolve(tab);
+          };
+          browser.tabs.discard(...params, callback);
+        });
+      }
+
       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 awaitDiscardTab(tabs[0].id);
+      browser.test.succeed("tab is discarded");
 
-        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");
+      try {
+        await browser.tabs.discard(tabs[0].id);
+        browser.test.succeed("attempting to discard an already discarded tab");
+      } catch (e) {
+        browser.test.fail("attempting to discard an already discarded tab");
+      }
+      try {
+        await browser.tabs.discard(tabs[3].id);
+        browser.test.succeed("attempting to discard the active tab should not throw error");
+      } catch (e) {
+        browser.test.fail("attempting to discard the active tab should not throw error");
       }
 
-      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");
+      let discardedTab = await browser.tabs.get(tabs[3].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");
+
+      await browser.tabs.update(tabs[0].id, {active: true});
+
+      discardedTab = await awaitDiscardTabCallback([tabs[1].id]);
+      browser.test.assertEq(true, discardedTab.discarded, "callback runs when discarding a tab, and returns a Tab object showing the tab was discarded");
+
+      discardedTab = await awaitDiscardTabCallback([tabs[0].id]);
+      browser.test.assertEq(undefined, discardedTab, "callback runs when attempting to discard a tab which cannot be discarded, and returns undefined");
 
-          await finishTest();
-        }
-      });
+      await browser.tabs.discard(tabs[3].id);
+      discardedTab = await browser.tabs.get(tabs[3].id);
+      browser.test.assertEq(false, discardedTab.discarded, "attempt to discard a tab with a blocking beforeunload handler should not have succeeded");
+
+      await awaitDiscardTab(tabs[3].id, {forceDiscard: true});
+      browser.test.succeed("tab with a blocking beforeunload handler is discarded when forceDiscard property is true");
 
-      browser.tabs.discard(tabs[0].id);
+      await awaitDiscardTab([tabs[2].id, tabs[4].id], {forceDiscard: true});
+      browser.test.succeed("array of tabs which include a tab with a blocking beforeunload handler are discarded when forceDiscard property is true");
+
+      discardedTab = await awaitDiscardTabCallback([tabs[5].id, {forceDiscard: true}]);
+      browser.test.assertEq(true, discardedTab.discarded, "calling discardTab with single tab with a blocking beforeunload handler, forceDiscard property, and callback, discards properly; callback returns tab object");
+
+      discardedTab = await awaitDiscardTabCallback([[tabs[6].id, tabs[7].id], {forceDiscard: true}]);
+      browser.test.assertEq(undefined, discardedTab, "calling discardTab with array of tabs with blocking beforeunload handlers, forceDiscard property, and callback, discards properly; callback returns undefined");
+
+      browser.test.notifyPass("test-finished");
     },
   });
 
   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 BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com");
+  await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com");
+  await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URL + "file_onbeforeunload.html");
+  await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URL + "file_onbeforeunload.html");
+  await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URL + "file_onbeforeunload.html");
+  await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URL + "file_onbeforeunload.html");
+  await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URL + "file_onbeforeunload.html");
 
   await extension.startup();
 
   await extension.awaitFinish("test-finished");
   await extension.unload();
 
-  await BrowserTestUtils.removeTab(tab1);
-  await BrowserTestUtils.removeTab(tab2);
+  while (gBrowser.tabs.length > 1) {
+    await BrowserTestUtils.removeTab(gBrowser.tabs[1]);
+  }
 });
 
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_onbeforeunload.html
@@ -0,0 +1,14 @@
+<html>
+<head>
+<script>
+"use strict";
+
+window.onbeforeunload = function() {
+  return true;
+};
+</script>
+</head>
+<body>
+TEST PAGE
+</body>
+</html>